Merge remote-tracking branch 'origin/lei_dev' into hui_dev

# Conflicts:
#	src/views/admin/menu/form.vue
This commit is contained in:
32189 2023-03-03 09:47:48 +08:00
commit dcbddc1fdb
57 changed files with 9264 additions and 4718 deletions

4486
package-lock.json generated

File diff suppressed because it is too large Load Diff

3585
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
import request from '/@/utils/request';
export function fetchList(query) {
return request({
url: '/admin/wx-account-fans/page',
method: 'get',
params: query
})
}
export function addObj(obj) {
return request({
url: '/admin/wx-account-fans',
method: 'post',
data: obj
})
}
export function sync(appId) {
return request({
url: '/admin/wx-account-fans/sync/' + appId,
method: 'post'
})
}
export function getObj(id) {
return request({
url: '/admin/wx-account-fans/' + id,
method: 'get'
})
}
export function delObjs(id) {
return request({
url: '/admin/wx-account-fans/' + id,
method: 'delete'
})
}
export function putObj(obj) {
return request({
url: '/admin/wx-account-fans',
method: 'put',
data: obj
})
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
import request from '/@/utils/request';
export function getPage(query) {
return request({
url: '/admin/wx-account-tag/page',
method: 'get',
params: query
})
}
export function addObj(obj) {
return request({
url: '/admin/wx-account-tag',
method: 'post',
data: obj
})
}
export function delObjs(obj) {
return request({
url: '/admin/wx-account-tag',
method: 'delete',
data: obj
})
}
export function putObj(obj) {
return request({
url: '/admin/wx-account-tag',
method: 'put',
data: obj
})
}
export function sync(appId) {
return request({
url: '/admin/wx-account-tag/sync/' + appId,
method: 'post'
})
}
export function list(appId) {
return request({
url: '/admin/wx-account-tag/list/',
method: 'get',
params: { 'wxAccountAppid': appId }
})
}

85
src/api/mp/wx-account.ts Normal file
View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
import request from '/@/utils/request';
export function fetchList(query) {
return request({
url: '/admin/wx-account/page',
method: 'get',
params: query
})
}
export function addObj(obj) {
return request({
url: '/admin/wx-account',
method: 'post',
data: obj
})
}
export function getObj(id) {
return request({
url: '/admin/wx-account/' + id,
method: 'get'
})
}
export function generateQr(appid) {
return request({
url: '/admin/wx-account/qr/' + appid,
method: 'post'
})
}
export function clearQuota(appid) {
return request({
url: '/admin/wx-account/clear-quota/' + appid,
method: 'post'
})
}
export function delObjs(id) {
return request({
url: '/admin/wx-account/' + id,
method: 'delete'
})
}
export function putObj(obj) {
return request({
url: '/admin/wx-account',
method: 'put',
data: obj
})
}
export function fetchAccountList() {
return request({
url: '/admin/wx-account/list',
method: 'get'
})
}
export function fetchStatistics(q) {
return request({
url: '/admin/wx-account/statistics',
method: 'get',
params: q
})
}

View File

@ -0,0 +1,39 @@
import request from '/@/utils/request';
export function getPage(query) {
return request({
url: '/admin/wx-auto-reply/page',
method: 'get',
params: query
})
}
export function addObj(obj) {
return request({
url: '/admin/wx-auto-reply',
method: 'post',
data: obj
})
}
export function getObj(id) {
return request({
url: '/admin/wx-auto-reply/' + id,
method: 'get'
})
}
export function delObj(id) {
return request({
url: '/admin/wx-auto-reply/' + id,
method: 'delete'
})
}
export function putObj(obj) {
return request({
url: '/admin/wx-auto-reply',
method: 'put',
data: obj
})
}

79
src/api/mp/wx-fans-msg.ts Normal file
View File

@ -0,0 +1,79 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
import request from '/@/utils/request';
export function fetchList(query) {
return request({
url: '/admin/wx-fans-msg/page',
method: 'get',
params: query
})
}
export function addObj(obj) {
return request({
url: '/admin/wx-fans-msg',
method: 'post',
data: obj
})
}
export function getObj(id) {
return request({
url: '/admin/wxfansmsg/' + id,
method: 'get'
})
}
export function delObjs(id) {
return request({
url: '/admin/wxfansmsg/' + id,
method: 'delete'
})
}
export function putObj(obj) {
return request({
url: '/admin/wxfansmsg',
method: 'put',
data: obj
})
}
export function fetchResList(query) {
return request({
url: '/admin/wx-fans-msg/page',
method: 'get',
params: query
})
}
export function addResObj(obj) {
return request({
url: '/admin/wx-fans-msg',
method: 'post',
data: obj
})
}
export function delResObj(id) {
return request({
url: '/admin/wx-fans-msg/' + id,
method: 'delete'
})
}

74
src/api/mp/wx-material.ts Normal file
View File

@ -0,0 +1,74 @@
import request from '/@/utils/request';
export function getPage(query) {
return request({
url: '/admin/wx-material/page',
method: 'get',
params: query
})
}
export function addObj(obj) {
return request({
url: '/admin/wx-material/materialNews',
method: 'post',
data: obj
})
}
export function materialNewsUpdate(obj) {
return request({
url: '/admin/wx-material/materialNews',
method: 'put',
data: obj
})
}
export function getObj(id) {
return request({
url: '/admin/wx-material/' + id,
method: 'get'
})
}
export function delObj(query) {
return request({
url: '/admin/wx-material',
method: 'delete',
params: query
})
}
export function putObj(obj) {
return request({
url: '/admin/wx-material',
method: 'put',
data: obj
})
}
export function getMaterialOther(query) {
return request({
url: '/admin/wx-material/materialOther',
method: 'get',
params: query,
responseType: 'blob'
})
}
export function getMaterialVideo(query) {
return request({
url: '/admin/wx-material/materialVideo',
method: 'get',
params: query
})
}
export function getTempMaterialOther(query) {
return request({
url: '/admin/wx-material/tempMaterialOther',
method: 'get',
params: query,
responseType: 'blob'
})
}

40
src/api/mp/wx-menu.ts Normal file
View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
import request from '/@/utils/request';
export function getObj(id) {
return request({
url: '/admin/wx-menu/' + id,
method: 'get'
})
}
export function saveObj(appId, data) {
return request({
url: '/admin/wx-menu/' + appId,
method: 'post',
data: data
})
}
export function publishObj(id) {
return request({
url: '/admin/wx-menu/' + id,
method: 'put'
})
}

View File

@ -0,0 +1,129 @@
$d-type: (
flex: flex,
block: block,
none: none,
);
$flex-jc: (
start: flex-start,
end: flex-end,
center: center,
between: space-between,
around: space-around,
);
$flex-ai: (
start: flex-start,
end: flex-end,
center: center,
stretch: stretch,
);
//spacing
$spacing-types: (
m: margin,
p: padding,
);
$spacing-directions: (
t: top,
r: right,
b: bottom,
l: left,
);
$spacing-base-size: 5px;
$spacing-sizes: (
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
);
@each $key, $value in $d-type {
.d-#{$key} {
display: $value;
}
}
.flex-column {
flex-direction: column;
}
.text-ellipsis {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.w-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-grow-1 {
flex: 1;
}
@each $dir in(top, bottom, right, left) {
.border-#{$dir} {
border-#{$dir}: 1px solid;
}
}
@each $key, $value in $flex-jc {
.jc-#{$key} {
justify-content: $value;
}
}
@each $key, $value in $flex-ai {
.ai-#{$key} {
align-items: $value;
}
}
//text
@each $var in (left, center, right) {
.text-#{$var} {
text-align: $var !important;
}
}
@each $typeKey, $type in $spacing-types {
@each $sizeKey, $size in $spacing-sizes {
.#{$typeKey}-#{$sizeKey} {
#{$type}: $size * $spacing-base-size;
}
}
@each $sizeKey, $size in $spacing-sizes {
.#{$typeKey}x-#{$sizeKey} {
#{$type}-left: $size * $spacing-base-size;
#{$type}-right: $size * $spacing-base-size;
}
.#{$typeKey}y-#{$sizeKey} {
#{$type}-top: $size * $spacing-base-size;
#{$type}-bottom: $size * $spacing-base-size;
}
}
@each $directionKey, $direction in $spacing-directions {
@each $sizeKey, $size in $spacing-sizes {
.#{$typeKey}#{$directionKey}-#{$sizeKey} {
#{$type}-#{$direction}: $size * $spacing-base-size;
}
}
}
}

View File

