commit
						5e705776b8
					
				
							
								
								
									
										3
									
								
								.env
								
								
								
								
							
							
						
						
									
										3
									
								
								.env
								
								
								
								
							|  | @ -15,3 +15,6 @@ VITE_APP_CAPTCHA_ENABLE=true | |||
| 
 | ||||
| # 验证码的开关 | ||||
| VITE_APP_CAPTCHA_ENABLE=true | ||||
| 
 | ||||
| # 百度统计 | ||||
| VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.env.dev
								
								
								
								
							
							
						
						
									
										2
									
								
								.env.dev
								
								
								
								
							|  | @ -16,7 +16,7 @@ VITE_API_BASEPATH=/dev-api | |||
| VITE_API_URL=/admin-api | ||||
| 
 | ||||
| # 打包路径 | ||||
| VITE_BASE_PATH=/dist-dev/ | ||||
| VITE_BASE_PATH=/ | ||||
| 
 | ||||
| # 是否删除debugger | ||||
| VITE_DROP_DEBUGGER=false | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
|   "private": false, | ||||
|   "scripts": { | ||||
|     "i": "pnpm install", | ||||
|     "dev": "pnpm vite", | ||||
|     "dev": "vite --mode base", | ||||
|     "front": "vite --mode front", | ||||
|     "ts:check": "vue-tsc --noEmit", | ||||
|     "build:pro": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode pro", | ||||
|  |  | |||
|  | @ -22,6 +22,6 @@ export const getLeave = async (id: number) => { | |||
| } | ||||
| 
 | ||||
| // 获得请假申请分页
 | ||||
| export const getLeavePage = async (params) => { | ||||
| export const getLeavePage = async (params: PageParam) => { | ||||
|   return await request.get({ url: '/bpm/oa/leave/page', params }) | ||||
| } | ||||
|  |  | |||
|  | @ -6,39 +6,3 @@ import request from '@/config/axios' | |||
| export const getCache = () => { | ||||
|   return request.get({ url: '/infra/redis/get-monitor-info' }) | ||||
| } | ||||
| 
 | ||||
| // 获取模块
 | ||||
| export const getKeyDefineList = () => { | ||||
|   return request.get({ url: '/infra/redis/get-key-define-list' }) | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 获取redis key列表 | ||||
|  */ | ||||
| export const getKeyList = (keyTemplate: string) => { | ||||
|   return request.get({ | ||||
|     url: '/infra/redis/get-key-list', | ||||
|     params: { | ||||
|       keyTemplate | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 获取缓存内容
 | ||||
| export const getKeyValue = (key: string) => { | ||||
|   return request.get({ url: '/infra/redis/get-key-value?key=' + key }) | ||||
| } | ||||
| 
 | ||||
| // 根据键名删除缓存
 | ||||
| export const deleteKey = (key: string) => { | ||||
|   return request.delete({ url: '/infra/redis/delete-key?key=' + key }) | ||||
| } | ||||
| 
 | ||||
| export const deleteKeys = (keyTemplate: string) => { | ||||
|   return request.delete({ | ||||
|     url: '/infra/redis/delete-keys?', | ||||
|     params: { | ||||
|       keyTemplate | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|  |  | |||
|  | @ -174,12 +174,3 @@ export interface RedisCommandStatsVO { | |||
|   calls: number | ||||
|   usec: number | ||||
| } | ||||
| 
 | ||||
| export interface RedisKeyInfo { | ||||
|   keyTemplate: string | ||||
|   keyType: string | ||||
|   valueType: string | ||||
|   timeoutType: number | ||||
|   timeout: number | ||||
|   memo: string | ||||
| } | ||||
|  |  | |||
|  | @ -2,15 +2,11 @@ import request from '@/config/axios' | |||
| import { getRefreshToken } from '@/utils/auth' | ||||
| import type { UserLoginVO } from './types' | ||||
| 
 | ||||
| export interface CodeImgResult { | ||||
|   captchaOnOff: boolean | ||||
|   img: string | ||||
|   uuid: string | ||||
| } | ||||
| export interface SmsCodeVO { | ||||
|   mobile: string | ||||
|   scene: number | ||||
| } | ||||
| 
 | ||||
| export interface SmsLoginVO { | ||||
|   mobile: string | ||||
|   code: string | ||||
|  |  | |||
|  | @ -0,0 +1,41 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| // 获得授权信息
 | ||||
| export const getAuthorize = (clientId: string) => { | ||||
|   return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId }) | ||||
| } | ||||
| 
 | ||||
| // 发起授权
 | ||||
| export const authorize = ( | ||||
|   responseType: string, | ||||
|   clientId: string, | ||||
|   redirectUri: string, | ||||
|   state: string, | ||||
|   autoApprove: boolean, | ||||
|   checkedScopes: string[], | ||||
|   uncheckedScopes: string[] | ||||
| ) => { | ||||
|   // 构建 scopes
 | ||||
|   const scopes = {} | ||||
|   for (const scope of checkedScopes) { | ||||
|     scopes[scope] = true | ||||
|   } | ||||
|   for (const scope of uncheckedScopes) { | ||||
|     scopes[scope] = false | ||||
|   } | ||||
|   // 发起请求
 | ||||
|   return request.post({ | ||||
|     url: '/system/oauth2/authorize', | ||||
|     headers: { | ||||
|       'Content-type': 'application/x-www-form-urlencoded' | ||||
|     }, | ||||
|     params: { | ||||
|       response_type: responseType, | ||||
|       client_id: clientId, | ||||
|       redirect_uri: redirectUri, | ||||
|       state: state, | ||||
|       auto_approve: autoApprove, | ||||
|       scope: JSON.stringify(scopes) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|  | @ -26,17 +26,3 @@ export type UserVO = { | |||
|   loginIp: string | ||||
|   loginDate: string | ||||
| } | ||||
| 
 | ||||
| export type UserInfoVO = { | ||||
|   permissions: [] | ||||
|   roles: [] | ||||
|   user: { | ||||
|     avatar: string | ||||
|     id: number | ||||
|     nickname: string | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export type TentantNameVO = { | ||||
|   name: string | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,56 @@ | |||
| import request from '@/config/axios' | ||||
| 
 | ||||
| /** | ||||
|  * 商品品牌 | ||||
|  */ | ||||
| export interface BrandVO { | ||||
|   /** | ||||
|    * 品牌编号 | ||||
|    */ | ||||
|   id?: number | ||||
|   /** | ||||
|    * 品牌名称 | ||||
|    */ | ||||
|   name: string | ||||
|   /** | ||||
|    * 品牌图片 | ||||
|    */ | ||||
|   picUrl: string | ||||
|   /** | ||||
|    * 品牌排序 | ||||
|    */ | ||||
|   sort?: number | ||||
|   /** | ||||
|    * 品牌描述 | ||||
|    */ | ||||
|   description?: string | ||||
|   /** | ||||
|    * 开启状态 | ||||
|    */ | ||||
|   status: number | ||||
| } | ||||
| 
 | ||||
| // 创建商品品牌
 | ||||
| export const createBrand = (data: BrandVO) => { | ||||
|   return request.post({ url: '/product/brand/create', data }) | ||||
| } | ||||
| 
 | ||||
| // 更新商品品牌
 | ||||
| export const updateBrand = (data: BrandVO) => { | ||||
|   return request.put({ url: '/product/brand/update', data }) | ||||
| } | ||||
| 
 | ||||
| // 删除商品品牌
 | ||||
| export const deleteBrand = (id: number) => { | ||||
|   return request.delete({ url: `/product/brand/delete?id=${id}` }) | ||||
| } | ||||
| 
 | ||||
| // 获得商品品牌
 | ||||
| export const getBrand = (id: number) => { | ||||
|   return request.get({ url: `/product/brand/get?id=${id}` }) | ||||
| } | ||||
| 
 | ||||
| // 获得商品品牌列表
 | ||||
| export const getBrandParam = (params: PageParam) => { | ||||
|   return request.get({ url: '/product/brand/page', params }) | ||||
| } | ||||
|  | @ -33,9 +33,15 @@ | |||
|         /> | ||||
|         <el-table-column label="操作" width="90px"> | ||||
|           <template #default="scope"> | ||||
|             <el-button type="text" @click="openFieldForm(scope, scope.$index)">编辑</el-button> | ||||
|             <el-button type="primary" link @click="openFieldForm(scope, scope.$index)" | ||||
|               >编辑</el-button | ||||
|             > | ||||
|             <el-divider direction="vertical" /> | ||||
|             <el-button type="text" style="color: #ff4d4f" @click="removeField(scope, scope.$index)" | ||||
|             <el-button | ||||
|               type="primary" | ||||
|               link | ||||
|               style="color: #ff4d4f" | ||||
|               @click="removeField(scope, scope.$index)" | ||||
|               >移除</el-button | ||||
|             > | ||||
|           </template> | ||||
|  | @ -97,7 +103,10 @@ | |||
|           <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip /> | ||||
|           <el-table-column label="操作" width="90px"> | ||||
|             <template #default="scope"> | ||||
|               <el-button type="text" @click="openFieldOptionForm(scope, scope.$index, 'enum')" | ||||
|               <el-button | ||||
|                 type="primary" | ||||
|                 link | ||||
|                 @click="openFieldOptionForm(scope, scope.$index, 'enum')" | ||||
|                 >编辑</el-button | ||||
|               > | ||||
|               <el-divider direction="vertical" /> | ||||
|  | @ -126,7 +135,10 @@ | |||
|         <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip /> | ||||
|         <el-table-column label="操作" width="90px"> | ||||
|           <template #default="scope"> | ||||
|             <el-button type="text" @click="openFieldOptionForm(scope, scope.$index, 'constraint')" | ||||
|             <el-button | ||||
|               type="primary" | ||||
|               link | ||||
|               @click="openFieldOptionForm(scope, scope.$index, 'constraint')" | ||||
|               >编辑</el-button | ||||
|             > | ||||
|             <el-divider direction="vertical" /> | ||||
|  | @ -154,7 +166,10 @@ | |||
|         <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip /> | ||||
|         <el-table-column label="操作" width="90px"> | ||||
|           <template #default="scope"> | ||||
|             <el-button type="text" @click="openFieldOptionForm(scope, scope.$index, 'property')" | ||||
|             <el-button | ||||
|               type="primary" | ||||
|               link | ||||
|               @click="openFieldOptionForm(scope, scope.$index, 'property')" | ||||
|               >编辑</el-button | ||||
|             > | ||||
|             <el-divider direction="vertical" /> | ||||
|  |  | |||
|  | @ -7,9 +7,9 @@ export const template = (isTaskListener) => { | |||
|       <el-table-column label="监听器类型" min-width="100px" show-overflow-tooltip :formatter="row => listenerTypeObject[row.listenerType]" /> | ||||
|       <el-table-column label="操作" width="90px"> | ||||
|         <template #default="scope"> | ||||
|           <el-button size="small" type="text" @click="openListenerForm(scope, scope.$index)">编辑</el-button> | ||||
|           <el-button size="small" type="primary" link @click="openListenerForm(scope, scope.$index)">编辑</el-button> | ||||
|           <el-divider direction="vertical" /> | ||||
|           <el-button size="small" type="text" style="color: #ff4d4f" @click="removeListener(scope, scope.$index)">移除</el-button> | ||||
|           <el-button size="small" type="primary" link style="color: #ff4d4f" @click="removeListener(scope, scope.$index)">移除</el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|  | @ -125,9 +125,9 @@ export const template = (isTaskListener) => { | |||
|         <el-table-column label="字段值/表达式" min-width="100px" show-overflow-tooltip :formatter="row => row.string || row.expression" /> | ||||
|         <el-table-column label="操作" width="100px"> | ||||
|           <template #default="scope"> | ||||
|             <el-button size="small" type="text" @click="openListenerFieldForm(scope, scope.$index)">编辑</el-button> | ||||
|             <el-button size="small" type="primary" link @click="openListenerFieldForm(scope, scope.$index)">编辑</el-button> | ||||
|             <el-divider direction="vertical" /> | ||||
|             <el-button size="small" type="text" style="color: #ff4d4f" @click="removeListenerField(scope, scope.$index)">移除</el-button> | ||||
|             <el-button size="small" type="primary" link style="color: #ff4d4f" @click="removeListenerField(scope, scope.$index)">移除</el-button> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|       </el-table> | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ | |||
|     </el-table> | ||||
| 
 | ||||
|     <el-dialog | ||||
|       v-model="modelVisible" | ||||
|       v-model="dialogVisible" | ||||
|       :title="modelConfig.title" | ||||
|       :close-on-click-modal="false" | ||||
|       width="400px" | ||||
|  | @ -39,7 +39,7 @@ | |||
|         </el-form-item> | ||||
|       </el-form> | ||||
|       <template #footer> | ||||
|         <el-button @click="modelVisible = false">取 消</el-button> | ||||
|         <el-button @click="dialogVisible = false">取 消</el-button> | ||||
|         <el-button type="primary" @click="addNewObject">保 存</el-button> | ||||
|       </template> | ||||
|     </el-dialog> | ||||
|  | @ -49,7 +49,7 @@ | |||
| const message = useMessage() | ||||
| const signalList = ref<any[]>([]) | ||||
| const messageList = ref<any[]>([]) | ||||
| const modelVisible = ref(false) | ||||
| const dialogVisible = ref(false) | ||||
| const modelType = ref('') | ||||
| const modelObjectForm = ref<any>({}) | ||||
| const rootElements = ref() | ||||
|  | @ -85,7 +85,7 @@ const initDataList = () => { | |||
| const openModel = (type) => { | ||||
|   modelType.value = type | ||||
|   modelObjectForm.value = {} | ||||
|   modelVisible.value = true | ||||
|   dialogVisible.value = true | ||||
| } | ||||
| const addNewObject = () => { | ||||
|   if (modelType.value === 'message') { | ||||
|  | @ -101,7 +101,7 @@ const addNewObject = () => { | |||
|     const signalRef = bpmnInstances().moddle.create('bpmn:Signal', modelObjectForm.value) | ||||
|     rootElements.value.push(signalRef) | ||||
|   } | ||||
|   modelVisible.value = false | ||||
|   dialogVisible.value = false | ||||
|   initDataList() | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import axios, { | ||||
|   AxiosError, | ||||
|   AxiosInstance, | ||||
|   AxiosRequestHeaders, | ||||
|   AxiosResponse, | ||||
|   AxiosError, | ||||
|   InternalAxiosRequestConfig | ||||
| } from 'axios' | ||||
| 
 | ||||
|  | @ -230,7 +230,8 @@ const handleAuthorized = () => { | |||
|       wsCache.clear() | ||||
|       removeToken() | ||||
|       isRelogin.show = false | ||||
|       window.location.href = import.meta.env.VITE_BASE_PATH | ||||
|       // 干掉token后再走一次路由让它过router.beforeEach的校验
 | ||||
|       window.location.href = window.location.href | ||||
|     }) | ||||
|   } | ||||
|   return Promise.reject(t('sys.api.timeoutMessage')) | ||||
|  |  | |||
|  | @ -352,6 +352,7 @@ export default { | |||
|     login: { | ||||
|       backSignIn: '返回', | ||||
|       signInFormTitle: '登录', | ||||
|       ssoFormTitle: '三方授权', | ||||
|       mobileSignInFormTitle: '手机登录', | ||||
|       qrSignInFormTitle: '二维码登录', | ||||
|       signUpFormTitle: '注册', | ||||
|  |  | |||
|  | @ -52,6 +52,8 @@ import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css' | |||
| import hljs from 'highlight.js' //导入代码高亮文件
 | ||||
| import 'highlight.js/styles/github.css' //导入代码高亮样式  新版
 | ||||
| 
 | ||||
| import '@/plugins/tongji' // 百度统计
 | ||||
| 
 | ||||
| import Logger from '@/utils/Logger' | ||||
| 
 | ||||
| // 本地开发模式 全局引入 element-plus 样式,加快第一次进入速度
 | ||||
|  |  | |||
|  | @ -0,0 +1,23 @@ | |||
| import router from '@/router' | ||||
| 
 | ||||
| // 用于 router push
 | ||||
| window._hmt = window._hmt || [] | ||||
| // HM_ID
 | ||||
| const HM_ID = import.meta.env.VITE_APP_BAIDU_CODE | ||||
| ;(function () { | ||||
|   // 有值的时候,才开启
 | ||||
|   if (!HM_ID) { | ||||
|     return | ||||
|   } | ||||
|   const hm = document.createElement('script') | ||||
|   hm.src = 'https://hm.baidu.com/hm.js?' + HM_ID | ||||
|   const s = document.getElementsByTagName('script')[0] | ||||
|   s.parentNode.insertBefore(hm, s) | ||||
| })() | ||||
| 
 | ||||
| router.afterEach(function (to) { | ||||
|   if (!HM_ID) { | ||||
|     return | ||||
|   } | ||||
|   _hmt.push(['_trackPageview', to.fullPath]) | ||||
| }) | ||||
|  | @ -1,11 +1,11 @@ | |||
| import type { App } from 'vue' | ||||
| import type { RouteRecordRaw } from 'vue-router' | ||||
| import { createRouter, createWebHashHistory } from 'vue-router' | ||||
| import { createRouter, createWebHistory } from 'vue-router' | ||||
| import remainingRouter from './modules/remaining' | ||||
| 
 | ||||
| // 创建路由实例
 | ||||
| const router = createRouter({ | ||||
|   history: createWebHashHistory(), // createWebHashHistory URL带#,createWebHistory URL不带#
 | ||||
|   history: createWebHistory(), // createWebHashHistory URL带#,createWebHistory URL不带#
 | ||||
|   strict: true, | ||||
|   routes: remainingRouter as RouteRecordRaw[], | ||||
|   scrollBehavior: () => ({ left: 0, top: 0 }) | ||||
|  |  | |||
|  | @ -116,7 +116,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|       { | ||||
|         path: 'type/data/:dictType', | ||||
|         component: () => import('@/views/system/dict/data/index.vue'), | ||||
|         name: 'data', | ||||
|         name: 'SystemDictData', | ||||
|         meta: { | ||||
|           title: '字典数据', | ||||
|           noCache: true, | ||||
|  | @ -140,7 +140,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|       { | ||||
|         path: 'edit', | ||||
|         component: () => import('@/views/infra/codegen/EditTable.vue'), | ||||
|         name: 'EditTable', | ||||
|         name: 'InfraCodegenEditTable', | ||||
|         meta: { | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|  | @ -163,7 +163,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|       { | ||||
|         path: 'job-log', | ||||
|         component: () => import('@/views/infra/job/logger/index.vue'), | ||||
|         name: 'JobLog', | ||||
|         name: 'InfraJobLog', | ||||
|         meta: { | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|  | @ -185,6 +185,16 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|       noTagsView: true | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     path: '/sso', | ||||
|     component: () => import('@/views/Login/Login.vue'), | ||||
|     name: 'SSOLogin', | ||||
|     meta: { | ||||
|       hidden: true, | ||||
|       title: t('router.login'), | ||||
|       noTagsView: true | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     path: '/403', | ||||
|     component: () => import('@/views/Error/403.vue'), | ||||
|  | @ -226,7 +236,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|       { | ||||
|         path: '/manager/form/edit', | ||||
|         component: () => import('@/views/bpm/form/editor/index.vue'), | ||||
|         name: 'bpmFormEditor', | ||||
|         name: 'BpmFormEditor', | ||||
|         meta: { | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|  | @ -238,7 +248,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|       { | ||||
|         path: '/manager/model/edit', | ||||
|         component: () => import('@/views/bpm/model/editor/index.vue'), | ||||
|         name: 'modelEditor', | ||||
|         name: 'BpmModelEditor', | ||||
|         meta: { | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|  | @ -250,7 +260,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|       { | ||||
|         path: '/manager/definition', | ||||
|         component: () => import('@/views/bpm/definition/index.vue'), | ||||
|         name: 'BpmProcessDefinitionList', | ||||
|         name: 'BpmProcessDefinition', | ||||
|         meta: { | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|  | @ -262,7 +272,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|       { | ||||
|         path: '/manager/task-assign-rule', | ||||
|         component: () => import('@/views/bpm/taskAssignRule/index.vue'), | ||||
|         name: 'BpmTaskAssignRuleList', | ||||
|         name: 'BpmTaskAssignRule', | ||||
|         meta: { | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|  | @ -305,18 +315,6 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|           title: '发起 OA 请假', | ||||
|           activeMenu: 'bpm/oa/leave/create' | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         path: '/bpm/oa/leave/detail', | ||||
|         component: () => import('@/views/bpm/oa/leave/detail.vue'), | ||||
|         name: 'OALeaveDetail', | ||||
|         meta: { | ||||
|           noCache: true, | ||||
|           hidden: true, | ||||
|           canTo: true, | ||||
|           title: '查看 OA 请假', | ||||
|           activeMenu: 'bpm/oa/leave/detail' | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|  | @ -331,7 +329,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ | |||
|       { | ||||
|         path: 'value/:propertyId(\\d+)', | ||||
|         component: () => import('@/views/mall/product/property/value/index.vue'), | ||||
|         name: 'PropertyValue', | ||||
|         name: 'ProductPropertyValue', | ||||
|         meta: { title: '商品属性值', icon: '', activeMenu: '/product/property' } | ||||
|       } | ||||
|     ] | ||||
|  |  | |||
|  | @ -25,13 +25,12 @@ declare module '@vue/runtime-core' { | |||
|     Echart: typeof import('./../components/Echart/src/Echart.vue')['default'] | ||||
|     Editor: typeof import('./../components/Editor/src/Editor.vue')['default'] | ||||
|     ElAlert: typeof import('element-plus/es')['ElAlert'] | ||||
|     ElAutoResizer: typeof import('element-plus/es')['ElAutoResizer'] | ||||
|     ElAvatar: typeof import('element-plus/es')['ElAvatar'] | ||||
|     ElBadge: typeof import('element-plus/es')['ElBadge'] | ||||
|     ElButton: typeof import('element-plus/es')['ElButton'] | ||||
|     ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] | ||||
|     ElCard: typeof import('element-plus/es')['ElCard'] | ||||
|     ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] | ||||
|     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] | ||||
|     ElCol: typeof import('element-plus/es')['ElCol'] | ||||
|     ElCollapse: typeof import('element-plus/es')['ElCollapse'] | ||||
|     ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] | ||||
|  | @ -71,11 +70,9 @@ declare module '@vue/runtime-core' { | |||
|     ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] | ||||
|     ElSelect: typeof import('element-plus/es')['ElSelect'] | ||||
|     ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] | ||||
|     ElSpace: typeof import('element-plus/es')['ElSpace'] | ||||
|     ElSwitch: typeof import('element-plus/es')['ElSwitch'] | ||||
|     ElTable: typeof import('element-plus/es')['ElTable'] | ||||
|     ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] | ||||
|     ElTableV2: typeof import('element-plus/es')['ElTableV2'] | ||||
|     ElTabPane: typeof import('element-plus/es')['ElTabPane'] | ||||
|     ElTabs: typeof import('element-plus/es')['ElTabs'] | ||||
|     ElTag: typeof import('element-plus/es')['ElTag'] | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ export {} | |||
| declare global { | ||||
|   const DICT_TYPE: typeof import('@/utils/dict')['DICT_TYPE'] | ||||
|   const EffectScope: typeof import('vue')['EffectScope'] | ||||
|   const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] | ||||
|   const computed: typeof import('vue')['computed'] | ||||
|   const createApp: typeof import('vue')['createApp'] | ||||
|   const customRef: typeof import('vue')['customRef'] | ||||
|  |  | |||
|  | @ -112,7 +112,6 @@ export enum DICT_TYPE { | |||
| 
 | ||||
|   // ========== INFRA 模块 ==========
 | ||||
|   INFRA_BOOLEAN_STRING = 'infra_boolean_string', | ||||
|   INFRA_REDIS_TIMEOUT_TYPE = 'infra_redis_timeout_type', | ||||
|   INFRA_JOB_STATUS = 'infra_job_status', | ||||
|   INFRA_JOB_LOG_STATUS = 'infra_job_log_status', | ||||
|   INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status', | ||||
|  |  | |||
|  | @ -9,19 +9,19 @@ | |||
|       > | ||||
|         <!-- 左上角的 logo + 系统标题 --> | ||||
|         <div class="flex items-center relative text-white"> | ||||
|           <img src="@/assets/imgs/logo.png" alt="" class="w-48px h-48px mr-10px" /> | ||||
|           <img alt="" class="w-48px h-48px mr-10px" src="@/assets/imgs/logo.png" /> | ||||
|           <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> | ||||
|         </div> | ||||
|         <!-- 左边的背景图 + 欢迎语 --> | ||||
|         <div class="flex justify-center items-center h-[calc(100%-60px)]"> | ||||
|           <TransitionGroup | ||||
|             appear | ||||
|             tag="div" | ||||
|             enter-active-class="animate__animated animate__bounceInLeft" | ||||
|             tag="div" | ||||
|           > | ||||
|             <img src="@/assets/svgs/login-box-bg.svg" key="1" alt="" class="w-350px" /> | ||||
|             <div class="text-3xl text-white" key="2">{{ t('login.welcome') }}</div> | ||||
|             <div class="mt-5 font-normal text-white text-14px" key="3"> | ||||
|             <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> | ||||
|             <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> | ||||
|             <div key="3" class="mt-5 font-normal text-white text-14px"> | ||||
|               {{ t('login.message') }} | ||||
|             </div> | ||||
|           </TransitionGroup> | ||||
|  | @ -31,7 +31,7 @@ | |||
|         <!-- 右上角的主题、语言选择 --> | ||||
|         <div class="flex justify-between items-center text-white @2xl:justify-end @xl:justify-end"> | ||||
|           <div class="flex items-center @2xl:hidden @xl:hidden"> | ||||
|             <img src="@/assets/imgs/logo.png" alt="" class="w-48px h-48px mr-10px" /> | ||||
|             <img alt="" class="w-48px h-48px mr-10px" src="@/assets/imgs/logo.png" /> | ||||
|             <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> | ||||
|           </div> | ||||
|           <div class="flex justify-end items-center space-x-10px"> | ||||
|  | @ -52,20 +52,23 @@ | |||
|             <QrCodeForm class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" /> | ||||
|             <!-- 注册 --> | ||||
|             <RegisterForm class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" /> | ||||
|             <!-- 三方登录 --> | ||||
|             <SSOLoginVue class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" /> | ||||
|           </div> | ||||
|         </Transition> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import { underlineToHump } from '@/utils' | ||||
| 
 | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { ThemeSwitch } from '@/layout/components/ThemeSwitch' | ||||
| import { LocaleDropdown } from '@/layout/components/LocaleDropdown' | ||||
| import { LoginForm, MobileForm, RegisterForm, QrCodeForm } from './components' | ||||
| 
 | ||||
| import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components' | ||||
| 
 | ||||
| const { t } = useI18n() | ||||
| const appStore = useAppStore() | ||||
|  |  | |||
|  | @ -137,7 +137,7 @@ import { useIcon } from '@/hooks/web/useIcon' | |||
| import * as authUtil from '@/utils/auth' | ||||
| import { usePermissionStore } from '@/store/modules/permission' | ||||
| import * as LoginApi from '@/api/login' | ||||
| import { LoginStateEnum, useLoginState, useFormValid } from './useLogin' | ||||
| import { LoginStateEnum, useFormValid, useLoginState } from './useLogin' | ||||
| 
 | ||||
| const { t } = useI18n() | ||||
| const message = useMessage() | ||||
|  | @ -240,7 +240,12 @@ const handleLogin = async (params) => { | |||
|     if (!redirect.value) { | ||||
|       redirect.value = '/' | ||||
|     } | ||||
|     // 判断是否为SSO登录 | ||||
|     if (redirect.value.indexOf('sso') !== -1) { | ||||
|       window.location.href = window.location.href.replace('/login?redirect=', '') | ||||
|     } else { | ||||
|       push({ path: redirect.value || permissionStore.addRouters[0].path }) | ||||
|     } | ||||
|   } catch { | ||||
|     loginLoading.value = false | ||||
|   } finally { | ||||
|  | @ -291,6 +296,7 @@ onMounted(() => { | |||
|     color: var(--el-color-primary) !important; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .login-code { | ||||
|   width: 100%; | ||||
|   height: 38px; | ||||
|  |  | |||
|  | @ -16,7 +16,8 @@ const getFormTitle = computed(() => { | |||
|     [LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'), | ||||
|     [LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'), | ||||
|     [LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'), | ||||
|     [LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle') | ||||
|     [LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'), | ||||
|     [LoginStateEnum.SSO]: t('sys.login.ssoFormTitle') | ||||
|   } | ||||
|   return titleObj[unref(getLoginState)] | ||||
| }) | ||||
|  |  | |||
|  | @ -0,0 +1,186 @@ | |||
| <template> | ||||
|   <div v-show="ssoVisible" class="form-cont"> | ||||
|     <!-- 应用名 --> | ||||
|     <LoginFormTitle style="width: 100%" /> | ||||
|     <el-tabs class="form" style="float: none" value="uname"> | ||||
|       <el-tab-pane :label="client.name" name="uname" /> | ||||
|     </el-tabs> | ||||
|     <div> | ||||
|       <el-form :model="formData" class="login-form"> | ||||
|         <!-- 授权范围的选择 --> | ||||
|         此第三方应用请求获得以下权限: | ||||
|         <el-form-item prop="scopes"> | ||||
|           <el-checkbox-group v-model="formData.scopes"> | ||||
|             <el-checkbox | ||||
|               v-for="scope in queryParams.scopes" | ||||
|               :key="scope" | ||||
|               :label="scope" | ||||
|               style="display: block; margin-bottom: -10px" | ||||
|             > | ||||
|               {{ formatScope(scope) }} | ||||
|             </el-checkbox> | ||||
|           </el-checkbox-group> | ||||
|         </el-form-item> | ||||
|         <!-- 下方的登录按钮 --> | ||||
|         <el-form-item class="w-1/1"> | ||||
|           <el-button | ||||
|             :loading="formLoading" | ||||
|             class="w-6/10" | ||||
|             type="primary" | ||||
|             @click.prevent="handleAuthorize(true)" | ||||
|           > | ||||
|             <span v-if="!formLoading">同意授权</span> | ||||
|             <span v-else>授 权 中...</span> | ||||
|           </el-button> | ||||
|           <el-button class="w-3/10" @click.prevent="handleAuthorize(false)">拒绝</el-button> | ||||
|         </el-form-item> | ||||
|       </el-form> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <script lang="ts" name="SSOLogin" setup> | ||||
| import LoginFormTitle from './LoginFormTitle.vue' | ||||
| import * as OAuth2Api from '@/api/login/oauth2' | ||||
| import { LoginStateEnum, useLoginState } from './useLogin' | ||||
| import type { RouteLocationNormalizedLoaded } from 'vue-router' | ||||
| const route = useRoute() // 路由 | ||||
| const { currentRoute } = useRouter() // 路由 | ||||
| const { getLoginState, setLoginState } = useLoginState() | ||||
| 
 | ||||
| const client = ref({ | ||||
|   // 客户端信息 | ||||
|   name: '', | ||||
|   logo: '' | ||||
| }) | ||||
| const queryParams = reactive({ | ||||
|   // URL 上的 client_id、scope 等参数 | ||||
|   responseType: '', | ||||
|   clientId: '', | ||||
|   redirectUri: '', | ||||
|   state: '', | ||||
|   scopes: [] // 优先从 query 参数获取;如果未传递,从后端获取 | ||||
| }) | ||||
| const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // 是否展示 SSO 登录的表单 | ||||
| const formData = reactive({ | ||||
|   scopes: [] // 已选中的 scope 数组 | ||||
| }) | ||||
| const formLoading = ref(false) // 表单是否提交中 | ||||
| 
 | ||||
| /** 初始化授权信息 */ | ||||
| const init = async () => { | ||||
|   // 防止在没有登录的情况下循环弹窗 | ||||
|   if (typeof route.query.client_id === 'undefined') return | ||||
|   // 解析参数 | ||||
|   // 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write | ||||
|   // 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read | ||||
|   queryParams.responseType = route.query.response_type as string | ||||
|   queryParams.clientId = route.query.client_id as string | ||||
|   queryParams.redirectUri = route.query.redirect_uri as string | ||||
|   queryParams.state = route.query.state as string | ||||
|   if (route.query.scope) { | ||||
|     queryParams.scopes = (route.query.scope as string).split(' ') | ||||
|   } | ||||
| 
 | ||||
|   // 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。 | ||||
|   if (queryParams.scopes.length > 0) { | ||||
|     const data = await doAuthorize(true, queryParams.scopes, []) | ||||
|     if (data) { | ||||
|       location.href = data | ||||
|       return | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // 获取授权页的基本信息 | ||||
|   const data = await OAuth2Api.getAuthorize(queryParams.clientId) | ||||
|   client.value = data.client | ||||
|   // 解析 scope | ||||
|   let scopes | ||||
|   // 1.1 如果 params.scope 非空,则过滤下返回的 scopes | ||||
|   if (queryParams.scopes.length > 0) { | ||||
|     scopes = [] | ||||
|     for (const scope of data.scopes) { | ||||
|       if (queryParams.scopes.indexOf(scope.key) >= 0) { | ||||
|         scopes.push(scope) | ||||
|       } | ||||
|     } | ||||
|     // 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它 | ||||
|   } else { | ||||
|     scopes = data.scopes | ||||
|     for (const scope of scopes) { | ||||
|       queryParams.scopes.push(scope.key) | ||||
|     } | ||||
|   } | ||||
|   // 生成已选中的 checkedScopes | ||||
|   for (const scope of scopes) { | ||||
|     if (scope.value) { | ||||
|       formData.scopes.push(scope.key) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 处理授权的提交 */ | ||||
| const handleAuthorize = async (approved) => { | ||||
|   // 计算 checkedScopes + uncheckedScopes | ||||
|   let checkedScopes | ||||
|   let uncheckedScopes | ||||
|   if (approved) { | ||||
|     // 同意授权,按照用户的选择 | ||||
|     checkedScopes = formData.scopes | ||||
|     uncheckedScopes = queryParams.scopes.filter((item) => checkedScopes.indexOf(item) === -1) | ||||
|   } else { | ||||
|     // 拒绝,则都是取消 | ||||
|     checkedScopes = [] | ||||
|     uncheckedScopes = queryParams.scopes | ||||
|   } | ||||
|   // 提交授权的请求 | ||||
|   formLoading.value = true | ||||
|   try { | ||||
|     const data = await doAuthorize(false, checkedScopes, uncheckedScopes) | ||||
|     if (!data) { | ||||
|       return | ||||
|     } | ||||
|     location.href = data | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 调用授权 API 接口 */ | ||||
| const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => { | ||||
|   return OAuth2Api.authorize( | ||||
|     queryParams.responseType, | ||||
|     queryParams.clientId, | ||||
|     queryParams.redirectUri, | ||||
|     queryParams.state, | ||||
|     autoApprove, | ||||
|     checkedScopes, | ||||
|     uncheckedScopes | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| /** 格式化 scope 文本 */ | ||||
| const formatScope = (scope) => { | ||||
|   // 格式化 scope 授权范围,方便用户理解。 | ||||
|   // 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。 | ||||
|   switch (scope) { | ||||
|     case 'user.read': | ||||
|       return '访问你的个人信息' | ||||
|     case 'user.write': | ||||
|       return '修改你的个人信息' | ||||
|     default: | ||||
|       return scope | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 监听当前路由为 SSOLogin 时,进行数据的初始化 */ | ||||
| watch( | ||||
|   () => currentRoute.value, | ||||
|   (route: RouteLocationNormalizedLoaded) => { | ||||
|     if (route.name === 'SSOLogin') { | ||||
|       setLoginState(LoginStateEnum.SSO) | ||||
|       init() | ||||
|     } | ||||
|   }, | ||||
|   { immediate: true } | ||||
| ) | ||||
| </script> | ||||
|  | @ -3,5 +3,6 @@ import MobileForm from './MobileForm.vue' | |||
| import LoginFormTitle from './LoginFormTitle.vue' | ||||
| import RegisterForm from './RegisterForm.vue' | ||||
| import QrCodeForm from './QrCodeForm.vue' | ||||
| import SSOLoginVue from './SSOLogin.vue' | ||||
| 
 | ||||
| export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm } | ||||
| export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue } | ||||
|  |  | |||
|  | @ -5,7 +5,8 @@ export enum LoginStateEnum { | |||
|   REGISTER, | ||||
|   RESET_PASSWORD, | ||||
|   MOBILE, | ||||
|   QR_CODE | ||||
|   QR_CODE, | ||||
|   SSO | ||||
| } | ||||
| 
 | ||||
| const currentState = ref(LoginStateEnum.LOGIN) | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
|       <el-table-column label="定义编号" align="center" prop="id" width="400" /> | ||||
|       <el-table-column label="流程名称" align="center" prop="name" width="200"> | ||||
|         <template #default="scope"> | ||||
|           <el-button type="text" @click="handleBpmnDetail(scope.row)"> | ||||
|           <el-button type="primary" link @click="handleBpmnDetail(scope.row)"> | ||||
|             <span>{{ scope.row.name }}</span> | ||||
|           </el-button> | ||||
|         </template> | ||||
|  | @ -23,7 +23,7 @@ | |||
|           > | ||||
|             <span>{{ scope.row.formName }}</span> | ||||
|           </el-button> | ||||
|           <el-button v-else type="text" @click="handleFormDetail(scope.row)"> | ||||
|           <el-button v-else type="primary" link @click="handleFormDetail(scope.row)"> | ||||
|             <span>{{ scope.row.formCustomCreatePath }}</span> | ||||
|           </el-button> | ||||
|         </template> | ||||
|  | @ -93,7 +93,7 @@ | |||
|   </Dialog> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts" name="Form"> | ||||
| <script setup lang="ts" name="BpmProcessDefinition"> | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as DefinitionApi from '@/api/bpm/definition' | ||||
|  |  | |||
|  | @ -83,12 +83,11 @@ | |||
|   </Dialog> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts" name="Form"> | ||||
| <script setup lang="ts" name="BpmForm"> | ||||
| 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() // 路由 | ||||
|  | @ -130,7 +129,7 @@ const resetQuery = () => { | |||
| /** 添加/修改操作 */ | ||||
| const openForm = (id?: number) => { | ||||
|   push({ | ||||
|     name: 'bpmFormEditor', | ||||
|     name: 'BpmFormEditor', | ||||
|     query: { | ||||
|       id | ||||
|     } | ||||
|  |  | |||
|  | @ -111,7 +111,7 @@ | |||
|   <UserGroupForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts" name="UserGroup"> | ||||
| <script setup lang="ts" name="BpmUserGroup"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as UserGroupApi from '@/api/bpm/userGroup' | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ | |||
|   </ContentWrap> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| <script setup lang="ts" name="BpmModelEditor"> | ||||
| // 自定义元素选中时的弹出菜单(修改 默认任务 为 用户任务) | ||||
| import CustomContentPadProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/content-pad' | ||||
| // 自定义左侧菜单(修改 默认任务 为 用户任务) | ||||
|  |  | |||
|  | @ -65,7 +65,7 @@ | |||
|       <el-table-column label="流程标识" align="center" prop="key" width="200" /> | ||||
|       <el-table-column label="流程名称" align="center" prop="name" width="200"> | ||||
|         <template #default="scope"> | ||||
|           <el-button type="text" @click="handleBpmnDetail(scope.row)"> | ||||
|           <el-button type="primary" link @click="handleBpmnDetail(scope.row)"> | ||||
|             <span>{{ scope.row.name }}</span> | ||||
|           </el-button> | ||||
|         </template> | ||||
|  | @ -224,7 +224,7 @@ | |||
|   </Dialog> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts" name="Form"> | ||||
| <script setup lang="ts" name="BpmModel"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter, formatDate } from '@/utils/formatTime' | ||||
| import * as ModelApi from '@/api/bpm/model' | ||||
|  | @ -319,7 +319,7 @@ const handleChangeState = async (row) => { | |||
| /** 设计流程 */ | ||||
| const handleDesign = (row) => { | ||||
|   push({ | ||||
|     name: 'modelEditor', | ||||
|     name: 'BpmModelEditor', | ||||
|     query: { | ||||
|       modelId: row.id | ||||
|     } | ||||
|  | @ -352,7 +352,7 @@ const handleAssignRule = (row) => { | |||
| /** 跳转到指定流程定义列表 */ | ||||
| const handleDefinitionList = (row) => { | ||||
|   push({ | ||||
|     name: 'BpmProcessDefinitionList', | ||||
|     name: 'BpmProcessDefinition', | ||||
|     query: { | ||||
|       key: row.key | ||||
|     } | ||||
|  |  | |||
|  | @ -1,56 +1,107 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <!-- 对话框(添加 / 修改) --> | ||||
|     <Form :schema="allSchemas.formSchema" :rules="rules" ref="formRef" /> | ||||
|     <!-- 按钮:保存 --> | ||||
|     <XButton | ||||
|       type="primary" | ||||
|       :title="t('action.save')" | ||||
|       :loading="actionLoading" | ||||
|       @click="submitForm" | ||||
|   <Dialog title="发起 OA 请假流程" v-model="modelVisible"> | ||||
|     <el-form | ||||
|       ref="formRef" | ||||
|       :model="formData" | ||||
|       :rules="formRules" | ||||
|       label-width="80px" | ||||
|       v-loading="formLoading" | ||||
|     > | ||||
|       <el-form-item label="请假类型" prop="type"> | ||||
|         <el-select v-model="formData.type" placeholder="请选择请假类型" clearable> | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|           /> | ||||
|   </ContentWrap> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="开始时间" prop="startTime"> | ||||
|         <el-date-picker | ||||
|           clearable | ||||
|           v-model="formData.startTime" | ||||
|           type="datetime" | ||||
|           value-format="x" | ||||
|           placeholder="请选择开始时间" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="结束时间" prop="endTime"> | ||||
|         <el-date-picker | ||||
|           clearable | ||||
|           v-model="formData.endTime" | ||||
|           type="datetime" | ||||
|           value-format="x" | ||||
|           placeholder="请选择结束时间" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="原因" prop="reason"> | ||||
|         <el-input v-model="formData.reason" type="textarea" placeholder="请输请假原因" /> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|     <template #footer> | ||||
|       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> | ||||
|       <el-button @click="modelVisible = false">取 消</el-button> | ||||
|     </template> | ||||
|   </Dialog> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { FormExpose } from '@/components/Form' | ||||
| // import XEUtils from 'xe-utils' | ||||
| 
 | ||||
| // 业务相关的 import | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import * as LeaveApi from '@/api/bpm/leave' | ||||
| import { rules, allSchemas } from './leave.data' | ||||
| 
 | ||||
| const { t } = useI18n() // 国际化 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { push } = useRouter() // 路由 | ||||
| 
 | ||||
| // 表单参数 | ||||
| const actionLoading = ref(false) // 按钮 Loading | ||||
| const formRef = ref<FormExpose>() // 表单 Ref | ||||
| const modelVisible = ref(false) // 弹窗的是否展示 | ||||
| const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 | ||||
| const formData = ref({ | ||||
|   type: undefined, | ||||
|   reason: undefined, | ||||
|   startTime: undefined, | ||||
|   endTime: undefined | ||||
| }) | ||||
| const formRules = reactive({ | ||||
|   type: [{ required: true, message: '请假类型不能为空', trigger: 'blur' }], | ||||
|   reason: [{ required: true, message: '请假原因不能为空', trigger: 'change' }], | ||||
|   startTime: [{ required: true, message: '请假开始时间不能为空', trigger: 'change' }], | ||||
|   endTime: [{ required: true, message: '请假结束时间不能为空', trigger: 'change' }] | ||||
| }) | ||||
| const formRef = ref() // 表单 Ref | ||||
| 
 | ||||
| // 提交按钮 | ||||
| /** 打开弹窗 */ | ||||
| const open = async () => { | ||||
|   modelVisible.value = true | ||||
|   resetForm() | ||||
| } | ||||
| defineExpose({ open }) // 提供 open 方法,用于打开弹窗 | ||||
| 
 | ||||
| /** 提交表单 */ | ||||
| const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 | ||||
| const submitForm = async () => { | ||||
|   const elForm = unref(formRef)?.getElFormRef() | ||||
|   if (!elForm) return | ||||
|   elForm.validate(async (valid) => { | ||||
|     if (!valid) { | ||||
|       return | ||||
|     } | ||||
|   // 校验表单 | ||||
|   if (!formRef) return | ||||
|   const valid = await formRef.value.validate() | ||||
|   if (!valid) return | ||||
|   // 提交请求 | ||||
|   formLoading.value = true | ||||
|   try { | ||||
|       // 设置提交中 | ||||
|       actionLoading.value = true | ||||
|       const data = unref(formRef)?.formModel as LeaveApi.LeaveVO | ||||
|       // data.startTime = XEUtils.toDateString(data.startTime, 'yyyy-MM-dd HH:mm:ss') | ||||
|       // data.endTime = XEUtils.toDateString(data.endTime, 'yyyy-MM-dd HH:mm:ss') | ||||
|       data.startTime = Date.parse(new Date(data.startTime).toString()).toString() | ||||
|       data.endTime = Date.parse(new Date(data.endTime).toString()).toString() | ||||
|       // 添加的提交 | ||||
|     const data = formData.value as unknown as LeaveApi.LeaveVO | ||||
|     await LeaveApi.createLeave(data) | ||||
|       message.success(t('common.createSuccess')) | ||||
|       // 关闭窗口 | ||||
|       push('/bpm/oa/leave') | ||||
|     message.success('新增成功') | ||||
|     modelVisible.value = false | ||||
|     // 发送操作成功的事件 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|       actionLoading.value = false | ||||
|     formLoading.value = false | ||||
|   } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     type: undefined, | ||||
|     reason: undefined, | ||||
|     startTime: undefined, | ||||
|     endTime: undefined | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,42 +1,40 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <!-- 详情 --> | ||||
|     <Descriptions :schema="allSchemas.detailSchema" :data="formData" /> | ||||
|     <el-button @click="routerReturn" type="primary">返回</el-button> | ||||
|   </ContentWrap> | ||||
|   <Dialog title="详情" v-model="modelVisible" :scroll="true" :max-height="200"> | ||||
|     <el-descriptions border :column="1"> | ||||
|       <el-descriptions-item label="请假类型"> | ||||
|         <dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="detailData.type" /> | ||||
|       </el-descriptions-item> | ||||
|       <el-descriptions-item label="开始时间"> | ||||
|         {{ formatDate(detailData.startTime) }} | ||||
|       </el-descriptions-item> | ||||
|       <el-descriptions-item label="结束时间"> | ||||
|         {{ formatDate(detailData.endTime) }} | ||||
|       </el-descriptions-item> | ||||
|       <el-descriptions-item label="原因"> | ||||
|         {{ detailData.reason }} | ||||
|       </el-descriptions-item> | ||||
|     </el-descriptions> | ||||
|   </Dialog> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| // 业务相关的 import | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import { formatDate } from '@/utils/formatTime' | ||||
| import * as LeaveApi from '@/api/bpm/leave' | ||||
| import { allSchemas } from '@/views/bpm/oa/leave/leave.data' | ||||
| import { useRouter } from 'vue-router' | ||||
| const router = useRouter() | ||||
| const { query } = useRoute() // 查询参数 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| 
 | ||||
| const id = ref() // 请假编号 | ||||
| // 表单参数 | ||||
| const formData = ref({ | ||||
|   startTime: undefined, | ||||
|   endTime: undefined, | ||||
|   type: undefined, | ||||
|   reason: undefined | ||||
| }) | ||||
| const modelVisible = ref(false) // 弹窗的是否展示 | ||||
| const detailLoading = ref(false) // 表单的加载中 | ||||
| const detailData = ref() // 详情数据 | ||||
| 
 | ||||
| const routerReturn = () => { | ||||
|   router.back() | ||||
| } | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   id.value = query.id | ||||
|   if (!id.value) { | ||||
|     message.error('未传递 id 参数,无法查看 OA 请假信息') | ||||
|     return | ||||
| /** 打开弹窗 */ | ||||
| const open = async (data: LeaveApi.LeaveVO) => { | ||||
|   modelVisible.value = true | ||||
|   // 设置数据 | ||||
|   detailLoading.value = true | ||||
|   try { | ||||
|     detailData.value = data | ||||
|   } finally { | ||||
|     detailLoading.value = false | ||||
|   } | ||||
|   // 获得请假信息 | ||||
|   LeaveApi.getLeave(id.value).then((data) => { | ||||
|     formData.value = data | ||||
|   }) | ||||
| }) | ||||
| } | ||||
| defineExpose({ open }) // 提供 open 方法,用于打开弹窗 | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,83 +1,236 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <XTable @register="registerTable"> | ||||
|       <template #toolbar_buttons> | ||||
|         <!-- 操作:发起请假 --> | ||||
|         <XButton type="primary" preIcon="ep:plus" title="发起请假" @click="handleCreate()" /> | ||||
|       </template> | ||||
|       <template #actionbtns_default="{ row }"> | ||||
|         <!-- 操作: 取消请假 --> | ||||
|         <XTextButton | ||||
|           preIcon="ep:delete" | ||||
|           title="取消请假" | ||||
|           v-hasPermi="['bpm:oa-leave:create']" | ||||
|           v-if="row.result === 1" | ||||
|           @click="cancelLeave(row)" | ||||
|     <!-- 搜索工作栏 --> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="请假类型" prop="type"> | ||||
|         <el-select | ||||
|           v-model="queryParams.type" | ||||
|           placeholder="请选择请假类型" | ||||
|           clearable | ||||
|           class="!w-240px" | ||||
|         > | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|           /> | ||||
|         <!-- 操作: 详情 --> | ||||
|         <XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" /> | ||||
|         <!-- 操作: 审批进度 --> | ||||
|         <XTextButton preIcon="ep:edit-pen" title="审批进度" @click="handleProcessDetail(row)" /> | ||||
|       </template> | ||||
|     </XTable> | ||||
|         </el-select> | ||||
|       </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" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="结果" prop="result"> | ||||
|         <el-select v-model="queryParams.result" placeholder="请选择结果" clearable class="!w-240px"> | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="原因" prop="reason"> | ||||
|         <el-input | ||||
|           v-model="queryParams.reason" | ||||
|           placeholder="请输入原因" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </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="handleCreate()"> | ||||
|           <Icon icon="ep:plus" class="mr-5px" /> 发起请假 | ||||
|         </el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| // 全局相关的 import | ||||
| import { ElMessageBox } from 'element-plus' | ||||
| // 业务相关的 import | ||||
| import { allSchemas } from './leave.data' | ||||
|   <!-- 列表 --> | ||||
|   <ContentWrap> | ||||
|     <el-table v-loading="loading" :data="list"> | ||||
|       <el-table-column label="申请编号" align="center" prop="id" /> | ||||
|       <el-table-column label="状态" align="center" prop="result"> | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         label="开始时间" | ||||
|         align="center" | ||||
|         prop="startTime" | ||||
|         width="180" | ||||
|         :formatter="dateFormatter" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         label="结束时间" | ||||
|         align="center" | ||||
|         prop="endTime" | ||||
|         width="180" | ||||
|         :formatter="dateFormatter" | ||||
|       /> | ||||
|       <el-table-column label="请假类型" align="center" prop="type"> | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="scope.row.type" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="原因" align="center" prop="reason" /> | ||||
|       <el-table-column | ||||
|         label="申请时间" | ||||
|         align="center" | ||||
|         prop="createTime" | ||||
|         width="180" | ||||
|         :formatter="dateFormatter" | ||||
|       /> | ||||
|       <el-table-column label="操作" align="center" width="200"> | ||||
|         <template #default="scope"> | ||||
|           <el-button | ||||
|             link | ||||
|             type="primary" | ||||
|             @click="handleDetail(scope.row)" | ||||
|             v-hasPermi="['bpm:oa-leave:query']" | ||||
|           > | ||||
|             详情 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             link | ||||
|             type="primary" | ||||
|             @click="handleProcessDetail(scope.row)" | ||||
|             v-hasPermi="['bpm:oa-leave:query']" | ||||
|           > | ||||
|             进度 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             link | ||||
|             type="danger" | ||||
|             @click="cancelLeave(scope.row)" | ||||
|             v-hasPermi="['bpm:oa-leave:create']" | ||||
|             v-if="scope.row.result === 1" | ||||
|           > | ||||
|             取消 | ||||
|           </el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|     <!-- 分页 --> | ||||
|     <Pagination | ||||
|       :total="total" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 表单弹窗:详情 --> | ||||
|   <LeaveDetail ref="detailRef" /> | ||||
|   <!-- 表单弹窗:添加 --> | ||||
|   <LeaveForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="BpmOALeave"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as LeaveApi from '@/api/bpm/leave' | ||||
| import * as ProcessInstanceApi from '@/api/bpm/processInstance' | ||||
| 
 | ||||
| const { t } = useI18n() // 国际化 | ||||
| import LeaveDetail from './detail.vue' | ||||
| import LeaveForm from './create.vue' | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { push } = useRouter() // 路由 | ||||
| const router = useRouter() // 路由 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| const [registerTable, { reload }] = useXTable({ | ||||
|   allSchemas: allSchemas, | ||||
|   getListApi: LeaveApi.getLeavePage | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   type: undefined, | ||||
|   result: undefined, | ||||
|   reason: undefined, | ||||
|   createTime: [] | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| 
 | ||||
| // 发起请假 | ||||
| const handleCreate = () => { | ||||
|   push({ | ||||
|     name: 'OALeaveCreate' | ||||
|   }) | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const data = await LeaveApi.getLeavePage(queryParams) | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 取消请假 | ||||
| const cancelLeave = (row) => { | ||||
|   ElMessageBox.prompt('请输入取消原因', '取消流程', { | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1 | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   queryFormRef.value.resetFields() | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| /** 添加操作 */ | ||||
| const formRef = ref() | ||||
| const handleCreate = () => { | ||||
|   formRef.value.open() | ||||
| } | ||||
| 
 | ||||
| /** 详情操作 */ | ||||
| const detailRef = ref() | ||||
| const handleDetail = (data: LeaveApi.LeaveVO) => { | ||||
|   detailRef.value.open(data) | ||||
| } | ||||
| 
 | ||||
| /** 取消请假操作 */ | ||||
| const cancelLeave = async (row) => { | ||||
|   // 二次确认 | ||||
|   const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', { | ||||
|     confirmButtonText: t('common.ok'), | ||||
|     cancelButtonText: t('common.cancel'), | ||||
|     inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格 | ||||
|     inputErrorMessage: '取消原因不能为空' | ||||
|   }).then(async ({ value }) => { | ||||
|   }) | ||||
|   // 发起取消 | ||||
|   await ProcessInstanceApi.cancelProcessInstance(row.id, value) | ||||
|   message.success('取消成功') | ||||
|     reload() | ||||
|   }) | ||||
|   // 刷新列表 | ||||
|   await getList() | ||||
| } | ||||
| 
 | ||||
| // 详情 | ||||
| const handleDetail = (row) => { | ||||
|   push({ | ||||
|     name: 'OALeaveDetail', | ||||
|     query: { | ||||
|       id: row.id | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 审批进度 | ||||
| /** 审批进度 */ | ||||
| const handleProcessDetail = (row) => { | ||||
|   push({ | ||||
|   router.push({ | ||||
|     name: 'BpmProcessInstanceDetail', | ||||
|     query: { | ||||
|       id: row.processInstanceId | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(() => { | ||||
|   getList() | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,91 +0,0 @@ | |||
| import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' | ||||
| 
 | ||||
| const { t } = useI18n() // 国际化
 | ||||
| 
 | ||||
| // 表单校验
 | ||||
| export const rules = reactive({ | ||||
|   startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }], | ||||
|   endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }], | ||||
|   type: [{ required: true, message: '请假类型不能为空', trigger: 'change' }] | ||||
| }) | ||||
| 
 | ||||
| // crudSchemas
 | ||||
| const crudSchemas = reactive<VxeCrudSchema>({ | ||||
|   primaryKey: 'id', | ||||
|   primaryType: 'id', | ||||
|   primaryTitle: '申请编号', | ||||
|   action: true, | ||||
|   actionWidth: '260', | ||||
|   searchSpan: 8, | ||||
|   columns: [ | ||||
|     { | ||||
|       title: t('common.status'), | ||||
|       field: 'result', | ||||
|       dictType: DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, | ||||
|       dictClass: 'number', | ||||
|       isSearch: true, | ||||
|       isForm: false | ||||
|     }, | ||||
|     { | ||||
|       title: t('common.startTimeText'), | ||||
|       field: 'startTime', | ||||
|       formatter: 'formatDay', | ||||
|       table: { | ||||
|         width: 180 | ||||
|       }, | ||||
|       detail: { | ||||
|         dateFormat: 'YYYY-MM-DD' | ||||
|       }, | ||||
|       form: { | ||||
|         component: 'DatePicker' | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       title: t('common.endTimeText'), | ||||
|       field: 'endTime', | ||||
|       formatter: 'formatDay', | ||||
|       table: { | ||||
|         width: 180 | ||||
|       }, | ||||
|       detail: { | ||||
|         dateFormat: 'YYYY-MM-DD' | ||||
|       }, | ||||
|       form: { | ||||
|         component: 'DatePicker' | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       title: '请假类型', | ||||
|       field: 'type', | ||||
|       dictType: DICT_TYPE.BPM_OA_LEAVE_TYPE, | ||||
|       dictClass: 'number', | ||||
|       isSearch: true | ||||
|     }, | ||||
|     { | ||||
|       title: '原因', | ||||
|       field: 'reason', | ||||
|       isSearch: true, | ||||
|       componentProps: { | ||||
|         type: 'textarea', | ||||
|         rows: 4 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       title: '申请时间', | ||||
|       field: 'createTime', | ||||
|       formatter: 'formatDate', | ||||
|       table: { | ||||
|         width: 180 | ||||
|       }, | ||||
|       isSearch: true, | ||||
|       search: { | ||||
|         show: true, | ||||
|         itemRender: { | ||||
|           name: 'XDataTimePicker' | ||||
|         } | ||||
|       }, | ||||
|       isForm: false | ||||
|     } | ||||
|   ] | ||||
| }) | ||||
| export const { allSchemas } = useVxeCrudSchemas(crudSchemas) | ||||
|  | @ -46,7 +46,7 @@ | |||
|     <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML" /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| <script setup lang="ts" name="BpmProcessInstanceCreate"> | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import * as DefinitionApi from '@/api/bpm/definition' | ||||
| import * as ProcessInstanceApi from '@/api/bpm/processInstance' | ||||
|  |  | |||
|  | @ -96,7 +96,7 @@ | |||
|     <TaskUpdateAssigneeForm ref="taskUpdateAssigneeFormRef" @success="getDetail" /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| <script setup lang="ts" name="BpmProcessInstanceDetail"> | ||||
| import { useUserStore } from '@/store/modules/user' | ||||
| import { setConfAndFields2 } from '@/utils/formCreate' | ||||
| import type { ApiAttrs } from '@form-create/element-ui/types/config' | ||||
|  |  | |||
|  | @ -1,64 +1,211 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <!-- 列表 --> | ||||
|     <XTable @register="registerTable"> | ||||
|       <template #toolbar_buttons> | ||||
|         <!-- 操作:新增 --> | ||||
|         <XButton | ||||
|     <!-- 搜索工作栏 --> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="流程名称" prop="name"> | ||||
|         <el-input | ||||
|           v-model="queryParams.name" | ||||
|           placeholder="请输入流程名称" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="所属流程" prop="processDefinitionId"> | ||||
|         <el-input | ||||
|           v-model="queryParams.processDefinitionId" | ||||
|           placeholder="请输入流程定义的编号" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="流程分类" prop="category"> | ||||
|         <el-select | ||||
|           v-model="queryParams.category" | ||||
|           placeholder="请选择流程分类" | ||||
|           clearable | ||||
|           class="!w-240px" | ||||
|         > | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="状态" prop="status"> | ||||
|         <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="结果" prop="result"> | ||||
|         <el-select v-model="queryParams.result" placeholder="请选择结果" clearable class="!w-240px"> | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </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" | ||||
|         /> | ||||
|       </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" | ||||
|           preIcon="ep:zoom-in" | ||||
|           title="发起流程" | ||||
|           plain | ||||
|           v-hasPermi="['bpm:process-instance:query']" | ||||
|           @click="handleCreate" | ||||
|         /> | ||||
|         > | ||||
|           <Icon icon="ep:plus" class="mr-5px" /> 发起流程 | ||||
|         </el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|   <ContentWrap> | ||||
|     <el-table v-loading="loading" :data="list"> | ||||
|       <el-table-column label="流程编号" align="center" prop="id" width="300px" /> | ||||
|       <el-table-column label="流程名称" align="center" prop="name" /> | ||||
|       <el-table-column label="流程分类" align="center" prop="category"> | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" /> | ||||
|         </template> | ||||
|       <!-- 流程分类 --> | ||||
|       <template #category_default="{ row }"> | ||||
|         <DictTag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="Number(row?.category)" /> | ||||
|       </template> | ||||
|       <!-- 当前审批任务 --> | ||||
|       <template #tasks_default="{ row }"> | ||||
|         <el-button v-for="task in row.tasks" :key="task.id" link> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="当前审批任务" align="center" prop="tasks"> | ||||
|         <template #default="scope"> | ||||
|           <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link> | ||||
|             <span>{{ task.name }}</span> | ||||
|           </el-button> | ||||
|         </template> | ||||
|       <!-- 操作 --> | ||||
|       <template #actionbtns_default="{ row }"> | ||||
|         <XTextButton | ||||
|           preIcon="ep:view" | ||||
|           :title="t('action.detail')" | ||||
|           v-hasPermi="['bpm:process-instance:cancel']" | ||||
|           @click="handleDetail(row)" | ||||
|         /> | ||||
|         <XTextButton | ||||
|           preIcon="ep:delete" | ||||
|           title="取消" | ||||
|           v-if="row.result === 1" | ||||
|           v-hasPermi="['bpm:process-instance:query']" | ||||
|           @click="handleCancel(row)" | ||||
|         /> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="状态" prop="status"> | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" /> | ||||
|         </template> | ||||
|     </XTable> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="结果" prop="result"> | ||||
|         <template #default="scope"> | ||||
|           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         label="提交时间" | ||||
|         align="center" | ||||
|         prop="createTime" | ||||
|         width="180" | ||||
|         :formatter="dateFormatter" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         label="结束时间" | ||||
|         align="center" | ||||
|         prop="endTime" | ||||
|         width="180" | ||||
|         :formatter="dateFormatter" | ||||
|       /> | ||||
|       <el-table-column label="操作" align="center"> | ||||
|         <template #default="scope"> | ||||
|           <el-button | ||||
|             link | ||||
|             type="primary" | ||||
|             v-hasPermi="['bpm:process-instance:cancel']" | ||||
|             @click="handleDetail(scope.row)" | ||||
|           > | ||||
|             详情 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             link | ||||
|             type="primary" | ||||
|             v-if="scope.row.result === 1" | ||||
|             v-hasPermi="['bpm:process-instance:query']" | ||||
|             @click="handleCancel(scope.row)" | ||||
|           > | ||||
|             取消 | ||||
|           </el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|     <!-- 分页 --> | ||||
|     <Pagination | ||||
|       :total="total" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| // 全局相关的 import | ||||
| <script setup lang="ts" name="BpmProcessInstance"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { ElMessageBox } from 'element-plus' | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| 
 | ||||
| // 业务相关的 import | ||||
| import * as ProcessInstanceApi from '@/api/bpm/processInstance' | ||||
| import { allSchemas } from './process.data' | ||||
| 
 | ||||
| const router = useRouter() // 路由 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| // ========== 列表相关 ========== | ||||
| const [registerTable, { reload }] = useXTable({ | ||||
|   allSchemas: allSchemas, | ||||
|   getListApi: ProcessInstanceApi.getMyProcessInstancePage | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   name: '', | ||||
|   processDefinitionId: undefined, | ||||
|   category: undefined, | ||||
|   status: undefined, | ||||
|   result: undefined, | ||||
|   createTime: [] | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const data = await ProcessInstanceApi.getMyProcessInstancePage(queryParams) | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1 | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   queryFormRef.value.resetFields() | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| /** 发起流程操作 **/ | ||||
| const handleCreate = () => { | ||||
|  | @ -67,7 +214,7 @@ const handleCreate = () => { | |||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 列表操作 | ||||
| /** 查看详情 */ | ||||
| const handleDetail = (row) => { | ||||
|   router.push({ | ||||
|     name: 'BpmProcessInstanceDetail', | ||||
|  | @ -78,16 +225,23 @@ const handleDetail = (row) => { | |||
| } | ||||
| 
 | ||||
| /** 取消按钮操作 */ | ||||
| const handleCancel = (row) => { | ||||
|   ElMessageBox.prompt('请输入取消原因', '取消流程', { | ||||
| const handleCancel = async (row) => { | ||||
|   // 二次确认 | ||||
|   const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', { | ||||
|     confirmButtonText: t('common.ok'), | ||||
|     cancelButtonText: t('common.cancel'), | ||||
|     inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格 | ||||
|     inputErrorMessage: '取消原因不能为空' | ||||
|   }).then(async ({ value }) => { | ||||
|   }) | ||||
|   // 发起取消 | ||||
|   await ProcessInstanceApi.cancelProcessInstance(row.id, value) | ||||
|   message.success('取消成功') | ||||
|     reload() | ||||
|   }) | ||||
|   // 刷新列表 | ||||
|   await getList() | ||||
| } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(() => { | ||||
|   getList() | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,94 +0,0 @@ | |||
| import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' | ||||
| 
 | ||||
| const { t } = useI18n() // 国际化
 | ||||
| 
 | ||||
| // CrudSchema
 | ||||
| const crudSchemas = reactive<VxeCrudSchema>({ | ||||
|   primaryKey: 'id', | ||||
|   primaryType: null, | ||||
|   primaryTitle: '编号', | ||||
|   action: true, | ||||
|   actionWidth: '200px', | ||||
|   columns: [ | ||||
|     { | ||||
|       title: '编号', | ||||
|       field: 'id', | ||||
|       table: { | ||||
|         width: 320 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       title: '流程名', | ||||
|       field: 'name', | ||||
|       isSearch: true | ||||
|     }, | ||||
|     { | ||||
|       title: '所属流程', | ||||
|       field: 'processDefinitionId', | ||||
|       isSearch: true, | ||||
|       isTable: false | ||||
|     }, | ||||
|     { | ||||
|       title: '流程分类', | ||||
|       field: 'category', | ||||
|       dictType: DICT_TYPE.BPM_MODEL_CATEGORY, | ||||
|       dictClass: 'number', | ||||
|       isSearch: true, | ||||
|       table: { | ||||
|         slots: { | ||||
|           default: 'category_default' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       title: '当前审批任务', | ||||
|       field: 'tasks', | ||||
|       table: { | ||||
|         width: 140, | ||||
|         slots: { | ||||
|           default: 'tasks_default' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       title: t('common.status'), | ||||
|       field: 'status', | ||||
|       dictType: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS, | ||||
|       dictClass: 'number', | ||||
|       isSearch: true | ||||
|     }, | ||||
|     { | ||||
|       title: '结果', | ||||
|       field: 'result', | ||||
|       dictType: DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, | ||||
|       dictClass: 'number', | ||||
|       isSearch: true | ||||
|     }, | ||||
|     { | ||||
|       title: '提交时间', | ||||
|       field: 'createTime', | ||||
|       formatter: 'formatDate', | ||||
|       table: { | ||||
|         width: 180 | ||||
|       }, | ||||
|       isForm: false, | ||||
|       isSearch: true, | ||||
|       search: { | ||||
|         show: true, | ||||
|         itemRender: { | ||||
|           name: 'XDataTimePicker' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       title: '结束时间', | ||||
|       field: 'endTime', | ||||
|       formatter: 'formatDate', | ||||
|       table: { | ||||
|         width: 180 | ||||
|       }, | ||||
|       isForm: false | ||||
|     } | ||||
|   ] | ||||
| }) | ||||
| export const { allSchemas } = useVxeCrudSchemas(crudSchemas) | ||||
|  | @ -74,7 +74,7 @@ | |||
|   <!-- 表单弹窗:详情 --> | ||||
|   <TaskDetail ref="detailRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="tsx"> | ||||
| <script setup lang="tsx" name="BpmTodoTask"> | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as TaskApi from '@/api/bpm/task' | ||||
|  |  | |||
|  | @ -1,32 +1,117 @@ | |||
| <template> | ||||
|   <ContentWrap> | ||||
|     <XTable @register="registerTable"> | ||||
|       <template #suspensionState_default="{ row }"> | ||||
|         <el-tag type="success" v-if="row.suspensionState === 1">激活</el-tag> | ||||
|         <el-tag type="warning" v-if="row.suspensionState === 2">挂起</el-tag> | ||||
|     <!-- 搜索工作栏 --> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="任务名称" prop="name"> | ||||
|         <el-input | ||||
|           v-model="queryParams.name" | ||||
|           placeholder="请输入任务名称" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </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" | ||||
|         /> | ||||
|       </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-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|   <ContentWrap> | ||||
|     <el-table v-loading="loading" :data="list"> | ||||
|       <el-table-column label="任务编号" align="center" prop="id" width="300px" /> | ||||
|       <el-table-column label="任务名称" align="center" prop="name" /> | ||||
|       <el-table-column label="所属流程" align="center" prop="processInstance.name" /> | ||||
|       <el-table-column label="流程发起人" align="center" prop="processInstance.startUserNickname" /> | ||||
|       <el-table-column | ||||
|         label="创建时间" | ||||
|         align="center" | ||||
|         prop="createTime" | ||||
|         width="180" | ||||
|         :formatter="dateFormatter" | ||||
|       /> | ||||
|       <el-table-column label="任务状态" prop="suspensionState"> | ||||
|         <template #default="scope"> | ||||
|           <el-tag type="success" v-if="scope.row.suspensionState === 1">激活</el-tag> | ||||
|           <el-tag type="warning" v-if="scope.row.suspensionState === 2">挂起</el-tag> | ||||
|         </template> | ||||
|       <template #actionbtns_default="{ row }"> | ||||
|         <!-- 操作: 审批进度 --> | ||||
|         <XTextButton preIcon="ep:edit-pen" title="审批进度" @click="handleAudit(row)" /> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="操作" align="center"> | ||||
|         <template #default="scope"> | ||||
|           <el-button link type="primary" @click="handleAudit(scope.row)">审批进度</el-button> | ||||
|         </template> | ||||
|     </XTable> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|     <!-- 分页 --> | ||||
|     <Pagination | ||||
|       :total="total" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| // 业务相关的 import | ||||
| import { allSchemas } from './todo.data' | ||||
| <script setup lang="tsx" name="BpmDoneTask"> | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| const { push } = useRouter() // 路由 | ||||
| import * as TaskApi from '@/api/bpm/task' | ||||
| 
 | ||||
| const { push } = useRouter() // 路由 | ||||
| 
 | ||||
| const [registerTable] = useXTable({ | ||||
|   allSchemas: allSchemas, | ||||
|   topActionSlots: false, | ||||
|   getListApi: TaskApi.getTodoTaskPage | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   name: '', | ||||
|   createTime: [] | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| 
 | ||||
| // 处理审批按钮 | ||||
| /** 查询任务列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const data = await TaskApi.getTodoTaskPage(queryParams) | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1 | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   queryFormRef.value.resetFields() | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| /** 处理审批按钮 */ | ||||
| const handleAudit = (row) => { | ||||
|   push({ | ||||
|     name: 'BpmProcessInstanceDetail', | ||||
|  | @ -35,4 +120,9 @@ const handleAudit = (row) => { | |||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(() => { | ||||
|   getList() | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,58 +0,0 @@ | |||
| import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' | ||||
| 
 | ||||
| const { t } = useI18n() // 国际化
 | ||||
| 
 | ||||
| // crudSchemas
 | ||||
| const crudSchemas = reactive<VxeCrudSchema>({ | ||||
|   primaryKey: 'id', | ||||
|   primaryType: null, | ||||
|   action: true, | ||||
|   searchSpan: 8, | ||||
|   columns: [ | ||||
|     { | ||||
|       title: '任务编号', | ||||
|       field: 'id', | ||||
|       table: { | ||||
|         width: 320 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       title: '任务名称', | ||||
|       field: 'name', | ||||
|       isSearch: true | ||||
|     }, | ||||
|     { | ||||
|       title: '所属流程', | ||||
|       field: 'processInstance.name' | ||||
|     }, | ||||
|     { | ||||
|       title: '流程发起人', | ||||
|       field: 'processInstance.startUserNickname' | ||||
|     }, | ||||
|     { | ||||
|       title: t('common.createTime'), | ||||
|       field: 'createTime', | ||||
|       formatter: 'formatDate', | ||||
|       table: { | ||||
|         width: 180 | ||||
|       }, | ||||
|       isSearch: true, | ||||
|       search: { | ||||
|         show: true, | ||||
|         itemRender: { | ||||
|           name: 'XDataTimePicker' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       title: '任务状态', | ||||
|       field: 'suspensionState', | ||||
|       table: { | ||||
|         slots: { | ||||
|           default: 'suspensionState_default' | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| }) | ||||
| export const { allSchemas } = useVxeCrudSchemas(crudSchemas) | ||||
|  | @ -32,7 +32,7 @@ | |||
|   <!-- 添加/修改弹窗 --> | ||||
|   <TaskAssignRuleForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="TaskAssignRule"> | ||||
| <script setup lang="ts" name="BpmTaskAssignRule"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule' | ||||
| import * as RoleApi from '@/api/system/role' | ||||
|  |  | |||
|  | @ -139,7 +139,7 @@ | |||
|   <!-- 表单弹窗:详情 --> | ||||
|   <ApiAccessLogDetail ref="detailRef" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="ApiAccessLog"> | ||||
| <script setup lang="ts" name="InfraApiAccessLog"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import download from '@/utils/download' | ||||
| import { formatDate } from '@/utils/formatTime' | ||||
|  |  | |||
|  | @ -158,14 +158,13 @@ | |||
|   <ApiErrorLogDetail ref="detailRef" /> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts" name="ApiErrorLog"> | ||||
| <script setup lang="ts" name="InfraApiErrorLog"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import download from '@/utils/download' | ||||
| import * as ApiErrorLogApi from '@/api/infra/apiErrorLog' | ||||
| import ApiErrorLogDetail from './ApiErrorLogDetail.vue' | ||||
| import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants' | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ | |||
|     </div> | ||||
|   </Dialog> | ||||
| </template> | ||||
| <script setup lang="ts" name="Build"> | ||||
| <script setup lang="ts" name="InfraBuild"> | ||||
| import formCreate from '@form-create/element-ui' | ||||
| import { useClipboard } from '@vueuse/core' | ||||
| const { t } = useI18n() // 国际化 | ||||
|  |  | |||
|  | @ -142,7 +142,7 @@ | |||
|   <!-- 弹窗:预览代码 --> | ||||
|   <PreviewCode ref="previewRef" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="Codegen"> | ||||
| <script setup lang="ts" name="InfraCodegen"> | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import download from '@/utils/download' | ||||
| import * as CodegenApi from '@/api/infra/codegen' | ||||
|  |  | |||
|  | @ -137,7 +137,7 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <ConfigForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="Config"> | ||||
| <script setup lang="ts" name="InfraConfig"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import download from '@/utils/download' | ||||
|  |  | |||
|  | @ -57,7 +57,7 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <DataSourceConfigForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="DataSourceConfig"> | ||||
| <script setup lang="ts" name="InfraDataSourceConfig"> | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig' | ||||
| import DataSourceConfigForm from './DataSourceConfigForm.vue' | ||||
|  |  | |||
|  | @ -2,46 +2,38 @@ | |||
|   <doc-alert title="数据库文档" url="https://doc.iocoder.cn/db-doc/" /> | ||||
| 
 | ||||
|   <ContentWrap title="数据库文档"> | ||||
|     <!-- 操作工具栏 --> | ||||
|     <div class="mb-10px"> | ||||
|       <XButton | ||||
|         type="primary" | ||||
|         preIcon="ep:download" | ||||
|         :title="t('action.export') + ' HTML'" | ||||
|         @click="handleExport('HTML')" | ||||
|       /> | ||||
|       <XButton | ||||
|         type="primary" | ||||
|         preIcon="ep:download" | ||||
|         :title="t('action.export') + ' Word'" | ||||
|         @click="handleExport('Word')" | ||||
|       /> | ||||
|       <XButton | ||||
|         type="primary" | ||||
|         preIcon="ep:download" | ||||
|         :title="t('action.export') + ' Markdown'" | ||||
|         @click="handleExport('Markdown')" | ||||
|       /> | ||||
|       <el-button type="primary" plain @click="handleExport('HTML')"> | ||||
|         <Icon icon="ep:download" /> 导出 HTML | ||||
|       </el-button> | ||||
|       <el-button type="primary" plain @click="handleExport('Word')"> | ||||
|         <Icon icon="ep:download" /> 导出 Word | ||||
|       </el-button> | ||||
|       <el-button type="primary" plain @click="handleExport('Markdown')"> | ||||
|         <Icon icon="ep:download" /> 导出 Markdown | ||||
|       </el-button> | ||||
|     </div> | ||||
|     <IFrame v-if="!loding" v-loading="loding" :src="src" /> | ||||
|     <IFrame v-if="!loading" v-loading="loading" :src="src" /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts" name="DbDoc"> | ||||
| <script setup lang="ts" name="InfraDBDoc"> | ||||
| import download from '@/utils/download' | ||||
| 
 | ||||
| import * as DbDocApi from '@/api/infra/dbDoc' | ||||
| 
 | ||||
| const { t } = useI18n() // 国际化 | ||||
| const src = ref('') | ||||
| const loding = ref(true) | ||||
| const loading = ref(true) // 是否加载中 | ||||
| const src = ref('') // HTML 的地址 | ||||
| 
 | ||||
| /** 页面加载 */ | ||||
| const init = async () => { | ||||
|   const res = await DbDocApi.exportHtml() | ||||
|   let blob = new Blob([res], { type: 'text/html' }) | ||||
|   let blobUrl = window.URL.createObjectURL(blob) | ||||
|   src.value = blobUrl | ||||
|   loding.value = false | ||||
|   try { | ||||
|     const data = await DbDocApi.exportHtml() | ||||
|     const blob = new Blob([data], { type: 'text/html' }) | ||||
|     src.value = window.URL.createObjectURL(blob) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 处理导出  */ | ||||
| const handleExport = async (type: string) => { | ||||
|   if (type === 'HTML') { | ||||
|  | @ -57,6 +49,8 @@ const handleExport = async (type: string) => { | |||
|     download.markdown(res, '数据库文档.md') | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   await init() | ||||
| }) | ||||
|  |  | |||
|  | @ -3,10 +3,24 @@ | |||
|   <doc-alert title="多数据源(读写分离)" url="https://doc.iocoder.cn/dynamic-datasource/" /> | ||||
| 
 | ||||
|   <ContentWrap> | ||||
|     <IFrame :src="src" /> | ||||
|     <IFrame v-if="!loading" :src="url" /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts" name="Druid"> | ||||
| const BASE_URL = import.meta.env.VITE_BASE_URL | ||||
| const src = ref(BASE_URL + '/druid/index.html') | ||||
| <script setup lang="ts" name="InfraDruid"> | ||||
| import * as ConfigApi from '@/api/infra/config' | ||||
| 
 | ||||
| const loading = ref(true) // 是否加载中 | ||||
| const url = ref(import.meta.env.VITE_BASE_URL + '/druid/index.html') | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   try { | ||||
|     const data = await ConfigApi.getConfigKey('url.druid') | ||||
|     if (data && data.length > 0) { | ||||
|       url.value = data | ||||
|     } | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
|  | @ -2,17 +2,19 @@ | |||
|   <Dialog title="上传文件" v-model="dialogVisible"> | ||||
|     <el-upload | ||||
|       ref="uploadRef" | ||||
|       :limit="1" | ||||
|       accept=".jpg, .png, .gif" | ||||
|       :auto-upload="false" | ||||
|       drag | ||||
|       :headers="headers" | ||||
|       :action="url" | ||||
|       :data="data" | ||||
|       :disabled="formLoading" | ||||
|       :headers="uploadHeaders" | ||||
|       v-model:file-list="fileList" | ||||
|       drag | ||||
|       accept=".jpg, .png, .gif" | ||||
|       :limit="1" | ||||
|       :on-success="submitFormSuccess" | ||||
|       :on-exceed="handleExceed" | ||||
|       :on-error="submitFormError" | ||||
|       :on-change="handleFileChange" | ||||
|       :on-progress="handleFileUploadProgress" | ||||
|       :on-success="handleFileSuccess" | ||||
|       :auto-upload="false" | ||||
|       :disabled="formLoading" | ||||
|     > | ||||
|       <i class="el-icon-upload"></i> | ||||
|       <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div> | ||||
|  | @ -29,44 +31,47 @@ | |||
|   </Dialog> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { Dialog } from '@/components/Dialog' | ||||
| import { getAccessToken } from '@/utils/auth' | ||||
| import { getAccessToken, getTenantId } from '@/utils/auth' | ||||
| const { t } = useI18n() // 国际化 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| 
 | ||||
| const dialogVisible = ref(false) // 弹窗的是否展示 | ||||
| const dialogTitle = ref('') // 弹窗的标题 | ||||
| const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 | ||||
| const formLoading = ref(false) // 表单的加载中 | ||||
| const url = import.meta.env.VITE_UPLOAD_URL | ||||
| const headers = { Authorization: 'Bearer ' + getAccessToken() } | ||||
| const uploadHeaders = ref() // 上传 Header 头 | ||||
| const fileList = ref([]) // 文件列表 | ||||
| const data = ref({ path: '' }) | ||||
| const uploadRef = ref() | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| const open = async () => { | ||||
|   dialogVisible.value = true | ||||
|   resetForm() | ||||
| } | ||||
| defineExpose({ open }) // 提供 open 方法,用于打开弹窗 | ||||
| 
 | ||||
| /** 提交表单 */ | ||||
| const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 | ||||
| /** 处理上传的文件发生变化 */ | ||||
| const handleFileChange = (file) => { | ||||
|   data.value.path = file.name | ||||
| } | ||||
| 
 | ||||
| /** 处理文件上传中 */ | ||||
| const handleFileUploadProgress = () => { | ||||
|   formLoading.value = true // 禁止修改 | ||||
| } | ||||
| 
 | ||||
| /** 发起文件上传 */ | ||||
| /** 提交表单 */ | ||||
| const submitFileForm = () => { | ||||
|   if (fileList.value.length == 0) { | ||||
|     message.error('请上传文件') | ||||
|     return | ||||
|   } | ||||
|   // 提交请求 | ||||
|   uploadHeaders.value = { | ||||
|     Authorization: 'Bearer ' + getAccessToken(), | ||||
|     'tenant-id': getTenantId() | ||||
|   } | ||||
|   unref(uploadRef)?.submit() | ||||
| } | ||||
| 
 | ||||
| /** 文件上传成功处理 */ | ||||
| const handleFileSuccess = () => { | ||||
| const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 | ||||
| const submitFormSuccess = () => { | ||||
|   // 清理 | ||||
|   dialogVisible.value = false | ||||
|   formLoading.value = false | ||||
|  | @ -75,4 +80,22 @@ const handleFileSuccess = () => { | |||
|   message.success(t('common.createSuccess')) | ||||
|   emit('success') | ||||
| } | ||||
| 
 | ||||
| /** 上传错误提示 */ | ||||
| const submitFormError = (): void => { | ||||
|   message.error('上传失败,请您重新上传!') | ||||
|   formLoading.value = false | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   // 重置上传状态和文件 | ||||
|   formLoading.value = false | ||||
|   uploadRef.value?.clearFiles() | ||||
| } | ||||
| 
 | ||||
| /** 文件数超出提示 */ | ||||
| const handleExceed = (): void => { | ||||
|   message.error('最多只能上传一个文件!') | ||||
| } | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,9 +1,14 @@ | |||
| <template> | ||||
|   <doc-alert title="上传下载" url="https://doc.iocoder.cn/file/"/> | ||||
| 
 | ||||
|   <doc-alert title="上传下载" url="https://doc.iocoder.cn/file/" /> | ||||
|   <!-- 搜索 --> | ||||
|   <ContentWrap> | ||||
|     <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true"> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="文件路径" prop="path"> | ||||
|         <el-input | ||||
|           v-model="queryParams.path" | ||||
|  | @ -33,7 +38,7 @@ | |||
|       <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" @click="openForm"> | ||||
|         <el-button type="primary" plain @click="openForm"> | ||||
|           <Icon icon="ep:upload" class="mr-5px" /> 上传文件 | ||||
|         </el-button> | ||||
|       </el-form-item> | ||||
|  | @ -86,11 +91,11 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <FileForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="Config"> | ||||
| <script setup lang="ts" name="InfraFile"> | ||||
| import { fileSizeFormatter } from '@/utils' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as FileApi from '@/api/infra/file' | ||||
| import FileUploadForm from './FileForm.vue' | ||||
| import FileForm from './FileForm.vue' | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,17 +3,29 @@ | |||
| 
 | ||||
|   <!-- 搜索 --> | ||||
|   <ContentWrap> | ||||
|     <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true"> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="配置名" prop="name"> | ||||
|         <el-input | ||||
|           v-model="queryParams.name" | ||||
|           placeholder="请输入配置名" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="存储器" prop="storage"> | ||||
|         <el-select v-model="queryParams.storage" placeholder="请选择存储器" clearable> | ||||
|         <el-select | ||||
|           v-model="queryParams.storage" | ||||
|           placeholder="请选择存储器" | ||||
|           clearable | ||||
|           class="!w-240px" | ||||
|         > | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)" | ||||
|             :key="dict.value" | ||||
|  | @ -30,6 +42,7 @@ | |||
|           start-placeholder="开始日期" | ||||
|           end-placeholder="结束日期" | ||||
|           :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|  | @ -113,7 +126,7 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <FileConfigForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="Config"> | ||||
| <script setup lang="ts" name="InfraFileConfig"> | ||||
| import * as FileConfigApi from '@/api/infra/fileConfig' | ||||
| import FileConfigForm from './FileConfigForm.vue' | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
|  |  | |||
|  | @ -147,7 +147,7 @@ | |||
|   <!-- 表单弹窗:查看 --> | ||||
|   <JobDetail ref="detailRef" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="Job"> | ||||
| <script setup lang="ts" name="InfraJob"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { checkPermi } from '@/utils/permission' | ||||
| import JobForm from './JobForm.vue' | ||||
|  |  | |||
|  | @ -121,7 +121,7 @@ | |||
|   <!-- 表单弹窗:查看 --> | ||||
|   <JobLogDetail ref="detailRef" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="JobLog"> | ||||
| <script setup lang="ts" name="InfraJobLog"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { formatDate } from '@/utils/formatTime' | ||||
| import download from '@/utils/download' | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
| 
 | ||||
|   <el-scrollbar height="calc(100vh - 88px - 40px - 50px)"> | ||||
|     <el-row> | ||||
|       <!-- 基本信息 --> | ||||
|       <el-col :span="24" class="card-box" shadow="hover"> | ||||
|         <el-card> | ||||
|           <el-descriptions title="基本信息" :column="6" border> | ||||
|  | @ -47,106 +48,33 @@ | |||
|           </el-descriptions> | ||||
|         </el-card> | ||||
|       </el-col> | ||||
|       <!-- 命令统计 --> | ||||
|       <el-col :span="12" class="mt-3"> | ||||
|         <el-card :gutter="12" shadow="hover"> | ||||
|           <div ref="commandStatsRef" class="h-88"></div> | ||||
|         </el-card> | ||||
|       </el-col> | ||||
|       <!-- 内存使用量统计 --> | ||||
|       <el-col :span="12" class="mt-3"> | ||||
|         <el-card class="ml-3" :gutter="12" shadow="hover"> | ||||
|           <div ref="usedmemory" class="h-88"></div> | ||||
|         </el-card> | ||||
|       </el-col> | ||||
|     </el-row> | ||||
|     <el-row class="mt-3"> | ||||
|       <el-col :span="24" class="card-box" shadow="hover"> | ||||
|         <el-card> | ||||
|           <el-table | ||||
|             v-loading="keyListLoad" | ||||
|             :data="keyList" | ||||
|             row-key="id" | ||||
|             @row-click="openKeyTemplate" | ||||
|           > | ||||
|             <el-table-column prop="keyTemplate" label="Key 模板" width="200" /> | ||||
|             <el-table-column prop="keyType" label="Key 类型" width="100" /> | ||||
|             <el-table-column prop="valueType" label="Value 类型" /> | ||||
|             <el-table-column prop="timeoutType" label="超时时间" width="200"> | ||||
|               <template #default="{ row }"> | ||||
|                 <DictTag :type="DICT_TYPE.INFRA_REDIS_TIMEOUT_TYPE" :value="row?.timeoutType" /> | ||||
|                 <span v-if="row?.timeout > 0">({{ row?.timeout / 1000 }} 秒)</span> | ||||
|               </template> | ||||
|             </el-table-column> | ||||
|             <el-table-column prop="memo" label="备注" /> | ||||
|           </el-table> | ||||
|         </el-card> | ||||
|       </el-col> | ||||
|     </el-row> | ||||
|   </el-scrollbar> | ||||
|   <XModal v-model="dialogVisible" :title="keyTemplate + ' 模板'"> | ||||
|     <el-row> | ||||
|       <el-col :span="14" class="mt-3"> | ||||
|         <el-card shadow="always"> | ||||
|           <template #header> | ||||
|             <div class="card-header"> | ||||
|               <span>键名列表</span> | ||||
|             </div> | ||||
|           </template> | ||||
|           <el-table :data="cacheKeys" style="width: 100%" @row-click="handleKeyValue"> | ||||
|             <el-table-column label="缓存键名" align="center" :show-overflow-tooltip="true"> | ||||
|               <template #default="{ row }"> | ||||
|                 {{ row }} | ||||
|               </template> | ||||
|             </el-table-column> | ||||
|             <el-table-column label="操作" align="right" width="60"> | ||||
|               <template #default="{ row }"> | ||||
|                 <XTextButton preIcon="ep:delete" @click="handleDeleteKey(row)" /> | ||||
|               </template> | ||||
|             </el-table-column> | ||||
|           </el-table> | ||||
|         </el-card> | ||||
|       </el-col> | ||||
|       <el-col :span="10" class="mt-3"> | ||||
|         <el-card shadow="always"> | ||||
|           <template #header> | ||||
|             <div class="card-header"> | ||||
|               <span>缓存内容</span> | ||||
|               <XTextButton | ||||
|                 preIcon="ep:refresh" | ||||
|                 title="清理全部" | ||||
|                 @click="handleDeleteKeys(keyTemplate)" | ||||
|                 class="float-right p-1" | ||||
|               /> | ||||
|             </div> | ||||
|           </template> | ||||
|           <el-descriptions :column="1"> | ||||
|             <el-descriptions-item label="缓存键名:">{{ cacheForm.key }}</el-descriptions-item> | ||||
|             <el-descriptions-item label="缓存内容:">{{ cacheForm.value }}</el-descriptions-item> | ||||
|           </el-descriptions> | ||||
|         </el-card> | ||||
|       </el-col> | ||||
|     </el-row> | ||||
|   </XModal> | ||||
| </template> | ||||
| <script setup lang="ts" name="Redis"> | ||||
| <script setup lang="ts" name="InfraRedis"> | ||||
| import * as echarts from 'echarts' | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| 
 | ||||
| import * as RedisApi from '@/api/infra/redis' | ||||
| import { RedisKeyInfo, RedisMonitorInfoVO } from '@/api/infra/redis/types' | ||||
| const { t } = useI18n() // 国际化 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| import { RedisMonitorInfoVO } from '@/api/infra/redis/types' | ||||
| 
 | ||||
| const cache = ref<RedisMonitorInfoVO>() | ||||
| const keyListLoad = ref(true) | ||||
| const keyList = ref<RedisKeyInfo[]>([]) | ||||
| 
 | ||||
| // 基本信息 | ||||
| const readRedisInfo = async () => { | ||||
|   const data = await RedisApi.getCache() | ||||
|   cache.value = data | ||||
|   loadEchartOptions(data.commandStats) | ||||
|   const redisKeysInfo = await RedisApi.getKeyDefineList() | ||||
|   keyList.value = redisKeysInfo | ||||
|   keyListLoad.value = false //加载完成 | ||||
| } | ||||
| // 图表 | ||||
| const commandStatsRef = ref<HTMLElement>() | ||||
|  | @ -241,40 +169,8 @@ const loadEchartOptions = (stats) => { | |||
|     ] | ||||
|   }) | ||||
| } | ||||
| const dialogVisible = ref(false) | ||||
| const keyTemplate = ref('') | ||||
| const cacheKeys = ref() | ||||
| const cacheForm = ref<{ | ||||
|   key: string | ||||
|   value: string | ||||
| }>({ | ||||
|   key: '', | ||||
|   value: '' | ||||
| }) | ||||
| const openKeyTemplate = async (row: RedisKeyInfo) => { | ||||
|   keyTemplate.value = row.keyTemplate | ||||
|   cacheKeys.value = await RedisApi.getKeyList(row.keyTemplate) | ||||
|   dialogVisible.value = true | ||||
| } | ||||
| const handleDeleteKey = async (row) => { | ||||
|   RedisApi.deleteKey(row) | ||||
|   message.success(t('common.delSuccess')) | ||||
| } | ||||
| const handleDeleteKeys = async (row) => { | ||||
|   RedisApi.deleteKeys(row) | ||||
|   message.success(t('common.delSuccess')) | ||||
| } | ||||
| const handleKeyValue = async (row) => { | ||||
|   const res = await RedisApi.getKeyValue(row) | ||||
|   cacheForm.value = res | ||||
| } | ||||
| 
 | ||||
| onBeforeMount(() => { | ||||
|   readRedisInfo() | ||||
| }) | ||||
| </script> | ||||
| <style scoped> | ||||
| .redis { | ||||
|   height: 600px; | ||||
|   max-height: 860px; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,10 +1,25 @@ | |||
| <template> | ||||
|   <doc-alert title="服务监控" url="https://doc.iocoder.cn/server-monitor/" /> | ||||
| 
 | ||||
|   <ContentWrap> | ||||
|     <IFrame :src="src" /> | ||||
|     <IFrame v-if="!loading" v-loading="loading" :src="src" /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts" name="AdminServer"> | ||||
| const BASE_URL = import.meta.env.VITE_BASE_URL | ||||
| const src = ref(BASE_URL + '/admin/applications') | ||||
| <script setup lang="ts" name="InfraAdminServer"> | ||||
| import * as ConfigApi from '@/api/infra/config' | ||||
| 
 | ||||
| const loading = ref(true) // 是否加载中 | ||||
| const src = ref(import.meta.env.VITE_BASE_URL + '/admin/applications') | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   try { | ||||
|     const data = await ConfigApi.getConfigKey('url.spring-boot-admin') | ||||
|     if (data && data.length > 0) { | ||||
|       src.value = data | ||||
|     } | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,9 +1,25 @@ | |||
| <template> | ||||
|   <doc-alert title="服务监控" url="https://doc.iocoder.cn/server-monitor/" /> | ||||
| 
 | ||||
|   <ContentWrap> | ||||
|     <IFrame :src="src" /> | ||||
|     <IFrame v-if="!loading" v-loading="loading" :src="src" /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts" name="Skywalking"> | ||||
| <script setup lang="ts" name="InfraSkyWalking"> | ||||
| import * as ConfigApi from '@/api/infra/config' | ||||
| 
 | ||||
| const loading = ref(true) // 是否加载中 | ||||
| const src = ref('http://skywalking.shop.iocoder.cn') | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   try { | ||||
|     const data = await ConfigApi.getConfigKey('url.skywalking') | ||||
|     if (data && data.length > 0) { | ||||
|       src.value = data | ||||
|     } | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
|  | @ -5,8 +5,22 @@ | |||
|     <IFrame :src="src" /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup lang="ts" name="Swagger"> | ||||
| const BASE_URL = import.meta.env.VITE_BASE_URL | ||||
| // const src = ref(BASE_URL + '/doc.html') | ||||
| const src = ref(BASE_URL + '/swagger-ui') | ||||
| <script setup lang="ts" name="InfraSwagger"> | ||||
| import * as ConfigApi from '@/api/infra/config' | ||||
| 
 | ||||
| const loading = ref(true) // 是否加载中 | ||||
| const src = ref(import.meta.env.VITE_BASE_URL + '/doc.html') // Knife4j UI | ||||
| // const src = ref(import.meta.env.VITE_BASE_URL + '/swagger-ui') // Swagger UI | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   try { | ||||
|     const data = await ConfigApi.getConfigKey('url.swagger') | ||||
|     if (data && data.length > 0) { | ||||
|       src.value = data | ||||
|     } | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
|  | @ -0,0 +1,120 @@ | |||
| <template> | ||||
|   <Dialog :title="dialogTitle" v-model="dialogVisible"> | ||||
|     <el-form | ||||
|       ref="formRef" | ||||
|       :model="formData" | ||||
|       :rules="formRules" | ||||
|       label-width="80px" | ||||
|       v-loading="formLoading" | ||||
|     > | ||||
|       <el-form-item label="品牌名称" prop="name"> | ||||
|         <el-input v-model="formData.name" placeholder="请输入品牌名称" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="品牌图片" prop="picUrl"> | ||||
|         <UploadImg v-model="formData.picUrl" :limit="1" :is-show-tip="false" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="品牌排序" prop="sort"> | ||||
|         <el-input-number v-model="formData.sort" controls-position="right" :min="0" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="品牌状态" prop="status"> | ||||
|         <el-radio-group v-model="formData.status"> | ||||
|           <el-radio | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.value" | ||||
|           > | ||||
|             {{ dict.label }} | ||||
|           </el-radio> | ||||
|         </el-radio-group> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="品牌描述"> | ||||
|         <el-input v-model="formData.description" type="textarea" placeholder="请输入品牌描述" /> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|     <template #footer> | ||||
|       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> | ||||
|       <el-button @click="dialogVisible = false">取 消</el-button> | ||||
|     </template> | ||||
|   </Dialog> | ||||
| </template> | ||||
| <script setup lang="ts" name="ProductBrandForm"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { CommonStatusEnum } from '@/utils/constants' | ||||
| import * as ProductBrandApi from '@/api/mall/product/brand' | ||||
| const { t } = useI18n() // 国际化 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| 
 | ||||
| const dialogVisible = ref(false) // 弹窗的是否展示 | ||||
| const dialogTitle = ref('') // 弹窗的标题 | ||||
| const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 | ||||
| const formType = ref('') // 表单的类型:create - 新增;update - 修改 | ||||
| const formData = ref({ | ||||
|   id: undefined, | ||||
|   name: '', | ||||
|   picUrl: '', | ||||
|   status: CommonStatusEnum.ENABLE, | ||||
|   description: '' | ||||
| }) | ||||
| const formRules = reactive({ | ||||
|   name: [{ required: true, message: '品牌名称不能为空', trigger: 'blur' }], | ||||
|   picUrl: [{ required: true, message: '品牌图片不能为空', trigger: 'blur' }], | ||||
|   sort: [{ required: true, message: '品牌排序不能为空', trigger: 'blur' }] | ||||
| }) | ||||
| const formRef = ref() // 表单 Ref | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| const open = async (type: string, id?: number) => { | ||||
|   dialogVisible.value = true | ||||
|   dialogTitle.value = t('action.' + type) | ||||
|   formType.value = type | ||||
|   resetForm() | ||||
|   // 修改时,设置数据 | ||||
|   if (id) { | ||||
|     formLoading.value = true | ||||
|     try { | ||||
|       formData.value = await ProductBrandApi.getBrand(id) | ||||
|     } finally { | ||||
|       formLoading.value = false | ||||
|     } | ||||
|   } | ||||
| } | ||||
| defineExpose({ open }) // 提供 open 方法,用于打开弹窗 | ||||
| 
 | ||||
| /** 提交表单 */ | ||||
| const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 | ||||
| const submitForm = async () => { | ||||
|   // 校验表单 | ||||
|   if (!formRef) return | ||||
|   const valid = await formRef.value.validate() | ||||
|   if (!valid) return | ||||
|   // 提交请求 | ||||
|   formLoading.value = true | ||||
|   try { | ||||
|     const data = formData.value as ProductBrandApi.BrandVO | ||||
|     if (formType.value === 'create') { | ||||
|       await ProductBrandApi.createBrand(data) | ||||
|       message.success(t('common.createSuccess')) | ||||
|     } else { | ||||
|       await ProductBrandApi.updateBrand(data) | ||||
|       message.success(t('common.updateSuccess')) | ||||
|     } | ||||
|     dialogVisible.value = false | ||||
|     // 发送操作成功的事件 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     id: undefined, | ||||
|     name: '', | ||||
|     picUrl: '', | ||||
|     status: CommonStatusEnum.ENABLE, | ||||
|     description: '' | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,177 @@ | |||
| <template> | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="品牌名称" prop="name"> | ||||
|         <el-input | ||||
|           v-model="queryParams.name" | ||||
|           placeholder="请输入品牌名称" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="状态" prop="status"> | ||||
|         <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> | ||||
|           <el-option | ||||
|             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" | ||||
|             :key="dict.value" | ||||
|             :label="dict.label" | ||||
|             :value="dict.value" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </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" | ||||
|         /> | ||||
|       </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('create')" | ||||
|           v-hasPermi="['product:brand:create']" | ||||
|         > | ||||
|           <Icon icon="ep:plus" class="mr-5px" /> 新增 | ||||
|         </el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|   <ContentWrap> | ||||
|     <el-table v-loading="loading" :data="list" row-key="id" default-expand-all> | ||||
|       <el-table-column label="品牌名称" prop="name" sortable /> | ||||
|       <el-table-column label="品牌图片" align="center" prop="picUrl"> | ||||
|         <template #default="scope"> | ||||
|           <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="品牌图片" class="h-100px" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="品牌排序" align="center" prop="sort" /> | ||||
|       <el-table-column label="开启状态" align="center" 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="createTime" | ||||
|         width="180" | ||||
|         :formatter="dateFormatter" | ||||
|       /> | ||||
|       <el-table-column label="操作" align="center"> | ||||
|         <template #default="scope"> | ||||
|           <el-button | ||||
|             link | ||||
|             type="primary" | ||||
|             @click="openForm('update', scope.row.id)" | ||||
|             v-hasPermi="['product:brand:update']" | ||||
|           > | ||||
|             编辑 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|             link | ||||
|             type="danger" | ||||
|             @click="handleDelete(scope.row.id)" | ||||
|             v-hasPermi="['product:brand:delete']" | ||||
|           > | ||||
|             删除 | ||||
|           </el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|     <!-- 分页 --> | ||||
|     <Pagination | ||||
|       :total="total" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <BrandForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="ProductBrand"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as ProductBrandApi from '@/api/mall/product/brand' | ||||
| import BrandForm from './BrandForm.vue' | ||||
| const message = useMessage() // 消息弹窗 | ||||
| const { t } = useI18n() // 国际化 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref<any[]>([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   name: undefined, | ||||
|   status: undefined, | ||||
|   createTime: [] | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const data = await ProductBrandApi.getBrandParam(queryParams) | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   queryFormRef.value.resetFields() | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| /** 添加/修改操作 */ | ||||
| const formRef = ref() | ||||
| const openForm = (type: string, id?: number) => { | ||||
|   formRef.value.open(type, id) | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = async (id: number) => { | ||||
|   try { | ||||
|     // 删除的二次确认 | ||||
|     await message.delConfirm() | ||||
|     // 发起删除 | ||||
|     await ProductBrandApi.deleteBrand(id) | ||||
|     message.success(t('common.delSuccess')) | ||||
|     // 刷新列表 | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(() => { | ||||
|   getList() | ||||
| }) | ||||
| </script> | ||||
|  | @ -50,7 +50,7 @@ | |||
|     </template> | ||||
|   </Dialog> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| <script setup lang="ts" name="ProductCategory"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { CommonStatusEnum } from '@/utils/constants' | ||||
| import { handleTree } from '@/utils/tree' | ||||
|  |  | |||
|  | @ -92,7 +92,7 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <PropertyForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="Config"> | ||||
| <script setup lang="ts" name="ProductProperty"> | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as PropertyApi from '@/api/mall/product/property' | ||||
| import PropertyForm from './PropertyForm.vue' | ||||
|  |  | |||
|  | @ -88,7 +88,7 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <ValueForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="Config"> | ||||
| <script setup lang="ts" name="ProductPropertyValue"> | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as PropertyApi from '@/api/mall/product/property' | ||||
| import ValueForm from './ValueForm.vue' | ||||
|  |  | |||
|  | @ -1,3 +1,423 @@ | |||
| <template> | ||||
|   <span>开发中</span> | ||||
|   <doc-alert title="自动回复" url="https://doc.iocoder.cn/mp/auto-reply/" /> | ||||
| 
 | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> | ||||
|           <el-option | ||||
|             v-for="item in accountList" | ||||
|             :key="item.id" | ||||
|             :label="item.name" | ||||
|             :value="item.id" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button> | ||||
|         <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- tab 切换 --> | ||||
|   <ContentWrap> | ||||
|     <el-tabs v-model="type" @tab-change="handleTabChange"> | ||||
|       <!-- 操作工具栏 --> | ||||
|       <el-row :gutter="10" class="mb8"> | ||||
|         <el-col :span="1.5"> | ||||
|           <el-button | ||||
|             type="primary" | ||||
|             plain | ||||
|             @click="handleAdd" | ||||
|             v-hasPermi="['mp:auto-reply:create']" | ||||
|             v-if="type !== '1' || list.length <= 0" | ||||
|           > | ||||
|             <Icon icon="ep:plus" />新增 | ||||
|           </el-button> | ||||
|         </el-col> | ||||
|       </el-row> | ||||
|       <!-- tab 项 --> | ||||
|       <el-tab-pane name="1"> | ||||
|         <template #label> | ||||
|           <span><Icon icon="ep:star-off" /> 关注时回复</span> | ||||
|         </template> | ||||
|       </el-tab-pane> | ||||
|       <el-tab-pane name="2"> | ||||
|         <template #label> | ||||
|           <span><Icon icon="ep:chat-line-round" /> 消息回复</span> | ||||
|         </template> | ||||
|       </el-tab-pane> | ||||
|       <el-tab-pane name="3"> | ||||
|         <template #label> | ||||
|           <span><Icon icon="ep:news" /> 关键词回复</span> | ||||
|         </template> | ||||
|       </el-tab-pane> | ||||
|     </el-tabs> | ||||
|     <!-- 列表 --> | ||||
|     <el-table v-loading="loading" :data="list"> | ||||
|       <el-table-column | ||||
|         label="请求消息类型" | ||||
|         align="center" | ||||
|         prop="requestMessageType" | ||||
|         v-if="type === '2'" | ||||
|       /> | ||||
|       <el-table-column label="关键词" align="center" prop="requestKeyword" v-if="type === '3'" /> | ||||
|       <el-table-column label="匹配类型" align="center" prop="requestMatch" v-if="type === '3'"> | ||||
|         <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 :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 :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> | ||||
| 
 | ||||
|     <!-- 添加或修改自动回复的对话框 --> | ||||
|     <el-dialog :title="title" v-model="open" width="800px" append-to-body> | ||||
|       <el-form ref="formRef" :model="form" :rules="rules" label-width="80px"> | ||||
|         <el-form-item label="消息类型" prop="requestMessageType" v-if="type === '2'"> | ||||
|           <el-select v-model="form.requestMessageType" placeholder="请选择"> | ||||
|             <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value"> | ||||
|               <el-option | ||||
|                 v-if="requestMessageTypes.includes(dict.value)" | ||||
|                 :label="dict.label" | ||||
|                 :value="dict.value" | ||||
|               /> | ||||
|             </template> | ||||
|           </el-select> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="匹配类型" prop="requestMatch" v-if="type === '3'"> | ||||
|           <el-select v-model="form.requestMatch" placeholder="请选择匹配类型" clearable> | ||||
|             <el-option | ||||
|               v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)" | ||||
|               :key="dict.value" | ||||
|               :label="dict.label" | ||||
|               :value="dict.value" | ||||
|             /> | ||||
|           </el-select> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="关键词" prop="requestKeyword" v-if="type === '3'"> | ||||
|           <el-input v-model="form.requestKeyword" placeholder="请输入内容" clearable /> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="回复消息"> | ||||
|           <WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" /> | ||||
|         </el-form-item> | ||||
|       </el-form> | ||||
|       <template #footer> | ||||
|         <span class="dialog-footer"> | ||||
|           <el-button @click="cancel">取 消</el-button> | ||||
|           <el-button type="primary" @click="handleSubmit">确 定</el-button> | ||||
|         </span> | ||||
|       </template> | ||||
|     </el-dialog> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup name="MpAutoReply"> | ||||
| import { ref, reactive, onMounted, nextTick } from 'vue' | ||||
| 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 { getSimpleAccountList } from '@/api/mp/account' | ||||
| import { | ||||
|   createAutoReply, | ||||
|   deleteAutoReply, | ||||
|   getAutoReply, | ||||
|   getAutoReplyPage, | ||||
|   updateAutoReply | ||||
| } from '@/api/mp/autoReply' | ||||
| 
 | ||||
| import { DICT_TYPE, getDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { ContentWrap } from '@/components/ContentWrap' | ||||
| 
 | ||||
| const message = useMessage() | ||||
| 
 | ||||
| const queryFormRef = ref() | ||||
| const formRef = ref() | ||||
| 
 | ||||
| // tab 类型(1、关注时回复;2、消息回复;3、关键词回复) | ||||
| const type = ref('3') | ||||
| // 允许选择的请求消息类型 | ||||
| const requestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] | ||||
| // 遮罩层 | ||||
| const loading = ref(true) | ||||
| // 显示搜索条件 | ||||
| // const showSearch = ref(true) | ||||
| // 总条数 | ||||
| const total = ref(0) | ||||
| // 自动回复列表 | ||||
| const list = ref([]) | ||||
| // 查询参数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   accountId: undefined | ||||
| }) | ||||
| 
 | ||||
| // 弹出层标题 | ||||
| const title = ref('') | ||||
| // 是否显示弹出层 | ||||
| const open = ref(false) | ||||
| // 表单参数 | ||||
| const form = ref({}) | ||||
| // 回复消息 | ||||
| const objData = ref({ | ||||
|   type: 'text' | ||||
| }) | ||||
| // 表单校验 | ||||
| const rules = { | ||||
|   requestKeyword: [{ required: true, message: '请求的关键字不能为空', trigger: 'blur' }], | ||||
|   requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }] | ||||
| } | ||||
| 
 | ||||
| const hackResetWxReplySelect = ref(false) // 重置 WxReplySelect 组件,解决无法清除的问题 | ||||
| 
 | ||||
| // 公众号账号列表 | ||||
| const accountList = ref([]) | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   getSimpleAccountList().then((data) => { | ||||
|     accountList.value = data | ||||
|     // 默认选中第一个 | ||||
|     if (accountList.value.length > 0) { | ||||
|       queryParams.accountId = accountList.value[0].id | ||||
|     } | ||||
|     // 加载数据 | ||||
|     getList() | ||||
|   }) | ||||
| }) | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   // 如果没有选中公众号账号,则进行提示。 | ||||
|   if (!queryParams.accountId) { | ||||
|     message.error('未选中公众号,无法查询自动回复') | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   loading.value = false | ||||
|   // 处理查询参数 | ||||
|   let params = { | ||||
|     ...queryParams, | ||||
|     type: type.value | ||||
|   } | ||||
|   // 执行查询 | ||||
|   getAutoReplyPage(params).then((data) => { | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|     loading.value = false | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1 | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   queryFormRef.value?.resetFields() | ||||
|   // 默认选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     queryParams.accountId = accountList.value[0].id | ||||
|   } | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| const handleTabChange = (tabName) => { | ||||
|   type.value = tabName | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| /** 新增按钮操作 */ | ||||
| const handleAdd = () => { | ||||
|   reset() | ||||
|   resetEditor() | ||||
|   // 打开表单,并设置初始化 | ||||
|   open.value = true | ||||
|   title.value = '新增自动回复' | ||||
|   objData.value = { | ||||
|     type: 'text', | ||||
|     accountId: queryParams.accountId | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 修改按钮操作 */ | ||||
| const handleUpdate = (row) => { | ||||
|   reset() | ||||
|   resetEditor() | ||||
|   console.log(row) | ||||
| 
 | ||||
|   getAutoReply(row.id).then((data) => { | ||||
|     // 设置属性 | ||||
|     form.value = { ...data } | ||||
|     delete form.value['responseMessageType'] | ||||
|     delete form.value['responseContent'] | ||||
|     delete form.value['responseMediaId'] | ||||
|     delete form.value['responseMediaUrl'] | ||||
|     delete form.value['responseDescription'] | ||||
|     delete form.value['responseArticles'] | ||||
|     objData.value = { | ||||
|       type: data.responseMessageType, | ||||
|       accountId: queryParams.accountId, | ||||
|       content: data.responseContent, | ||||
|       mediaId: data.responseMediaId, | ||||
|       url: data.responseMediaUrl, | ||||
|       title: data.responseTitle, | ||||
|       description: data.responseDescription, | ||||
|       thumbMediaId: data.responseThumbMediaId, | ||||
|       thumbMediaUrl: data.responseThumbMediaUrl, | ||||
|       articles: data.responseArticles, | ||||
|       musicUrl: data.responseMusicUrl, | ||||
|       hqMusicUrl: data.responseHqMusicUrl | ||||
|     } | ||||
| 
 | ||||
|     // 打开表单 | ||||
|     open.value = true | ||||
|     title.value = '修改自动回复' | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| const handleSubmit = () => { | ||||
|   formRef.value?.validate((valid) => { | ||||
|     if (!valid) { | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     // 处理回复消息 | ||||
|     const form = { ...form.value } | ||||
|     form.responseMessageType = objData.value.type | ||||
|     form.responseContent = objData.value.content | ||||
|     form.responseMediaId = objData.value.mediaId | ||||
|     form.responseMediaUrl = objData.value.url | ||||
|     form.responseTitle = objData.value.title | ||||
|     form.responseDescription = objData.value.description | ||||
|     form.responseThumbMediaId = objData.value.thumbMediaId | ||||
|     form.responseThumbMediaUrl = objData.value.thumbMediaUrl | ||||
|     form.responseArticles = objData.value.articles | ||||
|     form.responseMusicUrl = objData.value.musicUrl | ||||
|     form.responseHqMusicUrl = objData.value.hqMusicUrl | ||||
| 
 | ||||
|     if (form.value.id !== undefined) { | ||||
|       updateAutoReply(form).then(() => { | ||||
|         message.success('修改成功') | ||||
|         open.value = false | ||||
|         getList() | ||||
|       }) | ||||
|     } else { | ||||
|       createAutoReply(form).then(() => { | ||||
|         message.success('新增成功') | ||||
|         open.value = false | ||||
|         getList() | ||||
|       }) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 表单重置 | ||||
| const reset = () => { | ||||
|   form.value = { | ||||
|     id: undefined, | ||||
|     accountId: queryParams.accountId, | ||||
|     type: type.value, | ||||
|     requestKeyword: undefined, | ||||
|     requestMatch: type.value === '3' ? 1 : undefined, | ||||
|     requestMessageType: undefined | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| 
 | ||||
| // 取消按钮 | ||||
| const cancel = () => { | ||||
|   open.value = false | ||||
|   reset() | ||||
| } | ||||
| 
 | ||||
| // 表单 Editor 重置 | ||||
| const resetEditor = () => { | ||||
|   hackResetWxReplySelect.value = false // 销毁组件 | ||||
|   nextTick(() => { | ||||
|     hackResetWxReplySelect.value = true // 重建组件 | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| const handleDelete = async (row) => { | ||||
|   await message.confirm('是否确认删除此数据?') | ||||
|   await deleteAutoReply(row.id) | ||||
|   await getList() | ||||
|   message.success('删除成功') | ||||
| } | ||||
| </script> | ||||
|  |  | |||
|  | @ -0,0 +1,201 @@ | |||
| <script setup> | ||||
| import { ref, reactive } from 'vue' | ||||
| import { QuillEditor } from '@vueup/vue-quill' | ||||
| import '@vueup/vue-quill/dist/vue-quill.snow.css' | ||||
| import { getAccessToken } from '@/utils/auth' | ||||
| import editorOptions from './quill-options' | ||||
| 
 | ||||
| const BASE_URL = import.meta.env.VITE_BASE_URL | ||||
| 
 | ||||
| const message = useMessage() | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   /* 公众号账号编号 */ | ||||
|   accountId: { | ||||
|     type: Number, | ||||
|     required: true | ||||
|   }, | ||||
|   /* 编辑器的内容 */ | ||||
|   value: { | ||||
|     type: String, | ||||
|     default: '' | ||||
|   }, | ||||
|   /* 图片大小 */ | ||||
|   maxSize: { | ||||
|     type: Number, | ||||
|     default: 4000 // kb | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| const emit = defineEmits(['input']) | ||||
| 
 | ||||
| const myQuillEditorRef = ref() | ||||
| 
 | ||||
| const content = ref(props.value.replace(/data-src/g, 'src')) | ||||
| 
 | ||||
| const loading = ref(false) // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示 | ||||
| 
 | ||||
| const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-news-image') // 这里写你要上传的图片服务器地址 | ||||
| const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部 | ||||
| const uploadData = reactive({ | ||||
|   type: 'image', // TODO 芋艿:试试要不要换成 thumb | ||||
|   accountId: props.accountId | ||||
| }) | ||||
| 
 | ||||
| const onEditorChange = () => { | ||||
|   //内容改变事件 | ||||
|   emit('input', content.value) | ||||
| } | ||||
| 
 | ||||
| // 富文本图片上传前 | ||||
| const beforeUpload = () => { | ||||
|   // 显示 loading 动画 | ||||
|   loading.value = true | ||||
| } | ||||
| 
 | ||||
| // 图片上传成功 | ||||
| // 注意!由于微信公众号的图片有访问限制,所以会显示“此图片来自微信公众号,未经允许不可引用” | ||||
| const uploadSuccess = (res) => { | ||||
|   // res为图片服务器返回的数据 | ||||
|   // 获取富文本组件实例 | ||||
|   const quill = myQuillEditorRef.value.quill | ||||
|   // 如果上传成功 | ||||
|   const link = res.data | ||||
|   if (link) { | ||||
|     // 获取光标所在位置 | ||||
|     let length = quill.getSelection().index | ||||
|     // 插入图片  res.info为服务器返回的图片地址 | ||||
|     quill.insertEmbed(length, 'image', link) | ||||
|     // 调整光标到最后 | ||||
|     quill.setSelection(length + 1) | ||||
|   } else { | ||||
|     message.error('图片插入失败') | ||||
|   } | ||||
|   // loading 动画消失 | ||||
|   loading.value = false | ||||
| } | ||||
| 
 | ||||
| // 富文本图片上传失败 | ||||
| const uploadError = () => { | ||||
|   // loading 动画消失 | ||||
|   loading.value = false | ||||
|   message.error('图片插入失败') | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div id="wxEditor"> | ||||
|     <div v-loading="loading" element-loading-text="请稍等,图片上传中"> | ||||
|       <!-- 图片上传组件辅助--> | ||||
|       <el-upload | ||||
|         class="avatar-uploader" | ||||
|         name="file" | ||||
|         :action="actionUrl" | ||||
|         :headers="headers" | ||||
|         :show-file-list="false" | ||||
|         :data="uploadData" | ||||
|         :on-success="uploadSuccess" | ||||
|         :on-error="uploadError" | ||||
|         :before-upload="beforeUpload" | ||||
|       /> | ||||
|       <QuillEditor | ||||
|         class="editor" | ||||
|         v-model="content" | ||||
|         ref="quillEditorRef" | ||||
|         :options="editorOptions" | ||||
|         @change="onEditorChange($event)" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style> | ||||
| .editor { | ||||
|   line-height: normal !important; | ||||
|   height: 500px; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-tooltip[data-mode='link']::before { | ||||
|   content: '请输入链接地址:'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-tooltip.ql-editing a.ql-action::after { | ||||
|   border-right: 0; | ||||
|   content: '保存'; | ||||
|   padding-right: 0; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-tooltip[data-mode='video']::before { | ||||
|   content: '请输入视频地址:'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-label::before, | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-item::before { | ||||
|   content: '14px'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before, | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before { | ||||
|   content: '10px'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before, | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before { | ||||
|   content: '18px'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before, | ||||
| .ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before { | ||||
|   content: '32px'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item::before { | ||||
|   content: '文本'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before { | ||||
|   content: '标题1'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before { | ||||
|   content: '标题2'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before { | ||||
|   content: '标题3'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before { | ||||
|   content: '标题4'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before { | ||||
|   content: '标题5'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before, | ||||
| .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before { | ||||
|   content: '标题6'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-label::before, | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-item::before { | ||||
|   content: '标准字体'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before, | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before { | ||||
|   content: '衬线字体'; | ||||
| } | ||||
| 
 | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before, | ||||
| .ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before { | ||||
|   content: '等宽字体'; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,45 @@ | |||
| const toolbarOptions = [ | ||||
|   ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
 | ||||
|   ['blockquote', 'code-block'], // 引用  代码块
 | ||||
|   [{ header: 1 }, { header: 2 }], // 1、2 级标题
 | ||||
|   [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
 | ||||
|   [{ script: 'sub' }, { script: 'super' }], // 上标/下标
 | ||||
|   [{ indent: '-1' }, { indent: '+1' }], // 缩进
 | ||||
|   // [{'direction': 'rtl'}],                         // 文本方向
 | ||||
|   [{ size: ['small', false, 'large', 'huge'] }], // 字体大小
 | ||||
|   [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
 | ||||
|   [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
 | ||||
|   [{ font: [] }], // 字体种类
 | ||||
|   [{ align: [] }], // 对齐方式
 | ||||
|   ['clean'], // 清除文本格式
 | ||||
|   ['link', 'image', 'video'] // 链接、图片、视频
 | ||||
| ] | ||||
| 
 | ||||
| export default { | ||||
|   theme: 'snow', | ||||
|   placeholder: '请输入文章内容', | ||||
|   modules: { | ||||
|     toolbar: { | ||||
|       container: toolbarOptions, | ||||
|       // container: "#toolbar",
 | ||||
|       handlers: { | ||||
|         image: function (value) { | ||||
|           if (value) { | ||||
|             // 触发input框选择图片文件
 | ||||
|             document.querySelector('.avatar-uploader input').click() | ||||
|           } else { | ||||
|             this.quill.format('image', false) | ||||
|           } | ||||
|         }, | ||||
|         link: function (value) { | ||||
|           if (value) { | ||||
|             const href = prompt('注意!只支持公众号图文链接') | ||||
|             this.quill.format('link', href) | ||||
|           } else { | ||||
|             this.quill.format('link', false) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -5,30 +5,29 @@ | |||
|   ① 移除 avue 组件,使用 ElementUI 原生组件 | ||||
| --> | ||||
| <template> | ||||
|   <!-- 类型:图片 --> | ||||
|   <div class="pb-30px"> | ||||
|     <!-- 类型:image --> | ||||
|     <div v-if="objData.type === 'image'"> | ||||
|       <div class="waterfall" v-loading="loading"> | ||||
|         <div class="waterfall-item" v-for="item in list" :key="item.mediaId"> | ||||
|           <img class="material-img" :src="item.url" /> | ||||
|           <p class="item-name">{{ item.name }}</p> | ||||
|           <el-row class="ope-row"> | ||||
|           <el-button type="success" @click="selectMaterialFun(item)" | ||||
|             >选择 | ||||
|             <i class="el-icon-circle-check el-icon--right"></i> | ||||
|             <el-button type="success" @click="selectMaterialFun(item)"> | ||||
|               选择 <Icon icon="ep:circle-check" /> | ||||
|             </el-button> | ||||
|           </el-row> | ||||
|         </div> | ||||
|       </div> | ||||
|       <!-- 分页组件 --> | ||||
|     <pagination | ||||
|       v-show="total > 0" | ||||
|       <Pagination | ||||
|         :total="total" | ||||
|         v-model:page="queryParams.pageNo" | ||||
|         v-model:limit="queryParams.pageSize" | ||||
|         @pagination="getMaterialPageFun" | ||||
|       /> | ||||
|     </div> | ||||
|   <!-- 类型:语音 --> | ||||
|     <!-- 类型:voice --> | ||||
|     <div v-else-if="objData.type === 'voice'"> | ||||
|       <!-- 列表 --> | ||||
|       <el-table v-loading="loading" :data="list"> | ||||
|  | @ -36,36 +35,33 @@ | |||
|         <el-table-column label="文件名" align="center" prop="name" /> | ||||
|         <el-table-column label="语音" align="center"> | ||||
|           <template #default="scope"> | ||||
|           <wx-voice-player :url="scope.row.url" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="上传时间" align="center" prop="createTime" width="180"> | ||||
|         <template #default="scope"> | ||||
|           <span>{{ formatDate(scope.row.createTime) }}</span> | ||||
|             <WxVoicePlayer :url="scope.row.url" /> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|         <el-table-column | ||||
|         label="操作" | ||||
|           label="上传时间" | ||||
|           align="center" | ||||
|         fixed="right" | ||||
|         class-name="small-padding fixed-width" | ||||
|       > | ||||
|           prop="createTime" | ||||
|           width="180" | ||||
|           :formatter="dateFormatter" | ||||
|         /> | ||||
|         <el-table-column label="操作" align="center" fixed="right"> | ||||
|           <template #default="scope"> | ||||
|           <el-button type="text" icon="el-icon-circle-plus" @click="selectMaterialFun(scope.row)" | ||||
|             >选择 | ||||
|             <el-button type="primary" link @click="selectMaterialFun(scope.row)" | ||||
|               >选择<Icon icon="ep:plus" /> | ||||
|             </el-button> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|       </el-table> | ||||
|       <!-- 分页组件 --> | ||||
|     <pagination | ||||
|       v-show="total > 0" | ||||
|       <Pagination | ||||
|         :total="total" | ||||
|         v-model:page="queryParams.pageNo" | ||||
|         v-model:limit="queryParams.pageSize" | ||||
|         @pagination="getPage" | ||||
|       /> | ||||
|     </div> | ||||
|     <!-- 类型:video --> | ||||
|     <div v-else-if="objData.type === 'video'"> | ||||
|       <!-- 列表 --> | ||||
|       <el-table v-loading="loading" :data="list"> | ||||
|  | @ -75,14 +71,16 @@ | |||
|         <el-table-column label="介绍" align="center" prop="introduction" /> | ||||
|         <el-table-column label="视频" align="center"> | ||||
|           <template #default="scope"> | ||||
|           <wx-video-player :url="scope.row.url" /> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="上传时间" align="center" prop="createTime" width="180"> | ||||
|         <template #default="scope"> | ||||
|           <span>{{ formatDate(scope.row.createTime) }}</span> | ||||
|             <WxVideoPlayer :url="scope.row.url" /> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|         <el-table-column | ||||
|           label="上传时间" | ||||
|           align="center" | ||||
|           prop="createTime" | ||||
|           width="180" | ||||
|           :formatter="dateFormatter" | ||||
|         /> | ||||
|         <el-table-column | ||||
|           label="操作" | ||||
|           align="center" | ||||
|  | @ -90,43 +88,43 @@ | |||
|           class-name="small-padding fixed-width" | ||||
|         > | ||||
|           <template #default="scope"> | ||||
|           <el-button type="text" icon="el-icon-circle-plus" @click="selectMaterialFun(scope.row)" | ||||
|             >选择 | ||||
|             <el-button type="primary" link @click="selectMaterialFun(scope.row)" | ||||
|               >选择<Icon icon="akar-icons:circle-plus" /> | ||||
|             </el-button> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|       </el-table> | ||||
|       <!-- 分页组件 --> | ||||
|     <pagination | ||||
|       v-show="total > 0" | ||||
|       <Pagination | ||||
|         :total="total" | ||||
|         v-model:page="queryParams.pageNo" | ||||
|         v-model:limit="queryParams.pageSize" | ||||
|         @pagination="getMaterialPageFun" | ||||
|       /> | ||||
|     </div> | ||||
|     <!-- 类型:news --> | ||||
|     <div v-else-if="objData.type === 'news'"> | ||||
|       <div class="waterfall" v-loading="loading"> | ||||
|         <div class="waterfall-item" v-for="item in list" :key="item.mediaId"> | ||||
|           <div v-if="item.content && item.content.newsItem"> | ||||
|           <wx-news :articles="item.content.newsItem" /> | ||||
|             <WxNews :articles="item.content.newsItem" /> | ||||
|             <el-row class="ope-row"> | ||||
|               <el-button type="success" @click="selectMaterialFun(item)"> | ||||
|               选择<i class="el-icon-circle-check el-icon--right"></i> | ||||
|                 选择<Icon icon="ep:circle-check" /> | ||||
|               </el-button> | ||||
|             </el-row> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <!-- 分页组件 --> | ||||
|     <pagination | ||||
|       v-show="total > 0" | ||||
|       <Pagination | ||||
|         :total="total" | ||||
|         v-model:page="queryParams.pageNo" | ||||
|         v-model:limit="queryParams.pageSize" | ||||
|         @pagination="getMaterialPageFun" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" name="WxMaterialSelect"> | ||||
|  | @ -136,7 +134,7 @@ import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' | |||
| import { getMaterialPage } from '@/api/mp/material' | ||||
| import { getFreePublishPage } from '@/api/mp/freePublish' | ||||
| import { getDraftPage } from '@/api/mp/draft' | ||||
| import { dateFormatter, formatDate } from '@/utils/formatTime' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { defineComponent, PropType } from 'vue' | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|  | @ -173,7 +171,7 @@ export default defineComponent({ | |||
|     const newsTypeRef = ref(props.newsType) | ||||
| 
 | ||||
|     const selectMaterialFun = (item) => { | ||||
|       ctx.emit('selectMaterial', item) | ||||
|       ctx.emit('select-material', item) | ||||
|     } | ||||
|     /** 搜索按钮操作 */ | ||||
|     const handleQuery = () => { | ||||
|  | @ -203,9 +201,10 @@ export default defineComponent({ | |||
|       total.value = data.total | ||||
|       loading.value = false | ||||
|     } | ||||
| 
 | ||||
|     const getFreePublishPageFun = async () => { | ||||
|       let data = await getFreePublishPage(queryParams) | ||||
|       data.list.foreach((item) => { | ||||
|       data.list.forEach((item) => { | ||||
|         const newsItem = item.content.newsItem | ||||
|         newsItem.forEach((article) => { | ||||
|           article.picUrl = article.thumbUrl | ||||
|  | @ -232,6 +231,7 @@ export default defineComponent({ | |||
|     onMounted(async () => { | ||||
|       getPage() | ||||
|     }) | ||||
| 
 | ||||
|     return { | ||||
|       handleQuery, | ||||
|       dateFormatter, | ||||
|  | @ -239,7 +239,6 @@ export default defineComponent({ | |||
|       getMaterialPageFun, | ||||
|       getPage, | ||||
|       formatDate, | ||||
|       newsTypeRef, | ||||
|       queryParams, | ||||
|       objDataRef, | ||||
|       list, | ||||
|  | @ -249,7 +248,6 @@ export default defineComponent({ | |||
|   } | ||||
| }) | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| /*瀑布流样式*/ | ||||
| .waterfall { | ||||
|  |  | |||
|  | @ -139,7 +139,7 @@ import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' | |||
| import WxNews from '@/views/mp/components/wx-news/main.vue' | ||||
| import WxLocation from '@/views/mp/components/wx-location/main.vue' | ||||
| import WxMusic from '@/views/mp/components/wx-music/main.vue' | ||||
| import { getUser } from '@/api/mp/mpuser' | ||||
| import { getUser } from '@/api/mp/user' | ||||
| import { defineComponent } from 'vue' | ||||
| 
 | ||||
| const message = useMessage() // 消息弹窗 | ||||
|  |  | |||
|  | @ -12,10 +12,7 @@ | |||
|     <!-- 类型 1:文本 --> | ||||
|     <el-tab-pane name="text"> | ||||
|       <template #label> | ||||
|         <el-row align="middle"> | ||||
|           <icon icon="ep:document" /> | ||||
|           文本 | ||||
|         </el-row> | ||||
|         <el-row align="middle"><Icon icon="ep:document" /> 文本</el-row> | ||||
|       </template> | ||||
|       <el-input | ||||
|         type="textarea" | ||||
|  | @ -28,18 +25,15 @@ | |||
|     <!-- 类型 2:图片 --> | ||||
|     <el-tab-pane name="image"> | ||||
|       <template #label> | ||||
|         <el-row align="middle"> | ||||
|           <icon icon="ep:picture" class="mr-5px" /> | ||||
|           图片 | ||||
|         </el-row> | ||||
|         <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"> | ||||
|         <el-row class="ope-row" justify="center"> | ||||
|           <el-button type="danger" circle @click="deleteObj"> | ||||
|             <icon icon="ep:delete" /> | ||||
|             <Icon icon="ep:delete" /> | ||||
|           </el-button> | ||||
|         </el-row> | ||||
|       </div> | ||||
|  | @ -48,11 +42,10 @@ | |||
|         <!-- 选择素材 --> | ||||
|         <el-col :span="12" class="col-select"> | ||||
|           <el-button type="success" @click="openMaterial"> | ||||
|             素材库选择 | ||||
|             <icon icon="ep:circle-check" /> | ||||
|             素材库选择 <Icon icon="ep:circle-check" /> | ||||
|           </el-button> | ||||
|           <el-dialog title="选择图片" v-model="dialogImageVisible" width="90%" append-to-body> | ||||
|             <wx-material-select :obj-data="objDataRef" @selectMaterial="selectMaterial" /> | ||||
|             <WxMaterialSelect :obj-data="objDataRef" @select-material="selectMaterial" /> | ||||
|           </el-dialog> | ||||
|         </el-col> | ||||
|         <!-- 文件上传 --> | ||||
|  | @ -70,10 +63,8 @@ | |||
|             <el-button type="primary">上传图片</el-button> | ||||
|             <template #tip> | ||||
|               <span> | ||||
|                 <div class="el-upload__tip" | ||||
|                   >支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div | ||||
|                 ></span | ||||
|               > | ||||
|                 <div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div> | ||||
|               </span> | ||||
|             </template> | ||||
|           </el-upload> | ||||
|         </el-col> | ||||
|  | @ -82,29 +73,25 @@ | |||
|     <!-- 类型 3:语音 --> | ||||
|     <el-tab-pane name="voice"> | ||||
|       <template #label> | ||||
|         <el-row align="middle"> | ||||
|           <icon icon="ep:phone" /> | ||||
|           语音 | ||||
|         </el-row> | ||||
|         <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"> | ||||
|           <wx-voice-player :url="objDataRef.url" /> | ||||
|           <WxVoicePlayer :url="objDataRef.url" /> | ||||
|         </div> | ||||
|         <el-row class="ope-row"> | ||||
|           <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" /> | ||||
|         <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"> | ||||
|             素材库选择<i class="el-icon-circle-check el-icon--right"></i> | ||||
|             素材库选择<Icon icon="ep:circle-check" /> | ||||
|           </el-button> | ||||
|           <el-dialog title="选择语音" v-model="dialogVoiceVisible" width="90%" append-to-body> | ||||
|             <WxMaterialSelect :objData="objData" @selectMaterial="selectMaterial" /> | ||||
|             <WxMaterialSelect :objData="objData" @select-material="selectMaterial" /> | ||||
|           </el-dialog> | ||||
|         </el-col> | ||||
|         <!-- 文件上传 --> | ||||
|  | @ -121,8 +108,8 @@ | |||
|           > | ||||
|             <el-button type="primary">点击上传</el-button> | ||||
|             <template #tip> | ||||
|               <div class="el-upload__tip" | ||||
|                 >格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s | ||||
|               <div class="el-upload__tip"> | ||||
|                 格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s | ||||
|               </div> | ||||
|             </template> | ||||
|           </el-upload> | ||||
|  | @ -132,10 +119,7 @@ | |||
|     <!-- 类型 4:视频 --> | ||||
|     <el-tab-pane name="video"> | ||||
|       <template #label> | ||||
|         <el-row align="middle"> | ||||
|           <icon icon="ep:share" /> | ||||
|           视频 | ||||
|         </el-row> | ||||
|         <el-row align="middle"><Icon icon="ep:share" /> 视频</el-row> | ||||
|       </template> | ||||
|       <el-row> | ||||
|         <el-input | ||||
|  | @ -151,18 +135,17 @@ | |||
|           @input="inputContent" | ||||
|         /> | ||||
|         <div style="text-align: center"> | ||||
|           <wx-video-player v-if="objDataRef.url" :url="objDataRef.url" /> | ||||
|           <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" /> | ||||
|                 素材库选择 <Icon icon="ep:circle-check" /> | ||||
|               </el-button> | ||||
|               <el-dialog title="选择视频" v-model="dialogVideoVisible" width="90%" append-to-body> | ||||
|                 <wx-material-select :objData="objDataRef" @selectMaterial="selectMaterial" /> | ||||
|                 <WxMaterialSelect :objData="objDataRef" @select-material="selectMaterial" /> | ||||
|               </el-dialog> | ||||
|             </el-col> | ||||
|             <!-- 文件上传 --> | ||||
|  | @ -177,10 +160,7 @@ | |||
|                 :before-upload="beforeVideoUpload" | ||||
|                 :on-success="handleUploadSuccess" | ||||
|               > | ||||
|                 <el-button type="primary" | ||||
|                   >新建视频 | ||||
|                   <icon icon="ep:upload" /> | ||||
|                 </el-button> | ||||
|                 <el-button type="primary">新建视频 <Icon icon="ep:upload" /></el-button> | ||||
|               </el-upload> | ||||
|             </el-col> | ||||
|           </el-row> | ||||
|  | @ -190,17 +170,14 @@ | |||
|     <!-- 类型 5:图文 --> | ||||
|     <el-tab-pane name="news"> | ||||
|       <template #label> | ||||
|         <el-row align="middle"> | ||||
|           <icon icon="ep:reading" /> | ||||
|           图文 | ||||
|         </el-row> | ||||
|         <el-row align="middle"><Icon icon="ep:reading" /> 图文</el-row> | ||||
|       </template> | ||||
|       <el-row> | ||||
|         <div class="select-item" v-if="objDataRef.articles.size > 0"> | ||||
|           <wx-news :articles="objDataRef.articles" /> | ||||
|         <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" /> | ||||
|               <Icon icon="ep:delete" /> | ||||
|             </el-button> | ||||
|           </el-col> | ||||
|         </div> | ||||
|  | @ -208,17 +185,17 @@ | |||
|         <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' ? '选择已发布图文' : '选择草稿箱图文' }} | ||||
|               <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> | ||||
|           <wx-material-select | ||||
|           <WxMaterialSelect | ||||
|             :objData="objDataRef" | ||||
|             @selectMaterial="selectMaterial" | ||||
|             @select-material="selectMaterial" | ||||
|             :newsType="newsType" | ||||
|           /> | ||||
|         </el-dialog> | ||||
|  | @ -227,10 +204,7 @@ | |||
|     <!-- 类型 6:音乐 --> | ||||
|     <el-tab-pane name="music"> | ||||
|       <template #label> | ||||
|         <el-row align="middle"> | ||||
|           <icon icon="ep:service" /> | ||||
|           音乐 | ||||
|         </el-row> | ||||
|         <el-row align="middle"><Icon icon="ep:service" />音乐</el-row> | ||||
|       </template> | ||||
|       <el-row align="middle" justify="center"> | ||||
|         <el-col :span="6"> | ||||
|  | @ -259,7 +233,7 @@ | |||
|                     <template #trigger> | ||||
|                       <el-button type="text">本地上传</el-button> | ||||
|                     </template> | ||||
|                     <el-button type="text" @click="openMaterial" style="margin-left: 5px" | ||||
|                     <el-button type="primary" link @click="openMaterial" style="margin-left: 5px" | ||||
|                       >素材库选择 | ||||
|                     </el-button> | ||||
|                   </el-upload> | ||||
|  | @ -268,9 +242,9 @@ | |||
|             </el-col> | ||||
|           </el-row> | ||||
|           <el-dialog title="选择图片" v-model="dialogThumbVisible" width="80%" append-to-body> | ||||
|             <wx-material-select | ||||
|             <WxMaterialSelect | ||||
|               :objData="{ type: 'image', accountId: objDataRef.accountId }" | ||||
|               @selectMaterial="selectMaterial" | ||||
|               @select-material="selectMaterial" | ||||
|             /> | ||||
|           </el-dialog> | ||||
|         </el-col> | ||||
|  | @ -295,7 +269,6 @@ | |||
|     </el-tab-pane> | ||||
|   </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' | ||||
|  | @ -482,7 +455,7 @@ export default defineComponent({ | |||
|       // 创建 tempObjItem 对象,并设置对应的值 | ||||
|       let tempObjItem = { | ||||
|         type: '', | ||||
|         articles: '', | ||||
|         articles: [], | ||||
|         thumbMediaId: '', | ||||
|         thumbMediaUrl: '', | ||||
|         introduction: '', | ||||
|  | @ -560,7 +533,7 @@ export default defineComponent({ | |||
|     } | ||||
|     const deleteObj = () => { | ||||
|       if (objDataRef.type === 'news') { | ||||
|         objDataRef.articles = '' | ||||
|         objDataRef.articles = [] | ||||
|       } else if (objDataRef.type === 'image') { | ||||
|         objDataRef.mediaId = null | ||||
|         objDataRef.url = null | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ | |||
|       <span class="amr-duration" v-if="duration">{{ duration }} 秒</span> | ||||
|     </el-icon> | ||||
|     <div v-if="content"> | ||||
|       <el-tag type="success" size="mini">语音识别</el-tag> | ||||
|       <el-tag type="success" size="small">语音识别</el-tag> | ||||
|       {{ content }} | ||||
|     </div> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -1,3 +1,813 @@ | |||
| <template> | ||||
|   <span>开发中</span> | ||||
|   <doc-alert title="公众号图文" url="https://doc.iocoder.cn/mp/article/" /> | ||||
| 
 | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <el-select v-model="queryParams.accountId" placeholder="请选择公众号"> | ||||
|           <el-option | ||||
|             v-for="item in accountList" | ||||
|             :key="item.id" | ||||
|             :label="item.name" | ||||
|             :value="item.id" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button> | ||||
|         <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button> | ||||
|         <el-button type="primary" plain @click="handleAdd" v-hasPermi="['mp:draft:create']"> | ||||
|           <Icon icon="ep:plus" />新增 | ||||
|         </el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|   <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"> | ||||
|           <wx-news :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> | ||||
|     <!-- 分页记录 --> | ||||
|     <Pagination | ||||
|       :total="total" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- TODO @Dhb52:迁移成独立路由 --> | ||||
|   <div class="app-container"> | ||||
|     <!-- 添加或修改草稿对话框 --> | ||||
|     <Teleport to="body"> | ||||
|       <el-dialog | ||||
|         :title="operateMaterial === 'add' ? '新建图文' : '修改图文'" | ||||
|         width="80%" | ||||
|         top="20px" | ||||
|         v-model="dialogNewsVisible" | ||||
|         :before-close="dialogNewsClose" | ||||
|         :close-on-click-modal="false" | ||||
|       > | ||||
|         <div class="left"> | ||||
|           <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" | ||||
|                       height="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(item)" | ||||
|                 v-if="articlesAdd.length < 8 && operateMaterial === 'add'" | ||||
|               > | ||||
|                 <Icon icon="ep:plus" /> | ||||
|               </el-button> | ||||
|             </el-row> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0"> | ||||
|           <br /> | ||||
|           <br /> | ||||
|           <br /> | ||||
|           <br /> | ||||
|           <!-- 标题、作者、原文地址 --> | ||||
|           <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" | ||||
|           /> | ||||
|           <!-- 封面和摘要 --> | ||||
|           <div class="input-tt">封面和摘要:</div> | ||||
|           <div> | ||||
|             <div class="thumb-div"> | ||||
|               <img | ||||
|                 class="material-img" | ||||
|                 v-if="articlesAdd[isActiveAddNews].thumbUrl" | ||||
|                 :src="articlesAdd[isActiveAddNews].thumbUrl" | ||||
|                 :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'" | ||||
|               /> | ||||
|               <Icon | ||||
|                 v-else | ||||
|                 icon="ep:plus" | ||||
|                 class="avatar-uploader-icon" | ||||
|                 :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'" | ||||
|               /> | ||||
|               <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 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> | ||||
|               <Teleport to="body"> | ||||
|                 <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%"> | ||||
|                   <WxMaterialSelect | ||||
|                     ref="materialSelectRef" | ||||
|                     :objData="{ type: 'image', accountId: queryParams.accountId }" | ||||
|                     @select-material="selectMaterial" | ||||
|                   /> | ||||
|                 </el-dialog> | ||||
|               </Teleport> | ||||
|             </div> | ||||
|             <el-input | ||||
|               :rows="8" | ||||
|               type="textarea" | ||||
|               v-model="articlesAdd[isActiveAddNews].digest" | ||||
|               placeholder="请输入摘要" | ||||
|               class="digest" | ||||
|               maxlength="120" | ||||
|               style="float: right" | ||||
|             /> | ||||
|           </div> | ||||
|           <!--富文本编辑器组件--> | ||||
|           <el-row> | ||||
|             <wx-editor | ||||
|               v-model="articlesAdd[isActiveAddNews].content" | ||||
|               :account-id="uploadData.accountId" | ||||
|               v-if="hackResetEditor" | ||||
|             /> | ||||
|           </el-row> | ||||
|         </div> | ||||
|         <template #footer> | ||||
|           <el-button @click="dialogNewsVisible = false">取 消</el-button> | ||||
|           <el-button type="primary" @click="submitForm">提 交</el-button> | ||||
|         </template> | ||||
|       </el-dialog> | ||||
|     </Teleport> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup name="MpDraft"> | ||||
| import WxEditor from '@/views/mp/components/wx-editor/WxEditor.vue' | ||||
| import WxNews from '@/views/mp/components/wx-news/main.vue' | ||||
| import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' | ||||
| import { getAccessToken } from '@/utils/auth' | ||||
| import * as MpAccountApi from '@/api/mp/account' | ||||
| import * as MpDraftApi from '@/api/mp/draft' | ||||
| import * as MpFreePublishApi from '@/api/mp/freePublish' | ||||
| const message = useMessage() // 消息 | ||||
| // 可以用改本地数据模拟,避免API调用超限 | ||||
| // import drafts from './mock' | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   accountId: undefined | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| const accountList = ref([]) // 公众号账号列表 | ||||
| 
 | ||||
| // ========== 文件上传 ========== | ||||
| const materialSelectRef = ref() | ||||
| const BASE_URL = import.meta.env.VITE_BASE_URL | ||||
| const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-permanent') // 上传永久素材的地址 | ||||
| const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部 | ||||
| const fileList = ref([]) | ||||
| const uploadData = reactive({ | ||||
|   type: 'image', | ||||
|   accountId: 1 | ||||
| }) | ||||
| 
 | ||||
| // ========== 草稿新建 or 修改 ========== | ||||
| const dialogNewsVisible = ref(false) | ||||
| const addMaterialLoading = ref(false) // 添加草稿的 loading 标识 | ||||
| const articlesAdd = ref([]) | ||||
| const isActiveAddNews = ref(0) | ||||
| const dialogImageVisible = ref(false) | ||||
| const operateMaterial = ref('add') | ||||
| const articlesMediaId = ref('') | ||||
| const hackResetEditor = ref(false) | ||||
| 
 | ||||
| /** 初始化 **/ | ||||
| onMounted(async () => { | ||||
|   accountList.value = await MpAccountApi.getSimpleAccountList() | ||||
|   // 选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     // @ts-ignore | ||||
|     queryParams.accountId = accountList.value[0].id | ||||
|   } | ||||
|   await getList() | ||||
| }) | ||||
| 
 | ||||
| // ======================== 列表查询 ======================== | ||||
| /** 设置账号编号 */ | ||||
| const setAccountId = (accountId) => { | ||||
|   queryParams.accountId = accountId | ||||
|   uploadData.accountId = accountId | ||||
| } | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   // 如果没有选中公众号账号,则进行提示。 | ||||
|   if (!queryParams.accountId) { | ||||
|     message.error('未选中公众号,无法查询草稿箱') | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const drafts = await MpDraftApi.getDraftPage(queryParams) | ||||
|     drafts.list.forEach((item) => { | ||||
|       const newsItem = item.content.newsItem | ||||
|       // 将 thumbUrl 转成 picUrl,保证 wx-news 组件可以预览封面 | ||||
|       newsItem.forEach((article) => { | ||||
|         article.picUrl = article.thumbUrl | ||||
|       }) | ||||
|     }) | ||||
|     list.value = drafts.list | ||||
|     total.value = drafts.total | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1 | ||||
|   // 默认选中第一个 | ||||
|   if (queryParams.accountId) { | ||||
|     setAccountId(queryParams.accountId) | ||||
|   } | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   queryFormRef.value.resetFields() | ||||
|   // 默认选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     setAccountId(accountList.value[0].id) | ||||
|   } | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| // ======================== 新增/修改草稿 ======================== | ||||
| /** 新增按钮操作 */ | ||||
| const handleAdd = () => { | ||||
|   resetEditor() | ||||
|   reset() | ||||
|   // 打开表单,并设置初始化 | ||||
|   operateMaterial.value = 'add' | ||||
|   dialogNewsVisible.value = true | ||||
| } | ||||
| 
 | ||||
| /** 更新按钮操作 */ | ||||
| const handleUpdate = (item) => { | ||||
|   resetEditor() | ||||
|   reset() | ||||
|   articlesMediaId.value = item.mediaId | ||||
|   articlesAdd.value = JSON.parse(JSON.stringify(item.content.newsItem)) | ||||
|   // 打开表单,并设置初始化 | ||||
|   operateMaterial.value = 'edit' | ||||
|   dialogNewsVisible.value = true | ||||
| } | ||||
| 
 | ||||
| /** 提交按钮 */ | ||||
| const submitForm = () => { | ||||
|   // TODO @Dhb52: 参考别的模块写法,改成 await 方式 | ||||
|   addMaterialLoading.value = true | ||||
|   if (operateMaterial.value === 'add') { | ||||
|     MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value) | ||||
|       .then(() => { | ||||
|         message.notifySuccess('新增成功') | ||||
|         dialogNewsVisible.value = false | ||||
|         getList() | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         addMaterialLoading.value = false | ||||
|       }) | ||||
|   } else { | ||||
|     MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value) | ||||
|       .then(() => { | ||||
|         message.notifySuccess('更新成功') | ||||
|         dialogNewsVisible.value = false | ||||
|         getList() | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         addMaterialLoading.value = false | ||||
|       }) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 关闭弹窗 | ||||
| const dialogNewsClose = async (done) => { | ||||
|   try { | ||||
|     await message.confirm('修改内容可能还未保存,确定关闭吗?') | ||||
|     reset() | ||||
|     resetEditor() | ||||
|     done() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| // 表单重置 | ||||
| const reset = () => { | ||||
|   isActiveAddNews.value = 0 | ||||
|   articlesAdd.value = [buildEmptyArticle()] | ||||
| } | ||||
| 
 | ||||
| // 表单 Editor 重置 | ||||
| const resetEditor = () => { | ||||
|   hackResetEditor.value = false // 销毁组件 | ||||
|   nextTick(() => { | ||||
|     hackResetEditor.value = true // 重建组件 | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 将图文向下移动 | ||||
| const downNews = (index) => { | ||||
|   let temp = articlesAdd.value[index] | ||||
|   articlesAdd.value[index] = articlesAdd.value[index + 1] | ||||
|   articlesAdd.value[index + 1] = temp | ||||
|   isActiveAddNews.value = index + 1 | ||||
| } | ||||
| 
 | ||||
| // 将图文向上移动 | ||||
| const upNews = (index) => { | ||||
|   let temp = articlesAdd.value[index] | ||||
|   articlesAdd.value[index] = articlesAdd.value[index - 1] | ||||
|   articlesAdd.value[index - 1] = temp | ||||
|   isActiveAddNews.value = index - 1 | ||||
| } | ||||
| 
 | ||||
| // 选中指定 index 的图文 | ||||
| const activeNews = (index) => { | ||||
|   resetEditor() | ||||
|   isActiveAddNews.value = index | ||||
| } | ||||
| 
 | ||||
| // 删除指定 index 的图文 | ||||
| const minusNews = async (index) => { | ||||
|   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 = () => { | ||||
|   return { | ||||
|     title: '', | ||||
|     thumbMediaId: '', | ||||
|     author: '', | ||||
|     digest: '', | ||||
|     showCoverPic: '', | ||||
|     content: '', | ||||
|     contentSourceUrl: '', | ||||
|     needOpenComment: '', | ||||
|     onlyFansCanComment: '', | ||||
|     thumbUrl: '' | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // ======================== 文件上传 ======================== | ||||
| const beforeThumbImageUpload = (file) => { | ||||
|   addMaterialLoading.value = true | ||||
|   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('上传图片格式不对!') | ||||
|     addMaterialLoading.value = false | ||||
|     return false | ||||
|   } | ||||
|   const isLt = file.size / 1024 / 1024 < 2 | ||||
|   if (!isLt) { | ||||
|     message.error('上传图片大小不能超过 2M!') | ||||
|     addMaterialLoading.value = false | ||||
|     return false | ||||
|   } | ||||
|   // 校验通过 | ||||
|   return true | ||||
| } | ||||
| 
 | ||||
| const handleUploadSuccess = (response, file, fileList) => { | ||||
|   addMaterialLoading.value = false | ||||
|   if (response.code !== 0) { | ||||
|     message.error('上传出错:' + response.msg) | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   // 重置上传文件的表单 | ||||
|   fileList.value = [] | ||||
| 
 | ||||
|   // 设置草稿的封面字段 | ||||
|   articlesAdd.value[isActiveAddNews.value].thumbMediaId = response.data.mediaId | ||||
|   articlesAdd.value[isActiveAddNews.value].thumbUrl = response.data.url | ||||
| } | ||||
| 
 | ||||
| // 选择 or 上传完素材,设置回草稿 | ||||
| const selectMaterial = (item) => { | ||||
|   dialogImageVisible.value = false | ||||
|   articlesAdd.value[isActiveAddNews.value].thumbMediaId = item.mediaId | ||||
|   articlesAdd.value[isActiveAddNews.value].thumbUrl = item.url | ||||
| } | ||||
| 
 | ||||
| // 打开素材选择 | ||||
| const openMaterial = () => { | ||||
|   dialogImageVisible.value = true | ||||
|   try { | ||||
|     materialSelectRef.value.queryParams.accountId = queryParams.accountId // 强制设置下 accountId,避免二次查询不对 | ||||
|     materialSelectRef.value.handleQuery() // 刷新列表,失败也无所谓 | ||||
|   } catch (e) {} | ||||
| } | ||||
| 
 | ||||
| // ======================== 草稿箱发布 ======================== | ||||
| const handlePublish = async (item) => { | ||||
|   const accountId = queryParams.accountId | ||||
|   const mediaId = item.mediaId | ||||
|   const content = | ||||
|     '你正在通过发布的方式发表内容。 发布不占用群发次数,一天可多次发布。已发布内容不会推送给用户,也不会展示在公众号主页中。 发布后,你可以前往发表记录获取链接,也可以将发布内容添加到自定义菜单、自动回复、话题和页面模板中。' | ||||
|   try { | ||||
|     await message.confirm(content) | ||||
|     await MpFreePublishApi.submitFreePublish(accountId, mediaId) | ||||
|     message.notifySuccess('发布成功') | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = async (item) => { | ||||
|   const accountId = queryParams.accountId | ||||
|   const mediaId = item.mediaId | ||||
|   try { | ||||
|     await message.confirm('此操作将永久删除该草稿, 是否继续?') | ||||
|     await MpDraftApi.deleteDraft(accountId, mediaId) | ||||
|     message.notifySuccess('删除成功') | ||||
|     await getList() | ||||
|   } 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; | ||||
| } | ||||
| 
 | ||||
| .item-name { | ||||
|   font-size: 12px; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .el-upload__tip { | ||||
|   margin-left: 5px; | ||||
| } | ||||
| 
 | ||||
| /*新增图文*/ | ||||
| .left { | ||||
|   display: inline-block; | ||||
|   width: 35%; | ||||
|   vertical-align: top; | ||||
|   margin-top: 200px; | ||||
| } | ||||
| 
 | ||||
| .right { | ||||
|   display: inline-block; | ||||
|   width: 60%; | ||||
|   margin-top: -40px; | ||||
| } | ||||
| 
 | ||||
| .avatar-uploader { | ||||
|   width: 20%; | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .avatar-uploader .el-upload { | ||||
|   border-radius: 6px; | ||||
|   cursor: pointer; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
|   text-align: unset !important; | ||||
| } | ||||
| 
 | ||||
| .avatar-uploader .el-upload:hover { | ||||
|   border-color: #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: 60%; | ||||
|   display: inline-block; | ||||
|   vertical-align: top; | ||||
| } | ||||
| 
 | ||||
| /*新增图文*/ | ||||
| /*瀑布流样式*/ | ||||
| .waterfall { | ||||
|   width: 100%; | ||||
|   column-gap: 10px; | ||||
|   column-count: 5; | ||||
|   margin: 0 auto; | ||||
| } | ||||
| 
 | ||||
| .waterfall-item { | ||||
|   padding: 10px; | ||||
|   margin-bottom: 10px; | ||||
|   break-inside: avoid; | ||||
|   border: 1px solid #eaeaea; | ||||
| } | ||||
| 
 | ||||
| p { | ||||
|   line-height: 30px; | ||||
| } | ||||
| 
 | ||||
| @media (min-width: 992px) and (max-width: 1300px) { | ||||
|   .waterfall { | ||||
|     column-count: 3; | ||||
|   } | ||||
| 
 | ||||
|   p { | ||||
|     color: red; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (min-width: 768px) and (max-width: 991px) { | ||||
|   .waterfall { | ||||
|     column-count: 2; | ||||
|   } | ||||
| 
 | ||||
|   p { | ||||
|     color: orange; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 767px) { | ||||
|   .waterfall { | ||||
|     column-count: 1; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /*瀑布流样式*/ | ||||
| .news-main { | ||||
|   background-color: #ffffff; | ||||
|   width: 100%; | ||||
|   margin: auto; | ||||
|   height: 120px; | ||||
| } | ||||
| 
 | ||||
| .news-content { | ||||
|   background-color: #acadae; | ||||
|   width: 100%; | ||||
|   height: 120px; | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .news-content-title { | ||||
|   display: inline-block; | ||||
|   font-size: 15px; | ||||
|   color: #ffffff; | ||||
|   position: absolute; | ||||
|   left: 0px; | ||||
|   bottom: 0px; | ||||
|   background-color: black; | ||||
|   width: 98%; | ||||
|   padding: 1%; | ||||
|   opacity: 0.65; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
|   height: 25px; | ||||
| } | ||||
| 
 | ||||
| .news-main-item { | ||||
|   background-color: #ffffff; | ||||
|   padding: 5px 0px; | ||||
|   border-top: 1px solid #eaeaea; | ||||
|   width: 100%; | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| .news-content-item { | ||||
|   position: relative; | ||||
|   margin-left: -3px; | ||||
| } | ||||
| 
 | ||||
| .news-content-item-title { | ||||
|   display: inline-block; | ||||
|   font-size: 12px; | ||||
|   width: 70%; | ||||
| } | ||||
| 
 | ||||
| .news-content-item-img { | ||||
|   display: inline-block; | ||||
|   width: 25%; | ||||
|   background-color: #acadae; | ||||
| } | ||||
| 
 | ||||
| .input-tt { | ||||
|   padding: 5px; | ||||
| } | ||||
| 
 | ||||
| .activeAddNews { | ||||
|   border: 5px solid #2bb673; | ||||
| } | ||||
| 
 | ||||
| .news-main-plus { | ||||
|   width: 280px; | ||||
|   text-align: center; | ||||
|   margin: auto; | ||||
|   height: 50px; | ||||
| } | ||||
| 
 | ||||
| .icon-plus { | ||||
|   margin: 10px; | ||||
|   font-size: 25px; | ||||
| } | ||||
| 
 | ||||
| .select-item { | ||||
|   width: 60%; | ||||
|   padding: 10px; | ||||
|   margin: 0 auto 10px auto; | ||||
|   border: 1px solid #eaeaea; | ||||
| } | ||||
| 
 | ||||
| .father .child { | ||||
|   display: none; | ||||
|   text-align: center; | ||||
|   position: relative; | ||||
|   bottom: 25px; | ||||
| } | ||||
| 
 | ||||
| .father:hover .child { | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| .thumb-div { | ||||
|   display: inline-block; | ||||
|   width: 30%; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .thumb-but { | ||||
|   margin: 5px; | ||||
| } | ||||
| 
 | ||||
| .material-img { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -0,0 +1,151 @@ | |||
| export default { | ||||
|   list: [ | ||||
|     { | ||||
|       mediaId: 'r6ryvl6LrxBU0miaST4Y-q-G9pdsmZw0OYG4FzHQkKfpLfEwIH51wy2bxisx8PvW', | ||||
|       content: { | ||||
|         newsItem: [ | ||||
|           { | ||||
|             title: '我是标题(OOO)', | ||||
|             author: '我是作者', | ||||
|             digest: '我是摘要', | ||||
|             content: '我是内容', | ||||
|             contentSourceUrl: 'https://www.iocoder.cn', | ||||
|             thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn', | ||||
|             showCoverPic: 0, | ||||
|             needOpenComment: 0, | ||||
|             onlyFansCanComment: 0, | ||||
|             url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9XaFphcmtJVFh3VEc4Q1MxQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN2QxTE56SFBCYXc2RE9NcUxIeS1CQjJuUHhTWjBlN2VOeGRpRi1fZUhwN1FNQjdrQV9yRU9EU0hibHREZmZoVW5acnZrN3ZjaWsxejR3RGpKczBzTHFIM0dFNFZWVkpBc0dWWlAzUEhlVmpnfn4%3D&chksm=1f6354802814dd969ef83c0f3babe555c614270b30bc383beaf7ffd13b0257f0fe5ced9af694#rd', | ||||
|             thumbUrl: | ||||
|               'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png' | ||||
|           }, | ||||
|           { | ||||
|             title: '我是标题(XXX)', | ||||
|             author: '我是作者', | ||||
|             digest: '我是摘要', | ||||
|             content: '我是内容', | ||||
|             contentSourceUrl: 'https://www.iocoder.cn', | ||||
|             thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn', | ||||
|             showCoverPic: 0, | ||||
|             needOpenComment: 0, | ||||
|             onlyFansCanComment: 0, | ||||
|             url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9yTlYwOEs1clpwcE5OUEhCQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN0NSMjFqN3N1aUZMbFNVLTZHN2ZDME9qOGp2THk2RFNlSTlKZ3Y1czFVZDdQQm5IeUg3dEppSUtpQUh5SExOOTRkT3dHNUdBdHdWSWlOendlREV3dS1jUEVQbFpiVTZmVW5iRWhZcGdkNTFRfn4%3D&chksm=1f6354802814dd96a403151cd44c7da4eecf0e475d25423e46ecd795b513bafd829a75daef9b#rd', | ||||
|             thumbUrl: | ||||
|               'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png' | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       updateTime: 1673655730 | ||||
|     }, | ||||
|     { | ||||
|       mediaId: 'r6ryvl6LrxBU0miaST4Y-jGpXnO73ihN0lsNXknCRQHapp2xgHMRxHKG50LituFe', | ||||
|       content: { | ||||
|         newsItem: [ | ||||
|           { | ||||
|             title: '我是标题(修改)', | ||||
|             author: '我是作者', | ||||
|             digest: '我是摘要', | ||||
|             content: '我是内容', | ||||
|             contentSourceUrl: 'https://www.iocoder.cn', | ||||
|             thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn', | ||||
|             showCoverPic: 0, | ||||
|             needOpenComment: 0, | ||||
|             onlyFansCanComment: 0, | ||||
|             url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl95WVFXYndIZnZJd0t5cjgvQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN1dlNURPbWswbEF4RDd5dVJTdjQ4cm9Cc0Q1TWhpMUh6SE1hVEE3ZHljaHhlZjZYSGF5N2JNSHpDTlh6ajNZbkpGTGpTcUQ4M3NMdW41ZUpXNFZZQ1VKbVlaMVp5ekxEV1czREdsY1dOYTZnfn4%3D&chksm=1f6354be2814dda8e6238037c2ebd52b1c8e80e93249a861ad80e4d40e5ca7207233475ca689#rd', | ||||
|             thumbUrl: | ||||
|               'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png' | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       updateTime: 1673655584 | ||||
|     }, | ||||
|     { | ||||
|       mediaId: 'r6ryvl6LrxBU0miaST4Y-v5SrbNCPpD6M_p3TmSrYwTjKogs-0DMJgmjMyNZPeMO', | ||||
|       content: { | ||||
|         newsItem: [ | ||||
|           { | ||||
|             title: '1321', | ||||
|             author: '3232', | ||||
|             digest: '1333', | ||||
|             content: '<p>444</p>', | ||||
|             contentSourceUrl: 'http://www.iocoder.cn', | ||||
|             thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-tlQmcl3RdC-Jcgns6IQtf7zenGy3b86WLT7GzUcrb1T', | ||||
|             showCoverPic: 0, | ||||
|             needOpenComment: 0, | ||||
|             onlyFansCanComment: 0, | ||||
|             url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9jelJiaDAzbmdpSkJOZ2M2QWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNDNXVVc2ZDRYeTY0Zm1weXR6dE9vQWh1TzEwbEpUVnRfVzJyaGFDNXBkZ0ZXM2JFOTNaRHNhOHRUeFdEanhMeS01X01kMUNWQ1BpRER3cjYwTl9pMnpFLUJhZXFucVVfM1pDUXlTUEl1S25nfn4%3D&chksm=1f6354bc2814ddaa56a90ad5bc3d078601c8d1589ba01827a8170587bc830ff9747b5f59c3a0#rd', | ||||
|             thumbUrl: | ||||
|               'http://mmbiz.qpic.cn/mmbiz_png/btUmCVHwbJUoicwBiacjVeQbu6QxgBVrukfSJXz509boa21SpH8OVHAqXCJiaiaAaHQJNxwwsa0gHRXVr0G5EZYamw/0?wx_fmt=png' | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       updateTime: 1673628969 | ||||
|     }, | ||||
|     { | ||||
|       mediaId: 'r6ryvl6LrxBU0miaST4Y-vdWrisK5EZbk4Y3tzh8P0PG0eEUbnQrh0BcsEb3WNP0', | ||||
|       content: { | ||||
|         newsItem: [ | ||||
|           { | ||||
|             title: 'tudou', | ||||
|             author: 'haha', | ||||
|             digest: '312', | ||||
|             content: '<p>132312</p>', | ||||
|             contentSourceUrl: 'http://www.iocoder.cn', | ||||
|             thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pgFtUNLu1foMSAMkoOsrQrTZ8EtTMssBLfTtzP0dfjG', | ||||
|             showCoverPic: 0, | ||||
|             needOpenComment: 0, | ||||
|             onlyFansCanComment: 0, | ||||
|             url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9qdkJ1ZjBoUmg2Uk9TS3RlQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNVg2aTJsaC1fMkU2eXNacUplN3VDTTZFZkhtMjhuTUZvWkxsNDBRSXExY2tiVXRHb09TaHgtREhzY3doZ0JYeC1TSTZ5eWZldXJsOWtfbV8yMi1aYkcyZ2pOY0haM0Ntb3VSWEtxUGVFRlNBfn4%3D&chksm=1f6354ba2814ddacf0184b24d310483641ef190b1faac098c285eb416c70017e2f54decfa1af#rd', | ||||
|             thumbUrl: | ||||
|               'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pgFtUNLu1foMSAMkoOsrQrTZ8EtTMssBLfTtzP0dfjG.png' | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       updateTime: 1673628760 | ||||
|     }, | ||||
|     { | ||||
|       mediaId: 'r6ryvl6LrxBU0miaST4Y-u9kTIm1DhWZDdXyxsxUVv2Z5DAB99IPxkIRTUUD206k', | ||||
|       content: { | ||||
|         newsItem: [ | ||||
|           { | ||||
|             title: '12', | ||||
|             author: '333', | ||||
|             digest: '123', | ||||
|             content: '123', | ||||
|             contentSourceUrl: 'https://www.iocoder.cn', | ||||
|             thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-jVixJGgnBnkBPRbuVptOW0CHYuQFyiOVNtamctS8xU8', | ||||
|             showCoverPic: 0, | ||||
|             needOpenComment: 0, | ||||
|             onlyFansCanComment: 0, | ||||
|             url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9qVVhpSDZUaFJWTzBBWWRVQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNWRnTDJWYmF2NER0clV1bThmQ0xUR3hqQnJkZ3BJSUNmNDJmc0lCZ1dadkVnZ3Z5bkN4YWtVUjhoaWZWYzZURUR4NnpMd0Y4Z3U5aUdib0lkMzI4Rjg3SG9JX2FycTMxbUctOHplaTlQVVhnfn4%3D&chksm=1f6354b62814dda076c778af33f06580165d8aa81f7798d55cfabb1886b5c74d9b2124a3535c#rd', | ||||
|             thumbUrl: | ||||
|               'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-jVixJGgnBnkBPRbuVptOW0CHYuQFyiOVNtamctS8xU8.jpg' | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       updateTime: 1673626494 | ||||
|     }, | ||||
|     { | ||||
|       mediaId: 'r6ryvl6LrxBU0miaST4Y-sO24upobaENDmeByfBTfaozB3aOqSMAV0lGy-UkHXE7', | ||||
|       content: { | ||||
|         newsItem: [ | ||||
|           { | ||||
|             title: '我是标题', | ||||
|             author: '我是作者', | ||||
|             digest: '我是摘要', | ||||
|             content: '我是内容', | ||||
|             contentSourceUrl: 'https://www.iocoder.cn', | ||||
|             thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn', | ||||
|             showCoverPic: 0, | ||||
|             needOpenComment: 0, | ||||
|             onlyFansCanComment: 0, | ||||
|             url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9LT2dqRnpMNUpsR0hjYWtBQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNGNmazZTdlE5WkxvU0tfX2V5cjV2WjJiR0xjQUhyREFSZWo2eWNrUW9EYVh6ZkpWRXBLR3FmTEV6YldBMno3Q2ZvVXBSdzlaVDc3aFhndEpQWUwzWmFMUWt0YVVURE1VZ1FsQTdPMlRtc3JBfn4%3D&chksm=1f6354aa2814ddbcc2637382f963a8742993ac38ebcebe6e3411df5ac82ac7bbdb391be6494a#rd', | ||||
|             thumbUrl: | ||||
|               'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png' | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       updateTime: 1673534279 | ||||
|     } | ||||
|   ], | ||||
|   total: 6 | ||||
| } | ||||
|  | @ -59,7 +59,7 @@ | |||
|   </ContentWrap> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts" name="freePublish"> | ||||
| <script setup lang="ts" name="MpFreePublish"> | ||||
| import * as FreePublishApi from '@/api/mp/freePublish' | ||||
| import * as MpAccountApi from '@/api/mp/account' | ||||
| import WxNews from '@/views/mp/components/wx-news/main.vue' | ||||
|  |  | |||
|  | @ -1,3 +1,528 @@ | |||
| <template> | ||||
|   <span>开发中</span> | ||||
|   <doc-alert title="公众号素材" url="https://doc.iocoder.cn/mp/material/" /> | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> | ||||
|           <el-option | ||||
|             v-for="item in accountList" | ||||
|             :key="item.id" | ||||
|             :label="item.name" | ||||
|             :value="item.id" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button> | ||||
|         <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <ContentWrap> | ||||
|     <el-tabs v-model="type" @tab-change="handleTabChange"> | ||||
|       <!-- tab 1:图片  --> | ||||
|       <el-tab-pane name="image"> | ||||
|         <template #label> | ||||
|           <span><Icon icon="ep:picture" />图片</span> | ||||
|         </template> | ||||
|         <div class="add_but" v-hasPermi="['mp:material:upload-permanent']"> | ||||
|           <el-upload | ||||
|             :action="actionUrl" | ||||
|             :headers="headers" | ||||
|             multiple | ||||
|             :limit="1" | ||||
|             :file-list="fileList" | ||||
|             :data="uploadData" | ||||
|             :before-upload="beforeImageUpload" | ||||
|             :on-success="handleUploadSuccess" | ||||
|           > | ||||
|             <el-button type="primary" plain>点击上传</el-button> | ||||
|             <template #tip> | ||||
|               <span class="el-upload__tip" style="margin-left: 5px"> | ||||
|                 支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M | ||||
|               </span> | ||||
|             </template> | ||||
|           </el-upload> | ||||
|         </div> | ||||
|         <div class="waterfall" v-loading="loading"> | ||||
|           <div class="waterfall-item" v-for="item in list" :key="item.id"> | ||||
|             <a target="_blank" :href="item.url"> | ||||
|               <img class="material-img" :src="item.url" /> | ||||
|               <div class="item-name">{{ item.name }}</div> | ||||
|             </a> | ||||
|             <el-row class="ope-row" justify="center"> | ||||
|               <el-button | ||||
|                 type="danger" | ||||
|                 circle | ||||
|                 @click="handleDelete(item)" | ||||
|                 v-hasPermi="['mp:material:delete']" | ||||
|               > | ||||
|                 <Icon icon="ep:delete" /> | ||||
|               </el-button> | ||||
|             </el-row> | ||||
|           </div> | ||||
|         </div> | ||||
|         <!-- 分页组件 --> | ||||
|         <Pagination | ||||
|           :total="total" | ||||
|           v-model:page="queryParams.pageNo" | ||||
|           v-model:limit="queryParams.pageSize" | ||||
|           @pagination="getList" | ||||
|         /> | ||||
|       </el-tab-pane> | ||||
| 
 | ||||
|       <!-- tab 2:语音  --> | ||||
|       <el-tab-pane name="voice"> | ||||
|         <template #label> | ||||
|           <span><Icon icon="ep:microphone" />语音</span> | ||||
|         </template> | ||||
|         <div class="add_but" v-hasPermi="['mp:material:upload-permanent']"> | ||||
|           <el-upload | ||||
|             :action="actionUrl" | ||||
|             :headers="headers" | ||||
|             multiple | ||||
|             :limit="1" | ||||
|             :file-list="fileList" | ||||
|             :data="uploadData" | ||||
|             :on-success="handleUploadSuccess" | ||||
|             :before-upload="beforeVoiceUpload" | ||||
|           > | ||||
|             <el-button type="primary" plain>点击上传</el-button> | ||||
|             <template #tip> | ||||
|               <span class="el-upload__tip" style="margin-left: 5px"> | ||||
|                 格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s | ||||
|               </span> | ||||
|             </template> | ||||
|           </el-upload> | ||||
|         </div> | ||||
|         <el-table :data="list" stripe border v-loading="loading" style="margin-top: 10px"> | ||||
|           <el-table-column label="编号" align="center" prop="mediaId" /> | ||||
|           <el-table-column label="文件名" align="center" prop="name" /> | ||||
|           <el-table-column label="语音" align="center"> | ||||
|             <template #default="scope"> | ||||
|               <WxVoicePlayer :url="scope.row.url" /> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column label="上传时间" align="center" prop="createTime" width="180"> | ||||
|             <template #default="scope"> | ||||
|               <span>{{ formatDate(scope.row.createTime) }}</span> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> | ||||
|             <template #default="scope"> | ||||
|               <el-button type="primary" link plain @click="handleDownload(scope.row)"> | ||||
|                 <Icon icon="ep:download" />下载 | ||||
|               </el-button> | ||||
|               <el-button | ||||
|                 type="primary" | ||||
|                 link | ||||
|                 plain | ||||
|                 @click="handleDelete(scope.row)" | ||||
|                 v-hasPermi="['mp:material:delete']" | ||||
|               > | ||||
|                 <Icon icon="ep:delete" />删除 | ||||
|               </el-button> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|         </el-table> | ||||
|         <!-- 分页组件 --> | ||||
|         <Pagination | ||||
|           :total="total" | ||||
|           v-model:page="queryParams.pageNo" | ||||
|           v-model:limit="queryParams.pageSize" | ||||
|           @pagination="getList" | ||||
|         /> | ||||
|       </el-tab-pane> | ||||
| 
 | ||||
|       <!-- tab 3:视频 --> | ||||
|       <el-tab-pane name="video"> | ||||
|         <template #label> | ||||
|           <span><Icon icon="ep:video-play" /> 视频</span> | ||||
|         </template> | ||||
|         <div class="add_but" v-hasPermi="['mp:material:upload-permanent']"> | ||||
|           <el-button type="primary" plain @click="handleAddVideo">新建视频</el-button> | ||||
|         </div> | ||||
|         <!-- 新建视频的弹窗 --> | ||||
|         <el-dialog | ||||
|           title="新建视频" | ||||
|           v-model="dialogVideoVisible" | ||||
|           width="600px" | ||||
|           v-loading="addMaterialLoading" | ||||
|         > | ||||
|           <el-upload | ||||
|             :action="actionUrl" | ||||
|             :headers="headers" | ||||
|             multiple | ||||
|             :limit="1" | ||||
|             :file-list="fileList" | ||||
|             :data="uploadData" | ||||
|             :before-upload="beforeVideoUpload" | ||||
|             :on-success="handleUploadSuccess" | ||||
|             ref="uploadVideoRef" | ||||
|             :auto-upload="false" | ||||
|           > | ||||
|             <template #trigger> | ||||
|               <el-button size="small" type="primary">选择视频</el-button> | ||||
|             </template> | ||||
|             <span class="el-upload__tip" style="margin-left: 10px" | ||||
|               >格式支持 MP4,文件大小不超过 10MB</span | ||||
|             > | ||||
|           </el-upload> | ||||
|           <el-form :model="uploadData" :rules="uploadRules" ref="uploadFormRef" label-width="80px"> | ||||
|             <el-row> | ||||
|               <el-form-item label="标题" prop="title"> | ||||
|                 <el-input | ||||
|                   v-model="uploadData.title" | ||||
|                   placeholder="标题将展示在相关播放页面,建议填写清晰、准确、生动的标题" | ||||
|                 /> | ||||
|               </el-form-item> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-form-item label="描述" prop="introduction"> | ||||
|                 <el-input | ||||
|                   :rows="3" | ||||
|                   type="textarea" | ||||
|                   v-model="uploadData.introduction" | ||||
|                   placeholder="介绍语将展示在相关播放页面,建议填写简洁明确、有信息量的内容" | ||||
|                 /> | ||||
|               </el-form-item> | ||||
|             </el-row> | ||||
|           </el-form> | ||||
|           <template #footer> | ||||
|             <div class="dialog-footer"> | ||||
|               <el-button @click="cancelVideo">取 消</el-button> | ||||
|               <el-button type="primary" @click="submitVideo">提 交</el-button> | ||||
|             </div> | ||||
|           </template> | ||||
|         </el-dialog> | ||||
| 
 | ||||
|         <el-table :data="list" stripe border v-loading="loading" style="margin-top: 10px"> | ||||
|           <el-table-column label="编号" align="center" prop="mediaId" /> | ||||
|           <el-table-column label="文件名" align="center" prop="name" /> | ||||
|           <el-table-column label="标题" align="center" prop="title" /> | ||||
|           <el-table-column label="介绍" align="center" prop="introduction" /> | ||||
|           <el-table-column label="视频" align="center"> | ||||
|             <template #default="scope"> | ||||
|               <WxVideoPlayer :url="scope.row.url" /> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column label="上传时间" align="center" prop="createTime" width="180"> | ||||
|             <template #default="scope"> | ||||
|               <span>{{ formatDate(scope.row.createTime) }}</span> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column label="操作" align="center" fixed="right"> | ||||
|             <template #default="scope"> | ||||
|               <el-button type="primary" link plain @click="handleDownload(scope.row)" | ||||
|                 ><Icon icon="ep:download" />下载</el-button | ||||
|               > | ||||
|               <el-button | ||||
|                 type="primary" | ||||
|                 link | ||||
|                 size="small" | ||||
|                 plain | ||||
|                 @click="handleDelete(scope.row)" | ||||
|                 v-hasPermi="['mp:material:delete']" | ||||
|               > | ||||
|                 <Icon icon="ep:delete" />删除 | ||||
|               </el-button> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|         </el-table> | ||||
|         <!-- 分页组件 --> | ||||
|         <Pagination | ||||
|           :total="total" | ||||
|           v-model:page="queryParams.pageNo" | ||||
|           v-model:limit="queryParams.pageSize" | ||||
|           @pagination="getList" | ||||
|         /> | ||||
|       </el-tab-pane> | ||||
|     </el-tabs> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup name="MpMaterial"> | ||||
| import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' | ||||
| import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' | ||||
| import { getSimpleAccountList } from '@/api/mp/account' | ||||
| import { getMaterialPage, deletePermanentMaterial } from '@/api/mp/material' | ||||
| import { getAccessToken } from '@/utils/auth' | ||||
| import { formatDate } from '@/utils/formatTime' | ||||
| 
 | ||||
| const BASE_URL = import.meta.env.VITE_BASE_URL | ||||
| 
 | ||||
| const message = useMessage() | ||||
| 
 | ||||
| const queryFormRef = ref() | ||||
| const uploadFormRef = ref() | ||||
| const uploadVideoRef = ref() | ||||
| 
 | ||||
| const type = ref('image') | ||||
| // 遮罩层 | ||||
| const loading = ref(false) | ||||
| // 总条数 | ||||
| const total = ref(0) | ||||
| // 数据列表 | ||||
| const list = ref([]) | ||||
| // 查询参数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   accountId: undefined, | ||||
|   permanent: true | ||||
| }) | ||||
| 
 | ||||
| const actionUrl = BASE_URL + '/admin-api/mp/material/upload-permanent' | ||||
| const headers = { Authorization: 'Bearer ' + getAccessToken() } | ||||
| const fileList = ref([]) | ||||
| const uploadData = reactive({ | ||||
|   type: 'image', | ||||
|   title: '', | ||||
|   introduction: '' | ||||
| }) | ||||
| 
 | ||||
| // === 视频上传,独有变量 === | ||||
| const dialogVideoVisible = ref(false) | ||||
| const addMaterialLoading = ref(false) | ||||
| const uploadRules = reactive({ | ||||
|   // 视频上传的校验规则 | ||||
|   title: [{ required: true, message: '请输入标题', trigger: 'blur' }], | ||||
|   introduction: [{ required: true, message: '请输入描述', trigger: 'blur' }] | ||||
| }) | ||||
| 
 | ||||
| // 公众号账号列表 | ||||
| const accountList = ref([]) | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   getSimpleAccountList().then((data) => { | ||||
|     accountList.value = data | ||||
|     // 默认选中第一个 | ||||
|     if (accountList.value.length > 0) { | ||||
|       setAccountId(accountList.value[0].id) | ||||
|     } | ||||
|     // 加载数据 | ||||
|     getList() | ||||
|   }) | ||||
| }) | ||||
| 
 | ||||
| // ======================== 列表查询 ======================== | ||||
| /** 设置账号编号 */ | ||||
| const setAccountId = (accountId) => { | ||||
|   queryParams.accountId = accountId | ||||
|   uploadData.accountId = accountId | ||||
| } | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = () => { | ||||
|   // 如果没有选中公众号账号,则进行提示。 | ||||
|   if (!queryParams.accountId) { | ||||
|     message.error('未选中公众号,无法查询草稿箱') | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   loading.value = true | ||||
|   getMaterialPage({ | ||||
|     ...queryParams, | ||||
|     type: type.value | ||||
|   }) | ||||
|     .then((data) => { | ||||
|       list.value = data.list | ||||
|       total.value = data.total | ||||
|     }) | ||||
|     .finally(() => { | ||||
|       loading.value = false | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1 | ||||
|   // 默认选中第一个 | ||||
|   if (queryParams.accountId) { | ||||
|     setAccountId(queryParams.accountId) | ||||
|   } | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   queryFormRef.value?.resetFields() | ||||
|   // 默认选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     setAccountId(accountList.value[0].id) | ||||
|   } | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| const handleTabChange = (tabName) => { | ||||
|   // 设置 type | ||||
|   uploadData.type = tabName | ||||
|   // 从第一页开始查询 | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| // ======================== 文件上传 ======================== | ||||
| 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 | ||||
|   } | ||||
|   loading.value = true | ||||
|   return true | ||||
| } | ||||
| 
 | ||||
| const beforeVoiceUpload = (file) => { | ||||
|   const isType = | ||||
|     file.type === 'audio/mp3' || | ||||
|     file.type === 'audio/wma' || | ||||
|     file.type === 'audio/wav' || | ||||
|     file.type === 'audio/amr' | ||||
|   const isLt = file.size / 1024 / 1024 < 2 | ||||
|   if (!isType) { | ||||
|     message.error('上传语音格式不对!') | ||||
|     return false | ||||
|   } | ||||
|   if (!isLt) { | ||||
|     message.error('上传语音大小不能超过 2M!') | ||||
|     return false | ||||
|   } | ||||
|   loading.value = true | ||||
|   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 | ||||
|   } | ||||
|   addMaterialLoading.value = true | ||||
|   return true | ||||
| } | ||||
| 
 | ||||
| const handleUploadSuccess = (response, file, fileList) => { | ||||
|   loading.value = false | ||||
|   addMaterialLoading.value = false | ||||
|   if (response.code !== 0) { | ||||
|     message.error('上传出错:' + response.msg) | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   // 清空上传时的各种数据 | ||||
|   dialogVideoVisible.value = false | ||||
|   fileList.value = [] | ||||
|   uploadData.title = '' | ||||
|   uploadData.introduction = '' | ||||
| 
 | ||||
|   // 加载数据 | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| // 下载文件 | ||||
| const handleDownload = (row) => { | ||||
|   window.open(row.url, '_blank') | ||||
| } | ||||
| 
 | ||||
| // 提交 video 新建的表单 | ||||
| const submitVideo = () => { | ||||
|   uploadFormRef.value.validate((valid) => { | ||||
|     if (!valid) { | ||||
|       return false | ||||
|     } | ||||
|     uploadVideoRef.value.submit() | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| const handleAddVideo = () => { | ||||
|   resetVideo() | ||||
|   dialogVideoVisible.value = true | ||||
| } | ||||
| 
 | ||||
| /** 取消按钮 */ | ||||
| const cancelVideo = () => { | ||||
|   dialogVideoVisible.value = false | ||||
|   resetVideo() | ||||
| } | ||||
| 
 | ||||
| /** 表单重置 */ | ||||
| const resetVideo = () => { | ||||
|   fileList.value = [] | ||||
|   uploadData.title = '' | ||||
|   uploadData.introduction = '' | ||||
|   uploadFormRef.value?.resetFields() | ||||
| } | ||||
| 
 | ||||
| // ======================== 其它操作 ======================== | ||||
| const handleDelete = async (item) => { | ||||
|   await message.confirm('此操作将永久删除该文件, 是否继续?') | ||||
|   await deletePermanentMaterial(item.id) | ||||
|   message.alertSuccess('删除成功') | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| /*瀑布流样式*/ | ||||
| .waterfall { | ||||
|   width: 100%; | ||||
|   column-gap: 10px; | ||||
|   column-count: 5; | ||||
|   margin-top: 10px; /* 芋道源码:增加 10px,避免顶着上面 */ | ||||
| } | ||||
| .waterfall-item { | ||||
|   padding: 10px; | ||||
|   margin-bottom: 10px; | ||||
|   break-inside: avoid; | ||||
|   border: 1px solid #eaeaea; | ||||
| } | ||||
| .material-img { | ||||
|   width: 100%; | ||||
| } | ||||
| p { | ||||
|   line-height: 30px; | ||||
| } | ||||
| @media (min-width: 992px) and (max-width: 1300px) { | ||||
|   .waterfall { | ||||
|     column-count: 3; | ||||
|   } | ||||
|   p { | ||||
|     color: red; | ||||
|   } | ||||
| } | ||||
| @media (min-width: 768px) and (max-width: 991px) { | ||||
|   .waterfall { | ||||
|     column-count: 2; | ||||
|   } | ||||
|   p { | ||||
|     color: orange; | ||||
|   } | ||||
| } | ||||
| @media (max-width: 767px) { | ||||
|   .waterfall { | ||||
|     column-count: 1; | ||||
|   } | ||||
| } | ||||
| /*瀑布流样式*/ | ||||
| </style> | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 34 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.3 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 12 KiB | 
|  | @ -1,3 +1,782 @@ | |||
| <template> | ||||
|   <span>开发中</span> | ||||
|   <doc-alert title="公众号菜单" url="https://doc.iocoder.cn/mp/menu/" /> | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px"> | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <el-select v-model="accountId" placeholder="请选择公众号" class="!w-240px"> | ||||
|           <el-option | ||||
|             v-for="item in accountList" | ||||
|             :key="item.id" | ||||
|             :label="item.name" | ||||
|             :value="item.id" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button> | ||||
|         <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|   <ContentWrap> | ||||
|     <div class="public-account-management clearfix" v-loading="loading"> | ||||
|       <!--左边配置菜单--> | ||||
|       <div class="left"> | ||||
|         <div class="weixin-hd"> | ||||
|           <div class="weixin-title">{{ name }}</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> | ||||
|         <div class="save_div"> | ||||
|           <el-button | ||||
|             class="save_btn" | ||||
|             type="success" | ||||
|             @click="handleSave" | ||||
|             v-hasPermi="['mp:menu:save']" | ||||
|             >保存并发布菜单</el-button | ||||
|           > | ||||
|           <el-button | ||||
|             class="save_btn" | ||||
|             type="danger" | ||||
|             @click="handleDelete" | ||||
|             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(tempObj)"> | ||||
|               删除当前菜单<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="openMaterial"> | ||||
|                         素材库选择<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> | ||||
|       <!-- 一进页面就显示的默认页面,当点击左边按钮的时候,就不显示了--> | ||||
|       <div v-else class="right"> | ||||
|         <p>请选择菜单配置</p> | ||||
|       </div> | ||||
|     </div> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <script setup name="MpMenu"> | ||||
| import { handleTree } from '@/utils/tree' | ||||
| 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 { deleteMenu, getMenuList, saveMenu } from '@/api/mp/menu' | ||||
| import * as MpAccountApi from '@/api/mp/account' | ||||
| import menuOptions from './menuOptions' | ||||
| const message = useMessage() // 消息 | ||||
| 
 | ||||
| // ======================== 列表查询 ======================== | ||||
| const loading = ref(true) // 遮罩层 | ||||
| const accountId = ref(undefined) // 公众号Id | ||||
| const name = ref('') // 公众号名 | ||||
| const menuList = ref({ children: [] }) | ||||
| const accountList = ref([]) // 公众号账号列表 | ||||
| 
 | ||||
| // ======================== 菜单操作 ======================== | ||||
| const isActive = ref(-1) // 一级菜单点中样式 | ||||
| const isSubMenuActive = ref(-1) // 一级菜单点中样式 | ||||
| const isSubMenuFlag = 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({}) // 右边临时变量,作为中间值牵引关系 | ||||
| 
 | ||||
| const tempSelfObj = ref({ | ||||
|   // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数 | ||||
| }) | ||||
| const dialogNewsVisible = ref(false) // 跳转图文时的素材选择弹窗 | ||||
| 
 | ||||
| onMounted(async () => { | ||||
|   accountList.value = await MpAccountApi.getSimpleAccountList() | ||||
|   // 选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     // @ts-ignore | ||||
|     setAccountId(accountList.value[0].id) | ||||
|   } | ||||
|   await getList() | ||||
| }) | ||||
| 
 | ||||
| // ======================== 列表查询 ======================== | ||||
| /** 设置账号编号 */ | ||||
| const setAccountId = (id) => { | ||||
|   accountId.value = id | ||||
|   name.value = accountList.value.find((item) => item.id === accountId.value)?.name | ||||
| } | ||||
| 
 | ||||
| const getList = async () => { | ||||
|   loading.value = false | ||||
|   getMenuList(accountId.value) | ||||
|     .then((response) => { | ||||
|       const menuData = convertMenuList(response) | ||||
|       menuList.value = handleTree(menuData, 'id') | ||||
|     }) | ||||
|     .finally(() => { | ||||
|       loading.value = false | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   resetForm() | ||||
|   // 默认选中第一个 | ||||
|   if (accountId.value) { | ||||
|     setAccountId(accountId.value) | ||||
|   } | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   resetForm() | ||||
|   // 默认选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     setAccountId(accountList.value[0].id) | ||||
|   } | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| // 将后端返回的 menuList,转换成前端的 menuList | ||||
| const convertMenuList = (list) => { | ||||
|   if (!list) return [] | ||||
| 
 | ||||
|   const menuList = [] | ||||
|   list.forEach((item) => { | ||||
|     const menu = { | ||||
|       ...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 | ||||
|       } | ||||
|     } | ||||
|     menuList.push(menu) | ||||
|   }) | ||||
|   return menuList | ||||
| } | ||||
| 
 | ||||
| // 重置表单,清空表单数据 | ||||
| const resetForm = () => { | ||||
|   // 菜单操作 | ||||
|   isActive.value = -1 | ||||
|   isSubMenuActive.value = -1 | ||||
|   isSubMenuFlag.value = -1 | ||||
| 
 | ||||
|   // 菜单编辑 | ||||
|   showRightFlag.value = false | ||||
|   nameMaxLength.value = 0 | ||||
|   showConfigureContent.value = 0 | ||||
|   hackResetWxReplySelect.value = false | ||||
|   tempObj.value = {} | ||||
|   tempSelfObj.value = {} | ||||
|   dialogNewsVisible.value = false | ||||
| } | ||||
| 
 | ||||
| // ======================== 菜单操作 ======================== | ||||
| // 一级菜单点击事件 | ||||
| const menuClick = (i, item) => { | ||||
|   // 右侧的表单相关 | ||||
|   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) // 有子菜单,就不显示配置内容 | ||||
| 
 | ||||
|   // 左侧的选中 | ||||
|   isActive.value = i // 一级菜单选中样式 | ||||
|   isSubMenuFlag.value = i // 二级菜单显示标志 | ||||
|   isSubMenuActive.value = -1 // 二级菜单去除选中样式 | ||||
| } | ||||
| 
 | ||||
| // 二级菜单点击事件 | ||||
| const subMenuClick = (subItem, index, k) => { | ||||
|   // 右侧的表单相关 | ||||
|   resetEditor() | ||||
|   showRightFlag.value = true // 右边菜单 | ||||
|   console.log(subItem) | ||||
|   tempObj.value = subItem // 将点击的数据放到临时变量,对象有引用作用 | ||||
|   tempSelfObj.value.grand = '2' // 表示二级菜单 | ||||
|   tempSelfObj.value.index = index // 表示一级菜单索引 | ||||
|   tempSelfObj.value.secondIndex = k // 表示二级菜单索引 | ||||
|   nameMaxLength.value = 7 | ||||
|   showConfigureContent.value = true | ||||
| 
 | ||||
|   // 左侧的选中 | ||||
|   isActive.value = -1 // 一级菜单去除样式 | ||||
|   isSubMenuActive.value = index + '' + k // 二级菜单选中样式 | ||||
| } | ||||
| 
 | ||||
| // 添加横向一级菜单 | ||||
| const addMenu = () => { | ||||
|   const menuKeyLength = menuList.value.length | ||||
|   const addButton = { | ||||
|     name: '菜单名称', | ||||
|     children: [], | ||||
|     reply: { | ||||
|       // 用于存储回复内容 | ||||
|       type: 'text', | ||||
|       accountId: accountId.value // 保证组件里,可以使用到对应的公众号 | ||||
|     } | ||||
|   } | ||||
|   menuList.value[menuKeyLength] = addButton | ||||
|   menuClick(menuKeyLength.value - 1, addButton) | ||||
| } | ||||
| // 添加横向二级菜单;item 表示要操作的父菜单 | ||||
| const addSubMenu = (i, item) => { | ||||
|   // 清空父菜单的属性,因为它只需要 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 | ||||
|   } | ||||
| 
 | ||||
|   let subMenuKeyLength = item.children.length // 获取二级菜单key长度 | ||||
|   let addButton = { | ||||
|     name: '子菜单名称', | ||||
|     reply: { | ||||
|       // 用于存储回复内容 | ||||
|       type: 'text', | ||||
|       accountId: accountId.value // 保证组件里,可以使用到对应的公众号 | ||||
|     } | ||||
|   } | ||||
|   item.children[subMenuKeyLength] = addButton | ||||
|   subMenuClick(item.children[subMenuKeyLength], i, subMenuKeyLength) | ||||
| } | ||||
| 
 | ||||
| // 删除当前菜单 | ||||
| const handleDeleteMenu = async () => { | ||||
|   try { | ||||
|     await message.confirm('确定要删除吗?') | ||||
|     if (tempSelfObj.value.grand === '1') { | ||||
|       // 一级菜单的删除方法 | ||||
|       menuList.value.splice(tempSelfObj.value.index, 1) | ||||
|     } else if (tempSelfObj.value.grand === '2') { | ||||
|       // 二级菜单的删除方法 | ||||
|       menuList.value[tempSelfObj.value.index].children.splice(tempSelfObj.value.secondIndex, 1) | ||||
|     } | ||||
|     // 提示 | ||||
|     message.notifySuccess('删除成功') | ||||
| 
 | ||||
|     // 处理菜单的选中 | ||||
|     tempObj.value = {} | ||||
|     showRightFlag.value = false | ||||
|     isActive.value = -1 | ||||
|     isSubMenuActive.value = -1 | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| // ======================== 菜单编辑 ======================== | ||||
| const handleSave = async () => { | ||||
|   try { | ||||
|     await message.confirm('确定要删除吗?') | ||||
|     loading.value = true | ||||
|     await saveMenu(accountId.value, convertMenuFormList()) | ||||
|     getList() | ||||
|     message.notifySuccess('发布成功') | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 表单 Editor 重置 | ||||
| const resetEditor = () => { | ||||
|   hackResetWxReplySelect.value = false // 销毁组件 | ||||
|   nextTick(() => { | ||||
|     console.log('nextTick') | ||||
|     hackResetWxReplySelect.value = true // 重建组件 | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| const handleDelete = async () => { | ||||
|   try { | ||||
|     await message.confirm('确定要删除吗?') | ||||
|     loading.value = true | ||||
|     await deleteMenu(accountId.value) | ||||
|     handleQuery() | ||||
|     message.notifySuccess('清空成功') | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 将前端的 menuList,转换成后端接收的 menuList | ||||
| const convertMenuFormList = () => { | ||||
|   const result = [] | ||||
|   menuList.value.forEach((item) => { | ||||
|     let menu = convertMenuForm(item) | ||||
|     result.push(menu) | ||||
| 
 | ||||
|     // 处理子菜单 | ||||
|     if (!item.children || item.children.length <= 0) { | ||||
|       return | ||||
|     } | ||||
|     menu.children = [] | ||||
|     item.children.forEach((subItem) => { | ||||
|       menu.children.push(convertMenuForm(subItem)) | ||||
|     }) | ||||
|   }) | ||||
|   return result | ||||
| } | ||||
| 
 | ||||
| // 将前端的 menu,转换成后端接收的 menu | ||||
| const convertMenuForm = (menu) => { | ||||
|   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 | ||||
|   } | ||||
|   return result | ||||
| } | ||||
| 
 | ||||
| // ======================== 菜单编辑(素材选择) ======================== | ||||
| const openMaterial = () => { | ||||
|   dialogNewsVisible.value = true | ||||
| } | ||||
| 
 | ||||
| const selectMaterial = (item) => { | ||||
|   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> | ||||
| <!--本组件样式--> | ||||
| <style lang="scss" scoped="scoped"> | ||||
| /* 公共颜色变量 */ | ||||
| .clearfix { | ||||
|   *zoom: 1; | ||||
| } | ||||
| 
 | ||||
| .clearfix::after { | ||||
|   content: ''; | ||||
|   display: table; | ||||
|   clear: both; | ||||
| } | ||||
| 
 | ||||
| div { | ||||
|   text-align: left; | ||||
| } | ||||
| 
 | ||||
| .weixin-hd { | ||||
|   color: #fff; | ||||
|   text-align: center; | ||||
|   position: relative; | ||||
|   bottom: 426px; | ||||
|   left: 0px; | ||||
|   width: 300px; | ||||
|   height: 64px; | ||||
|   background: transparent url('./assets/menu_head.png') no-repeat 0 0; | ||||
|   background-position: 0 0; | ||||
|   background-size: 100%; | ||||
| } | ||||
| 
 | ||||
| .weixin-title { | ||||
|   color: #fff; | ||||
|   font-size: 14px; | ||||
|   width: 100%; | ||||
|   text-align: center; | ||||
|   position: absolute; | ||||
|   top: 33px; | ||||
|   left: 0px; | ||||
| } | ||||
| 
 | ||||
| .weixin-menu { | ||||
|   background: transparent url('./assets/menu_foot.png') no-repeat 0 0; | ||||
|   padding-left: 43px; | ||||
|   font-size: 12px; | ||||
| } | ||||
| 
 | ||||
| .menu_option { | ||||
|   width: 40% !important; | ||||
| } | ||||
| 
 | ||||
| .public-account-management { | ||||
|   min-width: 1200px; | ||||
|   width: 1200px; | ||||
|   margin: 0 auto; | ||||
| 
 | ||||
|   .left { | ||||
|     float: left; | ||||
|     display: inline-block; | ||||
|     width: 350px; | ||||
|     height: 715px; | ||||
|     background: url('./assets/iphone_backImg.png') no-repeat; | ||||
|     background-size: 100% auto; | ||||
|     padding: 518px 25px 88px; | ||||
|     position: relative; | ||||
|     box-sizing: border-box; | ||||
| 
 | ||||
|     /*第一级菜单*/ | ||||
|     .menu_main { | ||||
|       .menu_bottom { | ||||
|         position: relative; | ||||
|         float: left; | ||||
|         display: inline-block; | ||||
|         box-sizing: border-box; | ||||
|         width: 85.5px; | ||||
|         text-align: center; | ||||
|         border: 1px solid #ebedee; | ||||
|         background-color: #fff; | ||||
|         cursor: pointer; | ||||
| 
 | ||||
|         &.menu_addicon { | ||||
|           height: 46px; | ||||
|           line-height: 46px; | ||||
|         } | ||||
| 
 | ||||
|         .menu_item { | ||||
|           height: 44px; | ||||
|           line-height: 44px; | ||||
|           // text-align: center; | ||||
|           box-sizing: border-box; | ||||
|           width: 100%; | ||||
|           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; | ||||
| 
 | ||||
|       .save_btn { | ||||
|         bottom: 20px; | ||||
|         left: 100px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /*右边菜单内容*/ | ||||
|   .right { | ||||
|     float: left; | ||||
|     width: 63%; | ||||
|     background-color: #e8e7e7; | ||||
|     padding: 20px; | ||||
|     margin-left: 20px; | ||||
|     -webkit-box-sizing: border-box; | ||||
|     box-sizing: border-box; | ||||
| 
 | ||||
|     .configure_page { | ||||
|       .delete_btn { | ||||
|         text-align: right; | ||||
|         margin-bottom: 15px; | ||||
|       } | ||||
| 
 | ||||
|       .menu_content { | ||||
|         margin-top: 20px; | ||||
|       } | ||||
| 
 | ||||
|       .configur_content { | ||||
|         margin-top: 20px; | ||||
|         background-color: #fff; | ||||
|         padding: 20px 10px; | ||||
|         border-radius: 5px; | ||||
|       } | ||||
| 
 | ||||
|       .blue { | ||||
|         color: #29b6f6; | ||||
|         margin-top: 10px; | ||||
|       } | ||||
| 
 | ||||
|       .applet { | ||||
|         margin-bottom: 20px; | ||||
| 
 | ||||
|         span { | ||||
|           width: 20%; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       .input_width { | ||||
|         width: 40%; | ||||
|       } | ||||
| 
 | ||||
|       .material { | ||||
|         .input_width { | ||||
|           width: 30%; | ||||
|         } | ||||
| 
 | ||||
|         .el-textarea { | ||||
|           width: 80%; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .el-input { | ||||
|     width: 70%; | ||||
|     margin-right: 2%; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| <!--素材样式--> | ||||
| <style lang="scss" scoped> | ||||
| .pagination { | ||||
|   text-align: right; | ||||
|   margin-right: 25px; | ||||
| } | ||||
| 
 | ||||
| .select-item { | ||||
|   width: 280px; | ||||
|   padding: 10px; | ||||
|   margin: 0 auto 10px auto; | ||||
|   border: 1px solid #eaeaea; | ||||
| } | ||||
| 
 | ||||
| .select-item2 { | ||||
|   padding: 10px; | ||||
|   margin: 0 auto 10px auto; | ||||
|   border: 1px solid #eaeaea; | ||||
| } | ||||
| 
 | ||||
| .ope-row { | ||||
|   padding-top: 10px; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .item-name { | ||||
|   font-size: 12px; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
|   text-align: center; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -0,0 +1,42 @@ | |||
| export default [ | ||||
|   { | ||||
|     value: 'view', | ||||
|     label: '跳转网页' | ||||
|   }, | ||||
|   { | ||||
|     value: 'miniprogram', | ||||
|     label: '跳转小程序' | ||||
|   }, | ||||
|   { | ||||
|     value: 'click', | ||||
|     label: '点击回复' | ||||
|   }, | ||||
|   { | ||||
|     value: 'article_view_limited', | ||||
|     label: '跳转图文消息' | ||||
|   }, | ||||
|   { | ||||
|     value: 'scancode_push', | ||||
|     label: '扫码直接返回结果' | ||||
|   }, | ||||
|   { | ||||
|     value: 'scancode_waitmsg', | ||||
|     label: '扫码回复' | ||||
|   }, | ||||
|   { | ||||
|     value: 'pic_sysphoto', | ||||
|     label: '系统拍照发图' | ||||
|   }, | ||||
|   { | ||||
|     value: 'pic_photo_or_album', | ||||
|     label: '拍照或者相册' | ||||
|   }, | ||||
|   { | ||||
|     value: 'pic_weixin', | ||||
|     label: '微信相册' | ||||
|   }, | ||||
|   { | ||||
|     value: 'location_select', | ||||
|     label: '选择地理位置' | ||||
|   } | ||||
| ] | ||||
|  | @ -1,3 +0,0 @@ | |||
| <template> | ||||
|   <span>开发中</span> | ||||
| </template> | ||||
|  | @ -0,0 +1,99 @@ | |||
| <template> | ||||
|   <Dialog title="修改" v-model="dialogVisible"> | ||||
|     <el-form | ||||
|       ref="formRef" | ||||
|       :model="formData" | ||||
|       :rules="formRules" | ||||
|       label-width="80px" | ||||
|       v-loading="formLoading" | ||||
|     > | ||||
|       <el-form-item label="昵称" prop="nickname"> | ||||
|         <el-input v-model="formData.nickname" placeholder="请输入昵称" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="备注" prop="remark"> | ||||
|         <el-input v-model="formData.remark" placeholder="请输入备注" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="标签" prop="tagIds"> | ||||
|         <el-select v-model="formData.tagIds" multiple clearable placeholder="请选择标签"> | ||||
|           <el-option | ||||
|             v-for="item in tagList" | ||||
|             :key="item.tagId" | ||||
|             :label="item.name" | ||||
|             :value="item.tagId" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|     <template #footer> | ||||
|       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> | ||||
|       <el-button @click="dialogVisible = false">取 消</el-button> | ||||
|     </template> | ||||
|   </Dialog> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import * as MpTagApi from '@/api/mp/tag' | ||||
| import * as MpUserApi from '@/api/mp/user' | ||||
| const { t } = useI18n() // 国际化 | ||||
| const message = useMessage() // 消息弹窗 | ||||
| 
 | ||||
| const dialogVisible = ref(false) // 弹窗的是否展示 | ||||
| const formLoading = ref(false) // 表单的加载中 | ||||
| const formData = ref({ | ||||
|   id: undefined, | ||||
|   nickname: undefined, | ||||
|   remark: undefined, | ||||
|   tagIds: [] | ||||
| }) | ||||
| const formRules = reactive({}) // 表单的校验 | ||||
| const formRef = ref() // 表单 Ref | ||||
| const tagList = ref([]) // 公众号标签列表 | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| const open = async (id: number) => { | ||||
|   dialogVisible.value = true | ||||
|   resetForm() | ||||
|   // 修改时,设置数据 | ||||
|   if (id) { | ||||
|     formLoading.value = true | ||||
|     try { | ||||
|       formData.value = await MpUserApi.getUser(id) | ||||
|     } finally { | ||||
|       formLoading.value = false | ||||
|     } | ||||
|   } | ||||
|   // 加载标签 | ||||
|   tagList.value = await MpTagApi.getSimpleTagList() | ||||
| } | ||||
| defineExpose({ open }) // 提供 open 方法,用于打开弹窗 | ||||
| 
 | ||||
| /** 提交表单 */ | ||||
| const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 | ||||
| const submitForm = async () => { | ||||
|   // 校验表单 | ||||
|   if (!formRef) return | ||||
|   const valid = await formRef.value.validate() | ||||
|   if (!valid) return | ||||
|   // 提交请求 | ||||
|   formLoading.value = true | ||||
|   try { | ||||
|     await MpUserApi.updateUser(formData.value) | ||||
|     message.success(t('common.updateSuccess')) | ||||
|     dialogVisible.value = false | ||||
|     // 发送操作成功的事件 | ||||
|     emit('success') | ||||
|   } finally { | ||||
|     formLoading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     id: undefined, | ||||
|     nickname: undefined, | ||||
|     remark: undefined, | ||||
|     tagIds: [] | ||||
|   } | ||||
|   formRef.value?.resetFields() | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,187 @@ | |||
| <template> | ||||
|   <doc-alert title="公众号粉丝" url="https://doc.iocoder.cn/mp/user/" /> | ||||
| 
 | ||||
|   <!-- 搜索工作栏 --> | ||||
|   <ContentWrap> | ||||
|     <el-form | ||||
|       class="-mb-15px" | ||||
|       :model="queryParams" | ||||
|       ref="queryFormRef" | ||||
|       :inline="true" | ||||
|       label-width="68px" | ||||
|     > | ||||
|       <el-form-item label="公众号" prop="accountId"> | ||||
|         <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> | ||||
|           <el-option | ||||
|             v-for="item in accountList" | ||||
|             :key="item.id" | ||||
|             :label="item.name" | ||||
|             :value="item.id" | ||||
|           /> | ||||
|         </el-select> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="用户标识" prop="openid"> | ||||
|         <el-input | ||||
|           v-model="queryParams.openid" | ||||
|           placeholder="请输入用户标识" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item label="昵称" prop="nickname"> | ||||
|         <el-input | ||||
|           v-model="queryParams.nickname" | ||||
|           placeholder="请输入昵称" | ||||
|           clearable | ||||
|           @keyup.enter="handleQuery" | ||||
|           class="!w-240px" | ||||
|         /> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button> | ||||
|         <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button> | ||||
|         <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:user:sync']"> | ||||
|           <Icon icon="ep:refresh" class="mr-5px" /> 同步 | ||||
|         </el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 列表 --> | ||||
|   <ContentWrap> | ||||
|     <el-table v-loading="loading" :data="list"> | ||||
|       <el-table-column label="编号" align="center" prop="id" /> | ||||
|       <el-table-column label="用户标识" align="center" prop="openid" width="260" /> | ||||
|       <el-table-column label="昵称" align="center" prop="nickname" /> | ||||
|       <el-table-column label="备注" align="center" prop="remark" /> | ||||
|       <el-table-column label="标签" align="center" prop="tagIds" width="200"> | ||||
|         <template #default="scope"> | ||||
|           <span v-for="(tagId, index) in scope.row.tagIds" :key="index"> | ||||
|             <el-tag>{{ tagList.find((tag) => tag.tagId === tagId)?.name }} </el-tag>  | ||||
|           </span> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column label="订阅状态" align="center" prop="subscribeStatus"> | ||||
|         <template #default="scope"> | ||||
|           <el-tag v-if="scope.row.subscribeStatus === 0" type="success">已订阅</el-tag> | ||||
|           <el-tag v-else type="danger">未订阅</el-tag> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         label="订阅时间" | ||||
|         align="center" | ||||
|         prop="subscribeTime" | ||||
|         width="180" | ||||
|         :formatter="dateFormatter" | ||||
|       /> | ||||
|       <el-table-column label="操作" align="center"> | ||||
|         <template #default="scope"> | ||||
|           <el-button | ||||
|             type="primary" | ||||
|             link | ||||
|             @click="openForm(scope.row.id)" | ||||
|             v-hasPermi="['mp:user:update']" | ||||
|           > | ||||
|             修改 | ||||
|           </el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|     <!-- 分页 --> | ||||
|     <Pagination | ||||
|       :total="total" | ||||
|       v-model:page="queryParams.pageNo" | ||||
|       v-model:limit="queryParams.pageSize" | ||||
|       @pagination="getList" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| 
 | ||||
|   <!-- 表单弹窗:修改 --> | ||||
|   <UserForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script lang="ts" setup name="MpUser"> | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as MpAccountApi from '@/api/mp/account' | ||||
| import * as MpUserApi from '@/api/mp/user' | ||||
| import * as MpTagApi from '@/api/mp/tag' | ||||
| import UserForm from './UserForm.vue' | ||||
| const message = useMessage() // 消息 | ||||
| 
 | ||||
| const loading = ref(true) // 列表的加载中 | ||||
| const total = ref(0) // 列表的总页数 | ||||
| const list = ref([]) // 列表的数据 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   accountId: null, | ||||
|   openid: null, | ||||
|   nickname: null | ||||
| }) | ||||
| const queryFormRef = ref() // 搜索的表单 | ||||
| const accountList = ref([]) // 公众号账号列表 | ||||
| const tagList = ref([]) // 公众号标签列表 | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   // 如果没有选中公众号账号,则进行提示。 | ||||
|   if (!queryParams.accountId) { | ||||
|     message.error('未选中公众号,无法查询用户') | ||||
|     return false | ||||
|   } | ||||
|   try { | ||||
|     loading.value = true | ||||
|     const data = await MpUserApi.getUserPage(queryParams) | ||||
|     list.value = data.list | ||||
|     total.value = data.total | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1 | ||||
|   getList() | ||||
| } | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   queryFormRef.value.resetFields() | ||||
|   // 默认选中第一个 | ||||
|   if (accountList.value.length > 0) { | ||||
|     queryParams.accountId = accountList.value[0].id | ||||
|   } | ||||
|   handleQuery() | ||||
| } | ||||
| 
 | ||||
| /** 添加/修改操作 */ | ||||
| const formRef = ref() | ||||
| const openForm = (id: number) => { | ||||
|   formRef.value.open(id) | ||||
| } | ||||
| 
 | ||||
| /** 同步标签 */ | ||||
| const handleSync = async () => { | ||||
|   const accountId = queryParams.accountId | ||||
|   try { | ||||
|     await message.confirm('是否确认同步粉丝?') | ||||
|     await MpUserApi.syncUser(accountId) | ||||
|     message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询') | ||||
|     await getList() | ||||
|   } catch {} | ||||
| } | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   // 加载标签 | ||||
|   tagList.value = await MpTagApi.getSimpleTagList() | ||||
| 
 | ||||
|   // 加载账号 | ||||
|   accountList.value = await MpAccountApi.getSimpleAccountList() | ||||
|   if (accountList.value.length > 0) { | ||||
|     queryParams.accountId = accountList.value[0].id | ||||
|   } | ||||
|   await getList() | ||||
| }) | ||||
| </script> | ||||
|  | @ -75,7 +75,7 @@ | |||
|     </template> | ||||
|   </XModal> | ||||
| </template> | ||||
| <script setup lang="ts" name="App"> | ||||
| <script setup lang="ts" name="PayApp"> | ||||
| import type { FormExpose } from '@/components/Form' | ||||
| import { rules, allSchemas } from './app.data' | ||||
| import * as AppApi from '@/api/pay/app' | ||||
|  |  | |||
|  | @ -137,7 +137,7 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <MerchantForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="Merchant"> | ||||
| <script setup lang="ts" name="PayMerchant"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { CommonStatusEnum } from '@/utils/constants' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ | |||
|     </template> | ||||
|   </XModal> | ||||
| </template> | ||||
| <script setup lang="ts" name="Order"> | ||||
| <script setup lang="ts" name="PayOrder"> | ||||
| import { allSchemas } from './order.data' | ||||
| import * as OrderApi from '@/api/pay/order' | ||||
| 
 | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ | |||
|     </template> | ||||
|   </XModal> | ||||
| </template> | ||||
| <script setup lang="ts" name="Refund"> | ||||
| <script setup lang="ts" name="PayRefund"> | ||||
| import { allSchemas } from './refund.data' | ||||
| import * as RefundApi from '@/api/pay/refund' | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <AreaForm ref="formRef" /> | ||||
| </template> | ||||
| <script setup lang="tsx" name="Area"> | ||||
| <script setup lang="tsx" name="SystemArea"> | ||||
| import type { Column } from 'element-plus' | ||||
| import AreaForm from './AreaForm.vue' | ||||
| import * as AreaApi from '@/api/system/area' | ||||
|  |  | |||
|  | @ -103,7 +103,7 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <DeptForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="Dept"> | ||||
| <script setup lang="ts" name="SystemDept"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import { handleTree } from '@/utils/tree' | ||||
|  |  | |||
|  | @ -115,7 +115,7 @@ | |||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <DictDataForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="DictData"> | ||||
| <script setup lang="ts" name="SystemDictData"> | ||||
| import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import download from '@/utils/download' | ||||
|  |  | |||
|  | @ -132,7 +132,7 @@ | |||
|   <DictTypeForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts" name="DictType"> | ||||
| <script setup lang="ts" name="SystemDictType"> | ||||
| import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import * as DictTypeApi from '@/api/system/dict/dict.type' | ||||
|  |  | |||
|  | @ -137,7 +137,7 @@ | |||
|   <ErrorCodeForm ref="formRef" @success="getList" /> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts" name="ErrorCode"> | ||||
| <script setup lang="ts" name="SystemErrorCode"> | ||||
| import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import download from '@/utils/download' | ||||
|  |  | |||
|  | @ -104,7 +104,7 @@ | |||
|   <!-- 表单弹窗:详情 --> | ||||
|   <LoginLogDetail ref="detailRef" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="LoginLog"> | ||||
| <script setup lang="ts" name="SystemLoginLog"> | ||||
| import { DICT_TYPE } from '@/utils/dict' | ||||
| import { dateFormatter } from '@/utils/formatTime' | ||||
| import download from '@/utils/download' | ||||
|  |  | |||
|  | @ -0,0 +1,26 @@ | |||
| <template> | ||||
|   <Dialog title="详情" v-model="dialogVisible"> | ||||
|     <Descriptions :schema="allSchemas.detailSchema" :data="detailData" /> | ||||
|   </Dialog> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import * as MailAccountApi from '@/api/system/mail/account' | ||||
| import { allSchemas } from './account.data' | ||||
| 
 | ||||
| const dialogVisible = ref(false) // 弹窗的是否展示 | ||||
| const detailLoading = ref(false) // 表单的加载中 | ||||
| const detailData = ref() // 详情数据 | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| const open = async (id: number) => { | ||||
|   dialogVisible.value = true | ||||
|   // 设置数据 | ||||
|   detailLoading.value = true | ||||
|   try { | ||||
|     detailData.value = await MailAccountApi.getMailAccount(id) | ||||
|   } finally { | ||||
|     detailLoading.value = false | ||||
|   } | ||||
| } | ||||
| defineExpose({ open }) // 提供 open 方法,用于打开弹窗 | ||||
| </script> | ||||
|  | @ -61,12 +61,16 @@ const crudSchemas = reactive<CrudSchema[]>([ | |||
|     label: '创建时间', | ||||
|     field: 'createTime', | ||||
|     isForm: false, | ||||
|     formatter: dateFormatter | ||||
|     formatter: dateFormatter, | ||||
|     detail: { | ||||
|       dateFormat: 'YYYY-MM-DD HH:mm:ss' | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     label: '操作', | ||||
|     field: 'action', | ||||
|     isForm: false | ||||
|     isForm: false, | ||||
|     isDetail: false | ||||
|   } | ||||
| ]) | ||||
| export const { allSchemas } = useCrudSchemas(crudSchemas) | ||||
|  |  | |||
|  | @ -39,6 +39,14 @@ | |||
|         > | ||||
|           编辑 | ||||
|         </el-button> | ||||
|         <el-button | ||||
|           link | ||||
|           type="primary" | ||||
|           @click="openDetail(row.id)" | ||||
|           v-hasPermi="['system:mail-account:query']" | ||||
|         > | ||||
|           详情 | ||||
|         </el-button> | ||||
|         <el-button | ||||
|           link | ||||
|           type="danger" | ||||
|  | @ -53,11 +61,14 @@ | |||
| 
 | ||||
|   <!-- 表单弹窗:添加/修改 --> | ||||
|   <MailAccountForm ref="formRef" @success="getList" /> | ||||
|   <!-- 详情弹窗 --> | ||||
|   <MailAccountDetail ref="detailRef" /> | ||||
| </template> | ||||
| <script setup lang="ts" name="MailAccount"> | ||||
| <script setup lang="ts" name="SystemMailAccount"> | ||||
| import { allSchemas } from './account.data' | ||||
| import * as MailAccountApi from '@/api/system/mail/account' | ||||
| import MailAccountForm from './MailAccountForm.vue' | ||||
| import MailAccountDetail from './MailAccountDetail.vue' | ||||
| 
 | ||||
| // tableObject:表格的属性对象,可获得分页大小、条数等属性 | ||||
| // tableMethods:表格的操作对象,可进行获得分页、删除记录等操作 | ||||
|  | @ -75,6 +86,12 @@ const openForm = (type: string, id?: number) => { | |||
|   formRef.value.open(type, id) | ||||
| } | ||||
| 
 | ||||
| /** 详情操作 */ | ||||
| const detailRef = ref() | ||||
| const openDetail = (id: number) => { | ||||
|   detailRef.value.open(id) | ||||
| } | ||||
| 
 | ||||
| /** 删除按钮操作 */ | ||||
| const handleDelete = (id: number) => { | ||||
|   tableMethods.delList(id, false) | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	 芋道源码
						芋道源码