!133 版本 1.7.2 发布

Merge pull request !133 from 芋道源码/dev
This commit is contained in:
芋道源码 2023-04-19 12:22:06 +00:00 committed by Gitee
commit a364d57438
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
46 changed files with 2597 additions and 2123 deletions

View File

@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin-vue3",
"version": "1.7.1-snapshot.1961",
"version": "1.7.2-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,

View File

@ -272,7 +272,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
{
path: '/manager/task-assign-rule',
component: () => import('@/views/bpm/taskAssignRule/index.vue'),
name: 'BpmTaskAssignRule',
name: 'BpmTaskAssignRuleList',
meta: {
noCache: true,
hidden: true,

View File

@ -1,10 +1,22 @@
import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue-router'
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import type { Router, RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'
import { isUrl } from '@/utils/is'
import { omit, cloneDeep } from 'lodash-es'
import { cloneDeep, omit } from 'lodash-es'
const modules = import.meta.glob('../views/**/*.{vue,tsx}')
/**
*
* @param componentPath :/bpm/oa/leave/detail
*/
export const registerComponent = (componentPath: string) => {
for (const item in modules) {
if (item.includes(componentPath)) {
// 使用异步组件的方式来动态加载组件
// @ts-ignore
return defineAsyncComponent(modules[item])
}
}
}
/* Layout */
export const Layout = () => import('@/layout/Layout.vue')

View File

@ -44,10 +44,13 @@ import { CommonStatusEnum } from '@/utils/constants'
import * as FormApi from '@/api/bpm/form'
import FcDesigner from '@form-create/designer'
import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
import { useTagsViewStore } from '@/store/modules/tagsView'
const { t } = useI18n() //
const message = useMessage() //
const { query } = useRoute() //
const { push, currentRoute } = useRouter() //
const { query } = useRoute() //
const { delView } = useTagsViewStore() //
const designer = ref() //
const dialogVisible = ref(false) //
@ -88,10 +91,16 @@ const submitForm = async () => {
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
close()
} finally {
formLoading.value = false
}
}
/** 关闭按钮 */
const close = () => {
delView(unref(currentRoute))
push('/bpm/manager/form')
}
/** 初始化 **/
onMounted(async () => {

View File

@ -2,26 +2,33 @@
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="表单名" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入表单名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
clearable
placeholder="请输入表单名"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" plain @click="openForm" v-hasPermi="['bpm:form:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['bpm:form:create']" plain type="primary" @click="openForm">
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
</el-form-item>
</el-form>
@ -30,38 +37,38 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="表单名" align="center" prop="name" />
<el-table-column label="状态" align="center" prop="status">
<el-table-column align="center" label="编号" prop="id" />
<el-table-column align="center" label="表单名" prop="name" />
<el-table-column align="center" label="状态" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column align="center" label="备注" prop="remark" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
/>
<el-table-column label="操作" align="center">
<el-table-column align="center" label="操作">
<template #default="scope">
<el-button
v-hasPermi="['bpm:form:update']"
link
type="primary"
@click="openForm(scope.row.id)"
v-hasPermi="['bpm:form:update']"
>
编辑
</el-button>
<el-button link @click="openDetail(scope.row.id)" v-hasPermi="['bpm:form:query']">
<el-button v-hasPermi="['bpm:form:query']" link @click="openDetail(scope.row.id)">
详情
</el-button>
<el-button
v-hasPermi="['bpm:form:delete']"
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['bpm:form:delete']"
>
删除
</el-button>
@ -70,27 +77,28 @@
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单详情的弹窗 -->
<Dialog title="表单详情" v-model="detailVisible" width="800">
<form-create :rule="detailData.rule" :option="detailData.option" />
<Dialog v-model="detailVisible" title="表单详情" width="800">
<form-create :option="detailData.option" :rule="detailData.rule" />
</Dialog>
</template>
<script setup lang="ts" name="BpmForm">
<script lang="ts" name="BpmForm" setup>
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as FormApi from '@/api/bpm/form'
import { setConfAndFields2 } from '@/utils/formCreate'
const message = useMessage() //
const { t } = useI18n() //
const { push } = useRouter() //
const { currentRoute, push } = useRouter() //
const loading = ref(true) //
const total = ref(0) //
@ -128,12 +136,16 @@ const resetQuery = () => {
/** 添加/修改操作 */
const openForm = (id?: number) => {
push({
name: 'BpmFormEditor',
query: {
const toRouter: { name: string; query?: { id: number } } = {
name: 'BpmFormEditor'
}
// idevent
if (typeof id === 'number') {
toRouter.query = {
id
}
})
}
push(toRouter)
}
/** 删除按钮操作 */
@ -162,7 +174,16 @@ const openDetail = async (rowId: number) => {
//
detailVisible.value = true
}
/**表单保存返回后重新加载列表 */
watch(
() => currentRoute.value,
() => {
getList()
},
{
immediate: true
}
)
/** 初始化 **/
onMounted(() => {
getList()

View File

@ -11,7 +11,7 @@
<el-input v-model="formData.name" placeholder="请输入组名" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="formData.name" placeholder="请输入描述" type="textarea" />
<el-input v-model="formData.description" placeholder="请输入描述" type="textarea" />
</el-form-item>
<el-form-item label="成员" prop="memberUserIds">
<el-select v-model="formData.memberUserIds" multiple placeholder="请选择成员">

View File

@ -1,6 +1,6 @@
<template>
<ContentWrap>
<el-descriptions border :column="1">
<el-descriptions :column="1" border>
<el-descriptions-item label="请假类型">
<dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="detailData.type" />
</el-descriptions-item>
@ -21,6 +21,7 @@ import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import { propTypes } from '@/utils/propTypes'
import * as LeaveApi from '@/api/bpm/leave'
const { query } = useRoute() //
const props = defineProps({
@ -34,7 +35,7 @@ const queryId = query.id as unknown as number // 从 URL 传递过来的 id 编
const getInfo = async () => {
detailLoading.value = true
try {
detailData.value = await LeaveApi.getLeave(queryId || props.id)
detailData.value = await LeaveApi.getLeave(props.id || queryId)
} finally {
detailLoading.value = false
}

View File

@ -2,81 +2,78 @@
<ContentWrap>
<!-- 审批信息 -->
<el-card
class="box-card"
v-loading="processInstanceLoading"
v-for="(item, index) in runningTasks"
:key="index"
v-loading="processInstanceLoading"
class="box-card"
>
<template #header>
<span class="el-icon-picture-outline">审批任务{{ item.name }}</span>
</template>
<el-col :span="16" :offset="6">
<el-col :offset="6" :span="16">
<el-form
:ref="'form' + index"
:model="auditForms[index]"
:rules="auditRule"
label-width="100px"
>
<el-form-item label="流程名" v-if="processInstance && processInstance.name">
<el-form-item v-if="processInstance && processInstance.name" label="流程名">
{{ processInstance.name }}
</el-form-item>
<el-form-item label="流程发起人" v-if="processInstance && processInstance.startUser">
<el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人">
{{ processInstance.startUser.nickname }}
<el-tag type="info" size="small">{{ processInstance.startUser.deptName }}</el-tag>
<el-tag size="small" type="info">{{ processInstance.startUser.deptName }}</el-tag>
</el-form-item>
<el-form-item label="审批建议" prop="reason">
<el-input
type="textarea"
v-model="auditForms[index].reason"
placeholder="请输入审批建议"
type="textarea"
/>
</el-form-item>
</el-form>
<div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
<el-button type="success" @click="handleAudit(item, true)">
<Icon icon="ep:select" /> 通过
<Icon icon="ep:select" />
通过
</el-button>
<el-button type="danger" @click="handleAudit(item, false)">
<Icon icon="ep:close" /> 不通过
<Icon icon="ep:close" />
不通过
</el-button>
<el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
<Icon icon="ep:edit" /> 转办
<Icon icon="ep:edit" />
转办
</el-button>
<el-button type="primary" @click="handleDelegate(item)">
<Icon icon="ep:position" /> 委派
<Icon icon="ep:position" />
委派
</el-button>
<el-button type="warning" @click="handleBack(item)">
<Icon icon="ep:back" /> 回退
<Icon icon="ep:back" />
回退
</el-button>
</div>
</el-col>
</el-card>
<!-- 申请信息 -->
<el-card class="box-card" v-loading="processInstanceLoading">
<el-card v-loading="processInstanceLoading" class="box-card">
<template #header>
<span class="el-icon-document">申请信息{{ processInstance.name }}</span>
</template>
<!-- 情况一流程表单 -->
<el-col v-if="processInstance?.processDefinition?.formType === 10" :span="16" :offset="6">
<el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16">
<form-create
ref="fApi"
:rule="detailForm.rule"
:option="detailForm.option"
v-model="detailForm.value"
:option="detailForm.option"
:rule="detailForm.rule"
/>
</el-col>
<!-- 情况二流程表单 -->
<!-- 情况二业务表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20">
<router-link
:to="
processInstance.processDefinition.formCustomViewPath +
'?id=' +
processInstance.businessKey
"
>
<el-button type="primary"><Icon icon="ep:view" /> 点击查看</el-button>
</router-link>
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
</el-card>
@ -85,18 +82,18 @@
<!-- 高亮流程图 -->
<ProcessInstanceBpmnViewer
:id="id"
:process-instance="processInstance"
:loading="processInstanceLoading"
:tasks="tasks"
:id="`${id}`"
:bpmn-xml="bpmnXML"
:loading="processInstanceLoading"
:process-instance="processInstance"
:tasks="tasks"
/>
<!-- 弹窗转派审批人 -->
<TaskUpdateAssigneeForm ref="taskUpdateAssigneeFormRef" @success="getDetail" />
</ContentWrap>
</template>
<script setup lang="ts" name="BpmProcessInstanceDetail">
<script lang="ts" name="BpmProcessInstanceDetail" setup>
import { useUserStore } from '@/store/modules/user'
import { setConfAndFields2 } from '@/utils/formCreate'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
@ -106,6 +103,8 @@ import * as TaskApi from '@/api/bpm/task'
import TaskUpdateAssigneeForm from './TaskUpdateAssigneeForm.vue'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
import { registerComponent } from '@/utils/routerHelper'
const { query } = useRoute() //
const message = useMessage() //
const { proxy } = getCurrentInstance() as any
@ -186,6 +185,7 @@ const getDetail = () => {
}
/** 加载流程实例 */
const BusinessFormComponent = ref(null) //
const getProcessInstance = async () => {
try {
processInstanceLoading.value = true
@ -210,10 +210,12 @@ const getProcessInstance = async () => {
fApi.value?.fapi?.resetBtn.show(false)
fApi.value?.fapi?.disabled(true)
})
} else {
BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
}
//
bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id)
bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id as number)
} finally {
processInstanceLoading.value = false
}

View File

@ -5,46 +5,53 @@
<!-- 搜索 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="表名称" prop="tableName">
<el-input
v-model="queryParams.tableName"
placeholder="请输入表名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
clearable
placeholder="请输入表名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input
v-model="queryParams.tableComment"
placeholder="请输入表描述"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
clearable
placeholder="请输入表描述"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-dd HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-dd HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
<el-button type="primary" v-hasPermi="['infra:codegen:create']" @click="openImportTable()">
<Icon icon="ep:zoom-in" class="mr-5px" /> 导入
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['infra:codegen:create']" type="primary" @click="openImportTable()">
<Icon class="mr-5px" icon="ep:zoom-in" />
导入
</el-button>
</el-form-item>
</el-form>
@ -53,75 +60,75 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="数据源" align="center">
<el-table-column align="center" label="数据源">
<template #default="scope">
{{
dataSourceConfigList.find((config) => config.id === scope.row.dataSourceConfigId)?.name
}}
</template>
</el-table-column>
<el-table-column label="表名称" align="center" prop="tableName" width="200" />
<el-table-column align="center" label="表名称" prop="tableName" width="200" />
<el-table-column
label="表描述"
align="center"
prop="tableComment"
:show-overflow-tooltip="true"
align="center"
label="表描述"
prop="tableComment"
width="200"
/>
<el-table-column label="实体" align="center" prop="className" width="200" />
<el-table-column align="center" label="实体" prop="className" width="200" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="更新时间"
:formatter="dateFormatter"
align="center"
label="更新时间"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" width="300px" fixed="right">
<el-table-column align="center" fixed="right" label="操作" width="300px">
<template #default="scope">
<el-button
v-hasPermi="['infra:codegen:preview']"
link
type="primary"
@click="handlePreview(scope.row)"
v-hasPermi="['infra:codegen:preview']"
>
预览
</el-button>
<el-button
v-hasPermi="['infra:codegen:update']"
link
type="primary"
@click="handleUpdate(scope.row.id)"
v-hasPermi="['infra:codegen:update']"
>
编辑
</el-button>
<el-button
v-hasPermi="['infra:codegen:delete']"
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['infra:codegen:delete']"
>
删除
</el-button>
<el-button
v-hasPermi="['infra:codegen:update']"
link
type="primary"
@click="handleSyncDB(scope.row)"
v-hasPermi="['infra:codegen:update']"
>
同步
</el-button>
<el-button
v-hasPermi="['infra:codegen:download']"
link
type="primary"
@click="handleGenTable(scope.row)"
v-hasPermi="['infra:codegen:download']"
>
生成代码
</el-button>
@ -130,25 +137,26 @@
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- 弹窗导入表 -->
<ImportTable ref="importRef" success="getList" />
<ImportTable ref="importRef" @success="getList" />
<!-- 弹窗预览代码 -->
<PreviewCode ref="previewRef" />
</template>
<script setup lang="ts" name="InfraCodegen">
<script lang="ts" name="InfraCodegen" setup>
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as CodegenApi from '@/api/infra/codegen'
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
import ImportTable from './ImportTable.vue'
import PreviewCode from './PreviewCode.vue'
const message = useMessage() //
const { t } = useI18n() //
const { push } = useRouter() //

View File

@ -171,6 +171,7 @@ const loadEchartOptions = (stats) => {
}
onBeforeMount(() => {
// TODO @hiiwbs 使 Echart
readRedisInfo()
})
</script>

View File

@ -0,0 +1,115 @@
<template>
<el-table v-loading="props.loading" :data="props.list">
<el-table-column
label="请求消息类型"
align="center"
prop="requestMessageType"
v-if="msgType === MsgType.Message"
/>
<el-table-column
label="关键词"
align="center"
prop="requestKeyword"
v-if="msgType === MsgType.Keyword"
/>
<el-table-column
label="匹配类型"
align="center"
prop="requestMatch"
v-if="msgType === MsgType.Keyword"
>
<template #default="scope">
<dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
</template>
</el-table-column>
<el-table-column label="回复消息类型" align="center">
<template #default="scope">
<dict-tag :type="DICT_TYPE.MP_MESSAGE_TYPE" :value="scope.row.responseMessageType" />
</template>
</el-table-column>
<el-table-column label="回复内容" align="center">
<template #default="scope">
<div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
<div v-else-if="scope.row.responseMessageType === 'voice'">
<WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" />
</div>
<div v-else-if="scope.row.responseMessageType === 'image'">
<a target="_blank" :href="scope.row.responseMediaUrl">
<img :src="scope.row.responseMediaUrl" style="width: 100px" />
</a>
</div>
<div
v-else-if="
scope.row.responseMessageType === 'video' ||
scope.row.responseMessageType === 'shortvideo'
"
>
<WxVideoPlayer
v-if="scope.row.responseMediaUrl"
:url="scope.row.responseMediaUrl"
style="margin-top: 10px"
/>
</div>
<div v-else-if="scope.row.responseMessageType === 'news'">
<WxNews :articles="scope.row.responseArticles" />
</div>
<div v-else-if="scope.row.responseMessageType === 'music'">
<WxMusic
:title="scope.row.responseTitle"
:description="scope.row.responseDescription"
:thumb-media-url="scope.row.responseThumbMediaUrl"
:music-url="scope.row.responseMusicUrl"
:hq-music-url="scope.row.responseHqMusicUrl"
/>
</div>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
type="primary"
link
@click="emit('on-update', scope.row.id)"
v-hasPermi="['mp:auto-reply:update']"
>
修改
</el-button>
<el-button
type="danger"
link
@click="emit('on-delete', scope.row.id)"
v-hasPermi="['mp:auto-reply:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
import WxMusic from '@/views/mp/components/wx-music/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue'
import { dateFormatter } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
import { MsgType } from './types'
const props = defineProps<{
loading: boolean
list: any[]
msgType: MsgType
}>()
const emit = defineEmits<{
(e: 'on-update', v: number)
(e: 'on-delete', v: number)
}>()
</script>

View File

@ -0,0 +1,48 @@
// 消息类型Follow: 关注时回复Message: 消息回复Keyword: 关键词回复)
// 作为 tab.nameenum 的数字不能随意修改,与 api 参数相关
export enum MsgType {
Follow = 1,
Message = 2,
Keyword = 3
}
type ReplyType = 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link'
export interface ReplyForm {
// relation:
id?: number
accountId?: number
type?: MsgType
// request:
requestMessageType?: ReplyType
requestMatch?: number
requestKeyword?: string
// response:
responseMessageType?: ReplyType
responseContent?: string
responseMediaId?: number
responseMediaUrl?: string
responseTitle?: string
responseDescription?: number
responseThumbMediaId?: string
responseThumbMediaUrl?: string
responseArticles?: any[]
responseMusicUrl?: string
responseHqMusicUrl?: string
}
// TODO @Dhb52ObjData 这个类名可以在看看ObjData 有点通用
export interface ObjData {
type: ReplyType
accountId?: number
content?: string
mediaId?: number
url?: string
title?: string
description?: string
thumbMediaId?: number
thumbMediaUrl?: string
articles?: any[]
musicUrl?: string
hqMusicUrl?: string
}

View File

@ -12,14 +12,14 @@
<!-- tab 切换 -->
<ContentWrap>
<el-tabs v-model="msgType" @tab-change="handleTabChange">
<el-tabs v-model="msgType" @tab-change="onTabChange">
<!-- 操作工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
@click="handleAdd"
@click="onCreate"
v-hasPermi="['mp:auto-reply:create']"
v-if="msgType !== MsgType.Follow || list.length <= 0"
>
@ -30,117 +30,32 @@
<!-- tab -->
<el-tab-pane :name="MsgType.Follow">
<template #label>
<span><Icon icon="ep:star" /> 关注时回复</span>
<el-row align="middle"><Icon icon="ep:star" class="mr-2px" /> 关注时回复</el-row>
</template>
</el-tab-pane>
<el-tab-pane :name="MsgType.Message">
<template #label>
<span><Icon icon="ep:chat-line-round" /> 消息回复</span>
<el-row align="middle"><Icon icon="ep:chat-line-round" class="mr-2px" /> 消息回复</el-row>
</template>
</el-tab-pane>
<el-tab-pane :name="MsgType.Keyword">
<template #label>
<span><Icon icon="fa:newspaper-o" /> 关键词回复</span>
<el-row align="middle"><Icon icon="fa:newspaper-o" class="mr-2px" /> 关键词回复</el-row>
</template>
</el-tab-pane>
</el-tabs>
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column
label="请求消息类型"
align="center"
prop="requestMessageType"
v-if="msgType === MsgType.Message"
/>
<el-table-column
label="关键词"
align="center"
prop="requestKeyword"
v-if="msgType === MsgType.Keyword"
/>
<el-table-column
label="匹配类型"
align="center"
prop="requestMatch"
v-if="msgType === MsgType.Keyword"
>
<template #default="scope">
<dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
</template>
</el-table-column>
<el-table-column label="回复消息类型" align="center">
<template #default="scope">
<dict-tag :type="DICT_TYPE.MP_MESSAGE_TYPE" :value="scope.row.responseMessageType" />
</template>
</el-table-column>
<el-table-column label="回复内容" align="center">
<template #default="scope">
<div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
<div v-else-if="scope.row.responseMessageType === 'voice'">
<WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" />
</div>
<div v-else-if="scope.row.responseMessageType === 'image'">
<a target="_blank" :href="scope.row.responseMediaUrl">
<img :src="scope.row.responseMediaUrl" style="width: 100px" />
</a>
</div>
<div
v-else-if="
scope.row.responseMessageType === 'video' ||
scope.row.responseMessageType === 'shortvideo'
"
>
<WxVideoPlayer
v-if="scope.row.responseMediaUrl"
:url="scope.row.responseMediaUrl"
style="margin-top: 10px"
/>
</div>
<div v-else-if="scope.row.responseMessageType === 'news'">
<WxNews :articles="scope.row.responseArticles" />
</div>
<div v-else-if="scope.row.responseMessageType === 'music'">
<WxMusic
:title="scope.row.responseTitle"
:description="scope.row.responseDescription"
:thumb-media-url="scope.row.responseThumbMediaUrl"
:music-url="scope.row.responseMusicUrl"
:hq-music-url="scope.row.responseHqMusicUrl"
/>
</div>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
type="primary"
link
@click="handleUpdate(scope.row)"
v-hasPermi="['mp:auto-reply:update']"
>
修改
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
v-hasPermi="['mp:auto-reply:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<ReplyTable
:loading="loading"
:list="list"
:msg-type="msgType"
@on-update="onUpdate"
@on-delete="onDelete"
/>
<!-- 添加或修改自动回复的对话框 -->
<el-dialog :title="title" v-model="showReplyFormDialog" width="800px" destroy-on-close>
<!-- TODO @Dhb52 -->
<el-dialog :title="dialogTitle" v-model="showFormDialog" width="800px" destroy-on-close>
<el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px">
<el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message">
<el-select v-model="replyForm.requestMessageType" placeholder="请选择">
@ -156,7 +71,7 @@
<el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword">
<el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable>
<el-option
v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
v-for="dict in getIntDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
@ -172,45 +87,28 @@
</el-form>
<template #footer>
<el-button @click="cancel"> </el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button type="primary" @click="onSubmit"> </el-button>
</template>
</el-dialog>
</ContentWrap>
</template>
<script setup lang="ts" name="MpAutoReply">
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
import WxMusic from '@/views/mp/components/wx-music/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
import * as MpAutoReplyApi from '@/api/mp/autoReply'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
import { ContentWrap } from '@/components/ContentWrap'
import type { TabPaneName } from 'element-plus'
import ReplyTable from './components/ReplyTable.vue'
import { MsgType, ReplyForm, ObjData } from './components/types'
const message = useMessage() //
const message = useMessage()
const formRef = ref()
// Follow: Message: Keyword:
// tab.nameenumapi
enum MsgType {
Follow = 1,
Message = 2,
Keyword = 3
}
const msgType = ref<MsgType>(MsgType.Keyword)
//
const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link']
//
const loading = ref(true)
//
const total = ref(0)
//
const list = ref<any[]>([])
const msgType = ref<MsgType>(MsgType.Keyword) //
const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] //
const loading = ref(true) //
const total = ref(0) //
const list = ref<any[]>([]) //
const formRef = ref() // ref
//
interface QueryParams {
pageNo: number
@ -223,49 +121,9 @@ const queryParams: QueryParams = reactive({
accountId: undefined
})
//
const title = ref('')
//
const showReplyFormDialog = ref(false)
//
type ReplyType = 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link'
interface ReplyForm {
// relation:
id?: number
accountId?: number
type?: MsgType
// request:
requestMessageType?: ReplyType
requestMatch?: number
requestKeyword?: string
// response:
responseMessageType?: ReplyType
responseContent?: string
responseMediaId?: number
responseMediaUrl?: string
responseTitle?: string
responseDescription?: number
responseThumbMediaId?: string
responseThumbMediaUrl?: string
responseArticles?: any[]
responseMusicUrl?: string
responseHqMusicUrl?: string
}
interface ObjData {
type: ReplyType
accountId?: number
content?: string
mediaId?: number
url?: string
title?: string
description?: string
thumbMediaId?: number
thumbMediaUrl?: string
articles?: any[]
musicUrl?: string
hqMusicUrl?: string
}
const replyForm = ref<ReplyForm>({})
const dialogTitle = ref('') //
const showFormDialog = ref(false) //
const replyForm = ref<ReplyForm>({}) //
//
const objData = ref<ObjData>({
type: 'text',
@ -277,6 +135,7 @@ const rules = {
requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }]
}
/** 侦听账号变化 */
const onAccountChanged = (id?: number) => {
queryParams.accountId = id
getList()
@ -284,7 +143,7 @@ const onAccountChanged = (id?: number) => {
/** 查询列表 */
const getList = async () => {
loading.value = false
loading.value = true
try {
const data = await MpAutoReplyApi.getAutoReplyPage({
...queryParams,
@ -303,13 +162,13 @@ const handleQuery = () => {
getList()
}
const handleTabChange = (tabName: TabPaneName) => {
const onTabChange = (tabName: TabPaneName) => {
msgType.value = tabName as MsgType
handleQuery()
}
/** 新增按钮操作 */
const handleAdd = () => {
const onCreate = () => {
reset()
//
objData.value = {
@ -317,15 +176,15 @@ const handleAdd = () => {
accountId: queryParams.accountId
}
title.value = '新增自动回复'
showReplyFormDialog.value = true
dialogTitle.value = '新增自动回复'
showFormDialog.value = true
}
/** 修改按钮操作 */
const handleUpdate = async (row: any) => {
const onUpdate = async (id: number) => {
reset()
const data = await MpAutoReplyApi.getAutoReply(row.id)
const data = await MpAutoReplyApi.getAutoReply(id)
//
replyForm.value = { ...data }
delete replyForm.value['responseMessageType']
@ -350,11 +209,19 @@ const handleUpdate = async (row: any) => {
}
//
title.value = '修改自动回复'
showReplyFormDialog.value = true
dialogTitle.value = '修改自动回复'
showFormDialog.value = true
}
const handleSubmit = async () => {
/** 删除按钮操作 */
const onDelete = async (id: number) => {
await message.confirm('是否确认删除此数据?')
await MpAutoReplyApi.deleteAutoReply(id)
await getList()
message.success('删除成功')
}
const onSubmit = async () => {
const valid = await formRef.value?.validate()
if (!valid) return
@ -380,7 +247,7 @@ const handleSubmit = async () => {
message.success('新增成功')
}
showReplyFormDialog.value = false
showFormDialog.value = false
await getList()
}
@ -399,14 +266,7 @@ const reset = () => {
//
const cancel = () => {
showReplyFormDialog.value = false
showFormDialog.value = false
reset()
}
const handleDelete = async (row) => {
await message.confirm('是否确认删除此数据?')
await MpAutoReplyApi.deleteAutoReply(row.id)
await getList()
message.success('删除成功')
}
</script>

View File

@ -22,11 +22,14 @@ const handleQuery = async () => {
//
if (accountList.value.length > 0) {
account.id = accountList.value[0].id
account.name = accountList.value[0].name
emit('change', account.id, account.name)
}
}
const onChanged = () => {
const onChanged = (id?: number) => {
const found = accountList.value.find((v) => v.id === id)
account.name = found ? found.name : ''
emit('change', account.id, account.name)
}

View File

@ -1,5 +1,5 @@
<!--
微信消息 - 定位
微信消息 - 定位TODO @Dhb52 目前未启用
-->
<template>
<div>

View File

@ -126,7 +126,7 @@
</div>
<div class="msg-send" v-loading="sendLoading">
<WxReplySelect ref="replySelectRef" :objData="objData" />
<el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
<el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button>
</div>
</ContentWrap>
</template>
@ -231,12 +231,8 @@ const sendMsg = async () => {
list.value = [...list.value, ...[data]]
scrollToBottom()
//ts
// tab
const deleteObj = replySelectRef.value?.deleteObj
if (deleteObj) {
deleteObj()
}
//
replySelectRef.value?.clear()
}
const loadingMore = () => {
@ -333,6 +329,7 @@ const scrollToBottom = () => {
.send-but {
float: right;
margin-top: 8px !important;
margin-top: 8px;
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1,171 @@
<template>
<el-tab-pane name="image">
<template #label>
<el-row align="middle"><Icon icon="ep:picture" class="mr-5px" /> 图片</el-row>
</template>
<!-- 情况一已经选择好素材或者上传好图片 -->
<div class="select-item" v-if="objData.url">
<img class="material-img" :src="objData.url" />
<p class="item-name" v-if="objData.name">{{ objData.name }}</p>
<el-row class="ope-row" justify="center">
<el-button type="danger" circle @click="onDelete">
<Icon icon="ep:delete" />
</el-button>
</el-row>
</div>
<!-- 情况二未做完上述操作 -->
<el-row v-else style="text-align: center" align="middle">
<!-- 选择素材 -->
<el-col :span="12" class="col-select">
<el-button type="success" @click="showDialog = true">
素材库选择 <Icon icon="ep:circle-check" />
</el-button>
<el-dialog
title="选择图片"
v-model="showDialog"
width="90%"
append-to-body
destroy-on-close
>
<WxMaterialSelect :objData="objData" @select-material="selectMaterial" />
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12" class="col-add">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeImageUpload"
:on-success="onUploadSuccess"
>
<el-button type="primary">上传图片</el-button>
<template #tip>
<span>
<div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式大小不超过 2M</div>
</span>
</template>
</el-upload>
</el-col>
</el-row>
</el-tab-pane>
</template>
<script setup lang="ts">
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import type { UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth'
import { ObjData } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } //
const props = defineProps<{
modelValue: ObjData
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData)
}>()
const objData = computed<ObjData>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const fileList = ref([])
const uploadData = reactive({
accountId: objData.value.accountId,
type: 'image',
title: '',
introduction: ''
})
const beforeImageUpload = (rawFile: UploadRawFile) =>
useBeforeUpload(MaterialType.Image, 2)(rawFile)
const onUploadSuccess = (res: any) => {
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
//
fileList.value = []
uploadData.title = ''
uploadData.introduction = ''
//
selectMaterial(res.data)
}
const onDelete = () => {
objData.value.mediaId = null
objData.value.url = null
objData.value.name = null
}
const selectMaterial = (item) => {
showDialog.value = false
objData.value.type = 'image'
objData.value.mediaId = item.mediaId
objData.value.url = item.url
objData.value.name = item.name
}
</script>
<style lang="scss" scoped>
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
.material-img {
width: 100%;
}
.item-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
.item-infos {
width: 30%;
margin: auto;
}
.ope-row {
padding-top: 10px;
text-align: center;
}
}
.col-select {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%;
}
.col-add {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%;
float: right;
.el-upload__tip {
line-height: 18px;
text-align: center;
}
}
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<el-tab-pane name="music">
<template #label>
<el-row align="middle"><Icon icon="ep:service" />音乐</el-row>
</template>
<el-row align="middle" justify="center">
<el-col :span="6">
<el-row align="middle" justify="center" class="thumb-div">
<el-col :span="24">
<el-row align="middle" justify="center">
<img style="width: 100px" v-if="objData.thumbMediaUrl" :src="objData.thumbMediaUrl" />
<icon v-else icon="ep:plus" />
</el-row>
<el-row align="middle" justify="center" style="margin-top: 2%">
<div class="thumb-but">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeImageUpload"
:on-success="onUploadSuccess"
>
<template #trigger>
<el-button type="primary" link>本地上传</el-button>
</template>
<el-button type="primary" link @click="showDialog = true" style="margin-left: 5px"
>素材库选择
</el-button>
</el-upload>
</div>
</el-row>
</el-col>
</el-row>
<el-dialog
title="选择图片"
v-model="showDialog"
width="80%"
append-to-body
destroy-on-close
>
<WxMaterialSelect
:objData="{ type: 'image', accountId: objData.accountId }"
@select-material="selectMaterial"
/>
</el-dialog>
</el-col>
<el-col :span="18">
<el-input v-model="objData.title" placeholder="请输入标题" />
<div style="margin: 20px 0"></div>
<el-input v-model="objData.description" placeholder="请输入描述" />
</el-col>
</el-row>
<div style="margin: 20px 0"></div>
<el-input v-model="objData.musicUrl" placeholder="请输入音乐链接" />
<div style="margin: 20px 0"></div>
<el-input v-model="objData.hqMusicUrl" placeholder="请输入高质量音乐链接" />
</el-tab-pane>
</template>
<script setup lang="ts">
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import type { UploadRawFile } from 'element-plus'
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import { getAccessToken } from '@/utils/auth'
import { ObjData } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } //
const props = defineProps<{
modelValue: ObjData
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData)
}>()
const objData = computed<ObjData>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const fileList = ref([])
const uploadData = reactive({
accountId: objData.value.accountId,
type: 'thumb', // thumb
title: '',
introduction: ''
})
const beforeImageUpload = (rawFile: UploadRawFile) =>
useBeforeUpload(MaterialType.Image, 2)(rawFile)
const onUploadSuccess = (res: any) => {
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
//
fileList.value = []
uploadData.title = ''
uploadData.introduction = ''
//
selectMaterial(res.data)
}
const selectMaterial = (item: any) => {
showDialog.value = false
objData.value.thumbMediaId = item.mediaId
objData.value.thumbMediaUrl = item.url
}
</script>

View File

@ -0,0 +1,78 @@
<template>
<el-tab-pane name="news">
<template #label>
<el-row align="middle"><Icon icon="ep:reading" /> 图文</el-row>
</template>
<el-row>
<div class="select-item" v-if="objData.articles?.length > 0">
<WxNews :articles="objData.articles" />
<el-col class="ope-row">
<el-button type="danger" circle @click="onDelete">
<Icon icon="ep:delete" />
</el-button>
</el-col>
</div>
<!-- 选择素材 -->
<el-col :span="24" v-if="!objData.content">
<el-row style="text-align: center" align="middle">
<el-col :span="24">
<el-button type="success" @click="showDialog = true">
{{ newsType === NewsType.Published ? '选择已发布图文' : '选择草稿箱图文' }}
<Icon icon="ep:circle-check" />
</el-button>
</el-col>
</el-row>
</el-col>
<el-dialog title="选择图文" v-model="showDialog" width="90%" append-to-body destroy-on-close>
<WxMaterialSelect
:objData="objData"
@select-material="selectMaterial"
:newsType="newsType"
/>
</el-dialog>
</el-row>
</el-tab-pane>
</template>
<script setup lang="ts">
import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import { ObjData, NewsType } from './types'
const props = defineProps<{
modelValue: ObjData
newsType: NewsType
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData)
}>()
const objData = computed<ObjData>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const selectMaterial = (item: any) => {
showDialog.value = false
objData.value.articles = item.content.newsItem
}
const onDelete = () => {
objData.value.articles = []
}
</script>
<style lang="scss" scoped>
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
.ope-row {
padding-top: 10px;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<el-tab-pane name="text">
<template #label>
<el-row align="middle"><Icon icon="ep:document" /> 文本</el-row>
</template>
<el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="content" />
</el-tab-pane>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: string | null
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: string | null)
(e: 'input', v: string | null)
}>()
const content = computed<string | null>({
get: () => props.modelValue,
set: (val: string | null) => {
emit('update:modelValue', val)
emit('input', val)
}
})
</script>

View File

@ -0,0 +1,132 @@
<template>
<el-tab-pane name="video">
<template #label>
<el-row align="middle"><Icon icon="ep:share" /> 视频</el-row>
</template>
<el-row>
<el-input v-model="objData.title" class="input-margin-bottom" placeholder="请输入标题" />
<el-input
class="input-margin-bottom"
v-model="objData.description"
placeholder="请输入描述"
/>
<el-row class="ope-row" justify="center">
<WxVideoPlayer v-if="objData.url" :url="objData.url" />
</el-row>
<el-col>
<el-row style="text-align: center" align="middle">
<!-- 选择素材 -->
<el-col :span="12">
<el-button type="success" @click="showDialog = true">
素材库选择 <Icon icon="ep:circle-check" />
</el-button>
<el-dialog
title="选择视频"
v-model="showDialog"
width="90%"
append-to-body
destroy-on-close
>
<WxMaterialSelect :objData="objData" @select-material="selectMaterial" />
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVideoUpload"
:on-success="onUploadSuccess"
>
<el-button type="primary">新建视频 <Icon icon="ep:upload" /></el-button>
</el-upload>
</el-col>
</el-row>
</el-col>
</el-row>
</el-tab-pane>
</template>
<script setup lang="ts">
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import type { UploadRawFile } from 'element-plus'
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import { getAccessToken } from '@/utils/auth'
import { ObjData } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() }
const props = defineProps<{
modelValue: ObjData
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData)
}>()
const objData = computed<ObjData>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const fileList = ref([])
const uploadData = reactive({
accountId: objData.value.accountId,
type: 'video',
title: '',
introduction: ''
})
const beforeVideoUpload = (rawFile: UploadRawFile) =>
useBeforeUpload(MaterialType.Video, 10)(rawFile)
const onUploadSuccess = (res: any) => {
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
//
fileList.value = []
uploadData.title = ''
uploadData.introduction = ''
selectMaterial(res.data)
}
/** 选择素材后设置 */
const selectMaterial = (item: any) => {
showDialog.value = false
objData.value.mediaId = item.mediaId
objData.value.url = item.url
objData.value.name = item.name
// titleintroduction item tempObjItem titleintroduction
if (item.title) {
objData.value.title = item.title || ''
}
if (item.introduction) {
objData.value.description = item.introduction || ''
}
}
</script>
<style lang="scss" scoped>
.input-margin-bottom {
margin-bottom: 2%;
}
.ope-row {
width: 100%;
padding-top: 10px;
text-align: center;
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<el-tab-pane name="voice">
<template #label>
<el-row align="middle"><Icon icon="ep:phone" /> 语音</el-row>
</template>
<div class="select-item2" v-if="objData.url">
<p class="item-name">{{ objData.name }}</p>
<el-row class="ope-row" justify="center">
<WxVoicePlayer :url="objData.url" />
</el-row>
<el-row class="ope-row" justify="center">
<el-button type="danger" circle @click="onDelete"><Icon icon="ep:delete" /></el-button>
</el-row>
</div>
<el-row v-else style="text-align: center">
<!-- 选择素材 -->
<el-col :span="12" class="col-select">
<el-button type="success" @click="showDialog = true">
素材库选择<Icon icon="ep:circle-check" />
</el-button>
<el-dialog
title="选择语音"
v-model="showDialog"
width="90%"
append-to-body
destroy-on-close
>
<WxMaterialSelect :objData="objData" @select-material="selectMaterial" />
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12" class="col-add">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVoiceUpload"
:on-success="onUploadSuccess"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">
格式支持 mp3/wma/wav/amr文件大小不超过 2M播放长度不超过 60s
</div>
</template>
</el-upload>
</el-col>
</el-row>
</el-tab-pane>
</template>
<script setup lang="ts">
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import type { UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth'
import { ObjData } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } //
const props = defineProps<{
modelValue: ObjData
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData)
}>()
const objData = computed<ObjData>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const fileList = ref([])
const uploadData = reactive({
accountId: objData.value.accountId,
type: 'voice',
title: '',
introduction: ''
})
const beforeVoiceUpload = (rawFile: UploadRawFile) =>
useBeforeUpload(MaterialType.Voice, 10)(rawFile)
const onUploadSuccess = (res: any) => {
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
//
fileList.value = []
uploadData.title = ''
uploadData.introduction = ''
//
selectMaterial(res.data)
}
const onDelete = () => {
objData.value.mediaId = null
objData.value.url = null
objData.value.name = null
}
const selectMaterial = (item: ObjData) => {
showDialog.value = false
objData.value.type = 'voice'
objData.value.mediaId = item.mediaId
objData.value.url = item.url
objData.value.name = item.name
}
</script>
<style lang="scss" scoped>
.select-item2 {
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
.item-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
.ope-row {
width: 100%;
padding-top: 10px;
text-align: center;
}
}
.col-select {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%;
}
.col-add {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%;
float: right;
.el-upload__tip {
line-height: 18px;
text-align: center;
}
}
}
</style>

View File

@ -0,0 +1,25 @@
type ReplyType = '' | 'news' | 'image' | 'voice' | 'video' | 'music' | 'text'
interface ObjData {
accountId: number
type: ReplyType
name: string | null
content: string | null
mediaId: string | null
url: string | null
title: string | null
description: string | null
thumbMediaId: string | null
thumbMediaUrl: string | null
musicUrl: string | null
hqMusicUrl: string | null
introduction: string | null
articles: any[]
}
enum NewsType {
Published = '1',
Draft = '2'
}
export { ObjData, NewsType }

View File

@ -8,607 +8,64 @@
支持发送视频消息时支持新建视频
-->
<template>
<el-tabs type="border-card" v-model="objDataRef.type" @tab-click="handleClick">
<el-tabs type="border-card" v-model="objData.type" @tab-click="onTabClick">
<!-- 类型 1文本 -->
<el-tab-pane name="text">
<template #label>
<el-row align="middle"><Icon icon="ep:document" /> 文本</el-row>
</template>
<el-input
type="textarea"
:rows="5"
placeholder="请输入内容"
v-model="objDataRef.content"
@input="inputContent"
/>
</el-tab-pane>
<TabText v-model="objData.content" />
<!-- 类型 2图片 -->
<el-tab-pane name="image">
<template #label>
<el-row align="middle"><Icon icon="ep:picture" class="mr-5px" /> 图片</el-row>
</template>
<!-- 情况一已经选择好素材或者上传好图片 -->
<div class="select-item" v-if="objDataRef.url">
<img class="material-img" :src="objDataRef.url" />
<p class="item-name" v-if="objDataRef.name">{{ objDataRef.name }}</p>
<el-row class="ope-row" justify="center">
<el-button type="danger" circle @click="deleteObj">
<Icon icon="ep:delete" />
</el-button>
</el-row>
</div>
<!-- 情况二未做完上述操作 -->
<el-row v-else style="text-align: center" align="middle">
<!-- 选择素材 -->
<el-col :span="12" class="col-select">
<el-button type="success" @click="openMaterial">
素材库选择 <Icon icon="ep:circle-check" />
</el-button>
<el-dialog title="选择图片" v-model="dialogImageVisible" width="90%" append-to-body>
<WxMaterialSelect :obj-data="objDataRef" @select-material="selectMaterial" />
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12" class="col-add">
<el-upload
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeImageUpload"
:on-success="handleUploadSuccess"
>
<el-button type="primary">上传图片</el-button>
<template #tip>
<span>
<div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式大小不超过 2M</div>
</span>
</template>
</el-upload>
</el-col>
</el-row>
</el-tab-pane>
<TabImage v-model="objData" />
<!-- 类型 3语音 -->
<el-tab-pane name="voice">
<template #label>
<el-row align="middle"><Icon icon="ep:phone" /> 语音</el-row>
</template>
<div class="select-item2" v-if="objDataRef.url">
<p class="item-name">{{ objDataRef.name }}</p>
<div class="item-infos">
<WxVoicePlayer :url="objDataRef.url" />
</div>
<el-row class="ope-row" justify="center">
<el-button type="danger" circle @click="deleteObj"><Icon icon="ep:delete" /></el-button>
</el-row>
</div>
<el-row v-else style="text-align: center">
<!-- 选择素材 -->
<el-col :span="12" class="col-select">
<el-button type="success" @click="openMaterial">
素材库选择<Icon icon="ep:circle-check" />
</el-button>
<el-dialog title="选择语音" v-model="dialogVoiceVisible" width="90%" append-to-body>
<WxMaterialSelect :objData="objData" @select-material="selectMaterial" />
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12" class="col-add">
<el-upload
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVoiceUpload"
:on-success="handleUploadSuccess"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">
格式支持 mp3/wma/wav/amr文件大小不超过 2M播放长度不超过 60s
</div>
</template>
</el-upload>
</el-col>
</el-row>
</el-tab-pane>
<TabVoice v-model="objData" />
<!-- 类型 4视频 -->
<el-tab-pane name="video">
<template #label>
<el-row align="middle"><Icon icon="ep:share" /> 视频</el-row>
</template>
<el-row>
<el-input
v-model="objDataRef.title"
class="input-margin-bottom"
placeholder="请输入标题"
@input="inputContent"
/>
<el-input
class="input-margin-bottom"
v-model="objDataRef.description"
placeholder="请输入描述"
@input="inputContent"
/>
<div style="text-align: center">
<WxVideoPlayer v-if="objDataRef.url" :url="objDataRef.url" />
</div>
<el-col>
<el-row style="text-align: center" align="middle">
<!-- 选择素材 -->
<el-col :span="12">
<el-button type="success" @click="openMaterial">
素材库选择 <Icon icon="ep:circle-check" />
</el-button>
<el-dialog title="选择视频" v-model="dialogVideoVisible" width="90%" append-to-body>
<WxMaterialSelect :objData="objDataRef" @select-material="selectMaterial" />
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12">
<el-upload
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVideoUpload"
:on-success="handleUploadSuccess"
>
<el-button type="primary">新建视频 <Icon icon="ep:upload" /></el-button>
</el-upload>
</el-col>
</el-row>
</el-col>
</el-row>
</el-tab-pane>
<TabVideo v-model="objData" />
<!-- 类型 5图文 -->
<el-tab-pane name="news">
<template #label>
<el-row align="middle"><Icon icon="ep:reading" /> 图文</el-row>
</template>
<el-row>
<div class="select-item" v-if="objDataRef.articles?.length > 0">
<WxNews :articles="objDataRef.articles" />
<el-col class="ope-row">
<el-button type="danger" circle @click="deleteObj">
<Icon icon="ep:delete" />
</el-button>
</el-col>
</div>
<!-- 选择素材 -->
<el-col :span="24" v-if="!objDataRef.content">
<el-row style="text-align: center" align="middle">
<el-col :span="24">
<el-button type="success" @click="openMaterial">
{{ newsType === '1' ? '选择已发布图文' : '选择草稿箱图文' }}
<icon icon="ep:circle-check" />
</el-button>
</el-col>
</el-row>
</el-col>
<el-dialog title="选择图文" v-model="dialogNewsVisible" width="90%" append-to-body>
<WxMaterialSelect
:objData="objDataRef"
@select-material="selectMaterial"
:newsType="newsType"
/>
</el-dialog>
</el-row>
</el-tab-pane>
<TabNews v-model="objData" :news-type="newsType" />
<!-- 类型 6音乐 -->
<el-tab-pane name="music">
<template #label>
<el-row align="middle"><Icon icon="ep:service" />音乐</el-row>
</template>
<el-row align="middle" justify="center">
<el-col :span="6">
<el-row align="middle" justify="center" class="thumb-div">
<el-col :span="24">
<el-row align="middle" justify="center">
<img
style="width: 100px"
v-if="objDataRef.thumbMediaUrl"
:src="objDataRef.thumbMediaUrl"
/>
<icon v-else icon="ep:plus" />
</el-row>
<el-row align="middle" justify="center" style="margin-top: 2%">
<div class="thumb-but">
<el-upload
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeThumbImageUpload"
:on-success="handleUploadSuccess"
>
<template #trigger>
<el-button type="primary" link>本地上传</el-button>
</template>
<el-button type="primary" link @click="openMaterial" style="margin-left: 5px"
>素材库选择
</el-button>
</el-upload>
</div>
</el-row>
</el-col>
</el-row>
<el-dialog title="选择图片" v-model="dialogThumbVisible" width="80%" append-to-body>
<WxMaterialSelect
:objData="{ type: 'image', accountId: objDataRef.accountId }"
@select-material="selectMaterial"
/>
</el-dialog>
</el-col>
<el-col :span="18">
<el-input v-model="objDataRef.title" placeholder="请输入标题" @input="inputContent" />
<div style="margin: 20px 0"></div>
<el-input
v-model="objDataRef.description"
placeholder="请输入描述"
@input="inputContent"
/>
</el-col>
</el-row>
<div style="margin: 20px 0"></div>
<el-input v-model="objDataRef.musicUrl" placeholder="请输入音乐链接" @input="inputContent" />
<div style="margin: 20px 0"></div>
<el-input
v-model="objDataRef.hqMusicUrl"
placeholder="请输入高质量音乐链接"
@input="inputContent"
/>
</el-tab-pane>
<TabMusic v-model="objData" />
</el-tabs>
</template>
<script lang="ts" name="WxReplySelect">
import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
import { getAccessToken } from '@/utils/auth'
import { defineComponent } from 'vue'
<script setup lang="ts" name="WxReplySelect">
import { ObjData, NewsType } from './components/types'
import TabText from './components/TabText.vue'
import TabImage from './components/TabImage.vue'
import TabVoice from './components/TabVoice.vue'
import TabVideo from './components/TabVideo.vue'
import TabNews from './components/TabNews.vue'
import TabMusic from './components/TabMusic.vue'
export default defineComponent({
components: {
WxNews,
WxMaterialSelect,
WxVoicePlayer,
WxVideoPlayer
},
props: {
objData: {
//
type: Object, // Object 便
required: true
},
newsType: {
// 12稿
type: String,
default: '1'
}
},
setup(props) {
const objDataRef = reactive(props.objData)
const message = useMessage() //
const tempObj = new Map().set(objDataRef.type, Object.assign({}, objDataRef))
// ========== ==========
const dialogNewsVisible = ref(false) //
const dialogImageVisible = ref(false) //
const dialogVoiceVisible = ref(false) //
const dialogVideoVisible = ref(false) //
const dialogThumbVisible = ref(false) //
// ========== ==========
const fileList = ref([])
const uploadData = reactive({
accountId: undefined,
type: objDataRef.type,
title: '',
introduction: ''
})
const actionUrl = ref(
import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
)
const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) //
const beforeThumbImageUpload = (file) => {
const isType =
file.type === 'image/jpeg' ||
file.type === 'image/png' ||
file.type === 'image/gif' ||
file.type === 'image/bmp' ||
file.type === 'image/jpg'
if (!isType) {
message.error('上传图片格式不对!')
return false
}
const isLt = file.size / 1024 / 1024 < 2
if (!isLt) {
message.error('上传图片大小不能超过 2M!')
return false
}
uploadData.accountId = objDataRef.accountId
return true
}
const beforeVoiceUpload = (file) => {
//
const isType =
file.type === 'audio/mp3' ||
file.type === 'audio/mpeg' ||
file.type === 'audio/wma' ||
file.type === 'audio/wav' ||
file.type === 'audio/amr'
if (!isType) {
message.error('上传语音格式不对!' + file.type)
return false
}
//
const isLt = file.size / 1024 / 1024 < 2
if (!isLt) {
message.error('上传语音大小不能超过 2M!')
return false
}
uploadData.accountId = objDataRef.accountId
return true
}
const beforeImageUpload = (file) => {
//
const isType =
file.type === 'image/jpeg' ||
file.type === 'image/png' ||
file.type === 'image/gif' ||
file.type === 'image/bmp' ||
file.type === 'image/jpg'
if (!isType) {
message.error('上传图片格式不对!')
return false
}
//
const isLt = file.size / 1024 / 1024 < 2
if (!isLt) {
message.error('上传图片大小不能超过 2M!')
return false
}
uploadData.accountId = objDataRef.accountId
return true
}
const beforeVideoUpload = (file) => {
//
const isType = file.type === 'video/mp4'
if (!isType) {
message.error('上传视频格式不对!')
return false
}
//
const isLt = file.size / 1024 / 1024 < 10
if (!isLt) {
message.error('上传视频大小不能超过 10M!')
return false
}
uploadData.accountId = objDataRef.accountId
return true
}
const handleUploadSuccess = (response) => {
if (response.code !== 0) {
message.error('上传出错:' + response.msg)
return false
}
interface Props {
objData: ObjData
newsType?: NewsType
}
const props = withDefaults(defineProps<Props>(), {
newsType: () => NewsType.Published
})
//
fileList.value = []
uploadData.title = ''
uploadData.introduction = ''
const objData = reactive(props.objData)
// TODO @Dhb52Tab 12 TabXXX Form objData MusicMessageFormImageMessageForm
// const tempObj = new Map().set(objData.type, Object.assign({}, objData))
//
let item = response.data
selectMaterial(item)
}
/**
* 切换消息类型的 tab
*
* @param tab tab 没用 暂时删了tab
*/
const handleClick = () => {
//
uploadData.type = objDataRef.type
if (uploadData.type === 'music') {
//
uploadData.type = 'thumb'
}
/** 切换消息类型的 tab */
const onTabClick = () => {
clear()
}
// tempObj objDataRef
let tempObjItem = tempObj.get(objDataRef.type)
if (tempObjItem) {
objDataRef.content = tempObjItem.content ? tempObjItem.content : null
objDataRef.mediaId = tempObjItem.mediaId ? tempObjItem.mediaId : null
objDataRef.url = tempObjItem.url ? tempObjItem.url : null
objDataRef.name = tempObjItem.url ? tempObjItem.name : null
objDataRef.title = tempObjItem.title ? tempObjItem.title : null
objDataRef.description = tempObjItem.description ? tempObjItem.description : null
return
}
// objDataRef
// 使 $set input
objDataRef.content = ''
objDataRef.mediaId = ''
objDataRef.url = ''
objDataRef.title = ''
objDataRef.description = ''
}
/**
* 选择素材将设置设置到 objDataRef 变量
*
* @param item 素材
*/
const selectMaterial = (item) => {
//
closeMaterial()
/** 清除除了`type`的字段 */
const clear = () => {
objData.content = ''
objData.mediaId = ''
objData.url = ''
objData.title = ''
objData.description = ''
objData.articles = []
}
// tempObjItem
let tempObjItem = {
type: '',
articles: [],
thumbMediaId: '',
thumbMediaUrl: '',
introduction: '',
title: '',
musicUrl: '',
hqMusicUrl: '',
mediaId: '',
url: '',
name: '',
description: ''
}
tempObjItem.type = objDataRef.type
if (objDataRef.type === 'news') {
tempObjItem.articles = item.content.newsItem
objDataRef.articles = item.content.newsItem
} else if (objDataRef.type === 'music') {
//
tempObjItem.thumbMediaId = item.mediaId
objDataRef.thumbMediaId = item.mediaId
tempObjItem.thumbMediaUrl = item.url
objDataRef.thumbMediaUrl = item.url
// titleintroductionmusicUrlhqMusicUrl objDataRef tempObjItem
tempObjItem.title = objDataRef.title || ''
tempObjItem.introduction = objDataRef.introduction || ''
tempObjItem.musicUrl = objDataRef.musicUrl || ''
tempObjItem.hqMusicUrl = objDataRef.hqMusicUrl || ''
} else if (objDataRef.type === 'image' || objDataRef.type === 'voice') {
tempObjItem.mediaId = item.mediaId
objDataRef.mediaId = item.mediaId
tempObjItem.url = item.url
objDataRef.url = item.url
tempObjItem.name = item.name
objDataRef.name = item.name
} else if (objDataRef.type === 'video') {
tempObjItem.mediaId = item.mediaId
objDataRef.mediaId = item.mediaId
tempObjItem.url = item.url
objDataRef.url = item.url
tempObjItem.name = item.name
objDataRef.name = item.name
// titleintroduction item tempObjItem titleintroduction
if (item.title) {
objDataRef.title = item.title || ''
tempObjItem.title = item.title || ''
}
if (item.introduction) {
objDataRef.description = item.introduction || '' // 使 description使 introduction
tempObjItem.description = item.introduction || ''
}
} else if (objDataRef.type === 'text') {
objDataRef.content = item.content || ''
}
//
tempObj.set(objDataRef.type, tempObjItem)
}
const openMaterial = () => {
if (objDataRef.type === 'news') {
dialogNewsVisible.value = true
} else if (objDataRef.type === 'image') {
dialogImageVisible.value = true
} else if (objDataRef.type === 'voice') {
dialogVoiceVisible.value = true
} else if (objDataRef.type === 'video') {
dialogVideoVisible.value = true
} else if (objDataRef.type === 'music') {
dialogThumbVisible.value = true
}
}
const closeMaterial = () => {
dialogNewsVisible.value = false
dialogImageVisible.value = false
dialogVoiceVisible.value = false
dialogVideoVisible.value = false
dialogThumbVisible.value = false
}
const deleteObj = () => {
if (objDataRef.type === 'news') {
objDataRef.articles = []
} else if (objDataRef.type === 'image') {
objDataRef.mediaId = null
objDataRef.url = null
objDataRef.name = null
} else if (objDataRef.type === 'voice') {
objDataRef.mediaId = null
objDataRef.url = null
objDataRef.name = null
} else if (objDataRef.type === 'video') {
objDataRef.mediaId = null
objDataRef.url = null
objDataRef.name = null
objDataRef.title = null
objDataRef.description = null
} else if (objDataRef.type === 'music') {
objDataRef.thumbMediaId = null
objDataRef.thumbMediaUrl = null
objDataRef.title = null
objDataRef.description = null
objDataRef.musicUrl = null
objDataRef.hqMusicUrl = null
} else if (objDataRef.type === 'text') {
objDataRef.content = null
}
//
tempObj.set(objDataRef.type, Object.assign({}, objDataRef))
}
/**
* 输入时缓存每次 objDataRef tempObj
*
* why不确定为什么 v-model="objDataRef.content" 不能自动缓存所以通过这样的方式
*/
const inputContent = () => {
//
tempObj.set(objDataRef.type, Object.assign({}, objDataRef))
}
return {
inputContent,
dialogNewsVisible,
deleteObj,
openMaterial,
handleClick,
beforeImageUpload,
beforeVoiceUpload,
handleUploadSuccess,
beforeVideoUpload,
selectMaterial,
dialogImageVisible,
dialogVoiceVisible,
dialogThumbVisible,
actionUrl,
objDataRef,
headers,
fileList,
beforeThumbImageUpload,
uploadData,
dialogVideoVisible
}
}
defineExpose({
clear
})
</script>
<style lang="scss" scoped>
.public-account-management {
.el-input {
width: 70%;
margin-right: 2%;
}
}
.pagination {
text-align: right;
margin-right: 25px;
}
.select-item {
width: 280px;
padding: 10px;

View File

@ -14,26 +14,24 @@
<div @click="playVideo()">
<!-- 提示 -->
<div>
<Icon icon="ep:video-play" class="mr-5px" />
<p>点击播放视频</p>
<Icon icon="ep:video-play" :size="32" class="mr-5px" />
<p class="text-sm">点击播放视频</p>
</div>
<!-- 弹窗播放 -->
<el-dialog v-model="dialogVideo" title="视频播放" width="40%" append-to-body>
<template #footer>
<video-player
v-if="dialogVideo"
class="video-player vjs-big-play-centered"
:src="url"
poster=""
crossorigin="anonymous"
playsinline
controls
:volume="0.6"
:height="320"
:playback-rates="[0.7, 1.0, 1.5, 2.0]"
/>
</template>
<el-dialog v-model="dialogVideo" title="视频播放" append-to-body>
<video-player
v-if="dialogVideo"
class="video-player vjs-big-play-centered"
:src="props.url"
poster=""
crossorigin="anonymous"
playsinline
controls
:volume="0.6"
:width="800"
:playback-rates="[0.7, 1.0, 1.5, 2.0]"
/>
<!-- 事件暫時沒用
@mounted="handleMounted"-->
<!-- @ready="handleEvent($event)"-->
@ -50,36 +48,24 @@
</div>
</template>
<script lang="ts" name="WxVideoPlayer">
//videojs6.0,6.0
<script setup lang="ts" name="WxVideoPlayer">
import 'video.js/dist/video-js.css'
import { defineComponent } from 'vue'
import { VideoPlayer } from '@videojs-player/vue'
import 'video.js/dist/video-js.css'
export default defineComponent({
components: {
VideoPlayer
},
props: {
url: {
// https://vjs.zencdn.net/v/oceans.mp4
type: String,
required: true
}
},
setup() {
// const videoPlayerRef = ref(null)
const dialogVideo = ref(false)
const handleEvent = (log) => {
console.log('Basic player event', log)
}
const playVideo = () => {
dialogVideo.value = true
}
return { handleEvent, playVideo, dialogVideo }
const props = defineProps({
url: {
type: String,
required: true
}
})
const dialogVideo = ref(false)
// const handleEvent = (log) => {
// console.log('Basic player event', log)
// }
const playVideo = () => {
dialogVideo.value = true
}
</script>

View File

@ -12,8 +12,8 @@
<template>
<div class="wx-voice-div" @click="playVoice">
<el-icon>
<Icon v-if="playing !== true" icon="ep:video-play" />
<Icon v-else icon="ep:video-pause" />
<Icon v-if="playing !== true" icon="ep:video-play" :size="32" />
<Icon v-else icon="ep:video-pause" :size="32" />
<span class="amr-duration" v-if="duration">{{ duration }} </span>
</el-icon>
<div v-if="content">
@ -25,7 +25,6 @@
<script setup lang="ts" name="WxVoicePlayer">
// amr amr https://www.npmjs.com/package/benz-amr-recorder
import BenzAMRRecorder from 'benz-amr-recorder'
const props = defineProps({
@ -90,6 +89,12 @@ const amrStop = () => {
padding: 5px;
background-color: #eaeaea;
border-radius: 10px;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.amr-duration {
font-size: 11px;

View File

@ -0,0 +1,182 @@
<template>
<div>
<p>封面:</p>
<div class="thumb-div">
<el-image
v-if="newsItem.thumbUrl"
style="width: 300px; max-height: 300px"
:src="newsItem.thumbUrl"
fit="contain"
/>
<Icon
v-else
icon="ep:plus"
class="avatar-uploader-icon"
:class="isFirst ? 'avatar' : 'avatar1'"
/>
<div class="thumb-but">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="onBeforeUpload"
:on-error="onUploadError"
:on-success="onUploadSuccess"
>
<template #trigger>
<el-button size="small" type="primary" :loading="isUploading" disabled="isUploading">
{{ isUploading ? '正在上传' : '本地上传' }}
</el-button>
</template>
<el-button
size="small"
type="primary"
@click="showImageDialog = true"
style="margin-left: 5px"
>
素材库选择
</el-button>
<template #tip>
<div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式大小不超过 2M</div>
</template>
</el-upload>
</div>
<el-dialog
title="选择图片"
v-model="showImageDialog"
width="80%"
append-to-body
destroy-on-close
>
<WxMaterialSelect
:objData="{ type: 'image', accountId: accountId }"
@select-material="onMaterialSelected"
/>
</el-dialog>
</div>
</div>
</template>
<script setup lang="ts">
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import { getAccessToken } from '@/utils/auth'
import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
import { NewsItem } from './types'
const message = useMessage()
// const UPLOAD_URL = 'http://localhost:8000/upload/' //
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' //
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } //
const props = defineProps<{
modelValue: NewsItem
isFirst: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: NewsItem)
}>()
const newsItem = computed<NewsItem>({
get() {
return props.modelValue
},
set(val) {
emit('update:modelValue', val)
}
})
const accountId = inject<number>('accountId')
const showImageDialog = ref(false)
const fileList = ref<UploadFiles>([])
interface UploadData {
type: 'image' | 'video' | 'audio'
accountId?: number
}
const uploadData: UploadData = reactive({
type: 'image',
accountId: accountId
})
const isUploading = ref(false)
/** 素材选择完成事件*/
const onMaterialSelected = (item: any) => {
showImageDialog.value = false
newsItem.value.thumbMediaId = item.mediaId
newsItem.value.thumbUrl = item.url
}
const onBeforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes(
rawFile.type
)
if (!isType) {
message.error('上传图片格式不对!')
return false
}
if (rawFile.size / 1024 / 1024 > 2) {
message.error('上传图片大小不能超过 2M!')
return false
}
//
return true
}
const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
//
fileList.value = []
// 稿
newsItem.value.thumbMediaId = res.data.mediaId
newsItem.value.thumbUrl = res.data.url
}
const onUploadError = (err: Error) => {
message.error('上传失败: ' + err.message)
}
</script>
<style lang="scss" scoped>
.el-upload__tip {
margin-left: 5px;
}
.thumb-div {
display: inline-block;
width: 100%;
text-align: center;
.avatar-uploader-icon {
width: 120px;
height: 120px;
font-size: 28px;
line-height: 120px;
color: #8c939d;
text-align: center;
border: 1px solid #d9d9d9;
}
.avatar {
width: 230px;
height: 120px;
}
.avatar1 {
width: 120px;
height: 120px;
}
.thumb-but {
margin: 5px;
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="waterfall" v-loading="props.loading">
<template v-for="item in props.list" :key="item.articleId">
<div class="waterfall-item" v-if="item.content && item.content.newsItem">
<WxNews :articles="item.content.newsItem" />
<!-- 操作按钮 -->
<el-row>
<el-button
type="success"
circle
@click="emit('publish', item)"
v-hasPermi="['mp:free-publish:submit']"
>
<Icon icon="fa:upload" />
</el-button>
<el-button
type="primary"
circle
@click="emit('update', item)"
v-hasPermi="['mp:draft:update']"
>
<Icon icon="ep:edit" />
</el-button>
<el-button
type="danger"
circle
@click="emit('delete', item)"
v-hasPermi="['mp:draft:delete']"
>
<Icon icon="ep:delete" />
</el-button>
</el-row>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import WxNews from '@/views/mp/components/wx-news/main.vue'
import { Article } from './types'
const props = defineProps<{
list: Article[]
loading: boolean
}>()
const emit = defineEmits<{
(e: 'publish', v: Article)
(e: 'update', v: Article)
(e: 'delete', v: Article)
}>()
</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;
}
}
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
</style>

View File

@ -0,0 +1,302 @@
<template>
<el-container>
<el-aside width="40%">
<div class="select-item">
<div v-for="(news, index) in newsList" :key="index">
<div
class="news-main father"
v-if="index === 0"
:class="{ activeAddNews: activeNewsIndex === index }"
@click="activeNewsIndex = index"
>
<div class="news-content">
<img class="material-img" :src="news.thumbUrl" />
<div class="news-content-title">{{ news.title }}</div>
</div>
<div class="child" v-if="newsList.length > 1">
<el-button type="info" circle size="small" @click="() => moveDownNews(index)">
<Icon icon="ep:arrow-down-bold" />
</el-button>
<el-button
v-if="isCreating"
type="danger"
circle
size="small"
@click="() => removeNews(index)"
>
<Icon icon="ep:delete" />
</el-button>
</div>
</div>
<div
class="news-main-item father"
v-if="index > 0"
:class="{ activeAddNews: activeNewsIndex === index }"
@click="activeNewsIndex = index"
>
<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" width="100%" />
</div>
</div>
<div class="child">
<el-button
v-if="newsList.length > index + 1"
circle
type="info"
size="small"
@click="() => moveDownNews(index)"
>
<Icon icon="ep:arrow-down-bold" />
</el-button>
<el-button
v-if="index > 0"
type="info"
circle
size="small"
@click="() => moveUpNews(index)"
>
<Icon icon="ep:arrow-up-bold" />
</el-button>
<el-button
v-if="isCreating"
type="danger"
size="small"
circle
@click="() => removeNews(index)"
>
<Icon icon="ep:delete" />
</el-button>
</div>
</div>
</div>
<el-row justify="center" class="ope-row">
<el-button
type="primary"
circle
@click="plusNews"
v-if="newsList.length < 8 && isCreating"
>
<Icon icon="ep:plus" />
</el-button>
</el-row>
</div>
</el-aside>
<el-main>
<div v-if="newsList.length > 0">
<!-- 标题作者原文地址 -->
<el-row :gutter="20">
<el-input v-model="activeNewsItem.title" placeholder="请输入标题(必填)" />
<el-input
v-model="activeNewsItem.author"
placeholder="请输入作者"
style="margin-top: 5px"
/>
<el-input
v-model="activeNewsItem.contentSourceUrl"
placeholder="请输入原文地址"
style="margin-top: 5px"
/>
</el-row>
<!-- 封面和摘要 -->
<el-row :gutter="20">
<el-col :span="12">
<CoverSelect v-model="activeNewsItem" :is-first="activeNewsIndex === 0" />
</el-col>
<el-col :span="12">
<p>摘要:</p>
<el-input
:rows="8"
type="textarea"
v-model="activeNewsItem.digest"
placeholder="请输入摘要"
class="digest"
maxlength="120"
/>
</el-col>
</el-row>
<!--富文本编辑器组件-->
<el-row>
<Editor v-model="activeNewsItem.content" :editor-config="editorConfig" />
</el-row>
</div>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { Editor } from '@/components/Editor'
import { createEditorConfig } from '../editor-config'
import CoverSelect from './CoverSelect.vue'
import { type NewsItem, createEmptyNewsItem } from './types'
const message = useMessage()
const props = defineProps<{
isCreating: boolean
modelValue: NewsItem[] | null
}>()
const accountId = inject<number>('accountId')
// ========== ==========
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' //
const editorConfig = createEditorConfig(UPLOAD_URL, accountId)
// v-model=newsList
const emit = defineEmits<{
(e: 'update:modelValue', v: NewsItem[])
}>()
const newsList = computed<NewsItem[]>({
get() {
return props.modelValue === null ? [createEmptyNewsItem()] : props.modelValue
},
set(val) {
emit('update:modelValue', val)
}
})
const activeNewsIndex = ref(0)
const activeNewsItem = computed<NewsItem>(() => newsList.value[activeNewsIndex.value])
//
const moveDownNews = (index: number) => {
const temp = newsList.value[index]
newsList.value[index] = newsList.value[index + 1]
newsList.value[index + 1] = temp
activeNewsIndex.value = index + 1
}
//
const moveUpNews = (index: number) => {
const temp = newsList.value[index]
newsList.value[index] = newsList.value[index - 1]
newsList.value[index - 1] = temp
activeNewsIndex.value = index - 1
}
// index
const removeNews = async (index: number) => {
try {
await message.confirm('确定删除该图文吗?')
newsList.value.splice(index, 1)
if (activeNewsIndex.value === index) {
activeNewsIndex.value = 0
}
} catch {}
}
//
const plusNews = () => {
newsList.value.push(createEmptyNewsItem())
activeNewsIndex.value = newsList.value.length - 1
}
</script>
<style lang="scss" scoped>
.ope-row {
padding-top: 5px;
margin-top: 5px;
text-align: center;
border-top: 1px solid #eaeaea;
}
.el-row {
margin-bottom: 20px;
}
.el-row:last-child {
margin-bottom: 0;
}
.digest {
display: inline-block;
width: 100%;
vertical-align: top;
}
/* 新增图文 */
.news-main {
width: 100%;
height: 120px;
margin: auto;
background-color: #fff;
}
.news-content {
position: relative;
width: 100%;
height: 120px;
background-color: #acadae;
}
.news-content-title {
position: absolute;
bottom: 0;
left: 0;
display: inline-block;
width: 98%;
height: 25px;
padding: 1%;
overflow: hidden;
font-size: 15px;
color: #fff;
text-overflow: ellipsis;
white-space: nowrap;
background-color: black;
opacity: 0.65;
}
.news-main-item {
width: 100%;
padding: 5px 0;
margin: auto;
background-color: #fff;
border-top: 1px solid #eaeaea;
}
.news-content-item {
position: relative;
margin-left: -3px;
}
.news-content-item-title {
display: inline-block;
width: 70%;
font-size: 12px;
}
.news-content-item-img {
display: inline-block;
width: 25%;
background-color: #acadae;
}
.select-item {
width: 60%;
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #eaeaea;
.activeAddNews {
border: 5px solid #2bb673;
}
}
.father .child {
position: relative;
bottom: 25px;
display: none;
text-align: center;
}
.father:hover .child {
display: block;
}
.material-img {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,7 @@
import type { Article, NewsItem, NewsItemList } from './types'
import { createEmptyNewsItem } from './types'
import DraftTable from './DraftTable.vue'
import NewsForm from './NewsForm.vue'
export { DraftTable, NewsForm, createEmptyNewsItem }
export type { Article, NewsItem, NewsItemList }

View File

@ -0,0 +1,40 @@
interface NewsItem {
title: string
thumbMediaId: string
author: string
digest: string
showCoverPic: string
content: string
contentSourceUrl: string
needOpenComment: string
onlyFansCanComment: string
thumbUrl: string
}
interface NewsItemList {
newsItem: NewsItem[]
}
interface Article {
mediaId: string
content: NewsItemList
updateTime: number
}
const createEmptyNewsItem = (): NewsItem => {
return {
title: '',
thumbMediaId: '',
author: '',
digest: '',
showCoverPic: '',
content: '',
contentSourceUrl: '',
needOpenComment: '',
onlyFansCanComment: '',
thumbUrl: ''
}
}
export type { Article, NewsItem, NewsItemList }
export { createEmptyNewsItem }

View File

@ -14,7 +14,13 @@
<WxAccountSelect @change="onAccountChanged" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain @click="handleAdd" v-hasPermi="['mp:draft:create']">
<el-button
type="primary"
plain
@click="handleAdd"
v-hasPermi="['mp:draft:create']"
:disabled="accountId === 0"
>
<Icon icon="ep:plus" />新增
</el-button>
</el-form-item>
@ -23,40 +29,13 @@
<!-- 列表 -->
<ContentWrap>
<div class="waterfall" v-loading="loading">
<template v-for="item in list" :key="item.articleId">
<div class="waterfall-item" v-if="item.content && item.content.newsItem">
<WxNews :articles="item.content.newsItem" />
<!-- 操作按钮 -->
<el-row class="ope-row">
<el-button
type="success"
circle
@click="handlePublish(item)"
v-hasPermi="['mp:free-publish:submit']"
>
<Icon icon="fa:upload" />
</el-button>
<el-button
type="primary"
circle
@click="handleUpdate(item)"
v-hasPermi="['mp:draft:update']"
>
<Icon icon="ep:edit" />
</el-button>
<el-button
type="danger"
circle
@click="handleDelete(item)"
v-hasPermi="['mp:draft:delete']"
>
<Icon icon="ep:delete" />
</el-button>
</el-row>
</div>
</template>
</div>
<DraftTable
:loading="loading"
:list="list"
@update="onUpdate"
@delete="onDelete"
@publish="onPublish"
/>
<!-- 分页记录 -->
<Pagination
:total="total"
@ -66,287 +45,102 @@
/>
</ContentWrap>
<div class="app-container">
<!-- 添加或修改草稿对话框 -->
<el-dialog
:title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
width="80%"
v-model="dialogNewsVisible"
:before-close="dialogNewsClose"
destroy-on-close
>
<el-container>
<el-aside width="40%">
<div class="select-item">
<div v-for="(news, index) in articlesAdd" :key="news.id">
<div
class="news-main father"
v-if="index === 0"
:class="{ activeAddNews: isActiveAddNews === index }"
@click="activeNews(index)"
>
<div class="news-content">
<img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" />
<div class="news-content-title">{{ news.title }}</div>
</div>
<div class="child" v-if="articlesAdd.length > 1">
<el-button size="small" @click="downNews(index)"
><Icon icon="ep:sort-down" />下移</el-button
>
<el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)"
><Icon icon="ep:delete" />删除
</el-button>
</div>
</div>
<div
class="news-main-item father"
v-if="index > 0"
: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
class="material-img"
v-if="news.thumbUrl"
:src="news.thumbUrl"
width="100%"
/>
</div>
</div>
<div class="child">
<el-button
v-if="articlesAdd.length > index + 1"
size="small"
@click="downNews(index)"
><Icon icon="ep:sort-down" />下移
</el-button>
<el-button size="small" @click="upNews(index)"
><Icon icon="ep:sort-up" />上移</el-button
>
<el-button
v-if="operateMaterial === 'add'"
type="danger"
size="small"
@click="minusNews(index)"
><Icon icon="ep:delete" />删除
</el-button>
</div>
</div>
</div>
<el-row justify="center" class="ope-row">
<el-button
type="primary"
circle
@click="plusNews"
v-if="articlesAdd.length < 8 && operateMaterial === 'add'"
>
<Icon icon="ep:plus" />
</el-button>
</el-row>
</div>
</el-aside>
<el-main>
<div class="" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0">
<!-- 标题作者原文地址 -->
<el-row :gutter="20">
<el-input
v-model="articlesAdd[isActiveAddNews].title"
placeholder="请输入标题(必填)"
/>
<el-input
v-model="articlesAdd[isActiveAddNews].author"
placeholder="请输入作者"
style="margin-top: 5px"
/>
<el-input
v-model="articlesAdd[isActiveAddNews].contentSourceUrl"
placeholder="请输入原文地址"
style="margin-top: 5px"
/>
</el-row>
<!-- 封面和摘要 -->
<el-row :gutter="20">
<el-col :span="12">
<p>封面:</p>
<div class="thumb-div">
<el-image
v-if="articlesAdd[isActiveAddNews].thumbUrl"
style="width: 300px; max-height: 300px"
:src="articlesAdd[isActiveAddNews].thumbUrl"
fit="contain"
/>
<Icon
v-else
icon="ep:plus"
class="avatar-uploader-icon"
:class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
/>
<div class="thumb-but">
<el-upload
:action="uploadUrl"
:headers="headers"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeThumbImageUpload"
:on-success="handleUploadSuccess"
>
<template #trigger>
<el-button size="small" type="primary">本地上传</el-button>
</template>
<el-button
size="small"
type="primary"
@click="openMaterial"
style="margin-left: 5px"
>素材库选择</el-button
>
<template #tip>
<div class="el-upload__tip"
>支持 bmp/png/jpeg/jpg/gif 格式大小不超过 2M</div
>
</template>
</el-upload>
</div>
<el-dialog
title="选择图片"
v-model="dialogImageVisible"
width="80%"
append-to-body
>
<WxMaterialSelect
ref="materialSelectRef"
:objData="{ type: 'image', accountId: queryParams.accountId }"
@select-material="selectMaterial"
/>
</el-dialog>
</div>
</el-col>
<el-col :span="12">
<p>摘要:</p>
<el-input
:rows="8"
type="textarea"
v-model="articlesAdd[isActiveAddNews].digest"
placeholder="请输入摘要"
class="digest"
maxlength="120"
/>
</el-col>
</el-row>
<!--富文本编辑器组件-->
<el-row>
<Editor
v-model="articlesAdd[isActiveAddNews].content"
:editor-config="editorConfig"
/>
</el-row>
</div>
</el-main>
</el-container>
<template #footer>
<el-button @click="dialogNewsVisible = false"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</template>
</el-dialog>
</div>
<!-- 添加或修改草稿对话框 -->
<!-- TODO @Dhb52是不是整个做成一个组件 -->
<el-dialog
:title="isCreating ? '新建图文' : '修改图文'"
width="80%"
v-model="showDialog"
:before-close="onBeforeDialogClose"
destroy-on-close
>
<NewsForm v-model="newsList" v-loading="isSubmitting" :is-creating="isCreating" />
<template #footer>
<el-button @click="showDialog = false"> </el-button>
<el-button type="primary" @click="onSubmitNewsItem"> </el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="MpDraft">
import { Editor } from '@/components/Editor'
import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
import { getAccessToken } from '@/utils/auth'
import * as MpDraftApi from '@/api/mp/draft'
import * as MpFreePublishApi from '@/api/mp/freePublish'
import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
import { createEditorConfig } from './editor-config'
import {
type Article,
type NewsItem,
NewsForm,
DraftTable,
createEmptyNewsItem
} from './components/'
// import drafts from './mock' // API
import { IEditorConfig } from '@wangeditor/editor'
const message = useMessage() //
const accountId = ref(0)
provide('accountId', accountId)
const loading = ref(true) //
const list = ref<any[]>([]) //
const total = ref(0) //
interface QueryParams {
pageNo: number
pageSize: number
accountId?: number
accountId: number
}
const queryParams: QueryParams = reactive({
pageNo: 1,
pageSize: 10,
accountId: undefined
accountId: accountId.value
})
// ========== ==========
const BASE_URL = import.meta.env.VITE_BASE_URL
const uploadUrl = BASE_URL + '/admin-api/mp/material/upload-permanent' //
const headers = { Authorization: 'Bearer ' + getAccessToken() } //
const materialSelectRef = ref<InstanceType<typeof WxMaterialSelect> | null>(null)
const fileList = ref<UploadFiles>([])
interface UploadData {
type: 'image' | 'video' | 'audio'
accountId?: number
accountId: number
}
const uploadData: UploadData = reactive({
type: 'image',
accountId: 1
accountId: accountId.value
})
// ========== 稿 or ==========
interface Article {
title: string
thumbMediaId: string
author: string
digest: string
showCoverPic: string
content: string
contentSourceUrl: string
needOpenComment: string
onlyFansCanComment: string
thumbUrl: string
}
const dialogNewsVisible = ref(false)
const addMaterialLoading = ref(false) // 稿 loading
const articlesAdd = ref<Article[]>([])
const isActiveAddNews = ref(0)
const dialogImageVisible = ref(false)
const operateMaterial = ref<'add' | 'edit'>('add')
const articlesMediaId = ref('')
const showDialog = ref(false)
const newsList = ref<NewsItem[]>([])
const mediaId = ref('')
const isCreating = ref(true)
const isSubmitting = ref(false)
/** 侦听公众号变化 **/
const onAccountChanged = (id?: number) => {
const onAccountChanged = (id: number) => {
setAccountId(id)
getList()
}
// ======================== ========================
/** 设置账号编号 */
const setAccountId = (id?: number) => {
queryParams.accountId = id
uploadData.accountId = id
editorConfig.value = createEditorConfig(uploadUrl, queryParams.accountId)
//
const onBeforeDialogClose = async (onDone: () => {}) => {
try {
await message.confirm('修改内容可能还未保存,确定关闭吗?')
onDone()
} catch {}
}
const editorConfig = ref<Partial<IEditorConfig>>({})
// ======================== ========================
/** 设置账号编号 */
const setAccountId = (id: number) => {
queryParams.accountId = id
uploadData.accountId = id
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const drafts = await MpDraftApi.getDraftPage(queryParams)
drafts.list.forEach((item) => {
const newsItem = item.content.newsItem
drafts.list.forEach((draft) => {
const newsList = draft.content.newsItem
// thumbUrl picUrl wx-news
newsItem.forEach((article) => {
article.picUrl = article.thumbUrl
newsList.forEach((item) => {
item.picUrl = item.thumbUrl
})
})
list.value = drafts.list
@ -359,163 +153,45 @@ const getList = async () => {
// ======================== /稿 ========================
/** 新增按钮操作 */
const handleAdd = () => {
reset()
//
operateMaterial.value = 'add'
dialogNewsVisible.value = true
isCreating.value = true
newsList.value = [createEmptyNewsItem()]
showDialog.value = true
}
/** 更新按钮操作 */
const handleUpdate = (item: any) => {
reset()
articlesMediaId.value = item.mediaId
articlesAdd.value = JSON.parse(JSON.stringify(item.content.newsItem))
//
operateMaterial.value = 'edit'
dialogNewsVisible.value = true
const onUpdate = (item: Article) => {
mediaId.value = item.mediaId
newsList.value = JSON.parse(JSON.stringify(item.content.newsItem))
isCreating.value = false
showDialog.value = true
}
/** 提交按钮 */
const submitForm = async () => {
addMaterialLoading.value = true
const onSubmitNewsItem = async () => {
isSubmitting.value = true
try {
if (operateMaterial.value === 'add') {
await MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value)
if (isCreating.value) {
await MpDraftApi.createDraft(queryParams.accountId, newsList.value)
message.notifySuccess('新增成功')
} else {
await MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value)
await MpDraftApi.updateDraft(queryParams.accountId, mediaId.value, newsList.value)
message.notifySuccess('更新成功')
}
} finally {
dialogNewsVisible.value = false
addMaterialLoading.value = false
showDialog.value = false
isSubmitting.value = false
await getList()
}
}
//
const dialogNewsClose = async (onDone: () => {}) => {
try {
await message.confirm('修改内容可能还未保存,确定关闭吗?')
reset()
onDone()
} catch {}
}
//
const reset = () => {
isActiveAddNews.value = 0
articlesAdd.value = [buildEmptyArticle()]
}
//
const downNews = (index: number) => {
let temp = articlesAdd.value[index]
articlesAdd.value[index] = articlesAdd.value[index + 1]
articlesAdd.value[index + 1] = temp
isActiveAddNews.value = index + 1
}
//
const upNews = (index: number) => {
const temp = articlesAdd.value[index]
articlesAdd.value[index] = articlesAdd.value[index - 1]
articlesAdd.value[index - 1] = temp
isActiveAddNews.value = index - 1
}
// index
const activeNews = (index: number) => {
isActiveAddNews.value = index
}
// index
const minusNews = async (index: number) => {
try {
await message.confirm('确定删除该图文吗?')
articlesAdd.value.splice(index, 1)
if (isActiveAddNews.value === index) {
isActiveAddNews.value = 0
}
} catch {}
}
//
const plusNews = () => {
articlesAdd.value.push(buildEmptyArticle())
isActiveAddNews.value = articlesAdd.value.length - 1
}
// article
const buildEmptyArticle = (): Article => {
return {
title: '',
thumbMediaId: '',
author: '',
digest: '',
showCoverPic: '',
content: '',
contentSourceUrl: '',
needOpenComment: '',
onlyFansCanComment: '',
thumbUrl: ''
}
}
// ======================== ========================
const beforeThumbImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
addMaterialLoading.value = true
const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes(
rawFile.type
)
if (!isType) {
message.error('上传图片格式不对!')
addMaterialLoading.value = false
return false
}
if (rawFile.size / 1024 / 1024 > 2) {
message.error('上传图片大小不能超过 2M!')
addMaterialLoading.value = false
return false
}
//
return true
}
const handleUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
addMaterialLoading.value = false
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
//
fileList.value = []
// 稿
articlesAdd.value[isActiveAddNews.value].thumbMediaId = res.data.mediaId
articlesAdd.value[isActiveAddNews.value].thumbUrl = res.data.url
}
// or 稿
const selectMaterial = (item: any) => {
dialogImageVisible.value = false
articlesAdd.value[isActiveAddNews.value].thumbMediaId = item.mediaId
articlesAdd.value[isActiveAddNews.value].thumbUrl = item.url
}
//
const openMaterial = () => {
dialogImageVisible.value = true
}
// ======================== 稿 ========================
const handlePublish = async (item: any) => {
const onPublish = async (item: Article) => {
const accountId = queryParams.accountId
const mediaId = item.mediaId
const content =
'你正在通过发布的方式发表内容。 发布不占用群发次数,一天可多次发布。已发布内容不会推送给用户,也不会展示在公众号主页中。 发布后,你可以前往发表记录获取链接,也可以将发布内容添加到自定义菜单、自动回复、话题和页面模板中。'
'你正在通过发布的方式发表内容。 发布不占用群发次数,一天可多次发布。' +
'已发布内容不会推送给用户,也不会展示在公众号主页中。 ' +
'发布后,你可以前往发表记录获取链接,也可以将发布内容添加到自定义菜单、自动回复、话题和页面模板中。'
try {
await message.confirm(content)
await MpFreePublishApi.submitFreePublish(accountId, mediaId)
@ -525,7 +201,7 @@ const handlePublish = async (item: any) => {
}
/** 删除按钮操作 */
const handleDelete = async (item: any) => {
const onDelete = async (item: Article) => {
const accountId = queryParams.accountId
const mediaId = item.mediaId
try {
@ -536,234 +212,10 @@ const handleDelete = async (item: any) => {
} catch {}
}
</script>
<style lang="scss" scoped>
.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;
}
.el-row {
margin-bottom: 20px;
}
.el-row:last-child {
margin-bottom: 0;
}
.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;
// vertical-align: top;
// margin-top: 200px;
// }
// .right {
// display: inline-block;
// width: 100%;
// }
// .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: #165dff;
// }
.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: 100%;
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;
}
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
}
@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;
}
.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: 100%;
text-align: center;
}
.thumb-but {
margin: 5px;
}
.material-img {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,50 @@
import type { UploadRawFile } from 'element-plus'
const message = useMessage() // 消息
enum MaterialType {
Image = 'image',
Voice = 'voice',
Video = 'video'
}
const useBeforeUpload = (type: MaterialType, maxSizeMB: number) => {
const fn = (rawFile: UploadRawFile): boolean => {
let allowTypes: string[] = []
let name = ''
switch (type) {
case MaterialType.Image:
allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']
maxSizeMB = 2
name = '图片'
break
case MaterialType.Voice:
allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr']
maxSizeMB = 2
name = '语音'
break
case MaterialType.Video:
allowTypes = ['video/mp4']
maxSizeMB = 10
name = '视频'
break
}
// 格式不正确
if (!allowTypes.includes(rawFile.type)) {
message.error(`上传${name}格式不对!`)
return false
}
// 大小不正确
if (rawFile.size / 1024 / 1024 > maxSizeMB) {
message.error(`上传${name}大小不能超过${maxSizeMB}M!`)
return false
}
return true
}
return fn
}
export { MaterialType, useBeforeUpload }

View File

@ -6,12 +6,13 @@
:limit="1"
:file-list="fileList"
:data="uploadData"
:on-progress="() => (uploading = true)"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-progress="(isUploading = true)"
:on-error="onUploadError"
:before-upload="onBeforeUpload"
:on-success="onUploadSuccess"
>
<el-button type="primary" plain :loading="uploading" :disabled="uploading">
{{ uploading ? '正在上传' : '点击上传' }}
<el-button type="primary" plain :loading="isUploading" :disabled="isUploading">
{{ isUploading ? '正在上传' : '点击上传' }}
</el-button>
<template #tip>
<span class="el-upload__tip" style="margin-left: 5px">
@ -33,7 +34,7 @@ import {
const message = useMessage()
const props = defineProps<{ type: boolean }>()
const props = defineProps<{ type: MaterialType }>()
const fileList = ref<UploadUserFile[]>([])
const emit = defineEmits<{
@ -45,11 +46,13 @@ const uploadData: UploadData = reactive({
title: '',
introduction: ''
})
const uploading = ref(false)
const isUploading = ref(false)
const beforeUpload = props.type == MaterialType.Image ? beforeImageUpload : beforeVoiceUpload
/** 上传前检查 */
const onBeforeUpload = props.type === MaterialType.Image ? beforeImageUpload : beforeVoiceUpload
const handleUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
/** 上传成功处理 */
const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
if (res.code !== 0) {
message.alertError('上传出错:' + res.msg)
return false
@ -61,7 +64,16 @@ const handleUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
uploadData.introduction = ''
message.notifySuccess('上传成功')
uploading.value = false
isUploading.value = false
emit('uploaded')
}
/** 上传失败处理 */
const onUploadError = (err: Error) => message.error('上传失败: ' + err.message)
</script>
<style lang="scss" scoped>
.el-upload__tip {
margin-left: 5px;
}
</style>

View File

@ -8,8 +8,9 @@
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVideoUpload"
:on-progress="() => (uploading = true)"
:on-success="handleUploadSuccess"
:on-progress="(isUploading = true)"
:on-error="onUploadError"
:on-success="onUploadSuccess"
ref="uploadVideoRef"
:auto-upload="false"
class="mb-5"
@ -22,7 +23,7 @@
>
</el-upload>
<el-divider />
<el-form :model="uploadData" :rules="uploadRules" ref="uploadFormRef" v-loading="uploading">
<el-form :model="uploadData" :rules="uploadRules" ref="uploadFormRef" v-loading="isUploading">
<el-form-item label="标题" prop="title">
<el-input
v-model="uploadData.title"
@ -40,7 +41,7 @@
</el-form>
<template #footer>
<el-button @click="showDialog = false"> </el-button>
<el-button type="primary" @click="submitVideo" :loading="uploading" :disabled="uploading"
<el-button type="primary" @click="submitVideo" :loading="isUploading" :disabled="isUploading"
> </el-button
>
</template>
@ -75,7 +76,7 @@ const emit = defineEmits<{
(e: 'uploaded', v: void)
}>()
const showDialog = computed({
const showDialog = computed<boolean>({
get() {
return props.modelValue
},
@ -84,7 +85,7 @@ const showDialog = computed({
}
})
const uploading = ref(false)
const isUploading = ref(false)
const fileList = ref<UploadUserFile[]>([])
@ -106,8 +107,9 @@ const submitVideo = () => {
})
}
const handleUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
uploading.value = false
/** 上传成功处理 */
const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
isUploading.value = false
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
@ -122,4 +124,7 @@ const handleUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
message.notifySuccess('上传成功')
emit('uploaded')
}
/** 上传失败处理 */
const onUploadError = (err: Error) => message.error(`上传失败: ${err.message}`)
</script>

View File

@ -1,15 +1,9 @@
import type { UploadProps, UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth'
const message = useMessage() // 消息
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头
// const UPLOAD_URL = 'http://127.0.0.1:8000/upload/' // 上传地址
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
enum MaterialType {
Image = 'image',
Voice = 'voice',
Video = 'video'
}
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址
interface UploadData {
type: MaterialType
@ -17,48 +11,14 @@ interface UploadData {
introduction: string
}
const beforeUpload = (rawFile: UploadRawFile, materialType: MaterialType): boolean => {
let allowTypes: string[] = []
let maxSizeMB = 0
let name = ''
switch (materialType) {
case MaterialType.Image:
allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']
maxSizeMB = 2
name = '图片'
break
case MaterialType.Voice:
allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr']
maxSizeMB = 2
name = '图片'
break
case MaterialType.Video:
allowTypes = ['video/mp4']
maxSizeMB = 10
name = '视频'
break
}
// 格式不正确
if (!allowTypes.includes(rawFile.type)) {
message.error(`上传${name}格式不对!`)
return false
}
// 大小不正确
if (rawFile.size / 1024 / 1024 > maxSizeMB) {
message.error(`上传${name}大小不能超过${maxSizeMB}M!`)
return false
}
return true
}
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
beforeUpload(rawFile, MaterialType.Image)
useBeforeUpload(MaterialType.Image, 2)(rawFile)
const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
beforeUpload(rawFile, MaterialType.Voice)
useBeforeUpload(MaterialType.Voice, 2)(rawFile)
const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
beforeUpload(rawFile, MaterialType.Video)
useBeforeUpload(MaterialType.Video, 10)(rawFile)
export {
HEADERS,

View File

@ -0,0 +1,244 @@
<template>
<div>
<div class="configure_page">
<div class="delete_btn">
<el-button type="danger" @click="emit('delete')">
<Icon icon="ep:delete" />
删除当前菜单
</el-button>
</div>
<div>
<span>菜单名称</span>
<el-input
class="input_width"
v-model="menu.name"
placeholder="请输入菜单名称"
:maxlength="isParent ? 4 : 7"
clearable
/>
</div>
<div v-if="isLeave">
<div class="menu_content">
<span>菜单标识</span>
<el-input
class="input_width"
v-model="menu.menuKey"
placeholder="请输入菜单 KEY"
clearable
/>
</div>
<div class="menu_content">
<span>菜单内容</span>
<el-select v-model="menu.type" clearable placeholder="请选择" class="menu_option">
<el-option
v-for="item in menuOptions"
:label="item.label"
:value="item.value"
:key="item.value"
/>
</el-select>
</div>
<div class="configur_content" v-if="menu.type === 'view'">
<span>跳转链接</span>
<el-input class="input_width" v-model="menu.url" placeholder="请输入链接" clearable />
</div>
<div class="configur_content" v-if="menu.type === 'miniprogram'">
<div class="applet">
<span>小程序的 appid </span>
<el-input
class="input_width"
v-model="menu.miniProgramAppId"
placeholder="请输入小程序的appid"
clearable
/>
</div>
<div class="applet">
<span>小程序的页面路径</span>
<el-input
class="input_width"
v-model="menu.miniProgramPagePath"
placeholder="请输入小程序的页面路径pages/index"
clearable
/>
</div>
<div class="applet">
<span>小程序的备用网页</span>
<el-input
class="input_width"
v-model="menu.url"
placeholder="不支持小程序的老版本客户端将打开本网页"
clearable
/>
</div>
<p class="blue">tips:需要和公众号进行关联才可以把小程序绑定带微信菜单上哟</p>
</div>
<div class="configur_content" v-if="menu.type === 'article_view_limited'">
<el-row>
<div class="select-item" v-if="menu && menu.replyArticles">
<WxNews :articles="menu.replyArticles" />
<el-row class="ope-row" justify="center" align="middle">
<el-button type="danger" circle @click="deleteMaterial">
<icon icon="ep:delete" />
</el-button>
</el-row>
</div>
<div v-else>
<el-row justify="center">
<el-col :span="24" style="text-align: center">
<el-button type="success" @click="showNewsDialog = true">
素材库选择
<Icon icon="ep:circle-check" />
</el-button>
</el-col>
</el-row>
</div>
<el-dialog title="选择图文" v-model="showNewsDialog" width="80%" destroy-on-close>
<WxMaterialSelect
:objData="{ type: 'news', accountId: props.accountId }"
@select-material="selectMaterial"
/>
</el-dialog>
</el-row>
</div>
<div
class="configur_content"
v-if="menu.type === 'click' || menu.type === 'scancode_waitmsg'"
>
<WxReplySelect v-if="hackResetWxReplySelect" :objData="menu.reply" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import menuOptions from './menuOptions'
const message = useMessage()
const props = defineProps<{
accountId?: number
modelValue: any
isParent: boolean
}>()
const emit = defineEmits<{
(e: 'delete', v: void)
(e: 'update:modelValue', v: any)
}>()
// TODO @Dhb52 table
const menu = computed({
get() {
return props.modelValue
},
set(val) {
emit('update:modelValue', val)
}
})
const showNewsDialog = ref(false)
const hackResetWxReplySelect = ref(false)
const isLeave = computed<boolean>(() => !(menu.value.children?.length > 0))
watch(menu, () => {
hackResetWxReplySelect.value = false //
nextTick(() => {
hackResetWxReplySelect.value = true //
})
})
// ======================== ========================
const selectMaterial = (item: any) => {
const articleId = item.articleId
const articles = item.content.newsItem
//
if (articles.length > 1) {
message.alertWarning('您选择的是多图文,将默认跳转第一篇')
}
showNewsDialog.value = false
//
menu.value.articleId = articleId
menu.value.replyArticles = []
articles.forEach((article) => {
menu.value.replyArticles.push({
title: article.title,
description: article.digest,
picUrl: article.picUrl,
url: article.url
})
})
}
const deleteMaterial = () => {
delete menu.value['articleId']
delete menu.value['replyArticles']
}
</script>
<style lang="scss" scoped>
.el-input {
width: 70%;
margin-right: 2%;
}
.configure_page {
.delete_btn {
margin-bottom: 15px;
text-align: right;
}
.menu_content {
margin-top: 20px;
}
.configur_content {
padding: 20px 10px;
margin-top: 20px;
background-color: #fff;
border-radius: 5px;
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #eaeaea;
.ope-row {
padding-top: 10px;
text-align: center;
}
}
}
.blue {
margin-top: 10px;
color: #29b6f6;
}
.applet {
margin-bottom: 20px;
span {
width: 20%;
}
}
.input_width {
width: 40%;
}
.material {
.input_width {
width: 30%;
}
.el-textarea {
width: 80%;
}
}
}
</style>

View File

@ -0,0 +1,158 @@
<template>
<div class="menu_bottom" v-for="(parent, x) of menuList" :key="x">
<!-- 一级菜单 -->
<div
@click="menuClicked(parent, x)"
class="menu_item"
:class="{ active: props.activeIndex === `${x}` }"
>
<Icon icon="ep:fold" color="black" />{{ parent.name }}
</div>
<!-- 以下为二级菜单-->
<div class="submenu" v-if="parentIndex === x && parent.children">
<div class="subtitle menu_bottom" v-for="(child, y) in parent.children" :key="y">
<div
class="menu_subItem"
v-if="parent.children"
:class="{ active: props.activeIndex === `${x}-${y}` }"
@click="subMenuClicked(child, x, y)"
>
{{ child.name }}
</div>
</div>
<!-- 二级菜单加号 当长度 小于 5 才显示二级菜单的加号 -->
<div
class="menu_bottom menu_addicon"
v-if="!parent.children || parent.children.length < 5"
@click="addSubMenu(x, parent)"
>
<Icon icon="ep:plus" class="plus" />
</div>
</div>
</div>
<!-- 一级菜单加号 -->
<div class="menu_bottom menu_addicon" v-if="menuList.length < 3" @click="addMenu">
<Icon icon="ep:plus" class="plus" />
</div>
</template>
<script setup lang="ts">
import { Menu } from './types'
const props = defineProps<{
modelValue: Menu[]
activeIndex: string
parentIndex: number
accountId?: number
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Menu[])
(e: 'menu-clicked', parent: Menu, x: number)
(e: 'submenu-clicked', child: Menu, x: number, y: number)
}>()
const menuList = computed<Menu[]>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
//
const addMenu = () => {
const index = menuList.value.length
const menu = {
name: '菜单名称',
children: [],
reply: {
//
type: 'text',
accountId: props.accountId // 使
}
}
menuList.value[index] = menu
menuClicked(menu, index - 1)
}
// parent
const addSubMenu = (i: number, parent: any) => {
const subMenuKeyLength = parent.children.length // key
const addButton = {
name: '子菜单名称',
reply: {
//
type: 'text',
accountId: props.accountId // 使
}
}
parent.children[subMenuKeyLength] = addButton
subMenuClicked(parent.children[subMenuKeyLength], i, subMenuKeyLength)
}
const menuClicked = (parent: Menu, x: number) => {
emit('menu-clicked', parent, x)
}
const subMenuClicked = (child: Menu, x: number, y: number) => {
emit('submenu-clicked', child, x, y)
}
</script>
<style lang="scss" scoped>
.menu_bottom {
position: relative;
display: inline-block;
float: left;
width: 85.5px;
text-align: center;
cursor: pointer;
background-color: #fff;
border: 1px solid #ebedee;
box-sizing: border-box;
&.menu_addicon {
height: 46px;
line-height: 46px;
.plus {
color: #2bb673;
}
}
.menu_item {
display: flex;
width: 100%;
height: 44px;
line-height: 44px;
// text-align: center;
box-sizing: border-box;
align-items: center;
justify-content: center;
&.active {
border: 1px solid #2bb673;
}
}
.menu_subItem {
height: 44px;
line-height: 44px;
text-align: center;
box-sizing: border-box;
&.active {
border: 1px solid #2bb673;
}
}
}
/* 第二级菜单 */
.submenu {
position: absolute;
bottom: 45px;
width: 85.5px;
.subtitle {
background-color: #fff;
box-sizing: border-box;
}
}
</style>

View File

@ -0,0 +1,73 @@
export interface Replay {
title: string
description: string
picUrl: string
url: string
}
export type MenuType =
| ''
| 'click'
| 'view'
| 'scancode_waitmsg'
| 'scancode_push'
| 'pic_sysphoto'
| 'pic_photo_or_album'
| 'pic_weixin'
| 'location_select'
| 'article_view_limited'
interface _RawMenu {
// db
id: number
parentId: number
accountId: number
appId: string
createTime: number
// mp-native
name: string
menuKey: string
type: MenuType
url: string
miniProgramAppId: string
miniProgramPagePath: string
articleId: string
replyMessageType: string
replyContent: string
replyMediaId: string
replyMediaUrl: string
replyThumbMediaId: string
replyThumbMediaUrl: string
replyTitle: string
replyDescription: string
replyArticles: Replay
replyMusicUrl: string
replyHqMusicUrl: string
}
export type RawMenu = Partial<_RawMenu>
interface _Reply {
type: string
accountId: number
content: string
mediaId: string
url: string
thumbMediaId: string
thumbMediaUrl: string
title: string
description: string
articles: null | Replay[]
musicUrl: string
hqMusicUrl: string
}
export type Reply = Partial<_Reply>
interface _Menu extends RawMenu {
children: _Menu[]
reply: Reply
}
export type Menu = Partial<_Menu>

View File

@ -9,7 +9,6 @@
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<div class="public-account-management clearfix" v-loading="loading">
<!--左边配置菜单-->
@ -17,169 +16,33 @@
<div class="weixin-hd">
<div class="weixin-title">{{ accountName }}</div>
</div>
<div class="weixin-menu menu_main clearfix">
<div class="menu_bottom" v-for="(item, i) of menuList" :key="i">
<!-- 一级菜单 -->
<div @click="menuClick(i, item)" class="menu_item" :class="{ active: isActive === i }"
><Icon icon="ep:fold" color="black" />{{ item.name }}
</div>
<!-- 以下为二级菜单-->
<div class="submenu" v-if="isSubMenuFlag === i">
<div class="subtitle menu_bottom" v-for="(subItem, k) in item.children" :key="k">
<div
class="menu_subItem"
v-if="item.children"
:class="{ active: isSubMenuActive === i + '' + k }"
@click="subMenuClick(subItem, i, k)"
>
{{ subItem.name }}
</div>
</div>
<!-- 二级菜单加号 当长度 小于 5 才显示二级菜单的加号 -->
<div
class="menu_bottom menu_addicon"
v-if="!item.children || item.children.length < 5"
@click="addSubMenu(i, item)"
>
<Icon icon="ep:plus" />
</div>
</div>
</div>
<!-- 一级菜单加号 -->
<div class="menu_bottom menu_addicon" v-if="menuList.length < 3" @click="addMenu">
<Icon icon="ep:plus" />
</div>
<div class="weixin-menu clearfix">
<MenuPreviewer
v-model="menuList"
:account-id="accountId"
:active-index="activeIndex"
:parent-index="parentIndex"
@menu-clicked="(parent, x) => menuClicked(parent, x)"
@submenu-clicked="(child, x, y) => subMenuClicked(child, x, y)"
/>
</div>
<div class="save_div">
<el-button
class="save_btn"
type="success"
@click="handleSave"
v-hasPermi="['mp:menu:save']"
<el-button class="save_btn" type="success" @click="onSave" v-hasPermi="['mp:menu:save']"
>保存并发布菜单</el-button
>
<el-button
class="save_btn"
type="danger"
@click="handleDelete"
v-hasPermi="['mp:menu:delete']"
<el-button class="save_btn" type="danger" @click="onClear" v-hasPermi="['mp:menu:delete']"
>清空菜单</el-button
>
</div>
</div>
<!--右边配置-->
<div v-if="showRightFlag" class="right">
<div class="configure_page">
<div class="delete_btn">
<el-button size="small" type="danger" @click="handleDeleteMenu">
删除当前菜单<Icon icon="ep:delete" />
</el-button>
</div>
<div>
<span>菜单名称</span>
<el-input
class="input_width"
v-model="tempObj.name"
placeholder="请输入菜单名称"
:maxlength="nameMaxLength"
clearable
/>
</div>
<div v-if="showConfigureContent">
<div class="menu_content">
<span>菜单标识</span>
<el-input
class="input_width"
v-model="tempObj.menuKey"
placeholder="请输入菜单 KEY"
clearable
/>
</div>
<div class="menu_content">
<span>菜单内容</span>
<el-select v-model="tempObj.type" clearable placeholder="请选择" class="menu_option">
<el-option
v-for="item in menuOptions"
:label="item.label"
:value="item.value"
:key="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">
<WxNews :articles="tempObj.replyArticles" />
<el-row class="ope-row" justify="center" align="middle">
<el-button type="danger" circle @click="deleteMaterial">
<icon icon="ep:delete" />
</el-button>
</el-row>
</div>
<div v-else>
<el-row justify="center">
<el-col :span="24" style="text-align: center">
<el-button type="success" @click="dialogNewsVisible = true">
素材库选择<Icon icon="ep:circle-check" />
</el-button>
</el-col>
</el-row>
</div>
<el-dialog title="选择图文" v-model="dialogNewsVisible" width="90%">
<WxMaterialSelect
:objData="{ type: 'news', accountId: accountId }"
@select-material="selectMaterial"
/>
</el-dialog>
</el-row>
</div>
<div
class="configur_content"
v-if="tempObj.type === 'click' || tempObj.type === 'scancode_waitmsg'"
>
<WxReplySelect :objData="tempObj.reply" v-if="hackResetWxReplySelect" />
</div>
</div>
</div>
<div class="right" v-if="showRightPanel">
<MenuEditor
:account-id="accountId"
:is-parent="isParent"
v-model="activeMenu"
@delete="onDeleteMenu"
/>
</div>
<!-- 一进页面就显示的默认页面当点击左边按钮的时候就不显示了-->
<div v-else class="right">
@ -188,37 +51,55 @@
</div>
</ContentWrap>
</template>
<script lang="ts" setup name="MpMenu">
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
import MenuEditor from './components/MenuEditor.vue'
import MenuPreviewer from './components/MenuPreviewer.vue'
import * as MpMenuApi from '@/api/mp/menu'
import { handleTree } from '@/utils/tree'
import menuOptions from './menuOptions'
import * as UtilsTree from '@/utils/tree'
import { RawMenu, Menu } from './components/types'
const message = useMessage() //
const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__'
// ======================== ========================
const loading = ref(false) //
const accountId = ref<number | undefined>()
const accountName = ref<string | undefined>('')
const menuList = ref<any>({ children: [] })
const menuList = ref<Menu[]>([])
// ======================== ========================
const isActive = ref(-1) //
const isSubMenuActive = ref<string | number>(-1) //
const isSubMenuFlag = ref(-1) //
//
// * 'x'
// * 'x-y'
// * MENU_NOT_SELECTED
const activeIndex = ref<string>(MENU_NOT_SELECTED)
// : index
// * -1
// * x
const parentIndex = ref(-1)
// ======================== ========================
const showRightFlag = ref(false) //
const nameMaxLength = ref(0) // 1 4 2 7
const showConfigureContent = ref(true) //
const hackResetWxReplySelect = ref(false) // WxReplySelect
const tempObj = ref<any>({}) //
const showRightPanel = ref(false) //
const isParent = ref<boolean>(true) // MenuEditorname
const activeMenu = ref<Menu>({}) // MenuEditormodelValue
// tempObjmenu
const tempSelfObj = ref<any>({})
// activeMenumenu
enum Level {
Undefined = '0',
Parent = '1',
Child = '2'
}
const tempSelfObj = ref<{
grand: Level
x: number
y: number
}>({
grand: Level.Undefined,
x: 0,
y: 0
})
const dialogNewsVisible = ref(false) //
/** 侦听公众号变化 **/
@ -233,8 +114,8 @@ const getList = async () => {
loading.value = false
try {
const data = await MpMenuApi.getMenuList(accountId.value)
const menuData = convertMenuList(data)
menuList.value = handleTree(menuData, 'id')
const menuData = menuListToFrontend(data)
menuList.value = UtilsTree.handleTree(menuData, 'id')
} finally {
loading.value = false
}
@ -247,37 +128,29 @@ const handleQuery = () => {
}
// menuList menuList
const convertMenuList = (list: any[]) => {
const menuListToFrontend = (list: any[]) => {
if (!list) return []
const result: any[] = []
list.forEach((item) => {
const menu = {
const result: RawMenu[] = []
list.forEach((item: RawMenu) => {
const menu: any = {
...item
}
if (item.type === 'click' || item.type === 'scancode_waitmsg') {
delete menu.replyMessageType
delete menu.replyContent
delete menu.replyMediaId
delete menu.replyMediaUrl
delete menu.replyDescription
delete menu.replyArticles
menu.reply = {
type: item.replyMessageType,
accountId: item.accountId,
content: item.replyContent,
mediaId: item.replyMediaId,
url: item.replyMediaUrl,
title: item.replyTitle,
description: item.replyDescription,
thumbMediaId: item.replyThumbMediaId,
thumbMediaUrl: item.replyThumbMediaUrl,
articles: item.replyArticles,
musicUrl: item.replyMusicUrl,
hqMusicUrl: item.replyHqMusicUrl
}
menu.reply = {
type: item.replyMessageType,
accountId: item.accountId,
content: item.replyContent,
mediaId: item.replyMediaId,
url: item.replyMediaUrl,
title: item.replyTitle,
description: item.replyDescription,
thumbMediaId: item.replyThumbMediaId,
thumbMediaUrl: item.replyThumbMediaUrl,
articles: item.replyArticles,
musicUrl: item.replyMusicUrl,
hqMusicUrl: item.replyHqMusicUrl
}
result.push(menu)
result.push(menu as RawMenu)
})
return result
}
@ -285,128 +158,72 @@ const convertMenuList = (list: any[]) => {
//
const resetForm = () => {
//
isActive.value = -1
isSubMenuActive.value = -1
isSubMenuFlag.value = -1
activeIndex.value = MENU_NOT_SELECTED
parentIndex.value = -1
//
showRightFlag.value = false
nameMaxLength.value = 0
showConfigureContent.value = false
hackResetWxReplySelect.value = false
tempObj.value = {}
tempSelfObj.value = {}
showRightPanel.value = false
activeMenu.value = {}
tempSelfObj.value = { grand: Level.Undefined, x: 0, y: 0 }
dialogNewsVisible.value = false
}
// ======================== ========================
//
const menuClick = (i: number, item: any) => {
const menuClicked = (parent: Menu, x: number) => {
//
resetEditor()
showRightFlag.value = true //
tempObj.value = item // flag
tempSelfObj.value.grand = '1' //
tempSelfObj.value.index = i //
nameMaxLength.value = 4
showConfigureContent.value = !(item.children && item.children.length > 0) //
showRightPanel.value = true //
activeMenu.value = parent // flag
tempSelfObj.value.grand = Level.Parent //
tempSelfObj.value.x = x //
isParent.value = true
//
isActive.value = i //
isSubMenuFlag.value = i //
isSubMenuActive.value = -1 //
activeIndex.value = `${x}` //
parentIndex.value = x //
}
//
const subMenuClick = (subItem: any, index: number, k: number) => {
const subMenuClicked = (child: Menu, x: number, y: number) => {
//
resetEditor()
showRightFlag.value = true //
tempObj.value = subItem //
tempSelfObj.value.grand = '2' //
tempSelfObj.value.index = index //
tempSelfObj.value.secondIndex = k //
nameMaxLength.value = 7
showConfigureContent.value = true
showRightPanel.value = true //
activeMenu.value = child //
tempSelfObj.value.grand = Level.Child //
tempSelfObj.value.x = x //
tempSelfObj.value.y = y //
isParent.value = false
//
isActive.value = -1 //
isSubMenuActive.value = index + '' + k //
}
//
const addMenu = () => {
const menuKeyLength: number = menuList.value.length
const addButton = {
name: '菜单名称',
children: [],
reply: {
//
type: 'text',
accountId: accountId.value // 使
}
}
menuList.value[menuKeyLength] = addButton
menuClick(menuKeyLength - 1, addButton)
}
// item
const addSubMenu = (i: number, item: any) => {
// name
if (!item.children || item.children.length <= 0) {
item.children = []
delete item['type']
delete item['menuKey']
delete item['miniProgramAppId']
delete item['miniProgramPagePath']
delete item['url']
delete item['reply']
delete item['articleId']
delete item['replyArticles']
//
showConfigureContent.value = false
}
const subMenuKeyLength = item.children.length // key
const addButton = {
name: '子菜单名称',
reply: {
//
type: 'text',
accountId: accountId.value // 使
}
}
item.children[subMenuKeyLength] = addButton
subMenuClick(item.children[subMenuKeyLength], i, subMenuKeyLength)
activeIndex.value = `${x}-${y}`
}
//
const handleDeleteMenu = async () => {
const onDeleteMenu = async () => {
try {
await message.confirm('确定要删除吗?')
if (tempSelfObj.value.grand === '1') {
if (tempSelfObj.value.grand === Level.Parent) {
//
menuList.value.splice(tempSelfObj.value.index, 1)
} else if (tempSelfObj.value.grand === '2') {
menuList.value.splice(tempSelfObj.value.x, 1)
} else if (tempSelfObj.value.grand === Level.Child) {
//
menuList.value[tempSelfObj.value.index].children.splice(tempSelfObj.value.secondIndex, 1)
menuList.value[tempSelfObj.value.x].children?.splice(tempSelfObj.value.y, 1)
}
//
message.notifySuccess('删除成功')
//
tempObj.value = {}
showRightFlag.value = false
isActive.value = -1
isSubMenuActive.value = -1
activeMenu.value = {}
showRightPanel.value = false
activeIndex.value = MENU_NOT_SELECTED
} catch {}
}
// ======================== ========================
const handleSave = async () => {
const onSave = async () => {
try {
await message.confirm('确定要保存吗?')
loading.value = true
await MpMenuApi.saveMenu(accountId.value, convertMenuFormList())
await MpMenuApi.saveMenu(accountId.value, menuListToBackend())
getList()
message.notifySuccess('发布成功')
} finally {
@ -414,15 +231,7 @@ const handleSave = async () => {
}
}
// Editor
const resetEditor = () => {
hackResetWxReplySelect.value = false //
nextTick(() => {
hackResetWxReplySelect.value = true //
})
}
const handleDelete = async () => {
const onClear = async () => {
try {
await message.confirm('确定要删除吗?')
loading.value = true
@ -435,10 +244,10 @@ const handleDelete = async () => {
}
// menuList menuList
const convertMenuFormList = () => {
const menuListToBackend = () => {
const result: any[] = []
menuList.value.forEach((item) => {
const menu = convertMenuForm(item)
const menu = menuToBackend(item)
result.push(menu)
//
@ -447,62 +256,34 @@ const convertMenuFormList = () => {
}
menu.children = []
item.children.forEach((subItem) => {
menu.children.push(convertMenuForm(subItem))
menu.children.push(menuToBackend(subItem))
})
})
return result
}
// menu menu
const convertMenuForm = (menu: any) => {
// TODO: @API
const menuToBackend = (menu: any) => {
let result = {
...menu,
children: undefined, //
reply: undefined //
}
if (menu.type === 'click' || menu.type === 'scancode_waitmsg') {
result.replyMessageType = menu.reply.type
result.replyContent = menu.reply.content
result.replyMediaId = menu.reply.mediaId
result.replyMediaUrl = menu.reply.url
result.replyTitle = menu.reply.title
result.replyDescription = menu.reply.description
result.replyThumbMediaId = menu.reply.thumbMediaId
result.replyThumbMediaUrl = menu.reply.thumbMediaUrl
result.replyArticles = menu.reply.articles
result.replyMusicUrl = menu.reply.musicUrl
result.replyHqMusicUrl = menu.reply.hqMusicUrl
}
result.replyMessageType = menu.reply.type
result.replyContent = menu.reply.content
result.replyMediaId = menu.reply.mediaId
result.replyMediaUrl = menu.reply.url
result.replyTitle = menu.reply.title
result.replyDescription = menu.reply.description
result.replyThumbMediaId = menu.reply.thumbMediaId
result.replyThumbMediaUrl = menu.reply.thumbMediaUrl
result.replyArticles = menu.reply.articles
result.replyMusicUrl = menu.reply.musicUrl
result.replyHqMusicUrl = menu.reply.hqMusicUrl
return result
}
// ======================== ========================
const selectMaterial = (item: any) => {
const articleId = item.articleId
const articles = item.content.newsItem
//
if (articles.length > 1) {
message.alertWarning('您选择的是多图文,将默认跳转第一篇')
}
dialogNewsVisible.value = false
//
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 deleteMaterial = () => {
delete tempObj.value['articleId']
delete tempObj.value['replyArticles']
}
</script>
<!--本组件样式-->
@ -513,9 +294,9 @@ const deleteMaterial = () => {
}
.clearfix::after {
content: '';
display: table;
clear: both;
content: '';
}
div {
@ -523,116 +304,50 @@ div {
}
.weixin-hd {
color: #fff;
text-align: center;
position: relative;
bottom: 426px;
left: 0px;
left: 0;
width: 300px;
height: 64px;
color: #fff;
text-align: center;
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;
left: 0;
width: 100%;
font-size: 14px;
color: #fff;
text-align: center;
}
.weixin-menu {
background: transparent url('./assets/menu_foot.png') no-repeat 0 0;
padding-left: 43px;
font-size: 12px;
}
.menu_option {
width: 40% !important;
background: transparent url('./assets/menu_foot.png') no-repeat 0 0;
}
.public-account-management {
min-width: 1200px;
width: 1200px;
// min-width: 1200px;
margin: 0 auto;
.left {
float: left;
position: relative;
display: inline-block;
float: left;
width: 350px;
height: 715px;
padding: 518px 25px 88px;
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%;
display: flex;
align-items: center;
justify-content: center;
&.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;
@ -644,85 +359,29 @@ div {
}
}
/*右边菜单内容*/
/* 右边菜单内容 */
.right {
float: left;
width: 63%;
background-color: #e8e7e7;
padding: 20px;
margin-left: 20px;
-webkit-box-sizing: border-box;
background-color: #e8e7e7;
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;
text-align: right;
}
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.select-item2 {
padding: 10px;
margin: 0 auto 10px auto;
margin: 0 auto 10px;
border: 1px solid #eaeaea;
}
@ -732,10 +391,10 @@ div {
}
.item-name {
font-size: 12px;
overflow: hidden;
font-size: 12px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
</style>

View File

@ -212,7 +212,7 @@ const handleDelete = async (id: number) => {
//
await message.delConfirm()
//
await NotifyTemplateApi.deleteNotifyTemplateApi(id)
await NotifyTemplateApi.deleteNotifyTemplate(id)
message.success('删除成功')
//
await getList()

View File

@ -91,6 +91,7 @@ const open = async (row: RoleApi.RoleVO) => {
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
@ -109,6 +110,8 @@ const submitForm = async () => {
await PermissionApi.assignRoleMenu(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}

View File

@ -107,6 +107,7 @@ const open = async (row: RoleApi.RoleVO) => {
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
formLoading.value = true
try {
@ -121,6 +122,8 @@ const submitForm = async () => {
await PermissionApi.assignRoleDataScope(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}

View File

@ -156,9 +156,9 @@
<!-- 表单弹窗添加/修改 -->
<RoleForm ref="formRef" @success="getList" />
<!-- 表单弹窗菜单权限 -->
<RoleAssignMenuForm ref="assignMenuFormRef" />
<RoleAssignMenuForm ref="assignMenuFormRef" @success="getList" />
<!-- 表单弹窗数据权限 -->
<RoleDataPermissionForm ref="dataPermissionFormRef" />
<RoleDataPermissionForm ref="dataPermissionFormRef" @success="getList" />
</template>
<script lang="ts" name="SystemRole" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'

View File

@ -230,7 +230,7 @@ const queryFormRef = ref() // 搜索的表单
const getList = async () => {
loading.value = true
try {
const data = await UserApi.getUserPage(queryParams.value)
const data = await UserApi.getUserPage(queryParams)
list.value = data.list
total.value = data.total
} finally {