@ -0,0 +1,106 @@
<template>
<div :class="mode=='square'?'chatface':'brround avatar cover-image'" :style="transform"
style="overflow:hidden;width:40px;height:40px;">
<img v-if="faceUrl&&!num" :src="faceUrl" class="w-100 h-100"/>
<div v-else :style="styles" class="w-100 h-100 d-flex ai-center jc-center">{{ text }}</div>
</div>
</template>
<script>
export default {
name: "nameAvatar",
props: {
scale: {
type: String,
default: "1"
},
num: [Number, String],
name: String,
mode: {
type: String,
default: ""
},
fontColor: {
type: String,
default: "#fff"
},
backgroundColor: {
type: String,
default: "#3696F2"
},
faceUrl: {
type: String,
default: ""
}
},
data() {
return {};
},
watch: {},
computed: {
// eslint-disable-next-line vue/return-in-computed-property
text() {
if (this.num !== undefined) {
return `+${this.num}`;
} else {
if (this.name) {
return this.name.slice(-2);
}
}
},
transform() {
let style = {};
if (this.scale) {
style["transform"] = `scale(${this.scale}, ${this.scale})`;
}
return style;
},
styles() {
let style = {};
if (this.size) {
style["font-size"] = "12px";
}
if (this.fontColor) {
style.color = this.fontColor;
}
if (this.backgroundColor) {
style["background"] = this.backgroundColor;
}
return style;
}
},
methods: {}
};
</script>
<style lang="scss" scoped>
@import "./base.scss";
.avatar {
display: inline-block;
position: relative;
text-align: center;
vertical-align: bottom;
font-size: 8px;
user-select: none;
z-index: 10;
&:hover {
z-index: 100;
}
}
.brround {
border-radius: 50%;
}
.chatface {
display: block;
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -2,7 +2,7 @@
<div class="head-container">
<el-input v-model="searchName" :placeholder="placeholder" clearable style="margin-bottom: 20px"
@change="getDeptTree" />
<el-tree :data="state.List" :props="props" :expand-on-click-node="false" ref="deptTreeRef"
<el-tree :data="state.List" :props="props.props" :expand-on-click-node="false" ref="deptTreeRef"
:loading="state.localLoading" node-key="id" highlight-current default-expand-all @node-click="handleNodeClick" />
</div>
</template>
@ -13,7 +13,7 @@ import { onMounted, reactive, ref, unref } from "vue";
const emit = defineEmits(['search', 'nodeClick'])
const { placeholder, props, query, loading } = defineProps({
const props = defineProps({
props: {
type: Object,
default: () => {
@ -40,7 +40,7 @@ const { placeholder, props, query, loading } = defineProps({
const state = reactive({
List: [],
localLoading: loading
localLoading: props.loading
})
@ -51,12 +51,15 @@ const handleNodeClick = (item: any) => {
}
const getDeptTree = () => {
if (query instanceof Function) {
if (props.query instanceof Function) {
state.localLoading = true
const result = query(unref(searchName))
const result = props.query(unref(searchName))
if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
result.then((r: any) => {
state.List = r.data
if(r.data.length > 0){
handleNodeClick(r.data[0])
}
})
}
}

View File

@ -10,6 +10,8 @@
:limit="limit"
:on-error="handleUploadError"
:on-remove="handleRemove"
:data="data"
:auto-upload="autoUpload"
:on-success="handleUploadSuccess" class="upload-file-uploader" drag multiple>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
@ -33,8 +35,10 @@
:file-list="fileList"
:headers="headers"
:limit="limit"
:auto-upload="autoUpload"
:on-error="handleUploadError"
:on-remove="handleRemove"
:data="data"
:on-success="handleUploadSuccess" class="upload-file-uploader" multiple>
<el-button type="primary" link>点击上传</el-button>
</el-upload>
@ -76,6 +80,13 @@ const props = defineProps({
validator:(value: string) => {
return ['default','simple'].includes(value)
}
},
data: {
type: Object
},
autoUpload: {
type: Boolean,
default: true
}
});
@ -185,4 +196,13 @@ watch(() => props.modelValue, val => {
}, { deep: true, immediate: true });
const submit = () => {
fileUpload.value.submit()
}
defineExpose({
submit
})
</script>

View File

@ -74,7 +74,6 @@
<div class="vue3-cron-div">
<el-button
class="language"
type="text"
@click="state.language = state.language === 'en' ? 'cn' : 'en'"
>{{ state.language === 'en' ? 'cn' : 'en' }}</el-button
>
@ -556,7 +555,7 @@
<span>
cron预览:
</span>
<el-tag type="primary">
<el-tag type="primary">
{{ state.cron }}
</el-tag>
</div>

View File

@ -87,7 +87,7 @@
}
}
},
setup(props,context){
setup(props){
const {mode,captchaType} = toRefs(props)
const { proxy } = getCurrentInstance();
let secretKey = ref(''), //ase

View File

@ -101,8 +101,8 @@ export default {
}
}
},
setup(props, context) {
const {mode, captchaType, vSpace, imgSize, barSize, type, blockSize, explain} = toRefs(props)
setup(props) {
const {mode, captchaType, type, blockSize, explain} = toRefs(props)
const { proxy } = getCurrentInstance();
let secretKey = ref(''), //ase
passFlag = ref(''), //
@ -198,10 +198,11 @@ export default {
//
function start(e) {
e = e || window.event
let x = null
if (!e.touches) { //PC
var x = e.clientX;
x = e.clientX;
} else { //
var x = e.touches[0].pageX;
x = e.touches[0].pageX;
}
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left);
startMoveTime.value = +new Date(); //

View File

@ -0,0 +1,94 @@
<template>
<el-upload
ref="fileUpload"
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:on-success="handleUploadSuccess"
:file-list="fileList"
:before-upload="beforeThumbImageUpload"
:auto-upload="autoUpload"
:data="uploadData">
<template #tip>
<div class="el-upload__tip" v-if="props.type.length > 0">支持{{props.type.join("/")}}格式大小不超过2M</div>
</template>
<el-button type="primary">本地上传</el-button>
</el-upload>
</template>
<script setup lang="ts" name="wx-file-upload">
import {Local, Session} from "/@/utils/storage";
import {useMessage} from "/@/hooks/message";
const actionUrl = ref("/admin/wx-material/materialFileUpload")
const fileUpload = ref()
const headers = computed(() => {
const tenantId = Local.get("tenantId") ? Local.get("tenantId") : 1
return {
'Authorization': "Bearer " + Session.get("token"),
'TENANT-ID': tenantId
};
})
// emit
const emit = defineEmits(['success']);
const fileList = ref([])
const props = defineProps({
uploadData: {
type: Object,
default: () => {
return {
appId: '',
mediaType: 'image',
title: '',
introduction: '',
}
}
},
autoUpload: {
type: Boolean,
default: true
},
type: {
type: Array,
default: () => {
return []
}
}
})
const beforeThumbImageUpload = (file: any) => {
let isType = true
if(props.type?.length > 0){
isType = props.type?.includes(file.type)
}
const isLt = file.size / 1024 / 1024 < 2
if (!isType) {
useMessage().error("上传文件格式不对!")
}
if (!isLt) {
useMessage().error("上传文件大小不能超过2M!!")
}
return isType && isLt
}
const handleUploadSuccess = (response, file, fileList) => {
fileList.value = []
emit("success",response, file, fileList)
}
const submit = () => {
fileUpload.value.submit()
}
defineExpose({
submit
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,215 @@
<template>
<el-dialog title="选择图文" v-model="visible"
:close-on-click-modal="false" draggable>
<div v-if="objData.type === 'image'">
<div class="waterfall" v-loading="state.loading">
<div class="waterfall-item" v-for="item in state.dataList" :key="item.mediaId">
<img class="material-img" :src="item.url" />
<p class="item-name">{{ item.name }}</p>
<el-row class="ope-row">
<el-button type="success" @click="selectMaterial(item)"
>选择
<el-icon class="el-icon--right"></el-icon>
</el-button>
</el-row>
</div>
</div>
<pagination v-bind="state.pagination"
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"/>
</div>
<div v-else-if="objData.type === 'voice'">
<!-- 列表 -->
<el-table v-loading="state.loading" :data="state.dataList">
<el-table-column label="编号" align="center" prop="mediaId" />
<el-table-column label="文件名" align="center" prop="name" />
<el-table-column label="语音" align="center" prop="url">
</el-table-column>
<el-table-column
label="上传时间"
align="center"
prop="createTime"
width="180"
>
<template v-slot="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
fixed="right"
class-name="small-padding fixed-width"
>
<template v-slot="scope">
<el-button
icon="el-icon-circle-plus"
@click="selectMaterial(scope.row)"
>选择</el-button
>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination"
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"/>
</div>
<div v-else-if="objData.type === 'video'">
<!-- 列表 -->
<el-table v-loading="state.loading" :data="state.dataList">
<el-table-column label="编号" align="center" prop="mediaId" />
<el-table-column label="文件名" align="center" prop="name" />
<el-table-column label="标题" align="center" prop="title" />
<el-table-column label="介绍" align="center" prop="introduction" />
<el-table-column label="视频" align="center" prop="url">
</el-table-column>
<el-table-column
label="上传时间"
align="center"
prop="createTime"
width="180"
>
<template v-slot="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
fixed="right"
class-name="small-padding fixed-width"
>
<template v-slot="scope">
<el-button
icon="el-icon-circle-plus"
@click="selectMaterial(scope.row)"
>选择</el-button
>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination"
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"/>
</div>
<div v-else-if="objData.type === 'news'">
<div class="waterfall" v-loading="state.loading">
<template v-for="item in state.dataList">
<div v-if="item.content && item.content.newsItem" class="waterfall-item" :key="item.id">
<wx-news :obj-data="item.content.newsItem"></wx-news>
<el-row class="ope-row">
<el-button type="success" @click="selectMaterial(item)">
选择<el-icon class="el-icon--right"/>
</el-button>
</el-row>
</div>
</template>
</div>
<pagination v-bind="state.pagination"
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"/>
</div>
</el-dialog>
</template>
<script setup lang="ts" name="wx-material-select">
import {defineEmits} from "vue";
import {BasicTableProps, useTable} from "/@/hooks/table";
import {getPage} from "/@/api/mp/wx-material";
const WxNews = defineAsyncComponent(() => import('../wx-news/index.vue'))
const emit = defineEmits(["selectMaterial"])
const objData = reactive({
repType: '',
accountId: '',
type: ''
})
const visible = ref(false)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
type: "",
appId: ""
},
pageList: getPage,
createdIsNeed: false,
props: {
item: 'items',
totalCount: 'totalCount'
}
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle
} = useTable(state)
const selectMaterial = (item : any) => {
emit('selectMaterial', item,objData.accountId)
visible.value = false
}
const openDialog = (data: any) => {
state.queryForm.type = data.type
state.queryForm.appId = data.accountId
objData.type = data.type
visible.value = true
getDataList()
}
//
defineExpose({
openDialog,
});
</script>
<style lang="scss" scoped>
/*瀑布流样式*/
.waterfall {
width: 100%;
column-gap:10px;
column-count: 5;
margin: 0 auto;
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
.material-img {
width: 100%;
}
p {
line-height: 30px;
}
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
p {
color:red;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
p {
color: orange;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
/*瀑布流样式*/
</style>

View File

@ -0,0 +1,101 @@
.avue-card{
&__item{
margin-bottom: 16px;
border: 1px solid #e8e8e8;
background-color: #fff;
box-sizing: border-box;
color: rgba(0,0,0,.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
list-style: none;
font-feature-settings: "tnum";
cursor: pointer;
height:200px;
&:hover{
border-color: rgba(0,0,0,.09);
box-shadow: 0 2px 8px rgba(0,0,0,.09);
}
&--add{
border:1px dashed #000;
width: 100%;
color: rgba(0,0,0,.45);
background-color: #fff;
border-color: #d9d9d9;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
i{
margin-right: 10px;
}
&:hover{
color: #40a9ff;
background-color: #fff;
border-color: #40a9ff;
}
}
}
&__body{
display: flex;
padding: 24px;
}
&__detail{
flex:1
}
&__avatar{
width: 48px;
height: 48px;
border-radius: 48px;
overflow: hidden;
margin-right: 12px;
img{
width: 100%;
height: 100%;
}
}
&__title{
color: rgba(0,0,0,.85);
margin-bottom: 12px;
font-size: 16px;
&:hover{
color:#1890ff;
}
}
&__info{
color: rgba(0,0,0,.45);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
height: 64px;
}
&__menu{
display: flex;
justify-content:space-around;
height: 50px;
background: #f7f9fa;
color: rgba(0,0,0,.45);
text-align: center;
line-height: 50px;
&:hover{
color:#1890ff;
}
}
}
/** joolun 额外加的 */
.avue-comment__main {
flex: unset!important;
border-radius: 5px!important;
margin: 0 8px!important;
}
.avue-comment__header {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.avue-comment__body {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}

View File

@ -0,0 +1,88 @@
/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */
.avue-comment{
margin-bottom: 30px;
display: flex;
align-items: flex-start;
&--reverse{
flex-direction:row-reverse;
.avue-comment__main{
&:before,&:after{
left: auto;
right: -8px;
border-width: 8px 0 8px 8px;
}
&:before{
border-left-color: #dedede;
}
&:after{
border-left-color: #f8f8f8;
margin-right: 1px;
margin-left: auto;
}
}
}
&__avatar{
width: 48px;
height: 48px;
border-radius: 50%;
border: 1px solid transparent;
box-sizing: border-box;
vertical-align: middle;
}
&__header{
padding: 5px 15px;
background: #f8f8f8;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
&__author{
font-weight: 700;
font-size: 14px;
color: #999;
}
&__main{
flex:1;
margin: 0 20px;
position: relative;
border: 1px solid #dedede;
border-radius: 2px;
&:before,&:after{
position: absolute;
top: 10px;
left: -8px;
right: 100%;
width: 0;
height: 0;
display: block;
content: " ";
border-color: transparent;
border-style: solid solid outset;
border-width: 8px 8px 8px 0;
pointer-events: none;
}
&:before {
border-right-color: #dedede;
z-index: 1;
}
&:after{
border-right-color: #f8f8f8;
margin-left: 1px;
z-index: 2;
}
}
&__body{
padding: 15px;
overflow: hidden;
background: #fff;
font-family: Segoe UI,Lucida Grande,Helvetica,Arial,Microsoft YaHei,FreeSans,Arimo,Droid Sans,wenquanyi micro hei,Hiragino Sans GB,Hiragino Sans GB W3,FontAwesome,sans-serif;color: #333;
font-size: 14px;
}
blockquote{
margin:0;
font-family: Georgia,Times New Roman,Times,Kai,Kaiti SC,KaiTi,BiauKai,FontAwesome,serif;
padding: 1px 0 1px 15px;
border-left: 4px solid #ddd;
}
}

View File

@ -0,0 +1,223 @@
<template>
<el-dialog title="用户消息" v-model="visible"
:close-on-click-modal="false" draggable>
<div v-loading="mainLoading" class="msg-main">
<div id="msg-div" class="msg-div">
<div v-if="!tableLoading">
<div v-if="loadMore" class="el-table__empty-block" @click="loadingMore"><span class="el-table__empty-text">点击加载更多</span>
</div>
<div v-if="!loadMore" class="el-table__empty-block"><span class="el-table__empty-text">没有更多了</span></div>
</div>
<div v-for="item in tableData" :key="item.id" class="execution">
<div class="avue-comment" :class="item.type === '2' ? 'avue-comment--reverse' : ''">
<div class="avatar-div">
<name-avatar v-if="item.type === '1'" scale="2" :name="item.nickName" />
<name-avatar v-if="item.type !== '1'" scale="2" :face-url="item.appLogo" />
</div>
<div class="avue-comment__main">
<div class="avue-comment__header">
<div class="avue-comment__create_time">{{ item.createTime }}</div>
</div>
<div class="avue-comment__body" :style="item.type === '2' ? 'background: #6BED72;' : ''">
<div v-if="item.repType === 'event' && item.repEvent === 'subscribe'">
<el-tag type="success" size="mini">关注</el-tag>
</div>
<div v-if="item.repType === 'event' && item.repEvent === 'unsubscribe'">
<el-tag type="danger" size="mini">取消关注</el-tag>
</div>
<div v-if="item.repType === 'event' && item.repEvent === 'CLICK'">
<el-tag size="mini">点击菜单</el-tag>
{{ item.repName }}
</div>
<div v-if="item.repType === 'event' && item.repEvent === 'VIEW'">
<el-tag size="mini">点击菜单链接</el-tag>
{{ item.repUrl }}
</div>
<div v-if="item.repType === 'event' && item.repEvent === 'scancode_waitmsg'">
<el-tag size="mini">扫码结果</el-tag>
{{ item.repContent }}
</div>
<div v-if="item.repType === 'text'">{{ item.repContent }}</div>
<div v-if="item.repType === 'image'">
<a target="_blank" :href="item.repUrl"><img :src="item.repUrl" style="width: 100px"></a>
</div>
<div v-if="item.repType === 'voice'">
{{item}}
<!-- <WxVoicePlayer :obj-data="item"></WxVoicePlayer>-->
</div>
<div v-if="item.repType === 'video'" style="text-align: center">
{{item}}
<!-- <WxVideoPlayer :obj-data="item"></WxVideoPlayer>-->
</div>
<div v-if="item.repType === 'shortvideo'" style="text-align: center">
{{item}}
<!-- <WxVideoPlayer :obj-data="item"></WxVideoPlayer>-->
</div>
<div v-if="item.repType === 'location'">
<el-link
type="primary"
target="_blank"
:href="'https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&pointx='+item.repLocationY+'&pointy='+item.repLocationX+'&name='+item.repContent+'&ref=joolun'">
<img
:src="'https://apis.map.qq.com/ws/staticmap/v2/?zoom=10&markers=color:blue|label:A|'+item.repLocationX+','+item.repLocationY+'&key=PFFBZ-RBM3V-IEEPP-UH6KE-6QUQE-C4BVJ&size=250*180'">
<p /><i class="el-icon-map-location"></i>{{ item.repContent }}
</el-link>
</div>
<div v-if="item.repType === 'link'" class="avue-card__detail">
<el-link type="success" :underline="false" target="_blank" :href="item.repUrl">
<div class="avue-card__title"><i class="el-icon-link"></i>{{ item.repName }}</div>
</el-link>
<div class="avue-card__info" style="height: unset">{{ item.repDesc }}</div>
</div>
<div v-if="item.repType === 'news'" style="width: 300px">
<wx-news :obj-data="JSON.parse(item.content).articles"></wx-news>
</div>
<div v-if="item.repType === 'music'">
<el-link type="success" :underline="false" target="_blank" :href="item.repUrl">
<div class="avue-card__body" style="padding:10px;background-color: #fff;border-radius: 5px">
<div class="avue-card__avatar"><img :src="item.repThumbUrl" alt=""></div>
<div class="avue-card__detail">
<div class="avue-card__title" style="margin-bottom:unset">{{ item.repName }}</div>
<div class="avue-card__info" style="height: unset">{{ item.repDesc }}</div>
</div>
</div>
</el-link>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-loading="sendLoading" class="msg-send">
<wx-reply :objData="objData"></wx-reply>
<el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts" name="wx-msg">
import { fetchList,addObj } from '/@/api/mp/wx-fans-msg'
import {useMessage} from "/@/hooks/message";
const WxReply = defineAsyncComponent(() => import("../wx-reply/index.vue"))
const WxNews = defineAsyncComponent(() => import("../wx-news/index.vue"))
const NameAvatar = defineAsyncComponent(() => import("/@/components/NameAvatar/index.vue"))
const visible = ref(false)
// loading
const mainLoading = ref(false)
const sendLoading = ref(false)
const loadMore = ref(false)
const objData = ref({
repType: 'text',
appId: ''
}) as any
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10, //
ascs: [], //
descs: 'create_time'//
})
const wxData = reactive({
appId: '',
wxUserId: ''
})
const sendMsg = () => {
if (objData.value) {
if (objData.value.repType === 'news') {
if(objData.value.content.newsItem.length > 1){
useMessage().error("图文消息条数限制在1条以内已默认发送第一条")
}
objData.value.content.newsItem = [objData.value.content.newsItem[0]]
objData.value.content = JSON.stringify(objData.value.content)
}
sendLoading.value = true
addObj(Object.assign({
wxUserId: wxData.wxUserId,
appId: wxData.appId
}, objData.value)).then(() => {
tableData.value = []
getData()
}).finally(() => {
sendLoading.value = false
})
}
}
const tableData = ref([] as any)
const tableLoading = ref(false)
const openDialog = (data: any) => {
wxData.wxUserId = data.wxUserId
wxData.appId = data.appId
objData.value.appId = data.appId
getData()
visible.value = true
}
const getData = () => {
tableLoading.value = true
fetchList({
...page,
...wxData
}).then(res=> {
const data = res.data.records.reverse()
tableData.value = [...data, ...tableData.value]
page.total = res.data.total
tableLoading.value = false
if (data.length < page.pageSize || data.length === 0) {
loadMore.value = false
}
})
}
const loadingMore = () => {
page.currentPage = page.currentPage + 1
getData()
}
//
defineExpose({
openDialog
});
</script>
<style lang="scss" scoped>
@import './comment.scss';
@import './card.scss';
.msg-main {
margin-top: -30px;
padding: 10px;
}
.msg-div {
height: 50vh;
overflow: auto;
background-color: #eaeaea;
margin-left: 10px;
margin-right: 10px;
}
.msg-send {
padding: 10px;
}
.avatar-div {
text-align: center;
width: 80px;
}
.send-but {
float: right;
margin-top: 8px!important;
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<div class="news-home">
<div v-for="(news, index) in props.objData" :key="index" class="news-div">
<a v-if="index===0" target="_blank" :href="news.url">
<div class="news-main">
<div class="news-content">
<img class="material-img" :src="news.thumbUrl" width="280px" height="120px" />
<div class="news-content-title">
<span>{{ news.title }}</span>
</div>
</div>
</div>
</a>
<a v-if="index>0" target="_blank" :href="news.url">
<div class="news-main-item">
<div class="news-content-item">
<div class="news-content-item-title">{{ news.title }}</div>
<div class="news-content-item-img">
<img class="material-img" :src="news.thumbUrl" height="100%" />
</div>
</div>
</div>
</a>
</div>
</div>
</template>
<script setup lang="ts" name="wx-news">
const props = defineProps({
objData: {
type: Array,
default: () => []
}
})
</script>
<style lang="scss" scoped>
.news-home{
background-color: #FFFFFF;
width: 100%;
margin: auto;
}
.news-main{
width: 100%;
margin: auto;
}
.news-content{
background-color: #acadae;
width: 100%;
position: relative;
}
.news-content-title{
display: inline-block;
font-size: 12px;
color: #FFFFFF;
position: absolute;
left: 0px;
bottom: 0px;
background-color: black;
width: 98%;
padding: 1%;
opacity: 0.65;
white-space: normal;
box-sizing: unset!important
}
.news-main-item{
background-color: #FFFFFF;
padding: 5px 0px;
border-top: 1px solid #eaeaea;
}
.news-content-item{
position: relative;
}
.news-content-item-title{
display: inline-block;
font-size: 10px;
width: 70%;
margin-left: 1%;
white-space: normal
}
.news-content-item-img{
display: inline-block;
width: 25%;
background-color: #acadae;
margin-right: 1%;
}
.material-img {
width: 100%;
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<el-tabs v-model="objData.repType" type="border-card" @tab-click="handleClick" style="width: 100%;">
<el-tab-pane name="text" label="text">
<template #label><i class="el-icon-document"></i> 文本</template>
<el-input
v-model="objData.repContent"
type="textarea"
:rows="5"
placeholder="请输入内容">
</el-input>
</el-tab-pane>
<el-tab-pane name="image" label="image">
<template #label><i class="el-icon-picture"></i> 图片</template>
<el-row>
<div v-if="objData.repUrl" class="select-item">
<img class="material-img" :src="objData.repUrl">
<p v-if="objData.repName" class="item-name">{{ objData.repName }}</p>
<el-row class="ope-row">
<el-button type="danger" icon="el-icon-delete" circle @click="deleteObj"></el-button>
</el-row>
</div>
<div v-if="!objData.repUrl" style="width: 100%;">
<el-row style="text-align: center">
<el-col :span="12" class="col-select">
<el-button type="success" @click="openMaterial({type: 'image',accountId: props.objData.appId})">素材库选择<i class="fansel-icon--right"></i>
</el-button>
</el-col>
<el-col :span="12" class="col-add">
<wx-file-upload :data="uploadData" @success="handelImage"></wx-file-upload>
</el-col>
</el-row>
</div>
</el-row>
</el-tab-pane>
<el-tab-pane name="voice" label="voice">
<template #label><i class="el-icon-phone"></i> 语音</template>
<el-row>
<div v-if="objData.repName" class="select-item">
<p class="item-name">{{ objData.repName }}</p>
<div class="item-infos">
<!-- <WxVoicePlayer :obj-data="Object.assign(tempPlayerObj,{repMediaId: objData.media_id, repName: objData.repName})"></WxVoicePlayer>-->
</div>
<el-row class="ope-row">
<el-button type="danger" icon="el-icon-delete" circle @click="deleteObj"></el-button>
</el-row>
</div>
<div v-if="!objData.repName" style="width: 100%;">
<el-row style="text-align: center">
<el-col :span="12" class="col-select">
<el-button type="success" @click="openMaterial({type: 'voice',accountId: props.objData.appId})">素材库选择<i class="fansel-icon--right"></i>
</el-button>
</el-col>
<el-col :span="12" class="col-add">
<wx-file-upload :data="uploadData"></wx-file-upload>
</el-col>
</el-row>
</div>
</el-row>
</el-tab-pane>
<el-tab-pane name="video" label="video">
<template #label><i class="el-icon-share"></i> 视频</template>
<el-row style="text-align: center">
<el-input v-model="objData.repName" placeholder="请输入标题"></el-input>
<el-input v-model="objData.repDesc" placeholder="请输入描述"></el-input>
<div style="text-align: center;">
<a v-if="objData.repUrl" target="_blank" :href="objData.repUrl"><i
class="icon-bofang">&nbsp;播放视频</i></a>
</div>
<div style="margin: 20px 0;"></div>
<div style="text-align: center">
<el-button type="success" @click="openMaterial({type: 'video',accountId: props.objData.appId})">素材库选择<i class="fansel-icon--right"></i>
</el-button>
</div>
</el-row>
</el-tab-pane>
<el-tab-pane name="news" label="news">
<template #label><i class="el-icon-news"></i> 图文</template>
<el-row>
<div v-if="objData.content" class="select-item">
<wx-news :obj-data="objData.content.newsItem"></wx-news>
<el-row class="ope-row">
<el-button type="danger" icon="el-icon-delete" circle @click="deleteObj"></el-button>
</el-row>
</div>
<div v-if="!objData.content" style="width: 100%;">
<el-row style="text-align: center">
<el-col :span="24" class="col-select2">
<el-button type="success" @click="openMaterial({type: 'news',accountId: props.objData.appId})">素材库选择<i class="fansel-icon--right"></i>
</el-button>
</el-col>
</el-row>
</div>
</el-row>
</el-tab-pane>
</el-tabs>
<wx-material-select ref="dialogNewsRef" @selectMaterial="selectMaterial"></wx-material-select>
</template>
<script setup lang="ts" name="wx-reply">
import {getMaterialVideo} from "/@/api/mp/wx-material";
import {useMessage} from "/@/hooks/message";
const WxMaterialSelect = defineAsyncComponent(() => import("/@/components/wechart/wx-material-select/main.vue"))
const WxFileUpload = defineAsyncComponent(() => import("/@/components/wechart/fileUpload/index.vue"))
const WxNews = defineAsyncComponent(() => import("/@/components/wechart/wx-news/index.vue"))
const props = defineProps({
objData: {
type: Object,
default: () => {
return {
repType: '',
repContent: '',
repName: '',
repDesc: '',
repUrl: ''
}
}
}
})
const uploadData = reactive({
mediaType: props.objData.repType,
title: '',
introduction: '',
appId: props.objData.appId
})
const handleClick = (tab) => {
uploadData.mediaType = tab.paneName
uploadData.appId = props.objData.appId
}
const deleteObj = () => {
props.objData.repName = ''
props.objData.repUrl = ''
props.objData.content = ''
}
const openMaterial = (data: any) => {
dialogNewsRef.value.openDialog(data)
}
const dialogNewsRef = ref()
const selectMaterial = (item, appId) => {
let tempObjItem = {
repType: '',
repMediaId: '',
media_id: '',
content: ''
} as any
tempObjItem.repType = props.objData.repType
tempObjItem.repMediaId = item.mediaId
tempObjItem.media_id = item.mediaId
tempObjItem.content = item.content
props.objData.repMediaId = item.mediaId
props.objData.media_id = item.mediaId
props.objData.content = item.content
if (props.objData.repType === 'music') {
tempObjItem.repThumbMediaId = item.mediaId
tempObjItem.repThumbUrl = item.url
props.objData.repThumbMediaId = item.mediaId
props.objData.repThumbUrl = item.url
} else {
tempObjItem.repName = item.name
tempObjItem.repUrl = item.url
props.objData.repName = item.name
props.objData.repUrl = item.url
}
if (props.objData.repType === 'video') {
getMaterialVideo({
mediaId: item.mediaId,
appId: appId
}).then(response => {
const data = response.data.data
tempObjItem.repDesc = data.description
tempObjItem.repUrl = data.downUrl
})
}
}
const handelImage = (response) => {
if (response.code === 0) {
const item = response.data
selectMaterial(item,props.objData.appId)
} else {
useMessage().error("上传错误" + response.msg)
}
}
</script>
<style scoped lang="scss">
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.select-item2 {
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.ope-row {
padding-top: 10px;
text-align: center;
}
.item-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.el-form-item__content {
line-height: unset !important;
}
.col-select {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%;
}
.col-select2 {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
}
.col-add {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%;
float: right
}
.avatar-uploader-icon {
border: 1px solid #d9d9d9;
font-size: 28px;
color: #8c939d;
width: 100px !important;
height: 100px !important;
line-height: 100px !important;
text-align: center;
}
.material-img {
width: 100%;
}
.thumb-div {
display: inline-block;
text-align: center;
}
.item-infos {
width: 30%;
margin: auto
}
</style>

View File

@ -62,6 +62,7 @@ export function wavesDirective(app: App) {
*/
export function dragDirective(app: App) {
app.directive('drag', {
// @ts-ignore
mounted(el, binding) {
if (!binding.value) return false;

View File

@ -26,6 +26,8 @@ export interface BasicTableProps {
descs?: string[]
ascs?: string[]
props?: any
}
export interface Pagination {
@ -60,7 +62,11 @@ export function useTable(options?: BasicTableProps) {
loading: false,
selectObjs: [],
descs: [],
ascs: []
ascs: [],
props: {
item: 'records',
totalCount: 'total'
}
}
const mergeDefaultOptions = (options: any, props: any): BasicTableProps => {
@ -86,10 +92,10 @@ export function useTable(options?: BasicTableProps) {
descs: state.descs,
ascs: state.ascs
}).then((res: any) => {
state.dataList = state.isPage ? res.data.records : res.data
state.pagination!.total = state.isPage ? res.data.total : 0
state.dataList = state.isPage ? res.data[state.props.item] : res.data
state.pagination!.total = state.isPage ? res.data[state.props.totalCount] : 0
}).catch((err: any) => {
ElMessage.error(err.data.msg)
ElMessage.error(err.msg || err.data.msg)
}).finally(() => {
state.loading = false;
})

View File

@ -2,8 +2,8 @@ import {createApp} from 'vue'
import pinia from '/@/stores/index'
import App from './App.vue'
import router from './router'
import {directive} from '/@/directive/index'
import {i18n} from '/@/i18n/index'
import {directive} from '/@/directive'
import {i18n} from '/@/i18n'
import other from '/@/utils/other'
import ElementPlus from 'element-plus'

View File

@ -1,6 +1,5 @@
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import errorCode from './errorCode'
import { ElMessage, ElMessageBox } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import { Session, Local } from '/@/utils/storage';
import qs from 'qs';
@ -20,6 +19,7 @@ const service: AxiosInstance = axios.create({
service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// get查询参数序列化
if (config.method === 'get') {
// @ts-ignore
config.paramsSerializer = (params: any) => {
return qs.stringify(params, { arrayFormat: 'repeat' })
}
@ -51,7 +51,6 @@ service.interceptors.response.use((res: any) => {
return res.data;
}, error => {
const status = Number(error.response.status) || 200
const message = error.response.data.msg || errorCode[status] || errorCode['default']
if (status === 424) {
ElMessageBox.confirm('令牌状态已过期,请点击重新登录', '系统提示', {
confirmButtonText: '重新登录',

View File

@ -1,226 +1,215 @@
<template>
<el-dialog :close-on-click-modal="false" :title="state.ruleForm.menuId ? $t('common.editBtn') : $t('common.addBtn')"
draggable v-model="visible">
<el-form :model="state.ruleForm" :rules="dataRules" label-width="90px" ref="menuDialogFormRef"
v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.menuType')" prop="menType">
<el-radio-group v-model="state.ruleForm.menuType">
<el-radio-button label="0">左菜单</el-radio-button>
<el-radio-button label="1">按钮</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.parentId')" prop="parentId">
<el-tree-select :data="state.parentData" :placeholder="$t('sysmenu.inputParentIdTip')"
:props="{ value: 'id', label: 'name', children: 'children' }"
:render-after-expand="false" check-strictly
class="w100" clearable
v-model="state.ruleForm.parentId">
</el-tree-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.name')" prop="name">
<el-input :placeholder="$t('sysmenu.inputNameTip')" clearable
v-model="state.ruleForm.name"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '0'">
<el-form-item :label="$t('sysmenu.path')" prop="path">
<el-input :placeholder="$t('sysmenu.inputPathTip')" v-model="state.ruleForm.path"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '1'">
<el-form-item :label="$t('sysmenu.permission')" prop="permission">
<el-input :placeholder="$t('sysmenu.inputPermissionTip')" maxlength="50"
v-model="state.ruleForm.permission"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.sortOrder')" prop="sortOrder">
<el-input-number :min="0" controls-position="right" v-model="state.ruleForm.sortOrder"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.icon')" prop="icon">
<IconSelector :placeholder="$t('sysmenu.inputIconTip')" v-model="state.ruleForm.icon"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '0' && showembedded">
<el-form-item :label="$t('sysmenu.embedded')" prop="embedded">
<el-radio-group v-model="state.ruleForm.embedded">
<el-radio-button label="0"></el-radio-button>
<el-radio-button label="1"></el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '0'">
<el-form-item :label="$t('sysmenu.keepAlive')" prop="keepAlive">
<el-radio-group v-model="state.ruleForm.keepAlive">
<el-radio-button label="0"></el-radio-button>
<el-radio-button label="1"></el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '0'">
<el-form-item :label="$t('sysmenu.visible')" prop="visible">
<el-radio-group v-model="state.ruleForm.visible">
<el-radio-button label="0"></el-radio-button>
<el-radio-button label="1"></el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-dialog :title="state.ruleForm.menuId ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible"
:close-on-click-modal="false" draggable>
<el-form ref="menuDialogFormRef" :model="state.ruleForm" :rules="dataRules" label-width="90px" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.menuType')" prop="menType">
<el-radio-group v-model="state.ruleForm.menuType">
<el-radio-button label="0">左菜单</el-radio-button>
<el-radio-button label="1">按钮</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.parentId')" prop="parentId">
<el-tree-select v-model="state.ruleForm.parentId" :data="state.parentData" :render-after-expand="false"
:props="{ value: 'id', label: 'name', children: 'children' }" class="w100" clearable check-strictly
:placeholder="$t('sysmenu.inputParentIdTip')">
</el-tree-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.name')" prop="name">
<el-input v-model="state.ruleForm.name" clearable :placeholder="$t('sysmenu.inputNameTip')"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '0'">
<el-form-item :label="$t('sysmenu.path')" prop="path">
<el-input v-model="state.ruleForm.path" :placeholder="$t('sysmenu.inputPathTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '1'">
<el-form-item :label="$t('sysmenu.permission')" prop="permission">
<el-input v-model="state.ruleForm.permission" maxlength="50"
:placeholder="$t('sysmenu.inputPermissionTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.sortOrder')" prop="sortOrder">
<el-input-number v-model="state.ruleForm.sortOrder" :min="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysmenu.icon')" prop="icon">
<IconSelector :placeholder="$t('sysmenu.inputIconTip')" v-model="state.ruleForm.icon" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '0' && showembedded">
<el-form-item :label="$t('sysmenu.embedded')" prop="embedded">
<el-radio-group v-model="state.ruleForm.embedded">
<el-radio-button label="0"></el-radio-button>
<el-radio-button label="1"></el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '0'">
<el-form-item :label="$t('sysmenu.keepAlive')" prop="keepAlive">
<el-radio-group v-model="state.ruleForm.keepAlive">
<el-radio-button label="0"></el-radio-button>
<el-radio-button label="1"></el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="state.ruleForm.menuType === '0'">
<el-form-item :label="$t('sysmenu.visible')" prop="visible">
<el-radio-group v-model="state.ruleForm.visible">
<el-radio-button label="0"></el-radio-button>
<el-radio-button label="1"></el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary">{{ $t('common.confirmButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
</el-dialog>
</template>
<script lang="ts" name="systemMenuDialog" setup>
import {addObj, info, pageList, update} from "/@/api/admin/menu";
import {useMessage} from "/@/hooks/message";
import {rule} from "/@/utils/validate";
<script setup lang="ts" name="systemMenuDialog">
import { info, pageList, update, addObj } from "/@/api/admin/menu";
import { useMessage } from "/@/hooks/message";
// /
const emit = defineEmits(['refresh']);
//
const IconSelector = defineAsyncComponent(() => import('/@/components/iconSelector/index.vue'));
// /
const emit = defineEmits(['refresh']);
//
const IconSelector = defineAsyncComponent(() => import('/@/components/iconSelector/index.vue'));
const visible = ref(false)
const loading = ref(false)
//
const menuDialogFormRef = ref();
//
const state = reactive({
ruleForm: {
id: '',
menuId: '',
name: '',
permission: '',
parentId: '',
icon: '',
path: '',
sortOrder: 0,
menuType: '0',
keepAlive: '0',
visible: '1',
embedded: '0',
},
parentData: [] as any[], //
const visible = ref(false)
const loading = ref(false)
//
const menuDialogFormRef = ref();
//
const state = reactive({
ruleForm: {
menuId: '',
name: '',
permission: '',
parentId: '',
icon: '',
path: '',
sortOrder: 0,
menuType: '0',
keepAlive: '0',
visible: '1',
embedded: '0',
},
parentData: [] as any[], //
});
//
const getMenuData = () => {
state.parentData = []
pageList({
type: '0'
}).then(res => {
let menu = {
createBy: "",
createTime: "",
delFlag: "",
icon: "",
keepAlive: "",
menuId: "",
menuType: "",
parentId: "",
path: "",
embedded: '0',
sortOrder: 0,
updateBy: "",
updateTime: "",
visible: "1",
id: '-1', name: '根菜单', children: []
};
menu.children = res.data;
state.parentData.push(menu)
})
};
const showembedded = ref(false)
watch(() => state.ruleForm.path, (val) => {
if (val.startsWith('http')) {
showembedded.value = true
} else {
showembedded.value = false
state.ruleForm.embedded = '0'
}
})
const dataRules = reactive({
menType: [{ required: true, message: "菜单类型不能为空", trigger: "blur" }],
parentId: [{ required: true, message: "上级菜单不能为空", trigger: "blur" }],
name: [{ required: true, message: "菜单不能为空", trigger: "blur" }],
path: [{ required: true, message: "路径不能为空", trigger: "blur" }],
permission: [{ required: true, message: "权限代码不能为空", trigger: "blur" }],
sortOrder: [{ required: true, message: "排序不能为空", trigger: "blur" }]
})
//
const openDialog = (type: string, row?: any) => {
if (row?.id && type === 'edit') {
state.ruleForm.menuId = row.id
//
loading.value = true
info(row.id).then(res => {
Object.assign(state.ruleForm, res.data)
}).finally(() => {
loading.value = false
})
} else {
// 使
nextTick(() => {
menuDialogFormRef?.value?.resetFields();
state.ruleForm.parentId = row?.id || '-1'
});
//
const getMenuData = () => {
state.parentData = []
pageList({
type: '0'
}).then(res => {
let menu = {
createBy: "",
createTime: "",
delFlag: "",
icon: "",
keepAlive: "",
menuId: "",
menuType: "",
parentId: "",
path: "",
embedded: '0',
sortOrder: 0,
updateBy: "",
updateTime: "",
visible: "1",
id: '-1', name: '根菜单', children: []
};
menu.children = res.data;
state.parentData.push(menu)
})
};
}
visible.value = true;
getMenuData();
};
const showembedded = ref(false)
watch(() => state.ruleForm.path, (val) => {
if (val.startsWith('http')) {
showembedded.value = true
} else {
showembedded.value = false
state.ruleForm.embedded = '0'
}
//
const onSubmit = () => {
//
if (state.ruleForm.menuId) {
loading.value = true
update(state.ruleForm).then(() => {
visible.value = false;
emit('refresh');
}).catch(err => {
useMessage().error(err.msg)
}).finally(() => {
loading.value = false
})
const dataRules = reactive({
menType: [{required: true, message: "菜单类型不能为空", trigger: "blur"}],
parentId: [{required: true, message: "上级菜单不能为空", trigger: "blur"}],
name: [{required: true, message: "菜单名称不能为空", trigger: "blur"}],
path: [{required: true, message: "路由路径不能为空", trigger: "blur"}
, {validator: rule.noChinese, trigger: 'blur'}],
permission: [{required: true, message: "排序不能为空", trigger: "blur"}],
visible: [{required: true, message: "显示不能为空", trigger: "blur"}],
sortOrder: [{required: true, message: "排序不能为空", trigger: "blur"}],
keepAlive: [{required: true, message: "缓冲不能为空", trigger: "blur"}]
} else {
loading.value = true
addObj(state.ruleForm).then(() => {
visible.value = false;
emit('refresh');
}).catch(err => {
useMessage().error(err.msg)
}).finally(() => {
loading.value = false
})
//
const openDialog = (type: string, row?: any) => {
if (row?.id && type === 'edit') {
state.ruleForm.id = row.id
//
loading.value = true
info(row.id).then(res => {
Object.assign(state.ruleForm, res.data)
}).finally(() => {
loading.value = false
})
} else {
// 使
nextTick(() => {
menuDialogFormRef?.value?.resetFields();
state.ruleForm.parentId = row?.id || '-1'
});
}
}
visible.value = true;
getMenuData();
};
};
//
const onSubmit = () => {
//
if (state.ruleForm.id) {
loading.value = true
update(state.ruleForm).then(() => {
visible.value = false;
emit('refresh');
}).catch(err => {
useMessage().error(err.msg)
}).finally(() => {
loading.value = false
})
} else {
loading.value = true
addObj(state.ruleForm).then(() => {
visible.value = false;
emit('refresh');
}).catch(err => {
useMessage().error(err.msg)
}).finally(() => {
loading.value = false
})
}
};
// 使
defineExpose({
openDialog,
});
// 使
defineExpose({
openDialog,
});
</script>

View File

@ -204,4 +204,9 @@ const handleDelete = (ids: string[]) => {
})
})
};
onMounted(() => {
state.dataList
})
</script>

View File

@ -153,7 +153,6 @@ const handleClick = (tab: TabsPaneContext) => {
}
}
const emit = defineEmits(['refreshDataList'])
const visible = ref(false)
const sortable = ref() as any

View File

@ -24,7 +24,6 @@
<script setup lang="ts" name="preview">
import { useGeneratorPreviewApi } from '/@/api/gen/table';
import { handleTree } from '/@/utils/other';
import {validatePhone} from "/@/api/admin/user";
const visible = ref(false)
// ======== ========

View File

@ -230,7 +230,7 @@ const initPieChart = () => {
state.global.homeChartTwo = markRaw(echarts.init(homePieRef.value, state.charts.theme));
var getname = ['房屋及结构物', '专用设备', '通用设备', '文物和陈列品', '图书、档案'];
var getvalue = [34.2, 38.87, 17.88, 9.05, 2.05];
var data = [];
var data = [] as any;
for (var i = 0; i < getname.length; i++) {
data.push({ name: getname[i], value: getvalue[i] });
}

View File

@ -0,0 +1,150 @@
<template>
<el-dialog v-model="visible" :close-on-click-modal="false"
:title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable>
<el-form ref="dataFormRef" v-loading="loading" :model="form" :rules="dataRules" label-width="90px">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item :label="t('fans.wxAccountName')" prop="wxAccountName">
<el-input v-model="form.wxAccountName" disabled/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('fans.wxAccountAppid')" prop="wxAccountAppid">
<el-input v-model="form.wxAccountAppid" disabled/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('fans.openid')" prop="openid">
<el-input v-model="form.openid" disabled/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('fans.nickname')" prop="nickname">
<el-input v-model="form.nickname" disabled/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('fans.remark')" prop="remark">
<el-input v-model="form.remark" :placeholder="t('fans.inputremarkTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('fans.tagIds')" prop="tagIds">
<el-select v-model="form.tagIds" :placeholder="t('fans.inputTagTip')" class="w100" clearable multiple>
<el-option v-for="item in tagOption" :key="item.tagId" :label="item.tag" :value="item.tagId"/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="wx-fans" setup>
import {addObj, getObj, putObj} from "/@/api/mp/wx-account-fans";
import {list} from '/@/api/mp/wx-account-tag'
import {useMessage} from "/@/hooks/message";
import {useI18n} from "vue-i18n"
const {t} = useI18n();
const emit = defineEmits(['refresh']);
//
const dataFormRef = ref();
const visible = ref(false)
const loading = ref(false)
const wxAccountAppid = ref()
//
const form = reactive({
id: ''
});
const dataRules = ref([])
//
const openDialog = (row: any, accountId: string) => {
visible.value = true
form.id = row.id
wxAccountAppid.value = accountId
//
if (dataFormRef.value) {
dataFormRef.value.resetFields()
}
if (form.id) {
getFansData()
}
getTagList()
};
const getFansData = () => {
loading.value = true
getObj(form.id).then(res => {
Object.assign(form, res.data)
}).finally(() => {
loading.value = false
})
}
//
const onSubmit = () => {
dataFormRef.value.validate((valid: boolean) => {
if (!valid) {
return false
}
loading.value = true
if (form.id) {
putObj(form).then(() => {
useMessage().success(t('common.editSuccessText'))
visible.value = false //
emit('refresh')
}).catch((err: any) => {
useMessage().error(err.msg)
}).finally(() => {
loading.value = false
})
} else {
addObj(form).then(() => {
useMessage().success(t('common.addSuccessText'))
visible.value = false //
emit('refresh')
}).catch((err: any) => {
useMessage().error(err.msg)
}).finally(() => {
loading.value = false
})
}
})
}
const tagOption = ref([])
const getTagList = () => {
list(wxAccountAppid.value).then(res => {
tagOption.value = res.data
})
}
//
defineExpose({
openDialog
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,22 @@
export default {
fans: {
index: 'index',
importwxAccountFansTip: 'import WxAccountFans',
id: 'id',
openid: 'openid',
subscribeStatus: 'subscribeStatus',
subscribeTime: 'subscribeTime',
nickname: 'nickname',
gender: 'gender',
language: 'language',
country: 'country',
province: 'province',
city: 'city',
tagIds: 'tagIds',
headimgUrl: 'headimgUrl',
remark: 'remark',
wxAccountId: 'wxAccountId',
wxAccountName: 'wxAccountName',
wxAccountAppid: 'wxAccountAppid'
}
}

View File

@ -0,0 +1,24 @@
export default {
fans: {
index: '序号',
importwxAccountFansTip: '导入微信公众号粉丝表',
id: '主键',
openid: '用户标识',
subscribeStatus: '订阅状态',
subscribeTime: '订阅时间',
nickname: '昵称',
gender: '性别',
language: '语言',
country: '国家',
province: '省份',
city: '城市',
tagIds: '分组',
headimgUrl: ' headimgUrl',
remark: '备注',
wxAccountId: '微信公众号ID',
wxAccountName: '微信公众号',
wxAccountAppid: '公众号appid',
inputremarkTip: '请输入备注',
inputTagTip: '请选择分组'
}
}

View File

@ -0,0 +1,181 @@
<template>
<div class="layout-padding">
<el-card class="layout-padding-auto">
<el-row v-show="showSearch" class="mb8">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-form-item :label="$t('fans.nickname')" prop="nickname">
<el-input v-model="state.queryForm.nickname"
style="max-width: 180px"/>
</el-form-item>
<el-form-item :label="$t('fans.wxAccountName')" prop="wxAccountAppid">
<el-select v-model="state.queryForm.wxAccountAppid" :placeholder="$t('fans.wxAccountName')" clearable class="w100">
<el-option v-for="item in accountList" :key="item.appid" :label="item.name" :value="item.appid"/>
</el-select>
</el-form-item>
<el-form-item class="ml2">
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button formDialogRef icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
<el-button formDialogRef icon="Sort" @click="asyncFans" v-auth="'mp_wxaccountfans_sync'">同步</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'mp_fans_export'" class="ml10" formDialogRef icon="Download" type="primary"
@click="exportExcel">
{{ $t('common.exportBtn') }}
</el-button>
<right-toolbar v-model:showSearch="showSearch" class="ml10" style="float: right;margin-right: 20px"
@queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table v-loading="state.loading" :data="state.dataList" style="width: 100%"
@selection-change="handleSelectionChange" @sort-change="sortChangeHandle">
<el-table-column align="center" type="selection" width="60"/>
<el-table-column :label="t('fans.index')" type="index" width="80"/>
<el-table-column :label="t('fans.id')" prop="id" show-overflow-tooltip/>
<el-table-column :label="t('fans.openid')" prop="openid" show-overflow-tooltip/>
<el-table-column :label="t('fans.subscribeStatus')" prop="subscribeStatus" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="subscribe" :value="scope.row.subscribeStatus"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="t('fans.subscribeTime')" prop="subscribeTime" show-overflow-tooltip/>
<el-table-column :label="t('fans.nickname')" prop="nickname" show-overflow-tooltip/>
<el-table-column :label="t('fans.language')" prop="language" show-overflow-tooltip/>
<el-table-column :label="t('fans.tagIds')" prop="tagIds" show-overflow-tooltip width="200">
<template #default="scope">
<span v-for="(tag, index) in scope.row.tagList" :key="index">
<el-tag>{{ tag.tag }} </el-tag>&nbsp;&nbsp;
</span>
</template>
</el-table-column>
<el-table-column :label="t('fans.remark')" prop="remark" show-overflow-tooltip/>
<el-table-column :label="t('fans.wxAccountName')" prop="wxAccountName" show-overflow-tooltip/>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button text type="primary"
@click="formDialogRef.openDialog(scope.row,state.queryForm.wxAccountAppid)">{{ $t('common.editBtn') }}
</el-button>
<el-button text type="primary" @click="handleDelete([scope.row.id])">{{
$t('common.delBtn')
}}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
<form-dialog ref="formDialogRef" @refresh="getDataList"></form-dialog>
</el-card>
</div>
</template>
<script lang="ts" name="systemWxAccountFans" setup>
import {BasicTableProps, useTable} from "/@/hooks/table";
import {delObjs, fetchList, sync} from "/@/api/mp/wx-account-fans";
import { fetchAccountList } from '/@/api/mp/wx-account'
import {useMessage, useMessageBox} from "/@/hooks/message";
import {useI18n} from "vue-i18n";
import {useDict} from "/@/hooks/dict";
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
const { subscribe } = useDict('subscribe')
//
const {t} = useI18n()
//
//
const formDialogRef = ref()
//
const queryRef = ref()
const showSearch = ref(true)
//
const selectObjs = ref([]) as any
const multiple = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
createdIsNeed: false
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile
} = useTable(state)
const accountList = ref([])
const getAccountList = () => {
fetchAccountList().then(res => {
accountList.value = res.data
if(accountList.value.length > 0){
state.queryForm.wxAccountAppid = accountList.value[0].appid
getDataList()
}
})
}
watch(() => state.queryForm.wxAccountAppid,() => {
getDataList()
})
const asyncFans = () => {
if(state.queryForm.wxAccountAppid){
sync(state.queryForm.wxAccountAppid).then(() => {
useMessage().success("已开始从微信同步粉丝信息,建议等待后查询")
getDataList()
})
}else{
useMessage().error("请选择公众号")
}
}
onMounted(() => {
getAccountList()
})
//
const resetQuery = () => {
//
queryRef.value.resetFields()
//
selectObjs.value = []
getDataList()
}
// excel
const exportExcel = () => {
downBlobFile('/mp/fans/export', state.queryForm, 'fans.xlsx')
}
//
const handleSelectionChange = (objs: any) => {
objs.forEach((val: any) => {
selectObjs.value.push(val.id)
});
multiple.value = !objs.length
}
//
const handleDelete = (ids: string[]) => {
useMessageBox().confirm(t('common.delConfirmText'))
.then(() => {
delObjs(ids).then(() => {
getDataList(false);
useMessage().success(t('common.delSuccessText'));
}).catch((err: any) => {
useMessage().error(err.msg)
})
})
};
</script>

View File

@ -0,0 +1,80 @@
<template>
<el-dialog v-model="visible" :close-on-click-modal="false"
:title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable>
<el-form ref="dataFormRef" v-loading="loading" :model="form" :rules="dataRules" formDialogRef label-width="90px">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item :label="t('wxAccountTag.tag')" prop="tag">
<el-input v-model="form.tag" :placeholder="t('wxAccountTag.inputTagTip')"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="WxAccountTagDialog" setup>
// /
const emit = defineEmits(['refresh']);
import {useMessage} from "/@/hooks/message";
import {addObj} from '/@/api/mp/wx-account-tag'
import {useI18n} from "vue-i18n"
const {t} = useI18n();
//
const dataFormRef = ref();
const visible = ref(false)
const loading = ref(false)
//
//
const form = reactive({
wxAccountAppid: '',
tag: '',
});
//
const dataRules = ref({})
//
const openDialog = (id: string) => {
visible.value = true
form.wxAccountAppid = id
//
if (dataFormRef.value) {
dataFormRef.value.resetFields()
}
};
//
const onSubmit = () => {
dataFormRef.value.validate((valid: boolean) => {
if (!valid) {
return false
}
loading.value = true
addObj(form).then(() => {
useMessage().success(t('common.addSuccessText'))
visible.value = false //
emit('refresh')
}).catch((err: any) => {
useMessage().error(err.msg)
}).finally(() => {
loading.value = false
})
})
}
//
defineExpose({
openDialog
});
</script>

View File

@ -0,0 +1,18 @@
export default {
wxAccountTag: {
index: 'index',
importwxAccountTagTip: 'import WxAccountTag',
id: 'id',
tag: 'tag',
wxAccountId: 'wxAccountId',
wxAccountName: 'wxAccountName',
wxAccountAppid: 'wxAccountAppid',
tagId: 'tagId',
inputIdTip: 'input id',
inputTagTip: 'input tag',
inputWxAccountIdTip: 'input wxAccountId',
inputWxAccountNameTip: 'input wxAccountName',
inputWxAccountAppidTip: 'input wxAccountAppid',
inputTagIdTip: 'input tagId',
}
}

View File

@ -0,0 +1,18 @@
export default {
wxAccountTag: {
index: '序号',
importwxAccountTagTip: '导入标签管理',
id: '主键',
tag: '标签',
wxAccountId: '微信账号ID',
wxAccountName: '微信账号名称',
wxAccountAppid: 'appID',
tagId: '标签ID',
inputIdTip: '请输入主键',
inputTagTip: '请输入标签',
inputWxAccountIdTip: '请输入微信账号ID',
inputWxAccountNameTip: '请输入微信账号名称',
inputWxAccountAppidTip: '请输入appID',
inputTagIdTip: '请输入标签ID',
}
}

View File

@ -0,0 +1,155 @@
<template>
<div class="layout-padding">
<el-card class="layout-padding-auto">
<el-row v-show="showSearch" class="mb8">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-form-item :label="$t('wxAccountTag.tag')" prop="tag">
<el-input v-model="state.queryForm.tag" :placeholder="t('wxAccountTag.inputTagTip')"
style="max-width: 180px"/>
</el-form-item>
<el-form-item :label="$t('wxAccountTag.wxAccountAppid')" prop="wxAccountAppid">
<el-select v-model="state.queryForm.wxAccountAppid" :placeholder="t('wxAccountTag.inputWxAccountAppidTip')" clearable class="w100">
<el-option v-for="item in accountList" :key="item.appid" :label="item.name" :value="item.appid"/>
</el-select>
</el-form-item>
<el-form-item class="ml2">
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button formDialogRef icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'mp_wx_account_tag_add'" class="ml10" formDialogRef icon="folder-add" type="primary"
@click="formDialogRef.openDialog(state.queryForm.wxAccountAppid)">
{{ $t('common.addBtn') }}
</el-button>
<el-button v-auth="'mp_wx_account_tag_export'" class="ml10" formDialogRef icon="Download" type="primary"
@click="exportExcel">
{{ $t('common.exportBtn') }}
</el-button>
<el-button v-auth="'mp_wx_account_tag_del'" :disabled="multiple" class="ml10" formDialogRef icon="Delete"
type="primary" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar v-model:showSearch="showSearch" class="ml10" style="float: right;margin-right: 20px"
@queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table v-loading="state.loading" :data="state.dataList" style="width: 100%"
@selection-change="handleSelectionChange" @sort-change="sortChangeHandle">
<el-table-column align="center" type="selection" width="60"/>
<el-table-column :label="t('wxAccountTag.index')" type="index" width="80"/>
<el-table-column :label="t('wxAccountTag.tag')" prop="tag" show-overflow-tooltip/>
<el-table-column :label="t('wxAccountTag.wxAccountId')" prop="wxAccountId" show-overflow-tooltip/>
<el-table-column :label="t('wxAccountTag.wxAccountName')" prop="wxAccountName" show-overflow-tooltip/>
<el-table-column :label="t('wxAccountTag.wxAccountAppid')" prop="wxAccountAppid" show-overflow-tooltip/>
<el-table-column :label="t('wxAccountTag.tagId')" prop="tagId" show-overflow-tooltip/>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button text type="primary" @click="formDialogRef.openDialog(scope.row.id)">{{ $t('common.editBtn') }}</el-button>
<el-button text type="primary" @click="handleDelete([scope.row.id])">{{$t('common.delBtn') }}</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
</el-card>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)"/>
</div>
</template>
<script lang="ts" name="systemWxAccountTag" setup>
import {BasicTableProps, useTable} from "/@/hooks/table";
import {delObjs, getPage} from "/@/api/mp/wx-account-tag";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {useI18n} from "vue-i18n";
import {fetchAccountList} from "/@/api/mp/wx-account";
//
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const {t} = useI18n()
//
//
const formDialogRef = ref()
//
const queryRef = ref()
const showSearch = ref(true)
//
const selectObjs = ref([]) as any
const multiple = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: getPage,
createdIsNeed: false
})
const accountList = ref([])
const getAccountList = () => {
fetchAccountList().then(res => {
accountList.value = res.data
if(accountList.value.length > 0){
state.queryForm.wxAccountAppid = accountList.value[0].appid
getDataList()
}
})
}
onMounted(() => {
getAccountList()
})
watch(() => state.queryForm.wxAccountAppid,() => {
getDataList()
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile
} = useTable(state)
//
const resetQuery = () => {
//
queryRef.value.resetFields()
//
selectObjs.value = []
getDataList()
}
// excel
const exportExcel = () => {
downBlobFile('/mp/wxAccountTag/export', state.queryForm, 'wxAccountTag.xlsx')
}
//
const handleSelectionChange = (objs: any) => {
objs.forEach((val: any) => {
selectObjs.value.push(val.id)
});
multiple.value = !objs.length
}
//
const handleDelete = (ids: string[]) => {
useMessageBox().confirm(t('common.delConfirmText'))
.then(() => {
delObjs(ids).then(() => {
getDataList(false);
useMessage().success(t('common.delSuccessText'));
}).catch((err: any) => {
useMessage().error(err.msg)
})
})
};
</script>

View File

@ -0,0 +1,161 @@
<template>
<el-drawer v-model="visible" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" size="50%">
<el-form ref="dataFormRef" v-loading="loading" :model="form" :rules="dataRules" formDialogRef label-width="90px">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item :label="t('account.name')" prop="name">
<el-input v-model="form.name" :placeholder="t('account.inputNameTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('account.account')" prop="account">
<el-input v-model="form.account" :placeholder="t('account.inputAccountTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('account.appid')" prop="appid">
<el-input v-model="form.appid" :placeholder="t('account.inputAppidTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('account.appsecret')" prop="appsecret">
<el-input v-model="form.appsecret" :placeholder="t('account.inputAppsecretTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('account.url')" prop="url">
<el-input v-model="form.url" :placeholder="t('account.inputUrlTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('account.token')" prop="token">
<el-input v-model="form.token" :placeholder="t('account.inputTokenTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('account.aeskey')" prop="aeskey">
<el-input v-model="form.aeskey" :placeholder="t('account.inputAeskeyTip')"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" name="WxAccountDialog" setup>
// /
import {useMessage} from "/@/hooks/message";
import {addObj, getObj, putObj} from '/@/api/mp/wx-account'
import {useI18n} from "vue-i18n"
const emit = defineEmits(['refresh']);
const {t} = useI18n();
//
const dataFormRef = ref();
const visible = ref(false)
const loading = ref(false)
//
//
const form = reactive({
id: '',
name: '',
account: '',
appid: '',
appsecret: '',
url: '',
token: '',
aeskey: ''
});
//
const dataRules = ref({
name: [{required: true, message: '名称不能为空', trigger: 'blur'}],
account: [{required: true, message: '微信号不能为空', trigger: 'blur'}],
appid: [{required: true, message: 'appid不能为空', trigger: 'blur'}],
appsecret: [{required: true, message: '密钥不能为空', trigger: 'blur'}],
url: [{required: true, message: 'url不能为空', trigger: 'blur'}],
token: [{required: true, message: 'token不能为空', trigger: 'blur'}],
aeskey: [{required: true, message: '加密密钥不能为空', trigger: 'blur'}]
})
//
const openDialog = (id: string) => {
visible.value = true
form.id = ''
//
if (dataFormRef.value) {
dataFormRef.value.resetFields()
}
// wxAccount
if (id) {
form.id = id
getwxAccountData(id)
}
};
//
const onSubmit = () => {
dataFormRef.value.validate((valid: boolean) => {
if (!valid) {
return false
}
//
if (form.id) {
loading.value = true
putObj(form).then(() => {
useMessage().success(t('common.editSuccessText'))
visible.value = false //
emit('refresh')
}).catch((err: any) => {
useMessage().error(err.msg)
}).finally(() => {
loading.value = false
})
} else {
loading.value = true
addObj(form).then(() => {
useMessage().success(t('common.addSuccessText'))
visible.value = false //
emit('refresh')
}).catch((err: any) => {
useMessage().error(err.msg)
}).finally(() => {
loading.value = false
})
}
})
}
//
const getwxAccountData = (id: string) => {
//
loading.value = true
getObj(id).then((res: any) => {
Object.assign(form, res.data)
}).finally(() => {
loading.value = false
})
};
//
defineExpose({
openDialog
});
</script>

View File

@ -0,0 +1,24 @@
export default {
account: {
index: 'index',
importwxAccountTip: 'import WxAccount',
id: 'id',
name: 'name',
account: 'account',
appid: 'appid',
appsecret: 'appsecret',
url: 'url',
token: 'token',
aeskey: 'aeskey',
qrUrl: 'qrUrl',
inputIdTip: 'input id',
inputNameTip: 'input name',
inputAccountTip: 'input account',
inputAppidTip: 'input appid',
inputAppsecretTip: 'input appsecret',
inputUrlTip: 'input url',
inputTokenTip: 'input token',
inputAeskeyTip: 'input aeskey',
inputQrUrlTip: 'input qrUrl',
}
}

View File

@ -0,0 +1,24 @@
export default {
account: {
index: '序号',
importwxAccountTip: '导入公众号账户表',
id: '主键',
name: '名称',
account: '微信号',
appid: 'appid',
appsecret: '密钥',
url: ' url',
token: 'token',
aeskey: '加密密钥',
qrUrl: '图片',
inputIdTip: '请输入主键',
inputNameTip: '请输入名称',
inputAccountTip: '请输入微信号',
inputAppidTip: '请输入appid',
inputAppsecretTip: '请输入密钥',
inputUrlTip: '请输入 url',
inputTokenTip: '请输入token',
inputAeskeyTip: '请输入加密密钥',
inputQrUrlTip: '请输入图片',
}
}

View File

@ -0,0 +1,172 @@
<template>
<div class="layout-padding">
<el-card class="layout-padding-auto">
<el-row v-show="showSearch" class="mb8">
<el-form ref="queryRef" :inline="true" :model="state.queryForm">
<el-form-item :label="$t('account.name')" prop="name">
<el-input v-model="state.queryForm.name" :placeholder="t('account.inputNameTip')"
style="max-width: 180px"/>
</el-form-item>
<el-form-item :label="$t('account.account')" prop="account">
<el-input v-model="state.queryForm.account" :placeholder="t('account.inputAccountTip')"
style="max-width: 180px"/>
</el-form-item>
<el-form-item class="ml2">
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button formDialogRef icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'mp_wxaccount_add'" class="ml10" formDialogRef icon="folder-add" type="primary"
@click="formDialogRef.openDialog()">
{{ $t('common.addBtn') }}
</el-button>
<el-button v-auth="'mp_wxaccount_export'" class="ml10" formDialogRef icon="Download" type="primary"
@click="exportExcel">
{{ $t('common.exportBtn') }}
</el-button>
<el-button v-auth="'mp_wxaccount_del'" :disabled="multiple" class="ml10" formDialogRef icon="Delete"
type="primary" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar v-model:showSearch="showSearch" class="ml10" style="float: right;margin-right: 20px"
@queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table v-loading="state.loading" :data="state.dataList" style="width: 100%"
@selection-change="handleSelectionChange" @sort-change="sortChangeHandle">
<el-table-column align="center" type="selection" width="60"/>
<el-table-column :label="t('account.index')" type="index" width="80"/>
<el-table-column :label="t('account.name')" prop="name" show-overflow-tooltip/>
<el-table-column :label="t('account.account')" prop="account" show-overflow-tooltip/>
<el-table-column :label="t('account.appid')" prop="appid" show-overflow-tooltip/>
<el-table-column :label="t('account.appsecret')" prop="appsecret" show-overflow-tooltip/>
<el-table-column :label="t('account.url')" prop="url" show-overflow-tooltip/>
<el-table-column :label="t('account.token')" prop="token" show-overflow-tooltip/>
<el-table-column :label="t('account.aeskey')" prop="aeskey" show-overflow-tooltip/>
<el-table-column :label="t('account.qrUrl')" prop="qrUrl" show-overflow-tooltip>
<template #default="scope">
<a target="_blank" :href="scope.row.qrUrl"><img :src="scope.row.qrUrl" style="width: 100px"></a>
</template>
</el-table-column>
<el-table-column :label="$t('common.action')" width="200">
<template #default="scope">
<el-button v-auth="'mp_wxaccount_edit'" text type="primary" @click="formDialogRef.openDialog(scope.row.id)">{{ $t('common.editBtn') }}</el-button>
<el-button v-auth="'mp_wxaccount_del'" text type="primary" @click="handleDelete([scope.row.id])">{{$t('common.delBtn') }}</el-button>
<el-button text type="primary" @click="access(scope.row,scope.index)">接入</el-button>
<el-button text type="primary" @click="generate(scope.row,scope.index)">二维码</el-button>
<el-button text type="primary" @click="quota(scope.row,scope.index)">quota</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
</el-card>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)"/>
<el-dialog v-model="dialogFormVisible" title="接入">
<el-input v-model="wxurl" readonly>
<template #append>
<el-button @click="copyText(wxurl)">复制链接</el-button>
</template>
</el-input>
</el-dialog>
</div>
</template>
<script lang="ts" name="systemWxAccount" setup>
import {BasicTableProps, useTable} from "/@/hooks/table";
import {clearQuota, delObjs, fetchList, generateQr} from "/@/api/mp/wx-account";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {useI18n} from "vue-i18n";
import commonFunction from '/@/utils/commonFunction';
const { copyText } = commonFunction();
//
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const {t} = useI18n()
//
//
const formDialogRef = ref()
//
const queryRef = ref()
const showSearch = ref(true)
//
const selectObjs = ref([]) as any
const multiple = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile
} = useTable(state)
//
const resetQuery = () => {
//
queryRef.value.resetFields()
//
selectObjs.value = []
getDataList()
}
// excel
const exportExcel = () => {
downBlobFile('/pigx/account/export', state.queryForm, 'account.xlsx')
}
//
const handleSelectionChange = (objs: any) => {
objs.forEach((val: any) => {
selectObjs.value.push(val.id)
});
multiple.value = !objs.length
}
//
const handleDelete = (ids: string[]) => {
useMessageBox().confirm(t('common.delConfirmText'))
.then(() => {
delObjs(ids).then(() => {
getDataList(false);
useMessage().success(t('common.delSuccessText'));
}).catch((err: any) => {
useMessage().error(err.msg)
})
})
};
const dialogFormVisible = ref(false)
const wxurl = ref("")
const access = (row: any) => {
dialogFormVisible.value = true
wxurl.value = row.url + '/mp/' + row.appid + '/portal'
}
const generate = (row: any) => {
generateQr(row.appid).then(() => {
useMessage().success("获取成功")
getDataList()
})
}
const quota = (row) => {
clearQuota(row.appid).then(() => {
useMessage().success("清空api的调用quota成功")
})
}
</script>

View File

@ -0,0 +1,347 @@
<template>
<div class="layout-padding">
<el-row :gutter="20">
<el-col :span="4" :xs="24">
<el-card class="layout-padding-auto" shadow="hover">
<query-tree :query="deptData.queryList"
@node-click="handleNodeClick"/>
</el-card>
</el-col>
<el-col :md="20">
<el-card class="layout-padding-auto" shadow="hover">
<el-tabs v-model="type" @tab-click="handleClick">
<el-tab-pane name="1" label="1">
<template #label>关注时回复</template>
<el-row>
<div class="mb8" style="width: 100%">
<el-button class="ml10" icon="folder-add" type="primary" @click="handleAdd">
{{ $t('common.addBtn') }}
</el-button>
</div>
</el-row>
<el-table v-loading="state.loading" :data="state.dataList" style="width: 100%" @sort-change="sortChangeHandle">
<el-table-column label="序号" type="index" width="80"/>
<el-table-column label="回复消息类型" prop="repType" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicDataRepType" :value="scope.row.repType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" prop="action" show-overflow-tooltip>
<template #default="scope">
<el-button
link
icon="el-icon-edit"
@click="handleEdit(scope.row)">编辑
</el-button>
<el-button
link
icon="el-icon-delete"
@click="handleDel(scope.row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
</el-tab-pane>
<el-tab-pane name="2" label="2">
<template #label>消息回复</template>
<el-row>
<div class="mb8" style="width: 100%">
<el-button class="ml10" icon="folder-add" type="primary" @click="handleAdd">
{{ $t('common.addBtn') }}
</el-button>
</div>
</el-row>
<el-table v-loading="state.loading" :data="state.dataList" style="width: 100%" @sort-change="sortChangeHandle">
<el-table-column label="序号" type="index" width="80"/>
<el-table-column label="请求消息类型" prop="reqType" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicDataReqType" :value="scope.row.reqType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="回复消息类型" prop="repType" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicDataRepType" :value="scope.row.repType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" prop="action" show-overflow-tooltip>
<template #default="scope">
<el-button
icon="el-icon-edit"
link
@click="handleEdit(scope.row)">编辑
</el-button>
<el-button
icon="el-icon-delete"
link
@click="handleDel(scope.row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
</el-tab-pane>
<el-tab-pane name="3" label="3">
<template #label>关键词回复</template>
<el-row>
<div class="mb8" style="width: 100%">
<el-button class="ml10" icon="folder-add" type="primary" @click="handleAdd">
{{ $t('common.addBtn') }}
</el-button>
</div>
</el-row>
<el-table v-loading="state.loading" :data="state.dataList" style="width: 100%" @sort-change="sortChangeHandle">
<el-table-column label="序号" type="index" width="80"/>
<el-table-column label="关键词" prop="reqKey" show-overflow-tooltip>
</el-table-column>
<el-table-column label="匹配类型" prop="repMate" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicRepMate" :value="scope.row.repMate"></dict-tag>
</template>
</el-table-column>
<el-table-column label="匹配类型" prop="repMate" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicDataRepType" :value="scope.row.repType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" prop="action" show-overflow-tooltip>
<template #default="scope">
<el-button
icon="el-icon-edit"
link
@click="handleEdit(scope.row)">编辑
</el-button>
<el-button
icon="el-icon-delete"
link
@click="handleDel(scope.row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
<el-dialog :title="handleType === 'add' ? '新增回复消息' : '修改回复消息'" v-model="dialog1Visible" width="50%">
<el-form label-width="100px">
<el-form-item v-if="type === '2'" label="请求消息类型">
<el-select v-model="objData.reqType" placeholder="请选择">
<template v-for="item in dicDataReqType">
<el-option
v-if="item.value !== 'event'"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled">
</el-option>
</template>
</el-select>
</el-form-item>
<el-form-item v-if="type === '3'" label="匹配类型">
<el-select v-model="objData.repMate" placeholder="请选择" style="width: 100px">
<el-option
v-for="item in dicRepMate"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item v-if="type === '3'" label="关键词">
<el-input v-model="objData.reqKey" placeholder="请输入内容" clearable></el-input>
</el-form-item>
<el-form-item label="回复消息">
<WxReply v-if="hackResetWxReplySelect" :obj-data="objData"></WxReply>
</el-form-item>
</el-form>
<template #footer >
<el-button @click="dialog1Visible = false"> </el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="wx-auto-reply">
import {fetchAccountList} from "/@/api/mp/wx-account";
import {BasicTableProps, useTable} from "/@/hooks/table";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {getPage,delObj,addObj,putObj} from '/@/api/mp/wx-auto-reply'
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'))
const WxReply = defineAsyncComponent(() => import("/@/components/wechart/wx-reply/index.vue"))
//
const handleNodeClick = (node: any) => {
accountId.value = node.appid
state.queryForm.appId = accountId.value
getDataList()
}
const dicDataRepType = ref([{
label: '文本',
value: 'text'
}, {
label: '图片',
value: 'image'
}, {
label: '语音',
value: 'voice'
}, {
label: '视频',
value: 'video'
}, {
label: '图文',
value: 'news'
}])
const dicDataReqType = ref(
[{
value: 'text',
label: '文本'
}, {
value: 'image',
label: '图片'
}, {
value: 'voice',
label: '语音'
}, {
value: 'video',
label: '视频'
}, {
value: 'shortvideo',
label: '小视频'
}, {
value: 'location',
label: '地理位置'
}, {
value: 'link',
label: '链接消息'
}, {
value: 'event',
label: '事件推送'
}]
)
const dicRepMate = ref([
{
value: '1',
label: '全匹配'
}, {
value: '2',
label: '半匹配'
}
])
const deptData = reactive({
queryList: () => {
return fetchAccountList()
}
})
const accountId = ref()
const type = ref("1")
const handleClick = (e: any) => {
type.value = e.paneName
state.queryForm.type = type.value
state.queryForm.appId = accountId.value
getDataList()
}
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
type: "",
appId: ""
},
pageList: getPage,
createdIsNeed: false
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
} = useTable(state)
const dialog1Visible = ref(false)
const handleType = ref('add')
const hackResetWxReplySelect = ref(true)
const objData = ref() as any
const handleEdit = (row: any) => {
hackResetWxReplySelect.value = false
nextTick(() => {
hackResetWxReplySelect.value = true
})
handleType.value = 'edit'
dialog1Visible.value = true
if (row.content && typeof row.content === 'string') {
row.content = JSON.parse(row.content)
}
objData.value = Object.assign({}, row)
}
const handleDel = (row) => {
useMessageBox().confirm("是否确认删除此数据?").then(() => {
delObj(row.id).then(() =>{
useMessage().success("删除成功")
getDataList()
})
})
}
const handleSubmit = () => {
if (objData.repType === 'news') {
objData.content = JSON.stringify(objData.content)
}
if (handleType.value === 'add') {
addObj(Object.assign({
type: type.value,
appId: accountId.value
}, objData.value)).then(() => {
useMessage().success("添加成功")
getDataList()
dialog1Visible.value = false
})
}
if (handleType.value === 'edit') {
putObj(objData.value).then(() => {
useMessage().success("修改成功")
getDataList()
dialog1Visible.value = false
})
}
}
const handleAdd = () => {
hackResetWxReplySelect.value = false//
nextTick(() => {
hackResetWxReplySelect.value = true//
})
handleType.value = 'add'
dialog1Visible.value = true
objData.value = {
repType: 'text',
appId: accountId.value
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,64 @@
export default {
wxFansMsg: {
index: 'index',
importwxMsgTip: 'import WxMsg',
id: 'id',
appName: 'appName',
appLogo: 'appLogo',
wxUserId: 'wxUserId',
nickName: 'nickName',
headimgUrl: 'headimgUrl',
type: 'type',
repType: 'repType',
repEvent: 'repEvent',
repContent: 'repContent',
repMediaId: 'repMediaId',
repName: 'repName',
repDesc: 'repDesc',
repUrl: 'repUrl',
repHqUrl: 'repHqUrl',
content: 'content',
repThumbMediaId: 'repThumbMediaId',
repThumbUrl: 'repThumbUrl',
repLocationX: 'repLocationX',
repLocationY: 'repLocationY',
repScale: 'repScale',
readFlag: 'readFlag',
appId: 'appId',
openId: 'openId',
remark: 'remark',
delFlag: 'delFlag',
createTime: 'createTime',
updateTime: 'updateTime',
tenantId: 'tenantId',
inputIdTip: 'input id',
inputAppNameTip: 'input appName',
inputAppLogoTip: 'input appLogo',
inputWxUserIdTip: 'input wxUserId',
inputNickNameTip: 'input nickName',
inputHeadimgUrlTip: 'input headimgUrl',
inputTypeTip: 'input type',
inputRepTypeTip: 'input repType',
inputRepEventTip: 'input repEvent',
inputRepContentTip: 'input repContent',
inputRepMediaIdTip: 'input repMediaId',
inputRepNameTip: 'input repName',
inputRepDescTip: 'input repDesc',
inputRepUrlTip: 'input repUrl',
inputRepHqUrlTip: 'input repHqUrl',
inputContentTip: 'input content',
inputRepThumbMediaIdTip: 'input repThumbMediaId',
inputRepThumbUrlTip: 'input repThumbUrl',
inputRepLocationXTip: 'input repLocationX',
inputRepLocationYTip: 'input repLocationY',
inputRepScaleTip: 'input repScale',
inputReadFlagTip: 'input readFlag',
inputAppIdTip: 'input appId',
inputOpenIdTip: 'input openId',
inputRemarkTip: 'input remark',
inputDelFlagTip: 'input delFlag',
inputCreateTimeTip: 'input createTime',
inputUpdateTimeTip: 'input updateTime',
inputTenantIdTip: 'input tenantId',
}
}

View File

@ -0,0 +1,64 @@
export default {
wxFansMsg: {
index: '序号',
importwxMsgTip: '导入微信消息',
id: '主键',
appName: '公众号名称',
appLogo: '公众号logo',
wxUserId: '微信用户ID',
nickName: '微信用户昵称',
headimgUrl: '微信用户头像',
type: '消息分类',
repType: '消息类型',
repEvent: '事件类型',
repContent: '内容',
repMediaId: '回复类型',
repName: '回复的素材名、视频和音乐的标题',
repDesc: '视频和音乐的描述',
repUrl: '链接',
repHqUrl: '高质量链接',
content: '图文消息的内容',
repThumbMediaId: '缩略图的媒体id',
repThumbUrl: '缩略图url',
repLocationX: '地理位置维度',
repLocationY: '地理位置经度',
repScale: '地图缩放大小',
readFlag: '已读标记',
appId: '公众号ID',
openId: '微信唯一标识',
remark: '备注',
delFlag: '逻辑删除标记0显示1隐藏',
createTime: '创建时间',
updateTime: '更新时间',
tenantId: '租户ID',
inputIdTip: '请输入主键',
inputAppNameTip: '请输入公众号名称',
inputAppLogoTip: '请输入公众号logo',
inputWxUserIdTip: '请输入微信用户ID',
inputNickNameTip: '请输入微信用户昵称',
inputHeadimgUrlTip: '请输入微信用户头像',
inputTypeTip: '请输入消息分类',
inputRepTypeTip: '请输入消息类型',
inputRepEventTip: '请输入事件类型',
inputRepContentTip: '请输入回复类型文本保存文字、地理位置信息',
inputRepMediaIdTip: '请输入回复类型',
inputRepNameTip: '请输入回复的素材名、视频和音乐的标题',
inputRepDescTip: '请输入视频和音乐的描述',
inputRepUrlTip: '请输入链接',
inputRepHqUrlTip: '请输入高质量链接',
inputContentTip: '请输入图文消息的内容',
inputRepThumbMediaIdTip: '请输入缩略图的媒体id',
inputRepThumbUrlTip: '请输入缩略图url',
inputRepLocationXTip: '请输入地理位置维度',
inputRepLocationYTip: '请输入地理位置经度',
inputRepScaleTip: '请输入地图缩放大小',
inputReadFlagTip: '请输入已读标记10',
inputAppIdTip: '请输入公众号ID',
inputOpenIdTip: '请输入微信唯一标识',
inputRemarkTip: '请输入备注',
inputDelFlagTip: '请输入逻辑删除标记0显示1隐藏',
inputCreateTimeTip: '请输入创建时间',
inputUpdateTimeTip: '请输入更新时间',
inputTenantIdTip: '请输入租户ID',
}
}

View File

@ -0,0 +1,193 @@
<template>
<div class="layout-padding">
<el-card class="layout-padding-auto">
<el-row v-show="showSearch" class="mb8">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-form-item :label="$t('wxFansMsg.appName')" prop="wxAccountAppid">
<el-select v-model="state.queryForm.wxAccountAppid" :placeholder="$t('fans.appName')" clearable class="w100">
<el-option v-for="item in accountList" :key="item.appid" :label="item.name" :value="item.appid"/>
</el-select>
</el-form-item>
<el-form-item :label="$t('wxFansMsg.nickName')" prop="nickName">
<el-input v-model="state.queryForm.nickName" :placeholder="t('wxFansMsg.inputNickNameTip')"
style="max-width: 180px"/>
</el-form-item>
<el-form-item :label="$t('wxFansMsg.repType')" prop="repType">
<el-select v-model="state.queryForm.repType" :placeholder="$t('wxFansMsg.repType')" clearable class="w100">
<el-option v-for="item in repType" :key="item.value" :label="item.label" :value="item.value"/>
</el-select>
</el-form-item>
<el-form-item class="ml2">
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button formDialogRef icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'mp_wxFansMsg_export'" class="ml10" formDialogRef icon="Download" type="primary"
@click="exportExcel">
{{ $t('common.exportBtn') }}
</el-button>
<el-button v-auth="'mp_wxmsg_del'" :disabled="multiple" class="ml10" formDialogRef icon="Delete"
type="primary" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar v-model:showSearch="showSearch" class="ml10" style="float: right;margin-right: 20px"
@queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table v-loading="state.loading" :data="state.dataList" style="width: 100%"
@selection-change="handleSelectionChange" @sort-change="sortChangeHandle">
<el-table-column align="center" type="selection" width="60"/>
<el-table-column :label="t('wxFansMsg.index')" type="index" width="80"/>
<el-table-column :label="t('wxFansMsg.appName')" prop="appName" show-overflow-tooltip/>
<el-table-column :label="t('wxFansMsg.repType')" prop="repType" show-overflow-tooltip/>
<el-table-column :label="t('wxFansMsg.openId')" prop="openId" show-overflow-tooltip/>
<el-table-column :label="t('wxFansMsg.repContent')" prop="repContent" show-overflow-tooltip>
<template #default="scope">
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'subscribe'"><el-tag type="success" size="mini">关注</el-tag></div>
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'unsubscribe'"><el-tag type="danger" size="mini">取消关注</el-tag></div>
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'CLICK'"><el-tag size="mini">点击菜单</el-tag>{{ scope.row.repName }}</div>
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'VIEW'"><el-tag size="mini">点击菜单链接</el-tag>{{ scope.row.repUrl }}</div>
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'scancode_waitmsg'"><el-tag size="mini">扫码结果</el-tag>{{ scope.row.repContent }}</div>
<div v-if="scope.row.repType === 'text'">{{ scope.row.repContent }}</div>
<div v-if="scope.row.repType === 'image'">
<a target="_blank" :href="scope.row.repUrl"><img :src="scope.row.repUrl" style="width: 100px"></a>
</div>
<div v-if="['video','voice','link','shortvideo'].includes(scope.row.repType)">
<el-tag>链接</el-tag><a :href="scope.row.repUrl" target="_blank">{{ scope.row.repName }}</a></div>
</template>
</el-table-column>
<el-table-column :label="t('wxFansMsg.readFlag')" prop="readFlag" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="readFlag" :value="scope.row.readFlag"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="t('wxFansMsg.createTime')" prop="createTime" show-overflow-tooltip/>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button link type="primary" @click="wxMsgDo(scope.row,scope.index)">消息</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
</el-card>
<wx-msg ref="WxmsgRef"></wx-msg>
</div>
</template>
<script lang="ts" name="systemWxMsg" setup>
import {BasicTableProps, useTable} from "/@/hooks/table";
import {delObjs, fetchList} from "/@/api/mp/wx-fans-msg";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {useDict} from '/@/hooks/dict';
import {useI18n} from "vue-i18n";
import {fetchAccountList} from "/@/api/mp/wx-account";
const WxMsg = defineAsyncComponent(() => import("/@/components/wechart/wx-msg/index.vue"))
const {t} = useI18n()
//
const {repType} = useDict('repType')
const readFlag = ref([
{
value: '1',
label: '是'
}, {
value: '0',
label: '否'
}
])
const WxmsgRef = ref()
//
const queryRef = ref()
const showSearch = ref(true)
//
const selectObjs = ref([]) as any
const multiple = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
descs: ['create_time']
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile
} = useTable(state)
//
const resetQuery = () => {
//
queryRef.value.resetFields()
//
selectObjs.value = []
getDataList()
}
const accountList = ref([])
const getAccountList = () => {
fetchAccountList().then(res => {
accountList.value = res.data
if(accountList.value.length > 0){
state.queryForm.wxAccountAppid = accountList.value[0].appid
getDataList()
}
})
}
watch(() => state.queryForm.wxAccountAppid,() => {
getDataList()
})
onMounted(() => {
getAccountList()
})
// excel
const exportExcel = () => {
downBlobFile('/mp/wxFansMsg/export', state.queryForm, 'wxFansMsg.xlsx')
}
//
const handleSelectionChange = (objs: any) => {
objs.forEach((val: any) => {
selectObjs.value.push(val.id)
});
multiple.value = !objs.length
}
//
const handleDelete = (ids: string[]) => {
useMessageBox().confirm(t('common.delConfirmText'))
.then(() => {
delObjs(ids).then(() => {
getDataList(false);
useMessage().success(t('common.delSuccessText'));
}).catch((err: any) => {
useMessage().error(err.msg)
})
})
};
const wxMsgDo = (row) => {
WxmsgRef.value.openDialog({
wxUserId: row.wxUserId,
appId: row.appId
})
}
</script>

View File

@ -0,0 +1,549 @@
<template>
<el-dialog
:title="operateMaterial === 'add'?'新建图文':'修改图文'"
:before-close="dialogNewsClose"
:close-on-click-modal="false"
v-model="dialogNewsVisible"
:destroy-on-close="true"
width="80%"
top="20px">
<div class="left">
<div class="select-item">
<div v-for="(news, index) in articlesAdd" :key="news.id">
<div v-if="index==0"
class="news-main father"
:class="{'activeAddNews': isActiveAddNews === index}"
@click="activeNews(index)">
<div class="news-content">
<img v-if="news.thumbUrl" class="material-img" :src="news.thumbUrl" />
<div class="news-content-title">{{ news.title }}</div>
</div>
<div v-if="articlesAdd.length>1" class="child">
<el-button icon="el-icon-top" @click="downNews(index)">下移</el-button>
<el-button
v-if="operateMaterial=='add'"
icon="el-icon-delete"
@click="minusNews(index)">删除
</el-button>
</div>
</div>
<div
v-if="index>0"
class="news-main-item father"
:class="{'activeAddNews': isActiveAddNews === index}"
@click="activeNews(index)">
<div class="news-content-item">
<div class="news-content-item-title ">{{ news.title }}</div>
<div class="news-content-item-img">
<img v-if="news.thumbUrl" class="material-img" :src="news.thumbUrl" height="100%" />
</div>
</div>
<div class="child">
<el-button
v-if="articlesAdd.length > index+1"
icon="el-icon-sort-down"
@click="downNews(index)">下移
</el-button>
<el-button icon="el-icon-sort-up" @click="upNews(index)">上移</el-button>
<el-button
v-if="operateMaterial=='add'"
icon="el-icon-delete"
@click="minusNews(index)">删除
</el-button>
</div>
</div>
</div>
<div v-if="articlesAdd.length<8 && operateMaterial=='add'" class="news-main-plus" @click="plusNews">
<el-icon><Plus /></el-icon>
</div>
</div>
</div>
<div v-loading="addMaterialLoading" class="right">
<!--富文本编辑器组件-->
<el-row>
<editor v-model:get-html="articlesAdd[isActiveAddNews].content" style="margin-top: 20px"></editor>
</el-row>
<br><br><br><br>
<div class="input-tt">封面和摘要</div>
<div>
<div class="thumb-div">
<img v-if="articlesAdd[isActiveAddNews].thumbUrl" class="material-img" :src="articlesAdd[isActiveAddNews].thumbUrl" :class="isActiveAddNews === 0 ? 'avatar':'avatar1'">
<i v-else class="el-icon-plus avatar-uploader-icon" :class="isActiveAddNews === 0 ? 'avatar':'avatar1'"></i>
<div class="thumb-but">
<wx-file-upload :uploadData="uploadData" @success="handleImageChange"></wx-file-upload>
<el-button type="primary" @click="openMaterial">素材库选择</el-button>
</div>
</div>
<el-input
v-model="articlesAdd[isActiveAddNews].digest"
:rows="8"
type="textarea"
placeholder="请输入摘要"
class="digest"
maxlength="120"></el-input>
</div>
<div class="input-tt">标题</div>
<el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题"></el-input>
<div class="input-tt">作者</div>
<el-input v-model="articlesAdd[isActiveAddNews].author" placeholder="请输入作者"></el-input>
<div class="input-tt">原文地址</div>
<el-input v-model="articlesAdd[isActiveAddNews].contentSourceUrl" placeholder="请输入原文地址"></el-input>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogNewsVisible = false" >{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" >{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
<wx-material-select ref="WxMaterialSelectRef"></wx-material-select>
</template>
<script setup lang="ts" name="wx-news-form">
import {useMessageBox} from "/@/hooks/message";
import {addObj, materialNewsUpdate} from '/@/api/mp/wx-material'
const WxMaterialSelect = defineAsyncComponent(() => import("/@/components/wechart/wx-material-select/main.vue"))
const WxFileUpload = defineAsyncComponent(() => import("/@/components/wechart/fileUpload/index.vue"))
const WxMaterialSelectRef = ref()
const dialogNewsVisible = ref(false)
const operateMaterial = ref("add")
const addMaterialLoading = ref(false)
// emit
const emit = defineEmits(['ok']);
const dialogNewsClose = () => {
useMessageBox().confirm("修改内容可能还未保存,确定关闭吗?").then(() => {
dialogNewsVisible.value = false
})
}
// id
const accountId = ref()
//
const articlesAdd = ref([
{
'title': '',
'thumbMediaId': '',
'author': '',
'digest': '',
'showCoverPic': '',
'content': '',
'contentSourceUrl': '',
'needOpenComment': '',
'onlyFansCanComment': '',
'thumbUrl': ''
}
])
//
const isActiveAddNews = ref(0)
// id
const articlesMediaId = ref()
const openDialog = (data: any,item?: any,mediaId?: any,type: any = 'add') => {
//
accountId.value = data.accountId
uploadData.appId = data.accountId
dialogNewsVisible.value = true
operateMaterial.value = 'add'
if(item){
articlesAdd.value = item
}
if(mediaId){
articlesMediaId.value = mediaId || ''
}
if(type){
operateMaterial.value = type
}
}
const uploadData = reactive({
mediaType: 'image',
title: '',
introduction: '',
appId: ''
})
const openMaterial = () => {
WxMaterialSelectRef.value.openDialog({
type: 'image',
accountId: accountId.value
})
}
const handleImageChange = (response) => {
articlesAdd.value[isActiveAddNews.value].thumbMediaId = response.data.mediaId
articlesAdd.value[isActiveAddNews.value].thumbUrl = response.data.url
}
const onSubmit = () => {
addMaterialLoading.value = true
if (operateMaterial.value === 'add') {
addObj({
articles: articlesAdd.value,
appId: accountId.value
}).then(() => {
addMaterialLoading.value = false
dialogNewsVisible.value = false
isActiveAddNews.value = 0
articlesAdd.value = [
{
'title': '',
'thumbMediaId': '',
'author': '',
'digest': '',
'showCoverPic': '',
'content': '',
'contentSourceUrl': '',
'needOpenComment': '',
'onlyFansCanComment': '',
'thumbUrl': ''
}
]
emit('ok',)
}).finally(() => {
addMaterialLoading.value = false
})
}
if (operateMaterial.value === 'edit') {
materialNewsUpdate({
articles: articlesAdd.value,
mediaId: articlesMediaId.value,
appId: accountId.value
}).then(() => {
addMaterialLoading.value = false
dialogNewsVisible.value = false
isActiveAddNews.value = 0
articlesAdd.value = [
{
'title': '',
'thumbMediaId': '',
'author': '',
'digest': '',
'showCoverPic': '',
'content': '',
'contentSourceUrl': '',
'needOpenComment': '',
'onlyFansCanComment': '',
'thumbUrl': ''
}
]
emit("ok")
}).finally(() => {
addMaterialLoading.value = false
})
}
}
const activeNews = (index) => {
isActiveAddNews.value = index
}
const minusNews = (index) => {
useMessageBox().confirm("确定删除该图文吗?").then(() => {
articlesAdd.value.splice(index, 1)
if (isActiveAddNews.value === index) {
isActiveAddNews.value = 0
}
})
}
const plusNews = () => {
articlesAdd.value.push({
'title': '',
'thumbMediaId': '',
'author': '',
'digest': '',
'showCoverPic': '',
'content': '',
'contentSourceUrl': '',
'needOpenComment': '',
'onlyFansCanComment': '',
'thumbUrl': ''
})
isActiveAddNews.value = articlesAdd.value.length -1
}
const downNews = (index) => {
const temp = articlesAdd.value[index]
articlesAdd.value[index] = articlesAdd.value[index + 1]
articlesAdd.value[index + 1] = temp
isActiveAddNews.value = index + 1
}
const upNews = (index) => {
const temp = articlesAdd[index]
articlesAdd[index] = articlesAdd[index - 1]
articlesAdd[index - 1] = temp
isActiveAddNews.value = index - 1
}
//
defineExpose({
openDialog,
});
</script>
<style lang="scss" scoped>
.tree-position {
margin: 12px 20px 0 0
}
.pagination {
float: right;
margin-right: 25px;
}
.add_but {
padding: 10px;
}
.ope-row {
margin-top: 5px;
text-align: center;
border-top: 1px solid #eaeaea;
padding-top: 5px;
}
.item-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.el-upload__tip {
margin-left: 5px;
}
/*新增图文*/
.left {
display: inline-block;
width: 35%;
vertical-align: top;
margin-top: 200px;
}
.right {
display: inline-block;
width: 60%;
margin-top: -40px;
}
.avatar-uploader {
width: 20%;
display: inline-block;
}
.avatar-uploader .el-upload {
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
text-align: unset !important;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
border: 1px solid #d9d9d9;
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
line-height: 120px;
text-align: center;
}
.avatar {
width: 230px;
height: 120px;
}
.avatar1 {
width: 120px;
height: 120px;
}
.digest {
width: 60%;
display: inline-block;
vertical-align: top;
}
/*新增图文*/
/*瀑布流样式*/
.waterfall {
width: 100%;
column-gap: 10px;
column-count: 5;
margin: 0 auto;
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
p {
line-height: 30px;
}
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
p {
color: red;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
p {
color: orange;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
/*瀑布流样式*/
.news-main {
background-color: #FFFFFF;
width: 100%;
margin: auto;
height: 120px;
}
.news-content {
background-color: #acadae;
width: 100%;
height: 120px;
position: relative;
}
.news-content-title {
display: inline-block;
font-size: 15px;
color: #FFFFFF;
position: absolute;
left: 0px;
bottom: 0px;
background-color: black;
width: 98%;
padding: 1%;
opacity: 0.65;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 25px;
}
.news-main-item {
background-color: #FFFFFF;
padding: 5px 0px;
border-top: 1px solid #eaeaea;
width: 100%;
margin: auto;
}
.news-content-item {
position: relative;
margin-left: -3px
}
.news-content-item-title {
display: inline-block;
font-size: 12px;
width: 70%;
}
.news-content-item-img {
display: inline-block;
width: 25%;
background-color: #acadae
}
.input-tt {
padding: 5px;
}
.activeAddNews {
border: 5px solid #2bb673;
}
.news-main-plus {
width: 280px;
text-align: center;
margin: auto;
height: 50px;
}
.icon-plus {
margin: 10px;
font-size: 25px;
}
.select-item {
width: 60%;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.father .child {
display: none;
text-align: center;
position: relative;
bottom: 25px;
}
.father:hover .child {
display: block;
}
.thumb-div {
display: inline-block;
width: 30%;
text-align: center;
}
.thumb-but {
margin: 5px;
}
.material-img {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,556 @@
<template>
<div class="layout-padding">
<el-row :gutter="20">
<el-col :md="4">
<el-card class="layout-padding-auto" shadow="hover">
<query-tree :query="deptData.queryList"
@node-click="handleNodeClick"/>
</el-card>
</el-col>
<el-col :span="20">
<el-card class="layout-padding-auto" shadow="hover">
<el-tabs v-model="materialType" @tab-click="handleClick">
<el-tab-pane name="image" label="image">
<template #label><i class="el-icon-picture"></i> 图片</template>
<div class="add_but">
<wx-file-upload @success="getDataList" :uploadData="uploadData" :type="['image/jpeg','image/png','image/gif','image/bmp','image/jpg']"></wx-file-upload>
</div>
<div v-loading="state.loading" class="waterfall">
<div v-for="item in state.dataList" :key="item.id" class="waterfall-item">
<a target="_blank" :href="item.url">
<img class="material-img" :src="item.url">
<div class="item-name">{{ item.name }}</div>
</a>
<el-row class="ope-row">
<el-button type="danger" icon="el-icon-delete" circle @click="delMaterial(item)"></el-button>
</el-row>
</div>
</div>
<div v-if="state.dataList.length <=0 && !state.loading" class="el-table__empty-block">
<span class="el-table__empty-text">暂无数据</span>
</div>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
</el-tab-pane>
<el-tab-pane name="voice" label="voice">
<template #label><i class="el-icon-microphone"></i> 语音</template>
<div class="add_but">
<wx-file-upload @success="getDataList" :uploadData="uploadData"></wx-file-upload>
</div>
<el-table
v-loading="state.loading"
:data="state.dataList"
stripe
border>
<el-table-column
prop="mediaId"
label="media_id">
</el-table-column>
<el-table-column
prop="name"
label="名称">
</el-table-column>
<el-table-column
prop="updateTime"
label="更新时间">
</el-table-column>
<el-table-column
fixed="right"
label="操作">
<template v-slot="scope">
<el-button
type="text"
icon="el-icon-download"
plain
@click="handleDown(scope.row)">下载
</el-button>
<el-button
type="text"
icon="el-icon-delete"
plain
@click="delMaterial(scope.row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
</el-tab-pane>
<el-tab-pane name="video" label="video">
<template #label><i class="el-icon-video-play"></i> 视频</template>
<div class="add_but">
<el-button type="primary" @click="handleAddVideo">新建</el-button>
</div>
<el-dialog title="新建视频" v-model="dialogVideoVisible">
<wx-file-upload @success="getDataList" :uploadData="uploadData" auto-upload="false" ref="uploadFileVideo" :type="['video/mp4']"></wx-file-upload>
<el-form
ref="uploadForm"
:model="uploadData"
v-loading="addMaterialLoading"
:rules="uploadRules">
<el-form-item label="标题" prop="title">
<el-input v-model="uploadData.title" placeholder="标题将展示在相关播放页面,建议填写清晰、准确、生动的标题"></el-input>
</el-form-item>
<el-form-item label="描述" prop="introduction">
<el-input
v-model="uploadData.introduction"
:rows="3"
type="textarea"
placeholder="介绍语将展示在相关播放页面,建议填写简洁明确、有信息量的内容"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVideoVisible = false"> </el-button>
<el-button type="primary" @click="subVideo"> </el-button>
</template>
</el-dialog>
<el-table
v-loading="state.loading"
:data="state.dataList"
stripe
border>
<el-table-column
prop="mediaId"
label="media_id">
</el-table-column>
<el-table-column
prop="name"
label="名称">
</el-table-column>
<el-table-column
prop="updateTime"
label="更新时间">
</el-table-column>
<el-table-column
fixed="right"
label="操作">
<template v-slot="scope">
<el-button type="text" icon="el-icon-view" @click="handleInfo(scope.row)">查看
</el-button>
<el-button type="text" icon="el-icon-delete" @click="delMaterial(scope.row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle"/>
</el-tab-pane>
<el-tab-pane name="news" label="news">
<template #label><i class="el-icon-news"></i> 图文</template>
<div class="add_but">
<el-button type="primary" @click="handleAddNews"> </el-button>
</div>
<news-form ref="dialogNewsRef" @ok="getDataList"></news-form>
<div v-loading="state.loading" class="waterfall">
<div
v-for="item in state.dataList"
:key="item.id"
class="waterfall-item">
<wx-news :obj-data="item.content.newsItem"></wx-news>
<el-row class="ope-row">
<el-button type="primary" icon="el-icon-edit" circle @click="handleEditNews(item)"></el-button>
<el-button type="danger" icon="el-icon-delete" circle @click="delMaterial(item)"></el-button>
</el-row>
</div>
</div>
<div v-if="state.dataList.length <=0 && !state.loading" class="el-table__empty-block">
<span class="el-table__empty-text">暂无数据</span>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts" name="wx-material">
import { fetchAccountList } from "/@/api/mp/wx-account";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {BasicTableProps, useTable} from "/@/hooks/table";
import {delObj, getMaterialVideo, getPage} from '/@/api/mp/wx-material'
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'))
const NewsForm = defineAsyncComponent(() => import("./components/news-form.vue"))
const WxFileUpload = defineAsyncComponent(() => import("/@/components/wechart/fileUpload/index.vue"))
const WxNews = defineAsyncComponent(() => import("/@/components/wechart/wx-news/index.vue"))
const deptData = reactive({
queryList: () => {
return fetchAccountList()
}
})
const checkAppId = ref()
const uploadData = ref({
appId: '',
mediaType: 'image',
title: '',
introduction: '',
})
const materialType = ref("image")
//
const handleNodeClick = (data: any) => {
checkAppId.value = data.appid
uploadData.value.appId = data.appid
state.queryForm.appId = data.appid
state.queryForm.type = materialType.value
getDataList()
}
const handleClick = (tab) => {
if (checkAppId.value) {
// getPage(this.page)
} else {
useMessage().error("请选择公众号")
}
materialType.value = tab.paneName
uploadData.value.mediaType = tab.paneName
state.queryForm.type = materialType.value
getDataList()
}
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
appId: '',
type: ''
},
pageList: getPage,
createdIsNeed: false,
props: {
item: 'items',
totalCount: 'totalCount'
}
})
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
} = useTable(state)
const delMaterial = (item: any) => {
useMessageBox().confirm("此操作将永久删除该文件, 是否继续?").then(() => {
delObj({
id: item.mediaId,
appId: checkAppId.value
}).then(() => {
getDataList()
})
})
}
//
const dialogVideoVisible = ref(false)
const addMaterialLoading = ref(false)
const handleAddVideo = () => {
dialogVideoVisible.value = true
}
const uploadRules = reactive({
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
introduction: [
{ required: true, message: '请输入描述', trigger: 'blur' }
]
})
const uploadForm = ref()
const uploadFileVideo = ref()
const subVideo = () => {
uploadForm.value.validate((valid: boolean) => {
if (!valid) {
return false
}
uploadFileVideo.value.submit()
}).finally(() => {
dialogVideoVisible.value = false
})
}
//
const dialogNewsRef = ref()
const handleAddNews = () => {
dialogNewsRef.value.openDialog({
accountId: checkAppId.value
})
}
const handleEditNews = (item) => {
dialogNewsRef.value.openDialog({
accountId: checkAppId.value,
},JSON.parse(JSON.stringify(item.content.newsItem)),item.mediaId,'edit')
}
const handleInfo = (row) => {
getMaterialVideo({
mediaId: row.mediaId,
appId: checkAppId.value
}).then((response) => {
const downUrl = response.data.downUrl
window.open(downUrl, '_blank')
})
}
</script>
<style lang="scss" scoped>
.tree-position {
margin: 12px 20px 0 0
}
.pagination {
float: right;
margin-right: 25px;
}
.add_but {
padding: 10px;
}
.ope-row {
margin-top: 5px;
text-align: center;
border-top: 1px solid #eaeaea;
padding-top: 5px;
}
.item-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.el-upload__tip {
margin-left: 5px;
}
/*新增图文*/
.left {
display: inline-block;
width: 35%;
vertical-align: top;
margin-top: 200px;
}
.right {
display: inline-block;
width: 60%;
margin-top: -40px;
}
.avatar-uploader {
width: 20%;
display: inline-block;
}
.avatar-uploader .el-upload {
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
text-align: unset !important;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
border: 1px solid #d9d9d9;
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
line-height: 120px;
text-align: center;
}
.avatar {
width: 230px;
height: 120px;
}
.avatar1 {
width: 120px;
height: 120px;
}
.digest {
width: 60%;
display: inline-block;
vertical-align: top;
}
/*新增图文*/
/*瀑布流样式*/
.waterfall {
width: 100%;
column-gap: 10px;
column-count: 5;
margin: 0 auto;
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
p {
line-height: 30px;
}
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
p {
color: red;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
p {
color: orange;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
/*瀑布流样式*/
.news-main {
background-color: #FFFFFF;
width: 100%;
margin: auto;
height: 120px;
}
.news-content {
background-color: #acadae;
width: 100%;
height: 120px;
position: relative;
}
.news-content-title {
display: inline-block;
font-size: 15px;
color: #FFFFFF;
position: absolute;
left: 0px;
bottom: 0px;
background-color: black;
width: 98%;
padding: 1%;
opacity: 0.65;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 25px;
}
.news-main-item {
background-color: #FFFFFF;
padding: 5px 0px;
border-top: 1px solid #eaeaea;
width: 100%;
margin: auto;
}
.news-content-item {
position: relative;
margin-left: -3px
}
.news-content-item-title {
display: inline-block;
font-size: 12px;
width: 70%;
}
.news-content-item-img {
display: inline-block;
width: 25%;
background-color: #acadae
}
.input-tt {
padding: 5px;
}
.activeAddNews {
border: 5px solid #2bb673;
}
.news-main-plus {
width: 280px;
text-align: center;
margin: auto;
height: 50px;
}
.icon-plus {
margin: 10px;
font-size: 25px;
}
.select-item {
width: 60%;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.father .child {
display: none;
text-align: center;
position: relative;
bottom: 25px;
}
.father:hover .child {
display: block;
}
.thumb-div {
display: inline-block;
width: 30%;
text-align: center;
}
.thumb-but {
margin: 5px;
}
.material-img {
width: 100%;
height: 100%;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,606 @@
<template>
<div class="layout-padding">
<el-row :gutter="20">
<el-col :span="4" :xs="24">
<el-card class="layout-padding-auto" shadow="hover">
<query-tree :query="deptData.queryList"
@node-click="handleNodeClick"/>
</el-card>
</el-col>
<el-col :span="20">
<el-card class="layout-padding-auto" shadow="hover">
<div v-loading="loading" class="public-account-management clearfix">
<div class="left">
<div class="weixin-hd">
<div class="weixin-title">{{ name }}</div>
</div>
<div class="weixin-menu menu_main clearfix">
<div v-for="(item, i) of menuList" :key="i" class="menu_bottom">
<div :class="{ active: isActive === i }" class="menu_item el-icon-s-fold" @click="menuClick(i, item)">
{{ item.name }}
</div>
<!-- 以下为二级菜单-->
<div v-if="isSubMenuFlag === i" class="submenu">
<template v-for="(subItem, k) in item.children">
<div v-if="item.children" :key="k" class="subtitle menu_bottom">
<div :class="{ active: isSubMenuActive === i + '' + k }" class="menu_subItem"
@click="subMenuClick(subItem, i, k)">
{{ subItem.name }}
</div>
</div>
</template>
<!-- 二级菜单加号 当长度 小于 5 才显示二级菜单的加号 -->
<div v-if="!item.children || item.children.length < 5" class="menu_bottom menu_addicon"
@click="addSubMenu(i, item)">
<el-icon>
<el-icon-plus/>
</el-icon>
</div>
</div>
</div>
<!-- 一级菜单加号 -->
<div v-if="menuList.length < 3" class="menu_bottom menu_addicon" @click="addMenu">
<el-icon>
<el-icon-plus/>
</el-icon>
</div>
</div>
<div class="save_div">
<el-button class="save_btn" type="success" size="small" @click="handleSave">保存并发布菜单</el-button>
<el-button class="save_btn" type="danger" size="small" @click="handleDelete">清空菜单</el-button>
</div>
</div>
<div v-if="showRightFlag" class="right">
<div class="configure_page">
<div class="delete_btn">
<el-button icon="Delete" size="mini" type="danger" @click="deleteMenu(tempObj)">删除当前菜单
</el-button>
</div>
<div>
<span>菜单名称</span>
<el-input v-model="tempObj.name" class="input_width" clearable placeholder="请输入菜单名称"/>
</div>
<div v-if="showConfigureContent">
<div class="menu_content">
<span>菜单标识</span>
<el-input v-model="tempObj.menuKey" class="input_width" clearable placeholder="请输入菜单 KEY"/>
</div>
<div class="menu_content">
<span>菜单内容</span>
<el-select v-model="tempObj.type" class="menu_option" clearable placeholder="请选择">
<el-option v-for="item in menuOptions" :key="item.value" :label="item.label" :value="item.value"/>
</el-select>
</div>
<div class="configur_content" v-if="tempObj.type === 'view'">
<span>跳转链接</span>
<el-input class="input_width" v-model="tempObj.url" placeholder="请输入链接" clearable />
</div>
<div class="configur_content" v-if="tempObj.type === 'miniprogram'">
<div class="applet">
<span>小程序的 appid </span>
<el-input class="input_width" v-model="tempObj.miniProgramAppId" placeholder="请输入小程序的appid" clearable />
</div>
<div class="applet">
<span>小程序的页面路径</span>
<el-input class="input_width" v-model="tempObj.miniProgramPagePath"
placeholder="请输入小程序的页面路径pages/index" clearable />
</div>
<div class="applet">
<span>小程序的备用网页</span>
<el-input class="input_width" v-model="tempObj.url" placeholder="不支持小程序的老版本客户端将打开本网页" clearable />
</div>
<p class="blue">tips:需要和公众号进行关联才可以把小程序绑定带微信菜单上哟</p>
</div>
<div class="configur_content" v-if="tempObj.type === 'article_view_limited'">
<el-row>
<div class="select-item" v-if="tempObj && tempObj.replyArticles">
<wx-news :objData="tempObj.replyArticles" />
<el-row class="ope-row">
<el-button type="danger" icon="el-icon-delete" circle @click="deleteMaterial" />
</el-row>
</div>
<div v-else>
<el-row>
<el-col :span="24" style="text-align: center">
<el-button type="success" @click="openMaterial">
素材库选择<i class="fansel-icon--right"></i>
</el-button>
</el-col>
</el-row>
</div>
<wx-material-select ref="dialogNewsRef" @selectMaterial="selectMaterial" />
</el-row>
</div>
<div class="configur_content" v-if="tempObj.type === 'click' || tempObj.type === 'scancode_waitmsg'">
<wx-reply :objData="tempObj" v-if="hackResetWxReplySelect"/>
</div>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" name="wx-menu" setup>
import { saveObj,getObj } from '/@/api/mp/wx-menu'
// 使
import {fetchAccountList} from "/@/api/mp/wx-account";
import {useMessage, useMessageBox} from "/@/hooks/message";
const WxMaterialSelect = defineAsyncComponent(() => import("/@/components/wechart/wx-material-select/main.vue"))
const WxReply = defineAsyncComponent(() => import("/@/components/wechart/wx-reply/index.vue"))
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'))
const WxNews = defineAsyncComponent(() => import("/@/components/wechart/wx-news/index.vue"))
//
const handleNodeClick = (node: any) => {
accountId.value = node.appid
name.value = node.name
getMenuFun()
}
const deptData = reactive({
queryList: () => {
return fetchAccountList()
}
})
const loading = ref(false)
const name = ref('测试公众号')
const accountId = ref('') // id
//
const isActive = ref(-1)
//
const isSubMenuActive = ref('-1')
//
const isSubMenuFlag = ref(-1)
const menuList = reactive([
{
name: '菜单名称',
children: []
}
] as any)
const hackResetWxReplySelect = ref(false)
const menuOptions = ref([
{
value: 'view',
label: '跳转网页',
},
{
value: 'miniprogram',
label: '跳转小程序',
},
{
value: 'click',
label: '点击回复',
},
{
value: 'article_view_limited',
label: '跳转图文消息',
},
{
value: 'scancode_push',
label: '扫码直接返回结果',
},
{
value: 'scancode_waitmsg',
label: '扫码回复',
},
{
value: 'pic_sysphoto',
label: '系统拍照发图',
},
{
value: 'pic_photo_or_album',
label: '拍照或者相册',
},
{
value: 'pic_weixin',
label: '微信相册',
},
{
value: 'location_select',
label: '选择地理位置',
},
])
const showRightFlag = ref(false)
let tempObj = ref({
replyArticles: [] as any,
articleId: '',
appId: ''
})
const tempSelfObj = reactive({
grand: '', //
index: '', //
secondIndex: '' //
})
const getMenuFun = () => {
getObj(accountId.value).then((res) => {
if (res.data) {
const data = JSON.parse(res.data)
if (data && data.button) {
Object.assign(menuList,data.button)
}
}
})
}
const showConfigureContent = ref(true)
//
const menuClick = (i, item) => {
hackResetWxReplySelect.value = false
nextTick(() => {
hackResetWxReplySelect.value = true
})
showRightFlag.value = true //
tempObj.value = item
tempObj.value.appId = accountId.value
showConfigureContent.value = !(item.children && item.children.length > 0) //
isActive.value = i
isSubMenuFlag.value = i
isSubMenuActive.value = '-1'
tempSelfObj.grand = '1'//
tempSelfObj.index = i//
}
//
const subMenuClick = (subItem, index, k) => {
hackResetWxReplySelect.value = false
nextTick(() => {
hackResetWxReplySelect.value = true
})
showRightFlag.value = true //
// Object.assign(tempObj, subItem) // flag
tempObj.value = subItem
tempObj.value.appId = accountId.value
showConfigureContent.value = true
isActive.value = -1 //
isSubMenuActive.value = (index + '' + k) //
tempSelfObj.grand = '2'//
tempSelfObj.index = index//
tempSelfObj.secondIndex = k//
}
// item
const addSubMenu = (i, item) => {
if (!item.children || item.children.length <= 0) {
item['children'] = []
showConfigureContent.value = false
}
let addButton = {
name: '子菜单名称',
reply: {
//
type: 'text',
accountId: accountId.value, // 使
},
}
item.children.push(addButton)
}
//
const addMenu = () => {
const addButton = {
name: '菜单名称',
children: [],
reply: {
//
type: 'text',
accountId: accountId.value, // 使
},
}
menuList.push(addButton)
}
const deleteMenu = () => {
useMessageBox().confirm("确定要删除吗?").then(() => {
if(tempSelfObj.grand === '1'){
menuList.splice(tempSelfObj.index, 1)
}else if(tempSelfObj.grand === '2'){
menuList[tempSelfObj.index].children.splice(tempSelfObj.secondIndex, 1)
}
useMessage().success("删除成功")
Object.assign(tempObj,{})
showRightFlag.value = false
isActive.value = -1
isSubMenuActive.value = '-1'
})
}
const handleSave = () => {
useMessageBox().confirm("确定要保存该菜单吗?").then(() => {
saveObj(accountId.value, {
button: menuList
}).then(() => {
useMessage().success("保存成功")
});
})
}
const deleteMaterial = () => {
tempObj.value.replyArticles = []
tempObj.value.articleId = ""
}
const dialogNewsRef = ref()
const openMaterial = () => {
dialogNewsRef.value.openDialog({type: 'news', accountId: accountId.value})
}
const selectMaterial = (item) => {
const articleId = item.articleId;
const articles = item.content.newsItem;
//
if (articles.length > 1) {
// this.$alert('', '', {
// confirmButtonText: ''
// })
}
//
tempObj.value.articleId = articleId;
tempObj.value.replyArticles = [];
articles.forEach(article => {
tempObj.value.replyArticles.push({
title: article.title,
description: article.digest,
picUrl: article.picUrl,
url: article.url,
})
})
}
const handleDelete = () => {
}
</script>
<style lang="scss" scoped>
/* 公共颜色变量 */
.clearfix {
*zoom: 1;
}
.clearfix::after {
content: "";
display: table;
clear: both;
}
div {
text-align: left;
}
.weixin-hd {
color: #fff;
text-align: center;
position: relative;
bottom: 426px;
left: 0px;
width: 300px;
height: 64px;
background: transparent url("assets/menu_head.png") no-repeat 0 0;
background-position: 0 0;
background-size: 100%
}
.weixin-title {
color: #fff;
font-size: 14px;
width: 100%;
text-align: center;
position: absolute;
top: 33px;
left: 0px;
}
.weixin-menu {
background: transparent url("assets/menu_foot.png") no-repeat 0 0;
padding-left: 43px;
font-size: 12px
}
.menu_option {
width: 40% !important;
}
.public-account-management {
min-width: 1200px;
width: 1200px;
margin: 0 auto;
.left {
float: left;
display: inline-block;
width: 350px;
height: 715px;
background: url("assets/iphone_backImg.png") no-repeat;
background-size: 100% auto;
padding: 518px 25px 88px;
position: relative;
box-sizing: border-box;
/*第一级菜单*/
.menu_main {
.menu_bottom {
position: relative;
float: left;
display: inline-block;
box-sizing: border-box;
width: 85.5px;
text-align: center;
border: 1px solid #ebedee;
background-color: #fff;
cursor: pointer;
&.menu_addicon {
height: 46px;
line-height: 46px;
}
.menu_item {
height: 44px;
line-height: 44px;
text-align: center;
box-sizing: border-box;
width: 100%;
&.active {
border: 1px solid #2bb673;
}
}
.menu_subItem {
height: 44px;
line-height: 44px;
text-align: center;
box-sizing: border-box;
&.active {
border: 1px solid #2bb673;
}
}
}
i {
color: #2bb673;
}
/*第二级菜单*/
.submenu {
position: absolute;
width: 85.5px;
bottom: 45px;
.subtitle {
background-color: #fff;
box-sizing: border-box;
}
}
}
.save_div {
margin-top: 15px;
text-align: center;
.save_btn {
bottom: 20px;
left: 100px;
}
}
}
/*右边菜单内容*/
.right {
float: left;
width: 63%;
background-color: #e8e7e7;
padding: 20px;
margin-left: 20px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
.configure_page {
.delete_btn {
text-align: right;
margin-bottom: 15px;
}
.menu_content {
margin-top: 20px;
}
.configur_content {
margin-top: 20px;
background-color: #fff;
padding: 20px 10px;
border-radius: 5px
}
.blue {
color: #29b6f6;
margin-top: 10px;
}
.applet {
margin-bottom: 20px;
span {
width: 20%;
}
}
.input_width {
width: 40%;
}
.material {
.input_width {
width: 30%;
}
.el-textarea {
width: 80%
}
}
}
}
.el-input {
width: 70%;
margin-right: 2%;
}
}
</style>
<!--素材样式-->
<style lang="scss" scoped>
.pagination {
text-align: right;
margin-right: 25px;
}
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.select-item2 {
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.ope-row {
padding-top: 10px;
text-align: center;
}
.item-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
</style>

View File

@ -0,0 +1,238 @@
<template>
<div class="layout-padding">
<el-card class="layout-padding-auto">
<el-row>
<div class="mb8" style="width: 100%">
<el-date-picker
v-model="beginTime"
class="input_width"
placeholder="选择开始时间"
@change="check">
</el-date-picker>
<el-date-picker
v-model="endTime"
class="input_width"
placeholder="选择结束时间"
@change="check">
</el-date-picker>
</div>
</el-row>
<el-row >
<el-col :span="4" :xs="24">
<query-tree :query="deptData.queryList"
@node-click="handleNodeClick"/>
</el-col>
<el-col :span="20">
<el-row :gutter="15" class="home-card-two mb15">
<el-col :span="12">
<div class="home-card-item">
<div style="height: 100%" ref="userCumulateRef"></div>
</div>
</el-col>
<el-col :span="12">
<div class="home-card-item">
<div style="height: 100%" ref="userShardRef"></div>
</div>
</el-col>
<el-col :span="12">
<div class="home-card-item">
<div style="height: 100%" ref="upstreamMsgDistMonthRef"></div>
</div>
</el-col>
<el-col :span="12">
<div class="home-card-item">
<div style="height: 100%" ref="interfaceSummaryRef"></div>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup lang="ts" name="wx-statistics">
import {useMessage} from "/@/hooks/message";
import {fetchAccountList, fetchStatistics} from "/@/api/mp/wx-account";
import {markRaw} from "vue";
import * as echarts from "echarts";
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'))
const beginTime = ref(new Date().getTime() - 3600 * 1000 * 24 * 7)
const endTime = ref(new Date().getTime() - 3600 * 1000 * 24)
const check = () => {
const start = new Date(beginTime.value)
const end = new Date(endTime.value)
if (end.getTime() >= new Date().getTime()) {
useMessage().error("统计结束日小于当前日期,请重新选择")
return false
}
if (end.getTime() - start.getTime() >= 3600 * 1000 * 24 * 7) {
useMessage().error("时间间隔7天以内请重新选择")
return false
}
}
const accountId = ref()
//
const handleNodeClick = (node: any) => {
accountId.value = node.appid
initdata()
}
const deptData = reactive({
queryList: () => {
return fetchAccountList()
}
})
const userCumulateRef = ref()
// 线
const userCumulate = () => {
const userCumulate = markRaw(echarts.init(userCumulateRef.value));
const option = {
title: {
text: '用户分析数据',
},
xAxis: {
type: 'category',
data: LintData.value[0],
},
yAxis: {
type: 'value'
},
series: [
{
type: 'line',
data: LintData.value[1]
}
]
}
userCumulate.setOption(option)
}
const userShardRef = ref()
// 线
const userShard = () => {
const userShard = markRaw(echarts.init(userShardRef.value));
const option = {
title: {
text: '接口分析数据',
},
xAxis: {
type: 'category',
data: LintData.value[2],
},
yAxis: {
type: 'value'
},
series: [
{
type: 'line',
data: LintData.value[3]
}
]
}
userShard.setOption(option)
}
const upstreamMsgDistMonthRef = ref()
// 线
const upstreamMsgDistMonth = () => {
const upstreamMsgDistMonth = markRaw(echarts.init(upstreamMsgDistMonthRef.value));
const option = {
title: {
text: '消息分析数据',
},
xAxis: {
type: 'category',
data: LintData.value[4],
},
yAxis: {
type: 'value'
},
series: [
{
type: 'line',
data: LintData.value[5]
}
]
}
upstreamMsgDistMonth.setOption(option)
}
const interfaceSummaryRef = ref()
// 线
const interfaceSummary = () => {
const interfaceSummary = markRaw(echarts.init(interfaceSummaryRef.value));
const option = {
title: {
text: '图文分享数据',
},
xAxis: {
type: 'category',
data: LintData.value[0],
},
yAxis: {
type: 'value'
},
series: [
{
type: 'line',
data: LintData.value[1]
}
]
}
interfaceSummary.setOption(option)
}
const LintData = ref([
[],[],[],[],[],[],[],[]
])
const initdata = () => {
fetchStatistics({
appId: accountId.value,
interval: new Date(beginTime.value).getTime() + '-' + new Date(endTime.value).getTime()
}).then((res) => {
LintData.value = res.data
}).finally(() => {
userCumulate()
userShard()
upstreamMsgDistMonth()
interfaceSummary()
})
}
</script>
<style scoped>
.home-card-item{
width: 100%;
height: 400px;
border-radius: 4px;
transition: all ease 0.3s;
padding: 20px;
overflow: hidden;
background: var(--el-color-white);
color: var(--el-text-color-primary);
border: 1px solid var(--next-border-color-light);
margin-top: 20px;
}
</style>