提交 333a0caa 作者: 毛细亚

合并分支 'release' 到 'master'

Release

查看合并请求 !6
---
description:
globs:
alwaysApply: false
alwaysApply: true
---
# 项目结构说明
这是一个 H5的移动端页面 这是一个 H5的移动端页面 最大宽度是 380px 最小宽度是 360px 样所以需要用到 css 自适应 弹性伸缩
......@@ -23,7 +23,7 @@ alwaysApply: false
- [public/](mdc:public):静态资源目录,供打包时复制到最终输出。
- [package.json](mdc:package.json):项目依赖和脚本配置。
- [README.md](mdc:README.md):项目说明文档。
如需详细了解某一目录或文件,可参考上述路径。
......@@ -35,6 +35,7 @@ alwaysApply: false
- 使用 element-ui 2.15.6 作为 UI 库
- axios 请求 api
- 基于企业微信 新版本 jssdk 来进行开发 企业微信的变量是 `ww`
- 使用 pnpm 作为包管理工具 安装命令都使用 pnpm
# 开发规范
- 组件命名规范
- Props类型声明 Props 尽量用规范的写法 并且声明默认值
......@@ -45,14 +46,13 @@ alwaysApply: false
- 使用 try/catch 块处理异步操作
- 使用组件化的概念 尽量按照功能切分成多个组件 一个组件的代码不要太多
工程化要求:
- ESLint + Prettier
- 性能优化
- 代码分割
- 懒加载实现
# UI 规范
因为 企业微信侧边栏 宽度有限 而且是一个 H5 移动端的页面 最大宽度是 380px 最小宽度 是 360px 所以尽量使用 自适应的样式来进行开发
因为 企业微信侧边栏 宽度有限 而且是一个 H5 的页面 最大宽度是 360px 用自适应样式来构建页面 不要用超过 360px 的宽度
---
description:
globs:
alwaysApply: false
---
---
description:
globs:
alwaysApply: true
---
---
description:
globs:
alwaysApply: false
---
# 企业微信 SDK 使用指南
## 📋 概述
企业微信初始啊的相关方法已封装到 Vuex `user` 模块中,提供了简洁的 API 和 Promise 支持。
企微微信的发送消息的方法 已封状态 `@/utils/index.js` 的 `sendChatMessage` 方法中 通过 type 参数来区分发送消息的类型
## 1. 企微微信 jssdk 初始化 只有在初始化成功以后才可以调用 企业微信的 jssdk 的相关 api
因为 jssdk 的签名的时候 和 path 有关 path 就是当前页面的 url 地址 所以当路由变化的时候
如果想在当前的路由页面 调用 企微 jssdk 的相关 api 就需要 重新 初始化 企微 jssdk 初始化 方法 在 1.4 中
注意:当前想要调用企微 jssdk 的相关 api 的时候 需要判断企微 jssdk 在当前路由页面 是否已经初始化成功 如果没有 你需要重新初始化
### 1.1 `getWecomSignature` - 获取企业微信签名
```javascript
// 自动获取签名(使用当前页面和缓存的 corp_id)
const signData = await this.getWecomSignature()
// 指定参数获取签名
const signData = await this.getWecomSignature({
corp_id: 'your_corp_id',
path: 'https://your-domain.com/path'
})
```
### 1.2 `registerWecomSDK` - 注册企业微信 SDK
```javascript
// 使用已获取的签名数据注册
const registerResult = await this.registerWecomSDK(signData)
// 或者使用 state 中的签名数据
const registerResult = await this.registerWecomSDK()
```
### 1.3 `initWecom` - 一键初始化(推荐)
```javascript
// 完整初始化(获取签名 + 注册 SDK)
try {
const result = await this.initWecom()
console.log('初始化成功:', result)
// result 包含:{ signData, registerResult, success: true }
} catch (error) {
console.error('初始化失败:', error)
}
```
### 1.4 🚀 使用方法
#### 在组件中使用
```javascript
import { mapActions, mapState } from 'vuex'
export default {
computed: {
...mapState('user', ['isWecomSDKReady', 'signData'])
},
methods: {
// 映射 Vuex actions
...mapActions('user', [
'getWecomSignature',
'registerWecomSDK',
'initWecom'
]),
// 初始化企业微信
async initializeWecom() {
try {
const result = await this.initWecom()
// 注册成功后的操作
this.onWecomReady(result.registerResult)
} catch (error) {
console.error('初始化失败:', error)
this.$message.error('企业微信初始化失败')
}
},
// SDK 准备就绪后的回调
onWecomReady(registerResult) {
console.log('企业微信 SDK 已准备就绪')
// 现在可以安全地使用企业微信 API
this.openEnterpriseChat()
this.getCurExternalContact()
// ... 其他企业微信 API 调用
},
// 检查 SDK 是否已准备就绪
checkSDKReady() {
if (this.isWecomSDKReady) {
console.log('SDK 已准备就绪')
return true
} else {
console.log('SDK 尚未准备就绪')
return false
}
}
},
// 在组件挂载时初始化
async mounted() {
await this.initializeWecom()
}
}
```
#### 在页面中使用
```javascript
// 在 login.vue、quickReply.vue 等页面中
export default {
async created() {
// 替代原有的 getSignature() 调用
await this.initializeWecom()
},
methods: {
...mapActions('user', ['initWecom']),
async initializeWecom() {
try {
const result = await this.initWecom()
console.log('企业微信初始化成功')
// 执行需要在 SDK 注册成功后的操作
this.handleWecomReady()
} catch (error) {
console.error('企业微信初始化失败:', error)
}
},
handleWecomReady() {
// 原来在 onAgentConfigSuccess 中的逻辑
this.getCurExternalContact()
// ... 其他操作
}
}
}
```
## 📊 状态管理
新增的 state:
```javascript
// user store 中的状态
{
signData: {}, // 企业微信签名数据
isWecomSDKReady: false // SDK 是否已准备就绪
}
```
## ✅ 优势
1. **统一管理**:所有企业微信相关逻辑集中在 Vuex 中
2. **Promise 支持**:注册成功/失败都会返回 Promise
3. **状态追踪**:可以通过 `isWecomSDKReady` 检查 SDK 状态
4. **错误处理**:统一的错误处理机制
5. **复用性**:多个组件可以共享同一套初始化逻辑
## 🎉 示例项目
参考 `skillPersonal.vue` 和 `quickReply.vue` 和 `sendGame.vue`中的实现方式。
# 2. 调用 企微微信发送消息接口 `ww.sendChatMessage`
## 2.1 在企业微信 jssdk 初始化成功后 我们就可以使用 jssdk api 了 当使用 `ww.sendChatMessage` api 时 我们简单封装了一下 这个 api的使用方法
```js
import {sendChatMessage} form '@/utils/index,js'
```
直接使用即可 以下是文档的具体说明
### 2.2 企业微信发送消息 API 参数文档
本文档描述了 `sendChatMessage` 函数的使用方法,该函数封装了企业微信 JSSDK 的 `ww.sendChatMessage` API,用于向客户发送各种类型的消息。
### 2.3 函数签名
```javascript
/**
* 向企业微信客户发送消息
* @param {string|object} content - 消息内容,根据消息类型不同而不同
* @param {string} type - 消息类型,支持 'text'、'link'、'image'、'miniprogram'、'video'、'file'
* @return {object} messageInfo - 消息发送相关信息
*/
async function sendChatMessage(content, type) { ... }
```
### 2.4 使用方法
```javascript
import { sendChatMessage } from '@/utils/index.js'
// 发送文本消息
await sendChatMessage('这是一条文本消息', 'text')
// 发送其他类型消息
// ...
```
### 2.5 支持的消息类型
#### 2.5.1 文本消息 (text)
```javascript
// content 参数为字符串
await sendChatMessage('这是一条文本消息', 'text')
```
#### 2.5.2 图文消息 (link)
```javascript
// content 参数为对象数组
await sendChatMessage({
articles: [
{
title: '图文消息标题',
desc: '图文消息描述',
url: 'https://example.com/news',
picurl: 'https://example.com/image.jpg'
}
// 可以有多个图文项
]
}, 'link')
```
#### 2.5.3 图片消息 (image)
```javascript
// content 参数为图片URL
await sendChatMessage('https://example.com/image.jpg', 'image')
```
#### 2.5.4 小程序消息 (miniprogram)
```javascript
// content 参数为对象
await sendChatMessage({
appid: 'wx123456789abcdef', // 小程序的appid
title: '小程序消息标题', // 小程序消息的标题
imgurl: 'https://example.com/image.jpg', // 小程序消息的图片
page: 'pages/index/index' // 小程序的页面路径
}, 'miniprogram')
```
#### 2.5.5 视频消息 (video)
```javascript
// content 参数为视频URL
await sendChatMessage('https://example.com/video.mp4', 'video')
```
### 2.5.6 文件消息 (file)
```javascript
// content 参数为文件URL
await sendChatMessage('https://example.com/document.pdf', 'file')
```
## 返回值说明
函数返回一个包含以下字段的对象:
```javascript
{
frontend_message_id: '唯一消息ID',
session_id: '会话ID',
message: content, // 原始消息内容
message_type: type, // 消息类型
timestamp: 1698123456789, // 发送时间戳
success: true/false, // 是否发送成功
response: {}, // 成功回调的响应数据
error: {} // 失败回调的错误数据
}
```
## 注意事项
1. 对于媒体类型消息(图片、视频、文件),系统会自动获取媒体ID
2. 媒体文件大小有限制,请参考企业微信开发文档
3. 所有消息发送都是异步的,返回值中的 `success` 字段仅表示请求是否成功发出
4. 在调用此函数前,确保企业微信 JSSDK 已经初始化成功
## 错误处理
函数内部会处理常见错误,并通过 Element UI 的 Message 组件显示错误提示。对于媒体ID获取失败等错误,会在返回值的 `error` 字段中提供详细信息。
## 相关依赖
- `@wecom/jssdk`:企业微信官方 JSSDK
- `@/api/works`:用于获取媒体ID的API
- `element-ui`:用于显示错误提示
......@@ -32,6 +32,7 @@
- 使用 element-ui 2.15.6 作为 UI 库
- axios 请求 api
- 基于企业微信 新版本 jssdk 来进行开发 企业微信的变量是 `ww`
- 使用 pnpm 作为包管理工具 安装命令都使用 pnpm
## 开发规范
- 组件命名规范
- Props类型声明 Props 尽量用规范的写法 并且声明默认值
......
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>company_app</title><script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script><script defer="defer" src="static/js/chunk-vendors.c5338b55.js"></script><script defer="defer" src="static/js/app.493a4164.js"></script><link href="static/css/chunk-vendors.34a02360.css" rel="stylesheet"><link href="static/css/app.da449827.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but company_app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
\ No newline at end of file
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><meta http-equiv="pragma" content="no-cache"><meta http-equiv="cache-control" content="no-cache"><meta http-equiv="expires" content="0"><meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9"/><title>company_app</title><script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script><script defer="defer" src="static/js/chunk-vendors.72a90e47.js"></script><script defer="defer" src="static/js/app.003145b5.js"></script><link href="static/css/chunk-vendors.8e901099.css" rel="stylesheet"><link href="static/css/app.8a15faf7.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but company_app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
\ No newline at end of file
# 1.侧边栏1.2版本的需求文档
# 1.侧边栏1.2版本的需求文档
需要用到的接口引入地址如下
```javascript
import {getClientStatus,remarkSessionIntelTag,finishRest,client_session_rest,checkSingleAgree,checkUserPermit,sendComment} from '@/api/user.js'
```
`getClientStatus`:获取用户的休息状态
`finishRest`:结束休息
`client_session_rest`:开始休息
`checkSingleAgree`:客户是否同意聊天内容存档
`checkUserPermit`:客服号是否开启会话会话内容存档
`remarkSessionIntelTag`:同步智能标签
页面原型图 是 [原型图片]('./images/侧边栏1.2.png')
## 1.1 新增客服休息状态
`src/views/userInfo/components/Info.vue` 新增一个休息中 和 开始休息的按钮状态
在 Info.vue 初始化的时候 调用以下接口 获取初始数据
1.获取客服的休息状态 通过接口 `getClientStatus` 获取用户的休息状态 不需要传入任何参数 返回 的数据 res.data.client_online_status
`client_online_status:` online上线 offline下线 rest休息中 保存在 vuex 中的 user 模块中 以后会用到的
2.调用 remarkSessionIntelTag 同步智能标签 同步智能标签的接口 `remarkSessionIntelTag` 传入参数 `corp_id` `external_userid` `userid` 从 vuex 中 获取参数 不需要保存 接口返回的值 只同步一下标签接口
3. 调用 `checkSingleAgree` 接口 获取用户 是否同步开启聊天内容 存档 传入参数 `external_userid` `userid` 返回的值在 Info.vue 中保存起来 以后会用到的 返回的值是 res.data.agree_status Agreen同意 Disagree不同意 在页面显示 当前微信用户未(已)开启会话内容存档
4. 调用 `checkUserPermit` 接口 获取客服号是否开启获取会话内容存档 传入参数 `userid` 返回的值在 Info.vue 中保存起来 以后会用到的 返回的值是 res.data.has_permit true 有 权限 false 无权限 在页面显示 当前客服号未(已)授权会话内容存档
上面的接口 中 需要在 created 的时候调用 进入页面的时候 就可以调用
5.获取客服的休息状态后 如果是休息中 显示结束休息 按钮 此时不能下线 必须先结束休息 结束休息 接口 `finishRest`
如果是在线状态 显示开始休息按钮 开始休息按钮 鼠标悬浮 显示 文字 `午休或者临时有事可点击休息` 点击 调用接口 `client_session_rest` 开始休息状态
6. `sendComment` 发送评价接口: 在休息按钮旁边 新增一个发送评价按钮 点击发送评价 调用 sendComment 接口 传入 参数 传入参数 `corp_id` `external_userid` `userid` 从 vuex 中 获取参数 返回的数据中 res.data.news 是发送评价的内容
6.1. 发送评价需要调用 企业微信的 jssdk 中 的 `ww.sendChatMessage` 在调用 企微页面的 jssdk 之前 需要 先签名授权当前的 url 地址 所以需要先进行以下操作
6.2. 先初始化 企业微信的 jssdk 之前已经封装过了 调用代码如下
```javascript
created() {
this.initializeWecom()
},
methods: {
...mapActions('user', ['initWecom']),
async initializeWecom() {
try {
console.log('🚀 开始初始化企业微信 SDK')
const result = await this.initWecom()
console.log('✅ 企业微信 SDK 初始化成功', result)
} catch (error) {
console.error('❌ 企业微信 SDK 初始化失败:', error)
}
},
}
```
6.3. 企业微信的 jssdk 初始化成功后 可以开始 调用 `ww.sendChatMessage` 这个方法的经过二次封载 在 `src/utils/index.js` 中 代码如下
```javascript
improt {sendChatMessage} from '@/utils/index.js'
sendChatMessage(res.data.news , 'news')
```
拿到之前 发送评价接口返回的数据 就可以发送评价了
\ No newline at end of file
# 客服休息状态功能文档
本文档描述了客服休息状态功能的实现方法和使用说明,该功能支持客服在忙碌或休息时暂时停止接收新消息。
## 功能概述
客服休息状态功能允许客服在午休或临时有事时设置自己为"休息中"状态,以便合理安排工作时间。同时,还提供了发送评价功能,方便客服向客户发送评价请求。
## 相关API
### 1. 获取客服休息状态
```javascript
import { getClientStatus } from '@/api/user.js'
// 获取客服休息状态
const response = await getClientStatus()
if (response.status_code === 1) {
const status = response.data.client_online_status
// status 可能的值:
// - online: 在线
// - offline: 离线
// - rest: 休息中
}
```
### 2. 开始休息
```javascript
import { client_session_rest } from '@/api/user.js'
// 开始休息
const response = await client_session_rest()
if (response.status_code === 1) {
// 休息开始成功
// 可以更新界面显示为"休息中"状态
}
```
### 3. 结束休息
```javascript
import { finishRest } from '@/api/user.js'
// 结束休息
const response = await finishRest()
if (response.status_code === 1) {
// 休息结束成功
// 可以更新界面显示为"在线"状态
}
```
### 4. 发送评价
```javascript
import { sendComment } from '@/api/user.js'
import { sendChatMessage } from '@/utils/index.js'
// 发送评价
const response = await sendComment({
corp_id: '企业ID',
external_userid: '外部联系人ID',
userid: '客服ID'
})
if (response.status_code === 1 && response.data.news) {
// 使用企业微信JSSDK发送评价
const result = await sendChatMessage(response.data.news, 'link')
if (result.success) {
// 评价发送成功
}
}
```
## 会话内容存档相关API
### 1. 检查客户是否同意聊天内容存档
```javascript
import { checkSingleAgree } from '@/api/user.js'
// 检查客户是否同意聊天内容存档
const response = await checkSingleAgree({
external_userid: '外部联系人ID',
userid: '客服ID'
})
if (response.status_code === 1) {
const agreeStatus = response.data.agree_status
// agreeStatus 可能的值:
// - Agreen: 已同意
// - Disagree: 未同意
}
```
### 2. 检查客服号是否开启会话内容存档
```javascript
import { checkUserPermit } from '@/api/user.js'
// 检查客服号是否开启会话内容存档
const response = await checkUserPermit({
userid: '客服ID'
})
if (response.status_code === 1) {
const hasPermit = response.data.has_permit
// hasPermit: true 已授权, false 未授权
}
```
### 3. 同步智能标签
```javascript
import { remarkSessionIntelTag } from '@/api/user.js'
// 同步智能标签
await remarkSessionIntelTag({
corp_id: '企业ID',
external_userid: '外部联系人ID',
userid: '客服ID'
})
```
## 使用示例
### 在Vue组件中整合所有功能
```javascript
import { mapState, mapMutations, mapActions } from 'vuex'
import {
getClientStatus,
remarkSessionIntelTag,
finishRest,
client_session_rest,
checkSingleAgree,
checkUserPermit,
sendComment
} from '@/api/user.js'
import { sendChatMessage } from '@/utils/index.js'
export default {
data() {
return {
// 相关状态
agreeStatus: '', // 用户是否同意聊天内容存档
hasPermit: false // 客服号是否开启会话内容存档权限
}
},
computed: {
...mapState('user', ['client_online_status', 'corp_id', 'external_userid', 'userid']),
// 状态文本转换
clientStatusText() {
const statusMap = {
'online': '在线',
'offline': '离线',
'rest': '休息中'
}
return statusMap[this.client_online_status] || '未知'
}
},
created() {
// 初始化企业微信SDK
this.initializeWecom()
// 获取各种状态
this.getInitialData()
},
methods: {
...mapActions('user', ['initWecom']),
// 获取初始数据
async getInitialData() {
// 实现获取状态逻辑
},
// 开始休息
async handleStartRest() {
// 实现开始休息逻辑
},
// 结束休息
async handleFinishRest() {
// 实现结束休息逻辑
},
// 发送评价
async handleSendComment() {
// 实现发送评价逻辑
}
}
}
```
## 界面展示
客服状态显示和按钮应该包含以下元素:
1. 当前状态显示:显示客服当前是"在线"、"离线"还是"休息中"
2. 休息相关按钮:
- 在线状态下显示"开始休息"按钮
- 休息中状态显示"结束休息"按钮
3. 发送评价按钮:用于向客户发送评价请求
4. 会话内容存档状态显示:
- 显示客户是否已开启会话内容存档
- 显示客服号是否已授权会话内容存档
## 注意事项
1. 客服在"休息中"状态时不能直接下线,必须先结束休息
2. 开始休息按钮悬停时应显示提示文字:"午休或者临时有事可点击休息"
3. 使用企业微信JSSDK前需确保已初始化成功
4. 所有状态变更应同步更新到Vuex store,以便在多个组件中共享
\ No newline at end of file
# Stagewise 工具栏集成文档
## 概述
Stagewise 是一个浏览器工具栏,可以连接前端 UI 到代码编辑器中的 AI 代理。它允许开发人员在网页应用中选择元素,添加注释,并让 AI 代理根据上下文进行代码修改。
## 集成方式
Stagewise 工具栏已经集成到项目中,并且只在开发环境下启用。集成代码位于 `src/main.js` 文件中:
```javascript
// 开发环境下初始化 stagewise 工具栏
if (process.env.NODE_ENV === 'development') {
import('@stagewise/toolbar').then(({ initToolbar }) => {
const stagewiseConfig = {
plugins: []
};
initToolbar(stagewiseConfig);
}).catch(err => {
console.error('Failed to initialize stagewise toolbar:', err);
});
}
```
## 使用方法
1. 在开发环境下启动应用(`pnpm dev` 或相应的开发命令)
2. 打开浏览器访问应用
3. Stagewise 工具栏将自动显示在浏览器中
4. 使用工具栏可以:
- 选择页面上的元素
- 添加注释和改进建议
- 将这些建议发送到您的代码编辑器中的 AI 代理
## 连接到 IDE
为了使 Stagewise 工具栏能够与您的代码编辑器通信,您需要:
1. 打开您的 IDE(Cursor、Windsurf 等)
2. 安装 Stagewise 扩展
- 在 Cursor 中,搜索扩展市场中的 "stagewise"
- 在 VSCode 中,访问扩展市场并搜索 "stagewise"
3. 确保扩展已激活
4. 在浏览器中的 Stagewise 工具栏上点击"刷新"或"重新连接"按钮
5. 如果成功连接,工具栏将显示已连接状态
## 注意事项
- Stagewise 工具栏仅在开发环境中可用,不会包含在生产构建中
- 如果需要配置更多插件,可以修改 `stagewiseConfig` 对象中的 `plugins` 数组
- 更多信息请参考 [Stagewise 官方文档](https://stagewise.ai)
- IDE扩展和浏览器工具栏必须同时运行才能正常工作
## 故障排除
如果 Stagewise 工具栏没有正确显示,请检查:
1. 确认应用是在开发模式下运行
2. 检查浏览器控制台是否有任何错误消息
3. 确认 `@stagewise/toolbar` 包已正确安装
4. 尝试清除浏览器缓存后重新加载页面
如果工具栏显示但无法连接到IDE:
1. 确认IDE中的Stagewise扩展已安装并激活
2. 确认您的IDE和浏览器都在运行
3. 在工具栏中点击"刷新"按钮
4. 尝试重启IDE和浏览器
5. 检查是否有防火墙或网络设置阻止了连接
\ No newline at end of file
# 企业微信发送消息 API 参数文档
本文档描述了 `sendChatMessage` 函数的使用方法,该函数封装了企业微信 JSSDK 的 `ww.sendChatMessage` API,用于向客户发送各种类型的消息。
## 函数签名
```javascript
/**
* 向企业微信客户发送消息
* @param {string|object} content - 消息内容,根据消息类型不同而不同
* @param {string} type - 消息类型,支持 'text'、'link'、'image'、'miniprogram'、'video'、'file'
* @return {object} messageInfo - 消息发送相关信息
*/
async function sendChatMessage(content, type) { ... }
```
## 使用方法
```javascript
import { sendChatMessage } from '@/utils/index.js'
// 发送文本消息
await sendChatMessage('这是一条文本消息', 'text')
// 发送其他类型消息
// ...
```
## 支持的消息类型
### 1. 文本消息 (text)
```javascript
// content 参数为字符串
await sendChatMessage('这是一条文本消息', 'text')
```
### 2. 图文消息 (link)
```javascript
// content 参数为对象数组
await sendChatMessage({
articles: [
{
title: '图文消息标题',
desc: '图文消息描述',
url: 'https://example.com/news',
picurl: 'https://example.com/image.jpg'
}
// 可以有多个图文项
]
}, 'link')
```
### 3. 图片消息 (image)
```javascript
// content 参数为图片URL
await sendChatMessage('https://example.com/image.jpg', 'image')
```
### 4. 小程序消息 (miniprogram)
```javascript
// content 参数为对象
await sendChatMessage({
appid: 'wx123456789abcdef', // 小程序的appid
title: '小程序消息标题', // 小程序消息的标题
imgurl: 'https://example.com/image.jpg', // 小程序消息的图片
page: 'pages/index/index' // 小程序的页面路径
}, 'miniprogram')
```
### 5. 视频消息 (video)
```javascript
// content 参数为视频URL
await sendChatMessage('https://example.com/video.mp4', 'video')
```
### 6. 文件消息 (file)
```javascript
// content 参数为文件URL
await sendChatMessage('https://example.com/document.pdf', 'file')
```
## 返回值说明
函数返回一个包含以下字段的对象:
```javascript
{
frontend_message_id: '唯一消息ID',
session_id: '会话ID',
message: content, // 原始消息内容
message_type: type, // 消息类型
timestamp: 1698123456789, // 发送时间戳
success: true/false, // 是否发送成功
response: {}, // 成功回调的响应数据
error: {} // 失败回调的错误数据
}
```
## 注意事项
1. 对于媒体类型消息(图片、视频、文件),系统会自动获取媒体ID
2. 媒体文件大小有限制,请参考企业微信开发文档
3. 所有消息发送都是异步的,返回值中的 `success` 字段仅表示请求是否成功发出
4. 在调用此函数前,确保企业微信 JSSDK 已经初始化成功
## 错误处理
函数内部会处理常见错误,并通过 Element UI 的 Message 组件显示错误提示。对于媒体ID获取失败等错误,会在返回值的 `error` 字段中提供详细信息。
## 相关依赖
- `@wecom/jssdk`:企业微信官方 JSSDK
- `@/api/works`:用于获取媒体ID的API
- `element-ui`:用于显示错误提示
\ No newline at end of file
# 企业微信客户端侧边栏应用开发
......@@ -44,3 +44,4 @@ import {getParams} from '@/utils/index.js'
本项目中会经历两次页面回调 一次是企微的授权验证回调 一次是 钉钉登录成功的回调 回调页面必须在首页完成 因为这个回调地址已经配置过了 钉钉回调成功后 页面 url 上会带一个 type=ding 的参数 需要根据这个参数来判断 是钉钉回调还是企微回调 如果是钉钉的回调 表明 微信授权的逻辑已经完成了 可以直接去 首页了
---
---
description:
globs:
alwaysApply: false
---
# 企业微信 SDK 使用指南
## 📋 概述
企业微信初始啊的相关方法已封装到 Vuex `user` 模块中,提供了简洁的 API 和 Promise 支持。
企微微信的发送消息的方法 已封状态 `@/utils/index.js``sendChatMessage` 方法中 通过 type 参数来区分发送消息的类型
## 1. 企微微信 jssdk 初始化 只有在初始化成功以后才可以调用 企业微信的 jssdk 的相关 api
因为 jssdk 的签名的时候 和 path 有关 path 就是当前页面的 url 地址 所以当路由变化的时候
如果想在当前的路由页面 调用 企微 jssdk 的相关 api 就需要 重新 初始化 企微 jssdk 初始化 方法 在 1.4 中
注意:当前想要调用企微 jssdk 的相关 api 的时候 需要判断企微 jssdk 在当前路由页面 是否已经初始化成功 如果没有 你需要重新初始化
### 1.1 `getWecomSignature` - 获取企业微信签名
```javascript
// 自动获取签名(使用当前页面和缓存的 corp_id)
const signData = await this.getWecomSignature()
// 指定参数获取签名
const signData = await this.getWecomSignature({
corp_id: 'your_corp_id',
path: 'https://your-domain.com/path'
})
```
### 1.2 `registerWecomSDK` - 注册企业微信 SDK
```javascript
// 使用已获取的签名数据注册
const registerResult = await this.registerWecomSDK(signData)
// 或者使用 state 中的签名数据
const registerResult = await this.registerWecomSDK()
```
### 1.3 `initWecom` - 一键初始化(推荐)
```javascript
// 完整初始化(获取签名 + 注册 SDK)
try {
const result = await this.initWecom()
console.log('初始化成功:', result)
// result 包含:{ signData, registerResult, success: true }
} catch (error) {
console.error('初始化失败:', error)
}
```
### 1.4 🚀 使用方法
#### 在组件中使用
```javascript
import { mapActions, mapState } from 'vuex'
export default {
computed: {
...mapState('user', ['isWecomSDKReady', 'signData'])
},
methods: {
// 映射 Vuex actions
...mapActions('user', [
'getWecomSignature',
'registerWecomSDK',
'initWecom'
]),
// 初始化企业微信
async initializeWecom() {
try {
const result = await this.initWecom()
// 注册成功后的操作
this.onWecomReady(result.registerResult)
} catch (error) {
console.error('初始化失败:', error)
this.$message.error('企业微信初始化失败')
}
},
// SDK 准备就绪后的回调
onWecomReady(registerResult) {
console.log('企业微信 SDK 已准备就绪')
// 现在可以安全地使用企业微信 API
this.openEnterpriseChat()
this.getCurExternalContact()
// ... 其他企业微信 API 调用
},
// 检查 SDK 是否已准备就绪
checkSDKReady() {
if (this.isWecomSDKReady) {
console.log('SDK 已准备就绪')
return true
} else {
console.log('SDK 尚未准备就绪')
return false
}
}
},
// 在组件挂载时初始化
async mounted() {
await this.initializeWecom()
}
}
```
#### 在页面中使用
```javascript
// 在 login.vue、quickReply.vue 等页面中
export default {
async created() {
// 替代原有的 getSignature() 调用
await this.initializeWecom()
},
methods: {
...mapActions('user', ['initWecom']),
async initializeWecom() {
try {
const result = await this.initWecom()
console.log('企业微信初始化成功')
// 执行需要在 SDK 注册成功后的操作
this.handleWecomReady()
} catch (error) {
console.error('企业微信初始化失败:', error)
}
},
handleWecomReady() {
// 原来在 onAgentConfigSuccess 中的逻辑
this.getCurExternalContact()
// ... 其他操作
}
}
}
```
## 📊 状态管理
新增的 state:
```javascript
// user store 中的状态
{
signData: {}, // 企业微信签名数据
isWecomSDKReady: false // SDK 是否已准备就绪
}
```
## ✅ 优势
1. **统一管理**:所有企业微信相关逻辑集中在 Vuex 中
2. **Promise 支持**:注册成功/失败都会返回 Promise
3. **状态追踪**:可以通过 `isWecomSDKReady` 检查 SDK 状态
4. **错误处理**:统一的错误处理机制
5. **复用性**:多个组件可以共享同一套初始化逻辑
## 🎉 示例项目
参考 `skillPersonal.vue``quickReply.vue``sendGame.vue`中的实现方式。
# 2. 调用 企微微信发送消息接口 `ww.sendChatMessage`
## 2.1 在企业微信 jssdk 初始化成功后
{
"recommendations": ["stagewise.stagewise-vscode-extension"]
}
\ No newline at end of file
......@@ -9,6 +9,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@aliyun-sls/web-track-browser": "^0.0.3",
"@wecom/jssdk": "^2.3.1",
"axios": "^1.9.0",
"babel-plugin-component": "^1.1.1",
......@@ -19,11 +20,13 @@
"cos-js-sdk-v5": "^1.10.1",
"dingtalk-jsapi": "^3.1.0",
"element-ui": "^2.15.14",
"html2canvas": "^1.4.1",
"js-cookie": "^3.0.5",
"lib-flexible": "^0.3.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"postcss-plugin-px2rem": "^0.8.1",
"qrcode.vue": "^1.7.0",
"sass": "^1.89.0",
"sass-loader": "^16.0.5",
"vconsole": "^3.15.1",
......@@ -33,6 +36,9 @@
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@stagewise-plugins/vue": "^0.4.6",
"@stagewise/toolbar": "^0.4.8",
"@stagewise/toolbar-vue": "^0.4.8",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
......
......@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@aliyun-sls/web-track-browser':
specifier: ^0.0.3
version: 0.0.3
'@wecom/jssdk':
specifier: ^2.3.1
version: 2.3.1
......@@ -38,6 +41,9 @@ importers:
element-ui:
specifier: ^2.15.14
version: 2.15.14(vue@2.7.16)
html2canvas:
specifier: ^1.4.1
version: 1.4.1
js-cookie:
specifier: ^3.0.5
version: 3.0.5
......@@ -53,6 +59,9 @@ importers:
postcss-plugin-px2rem:
specifier: ^0.8.1
version: 0.8.1
qrcode.vue:
specifier: ^1.7.0
version: 1.7.0(vue@2.7.16)
sass:
specifier: ^1.89.0
version: 1.89.0
......@@ -75,6 +84,15 @@ importers:
'@babel/core':
specifier: ^7.12.16
version: 7.27.1
'@stagewise-plugins/vue':
specifier: ^0.4.6
version: 0.4.6(@stagewise/toolbar@0.4.8)
'@stagewise/toolbar':
specifier: ^0.4.8
version: 0.4.8
'@stagewise/toolbar-vue':
specifier: ^0.4.8
version: 0.4.8(vue@2.7.16)
'@vue/cli-plugin-babel':
specifier: ~5.0.0
version: 5.0.8(@vue/cli-service@5.0.8(@vue/compiler-sfc@3.5.14)(lodash@4.17.21)(sass-loader@16.0.5(node-sass@4.14.1)(sass@1.89.0)(webpack@5.99.8))(vue-template-compiler@2.7.16)(vue@2.7.16)(webpack-sources@3.2.3))(core-js@3.42.0)(vue@2.7.16)
......@@ -100,6 +118,9 @@ packages:
resolution: {integrity: sha512-7s0VcTwiK/0tNOVdSX9FWMeFdOEcsAOz9HesBldXxFMaGvIak7KC2z9tV9EgsQXn6KUsWsfIkViMNuIo0GoZDQ==}
engines: {node: 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22}
'@aliyun-sls/web-track-browser@0.0.3':
resolution: {integrity: sha512-RYJaZN2A8TKTHsem29B/8IRvjBk4ow0IjrzsTlEfxJtUoRIgtKpI6pEgvtI2LKUqN1/vEz6WzzFA+zttMSN3Fw==}
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
......@@ -791,6 +812,19 @@ packages:
'@soda/get-current-script@1.0.2':
resolution: {integrity: sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==}
'@stagewise-plugins/vue@0.4.6':
resolution: {integrity: sha512-Y/cdDLXDN2cusvpmFYxbQT1DEW1fYzoFjmsnXBth52sSLYNc83XXMTXt2kvYSGWOW+ZxM1Dj+T4eE9bY8b/QSA==}
peerDependencies:
'@stagewise/toolbar': 0.4.8
'@stagewise/toolbar-vue@0.4.8':
resolution: {integrity: sha512-Zvxz59apepocu2cOD8UqUWIF7fIABGr+DNDSmz6Kp2nf1APNmfiK3QJ52rI2PfVDG9jaSE56EfYUA0t1xFJLYw==}
peerDependencies:
vue: '>=3.0.0'
'@stagewise/toolbar@0.4.8':
resolution: {integrity: sha512-0ByvC4hYdHHf3rK5M+xSR9mipHYr8naNn2OgDBtv4DE0SoSCr08KfQtZ6VpsBNbOW/Mh1Y4c/AoWcyCTOc2ocA==}
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
......@@ -1330,6 +1364,10 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
......@@ -1403,6 +1441,9 @@ packages:
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
......@@ -1877,6 +1918,9 @@ packages:
peerDependencies:
postcss: ^8.0.9
css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
css-loader@6.11.0:
resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==}
engines: {node: '>= 12.13.0'}
......@@ -2814,6 +2858,10 @@ packages:
webpack:
optional: true
html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
htmlparser2@6.1.0:
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
......@@ -4242,6 +4290,11 @@ packages:
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
qrcode.vue@1.7.0:
resolution: {integrity: sha512-R7t6Y3fDDtcU7L4rtqwGUDP9xD64gJhIwpfjhRCTKmBoYF6SS49PIJHRJ048cse6OI7iwTwgyy2C46N9Ygoc6g==}
peerDependencies:
vue: ^2.0.0
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
......@@ -4893,6 +4946,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
......@@ -5092,6 +5148,9 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
uuid@3.4.0:
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
......@@ -5390,6 +5449,8 @@ snapshots:
event-pubsub: 4.3.0
js-message: 1.0.7
'@aliyun-sls/web-track-browser@0.0.3': {}
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.8
......@@ -6225,6 +6286,17 @@ snapshots:
'@soda/get-current-script@1.0.2': {}
'@stagewise-plugins/vue@0.4.6(@stagewise/toolbar@0.4.8)':
dependencies:
'@stagewise/toolbar': 0.4.8
'@stagewise/toolbar-vue@0.4.8(vue@2.7.16)':
dependencies:
'@stagewise/toolbar': 0.4.8
vue: 2.7.16
'@stagewise/toolbar@0.4.8': {}
'@trysound/sax@0.2.0': {}
'@types/body-parser@1.19.5':
......@@ -7127,6 +7199,8 @@ snapshots:
balanced-match@1.0.2: {}
base64-arraybuffer@1.0.2: {}
base64-js@1.5.1: {}
batch@0.6.1: {}
......@@ -7265,6 +7339,12 @@ snapshots:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
optional: true
braces@3.0.3:
dependencies:
fill-range: 7.1.1
......@@ -7635,6 +7715,10 @@ snapshots:
dependencies:
postcss: 8.5.3
css-line-break@2.1.0:
dependencies:
utrie: 1.0.2
css-loader@6.11.0(webpack@5.99.8):
dependencies:
icss-utils: 5.1.0(postcss@8.5.3)
......@@ -8626,7 +8710,7 @@ snapshots:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.2
minimatch: 3.0.8
once: 1.4.0
path-is-absolute: 1.0.1
optional: true
......@@ -8827,6 +8911,11 @@ snapshots:
optionalDependencies:
webpack: 5.99.8
html2canvas@1.4.1:
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
htmlparser2@6.1.0:
dependencies:
domelementtype: 2.3.0
......@@ -9557,7 +9646,7 @@ snapshots:
minimatch@3.0.8:
dependencies:
brace-expansion: 1.1.11
brace-expansion: 1.1.12
optional: true
minimatch@3.1.2:
......@@ -10309,6 +10398,10 @@ snapshots:
q@1.5.1:
optional: true
qrcode.vue@1.7.0(vue@2.7.16):
dependencies:
vue: 2.7.16
qs@6.13.0:
dependencies:
side-channel: 1.1.0
......@@ -11135,6 +11228,10 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
text-segmentation@1.0.3:
dependencies:
utrie: 1.0.2
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
......@@ -11356,6 +11453,10 @@ snapshots:
utils-merge@1.0.1: {}
utrie@1.0.2:
dependencies:
base64-arraybuffer: 1.0.2
uuid@3.4.0:
optional: true
......@@ -11696,7 +11797,7 @@ snapshots:
wide-align@1.1.5:
dependencies:
string-width: 4.2.3
string-width: 1.0.2
optional: true
wildcard@2.0.1: {}
......
......@@ -5,6 +5,13 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!-- HTTP 1.1 -->
<meta http-equiv="pragma" content="no-cache">
<!-- HTTP 1.0 -->
<meta http-equiv="cache-control" content="no-cache">
<!-- Prevent caching at the proxy server -->
<meta http-equiv="expires" content="0">
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<title><%= htmlWebpackPlugin.options.title %></title>
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,shrink-to-fit=no,user-scalable=no"> -->
<script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
......
......@@ -3,12 +3,35 @@
<Debug />
<div class="mobile-menu-bar" v-if="token && external_userid && showMemberId">
<!-- 临时调试信息 -->
<el-menu :default-active="selectedPath" mode="horizontal" class="mobile-el-menu" background-color="#fff" router
@select="handleSelect">
<div class="menu-container">
<el-menu
:default-active="selectedPath"
mode="horizontal"
class="mobile-el-menu"
:class="{ 'collapsed': !isMenuExpanded && shouldShowToggle }"
background-color="#fff"
router
@select="handleSelect"
ref="menuRef"
>
<el-menu-item v-for="item in menuList" :key="item.path" :index="item.path" class="mobile-menu-item">
{{ item.label }}
</el-menu-item>
</el-menu>
<!-- 展开收起按钮 -->
<el-button
type="text"
size="mini"
v-if="shouldShowToggle"
class="menu-toggle-btn"
@click="toggleMenu"
>
<i :class="isMenuExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
<span>{{ isMenuExpanded ? '收起' : '展开' }}</span>
</el-button>
</div>
<!-- 绑定的 w 账号 -->
<bindUserList />
</div>
......@@ -34,24 +57,49 @@ export default {
return {
menuList: [
{
label: '客户信息',
label: '客户资料',
path: '/userInfo'
},
// {
// label: '快捷回复',
// path: '/quickReply'
// label: '角色信息',
// path: '/roleInfo'
// },
// {
// label: '礼包记录',
// path: '/giftRecord'
// label: '订单信息',
// path: '/orderList'
// },
{
label: '快捷回复',
path: '/quickReply'
},
// {
// label: '申请记录',
// path: '/applyRecord'
// label: '违规记录',
// path: '/violationRecord'
// },
{
label: '礼包记录',
path: '/giftRecord'
},
// {
// label: '快捷发送',
// path: '/quickSend'
// label: '任务记录',
// path: '/taskRecord'
// },
{
label: '申请记录',
path: '/applyRecord'
},
{
label: '通讯录',
path: '/addressBook'
},
{
label: '快捷发送',
path: '/quickSendGame'
},
// {
// label: '任务列表',
// path: '/taskList'
// },
// {
// label: '通讯录',
......@@ -60,6 +108,8 @@ export default {
],
selectedPath: '/userInfo',
showMemberId: false,
isMenuExpanded: false, // 菜单展开状态
shouldShowToggle: false, // 是否显示展开收起按钮
}
},
computed: {
......@@ -84,6 +134,8 @@ export default {
console.log('external_userid 已设置:', newVal, window.location.href, this.token, Cookies.get('token'))
// 强制更新组件
this.$forceUpdate()
// 检查是否需要显示展开收起按钮
this.checkMenuOverflow()
})
}
},
......@@ -99,6 +151,12 @@ export default {
},
mounted() {
this.initVuexValue()
// 页面刷新时从 Cookie 恢复 token 到 store
const cookieToken = Cookies.get('token')
if (cookieToken && !this.token) {
this.set_token(cookieToken)
console.log('从 Cookie 恢复 token:', cookieToken)
}
// 初始化时处理路径
const currentPath = this.$route.path
if (currentPath === '/' || currentPath === '' || currentPath === '/index.html') {
......@@ -107,9 +165,22 @@ export default {
this.selectedPath = currentPath
}
console.log('创建时路径:', currentPath, '选中路径:', this.selectedPath)
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize)
// 初始检查
this.$nextTick(() => {
this.checkMenuOverflow()
})
this.initVuexValue()
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
...mapMutations('user', ['set_userid', 'set_corp_id', 'set_token', 'set_cser_info', 'set_cser_id', 'set_cser_name', 'set_userInfo']),
...mapMutations('game', ['set_accountSelect']),
// 设置缓存
cacheCorp_id(corp_id) {
Cookies.set('corp_id', corp_id, { expires: 7 })
......@@ -133,6 +204,63 @@ export default {
},
handleSelect(key, keyPath) {
console.log('菜单选择:', key, keyPath, window.location.href)
},
initVuexValue(){
this.set_userid(Cookies.get('userid'))
this.set_corp_id(Cookies.get('corp_id'))
this.set_token(Cookies.get('token'))
this.set_cser_id(Cookies.get('cser_id'))
this.set_cser_name(Cookies.get('cser_name'))
const userinfo = {
cser_id:Cookies.get('cser_id'),
cser_name:Cookies.get('cser_name'),
username:Cookies.get('cser_name'),
id:Cookies.get('cser_id'),
}
this.set_userInfo(userinfo)
const cser_info = Cookies.get('cser_info')
console.log(Cookies.get('cser_id'),'cser_info',Cookies.get('cser_name'))
cser_info?this.set_cser_info(JSON.parse(cser_info)):this.set_cser_info({})
},
// 切换菜单展开收起状态
toggleMenu() {
this.isMenuExpanded = !this.isMenuExpanded
},
// 检查菜单是否需要换行
checkMenuOverflow() {
this.$nextTick(() => {
const menuElement = this.$refs.menuRef?.$el
if (!menuElement) return
// 临时展开菜单来检查实际高度
const wasCollapsed = !this.isMenuExpanded && this.shouldShowToggle
if (wasCollapsed) {
menuElement.classList.remove('collapsed')
}
const menuHeight = menuElement.scrollHeight
const singleLineHeight = 50 // 单行高度
// 如果菜单高度超过单行,说明需要换行
this.shouldShowToggle = menuHeight > singleLineHeight + 10 // 加10px容错
// 如果不需要展开收起按钮,则直接展开
if (!this.shouldShowToggle) {
this.isMenuExpanded = true
} else if (wasCollapsed) {
// 恢复收起状态
menuElement.classList.add('collapsed')
}
})
},
// 窗口大小变化处理
handleResize() {
clearTimeout(this.resizeTimer)
this.resizeTimer = setTimeout(() => {
this.checkMenuOverflow()
}, 150)
}
},
}
......@@ -162,12 +290,26 @@ export default {
z-index: 10;
}
.menu-container {
position: relative;
padding-right: 30px;
}
.mobile-el-menu {
border: none;
background: #fff;
display: flex;
justify-content: flex-start;
padding-left: 16px;
flex-wrap: wrap;
transition: all 0.3s ease;
overflow: hidden;
}
/* 收起状态:只显示第一行 */
.mobile-el-menu.collapsed {
max-height: 35px;
overflow: hidden;
}
.mobile-menu-item {
......@@ -186,6 +328,24 @@ export default {
font-weight: bold;
}
/* 展开收起按钮 */
.menu-toggle-btn {
position: absolute;
right: 10px;
top: 18px;
transform: translateY(-50%);
display: flex;
align-items: center;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
z-index: 20;
}
.menu-toggle-btn i {
font-size: 12px;
}
.mobile-content {
flex: 1;
overflow-y: auto;
......@@ -199,8 +359,8 @@ export default {
}
.el-menu--horizontal>.el-menu-item {
height: 50px;
line-height: 50px;
height: 35px;
line-height: 35px;
}
body {
......
import request from '@/utils/request'
import store from '@/store/index'
// 所属分组下拉
function returnApi(api){
return '/sidebar' + api
}
export function procedure_group(data) {
return request({
url: returnApi('/procedure_group/index'),
method: 'post',
data
})
}
// 话术列表
export function procedureList(data) {
return request({
url: returnApi('/procedure/index'),
method: 'post',
data
})
}
// 删除话术
export function proceduredel(data) {
return request({
url: returnApi('/procedure/del'),
method: 'post',
data
})
}
// 批量移动
export function proceduremove(data) {
return request({
url: returnApi('/procedure/move'),
method: 'post',
data
})
}
// 新增话术
export function procedureadd(data) {
return request({
url: returnApi('/procedure/add'),
method: 'post',
data
})
}
// 分组添加/修改
export function procedure_groupAdd(data) {
return request({
url: returnApi('/procedure_group/add'),
method: 'post',
data
})
}
// 分组删除
export function procedure_groupDel(data) {
return request({
url: returnApi('/procedure_group/del'),
method: 'post',
data
})
}
// 个人话术移到企业库
export function addCompany(data) {
return request({
url: returnApi('/procedure/addCompany'),
method: 'post',
data
})
}
// 个人话术详情
export function procedureInfo(data) {
return request({
url: returnApi('/procedure/info'),
method: 'post',
data
})
}
// 个人话术排序
export function procedureSort(data) {
return request({
url: returnApi('/procedure/sort'),
method: 'post',
data
})
}
// 个人话术租排序
export function procedureGroupSort(data) {
return request({
url: returnApi('/procedure_group/sort'),
method: 'post',
data
})
}
// 企业话术增至个人
export function addToPersonal(data) {
return request({
url: returnApi('/procedure/addToPersonal'),
method: 'post',
data
})
}
// 话术使用次数
export function skillQuote(data) {
return request({
url: '/admin/procedure/quote',
method: 'post',
data
})
}
/* ----------- 知识库的接口 ---------- */
// 知识库分组列表
export function groupList(data) {
return request({
url: returnApi('/knowledge_group/index'),
method: 'post',
data
})
}
// 知识库分组下拉
export function libraryIndex(data) {
return request({
url: returnApi('/knowledge_base/index'),
method: 'post',
data
})
}
// 新增和编辑
export function groupHandle(data) {
return request({
url: returnApi('/knowledge_group/add'),
method: 'post',
data
})
}
// 删除分组
export function groupDel(data) {
return request({
url: returnApi('/knowledge_group/del'),
method: 'post',
data
})
}
// 新增知识库任务
export function addLibraryTask(data) {
return request({
url: returnApi('/knowledge_base/add'),
method: 'post',
data
})
}
// 详情
export function libraryTaskView(data) {
return request({
url: returnApi('/knowledge_base/view'),
method: 'post',
data
})
}
// 编辑
export function libraryTaskEdit(data) {
return request({
url: returnApi('/knowledge_base/edit'),
method: 'post',
data
})
}
// 删除
export function libraryTaskDel(data) {
return request({
url: returnApi('/knowledge_base/del'),
method: 'post',
data
})
}
// 批量删除
export function multipleDel(data) {
return request({
url: returnApi('/knowledge_base/batchDel'),
method: 'post',
data
})
} // 导入
export function importData(data) {
return request({
url: returnApi('/knowledge_base/importKnowledgeBase'),
method: 'post',
data
})
}
// 知识库计数
export function logClickTime(data) {
return request({
url: returnApi('/knowledge_base/logClickTime'),
method: 'post',
data
})
}
// 大模型 ai
export function getCorpBetaConfig(data) {
return request({
url: '/admin/corp_beta_config/getCorpBetaConfig',
method: 'post',
data
})
}
// 问答模块
export function getAiResponse(data) {
return request({
url: returnApi('/corp_beta_question_log/getAiResponse'),
method: 'post',
data
})
}
// 问答模块
export function Aihistory(data) {
return request({
url: returnApi('/corp_beta_question_log/history'),
method: 'post',
data
})
}
// 赞同模块
export function answerComment(data) {
return request({
url: returnApi('/corp_beta_question_log/answerComment'),
method: 'post',
data
})
}
// 复制次数统计
export function calAnswerClickTime(data) {
return request({
url: returnApi('/corp_beta_question_log/calAnswerClickTime'),
method: 'post',
data
})
}
// 获取来源
export function getQuoteData(data) {
return request({
url: returnApi('/corp_beta_question_log/getQuoteData'),
method: 'post',
data
})
}
// 同步知识库
export function asyncKnowledge(data) {
return request({
url: returnApi('/knowledge_base/syncToCorp'),
method: 'post',
data
})
}
/* -------------------- 机器人知识库 ----------------------- */
// 新增机器人知识库任务
export function corp_robot_knowledge_add(data) {
return request({
url: returnApi('/corp_robot_knowledge_base/add'),
method: 'post',
data
})
}
// 机器人知识库列表
export function corp_robot_knowledge_list(data) {
return request({
url: returnApi('/corp_robot_knowledge_base/index'),
method: 'post',
data
})
}
// 机器人知识库编辑
export function corp_robot_knowledge_edit(data) {
return request({
url: returnApi('/corp_robot_knowledge_base/edit'),
method: 'post',
data
})
}
// 机器人知识库详情
export function corp_robot_knowledge_view(data) {
return request({
url: returnApi('/corp_robot_knowledge_base/view'),
method: 'post',
data
})
}
// 机器人知识库删除
export function corp_robot_knowledge_del(data) {
return request({
url: returnApi('/corp_robot_knowledge_base/delete'),
method: 'post',
data
})
}
// 机器人知识库批量删除
export function corp_robot_knowledge_batchDelete(data) {
return request({
url: returnApi('/corp_robot_knowledge_base/batchDelete'),
method: 'post',
data
})
}
// 机器人知识库批量导入
export function corp_robot_knowledge_import(data) {
return request({
url: returnApi('/corp_robot_knowledge_base/import'),
method: 'post',
data
})
}
/* --------------------机器人知识库分组-----------------------*/
// 知识库分组列表
export function corp_robot_knowledge_group_index(data) {
return request({
url: returnApi('/corp_robot_knowledge_group/index'),
method: 'post',
data
})
}
// 知识库分组新增
export function corp_robot_knowledge_group_add(data) {
return request({
url: returnApi('/corp_robot_knowledge_group/add'),
method: 'post',
data
})
}
// 知识库分组编辑
export function corp_robot_knowledge_group_edit(data) {
return request({
url: returnApi('/corp_robot_knowledge_group/edit'),
method: 'post',
data
})
}
// 知识库分组删除
export function corp_robot_knowledge_group_del(data) {
return request({
url: returnApi('/corp_robot_knowledge_group/delete'),
method: 'post',
data
})
}
// AI问答对列表
export function AI_list(data) {
return request({
url: returnApi('/corp_ai_question_answer/index'),
method: 'post',
data
})
}
// 业务下拉
export function search_condition(data) {
return request({
url: '/admin/search_condition',
method: 'post',
data
})
}
// 智能体列表
export function corp_beta_config(data) {
return request({
url: '/admin/corp_beta_config/index',
method: 'post',
data
})
}
// 标记有用/无用
export function markUseful(data) {
return request({
url: returnApi('/corp_ai_question_answer/markUseful'),
method: 'post',
data
})
}
......@@ -23,8 +23,12 @@ export function getAuthUser(data) {
}
// 获取组织列表
export function getOrganization(data) {
if(process.env.NODE_ENV === 'production'){
return axios.post('https://zq.wozhangwan.com/api/login/organization',data)
}else{
return axios.post('https://zq.zwwlkj03.top/api/login/organization',data)
}
}
// 获取签名信息
export function getSignature(data) {
return request({
......@@ -43,7 +47,6 @@ export function uploadCos(params) {
params
})
}
// 请求企业配置
export function companyviewConfig(data) {
return request({
......@@ -60,4 +63,68 @@ export function logout(data) {
data
})
}
// 客户是否同意聊天内容存档
export function checkSingleAgree(data) {
return request({
url: '/sidebar/external_user/checkSingleAgree',
method: 'post',
data
})
}
// 客服号是否开启会话会话内容存档
export function checkUserPermit(data) {
return request({
url: '/sidebar/external_user/checkUserPermit',
method: 'post',
data
})
}
// 发送评价
export function sendComment(data) {
return request({
url: '/sidebar/client_session/sendComment',
method: 'post',
data
})
}
// 休息
// 导出一个名为rest的函数,接收一个参数data
export function client_session_rest(data) {
// 发送一个post请求,请求的url为'/sidebar/client_session/rest',请求的数据为data
return request({
url: '/sidebar/work_wei_xin/rest',
method: 'post',
data
})
}
// 结束休息
export function finishRest(data) {
return request({
url: '/sidebar/work_wei_xin/finishRest',
method: 'post',
data
})
}
// 客户智能标签打标
export function remarkSessionIntelTag(data) {
return request({
url: '/sidebar/client_session/remarkSessionIntelTag',
method: 'post',
data
})
}
// 获取客户号的休息状态
export function getClientStatus(data) {
return request({
url: '/sidebar/work_wei_xin/info',
method: 'post',
data
})
}
......@@ -90,3 +90,156 @@ export function zyouUnBind(data) {
data
})
}
// 获取礼包码列表
export function getSendingCodeList(data) {
return request({
url: returnApi('/corp_gift_package_list/getSendingCodeList'),
method: 'post',
data
})
}
// 标签
export function searchTags(data) {
return request({
url: returnApi('/tag/index'),
method: 'post',
data
})
}
// 通讯录
export function externalUserList(data) {
return request({
url: returnApi('/corp_user/externalUserList'),
method: 'post',
data
})
}
// 获取图片id
export function getMediaId(data) {
return request({
url: returnApi('/common/getMedia'),
method: 'post',
data
})
}
// 通讯录红点
export function mailRedTip(data) {
return request({
url: returnApi('/external_user/redTip'),
method: 'post',
data
})
}
// 同步通讯录
export function refreshBindMail(data) {
return request({
url: returnApi('/external_user/refreshBind'),
method: 'post',
data
})
}
// 搜索客户
export function remarkSearchSelect(data) {
return request({
url: returnApi('/follow_user/preview'),
method: 'post',
data
})
}
// 礼包可发送礼包
export function giftCodeList(data) {
return request({
url: returnApi('/corp_gift_package_list/getCanSendPackage'),
method: 'post',
data
})
}
// 礼包码发送
export function sendGiftCode(data) {
return request({
url: returnApi('/corp_gift_package_list/sendGiftCode'),
method: 'post',
data
})
}
// 获取举报授权链接地址
export function getZyouAuthLink(data) {
return request({
url: returnApi('/corp_zyou_bind/getZyouAuthLink'),
method: 'post',
data
})
}
// 转游最近发送的记录 5 条
export function getRecentSendLog(data) {
return request({
url: returnApi('/corp_zyou_game_send_log/getRecentSendLog'),
method: 'post',
data
})
}
// 标记转端
export function markTransScene(data) {
return request({
url: returnApi('/external_user/markTransScene'),
method: 'post',
data
})
}
// 根据用户 id 获取掌权分组
export function getZqCserGroup(data) {
return request({
url: returnApi('/common/getZqCserGroup'),
method: 'post',
data
})
}
// 根据用户 id 获取掌权项目
export function getZqCserWxBelong(data) {
return request({
url: returnApi('/common/getZqCserWxBelong'),
method: 'post',
data
})
}
// 记录转游发送记录
export function send_log_add(data) {
return request({
url: returnApi('/corp_zyou_game_send_log/add'),
method: 'post',
data
})
}
// 发送渠道密码加密
export function zyouGetMemberLink(data) {
return request({
url: returnApi('/session/zyouGetMemberLink'),
method: 'post',
data
})
}
// 我的任务获取红点数组
export function getTaskUnReadData(data) {
return request({
url: returnApi('/corp_zyou_bind/getTaskUnReadData'),
method: 'post',
data
})
}
// 我的任务小时红点数字
export function clearTaskUnReadData(data) {
return request({
url: returnApi('/corp_zyou_bind/clearTaskUnReadData'),
method: 'post',
data
})
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>安卓</title>
<g id="企微客户端应用" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="发送游戏" transform="translate(-1144, -363)" fill-rule="nonzero">
<g id="4.数据展示/10.Popover气泡卡片/上左⬇️" transform="translate(1132, 287)">
<g id="编组-10备份-2" transform="translate(0, 60)">
<g id="安卓" transform="translate(12, 16)">
<rect id="矩形" fill="#323335" opacity="0" x="0" y="0" width="16" height="16"></rect>
<g id="编组" transform="translate(2.223, 1.0636)" fill="#93C85C">
<path d="M2.31100462,10.4052376 C2.31100462,10.7232376 2.57100462,10.9832376 2.88900462,10.9832376 L3.46600462,10.9832376 L3.46600462,13.0062376 C3.46600462,13.4847924 3.85394989,13.8727376 4.33250462,13.8727376 C4.81105936,13.8727376 5.19900462,13.4847924 5.19900462,13.0062376 L5.19900462,10.9832376 L6.35500462,10.9832376 L6.35500462,13.0062376 C6.35500462,13.4847924 6.74294989,13.8727376 7.22150462,13.8727376 C7.70005936,13.8727376 8.08800462,13.4847924 8.08800462,13.0062376 L8.08800462,10.9832376 L8.66500462,10.9832376 C8.98376958,10.9821403 9.24190731,10.7240026 9.24300462,10.4052376 L9.24300462,4.62823765 L2.31100462,4.62823765 L2.31100462,10.4062376 L2.31100462,10.4052376 Z M0.867004622,4.62823765 C0.636899047,4.62770499 0.416064587,4.71887832 0.253354938,4.88158796 C0.09064529,5.04429761 -0.000528032368,5.26513207 2.30072207e-06,5.49523765 L2.30072207e-06,9.53823765 C2.30072207e-06,10.0167924 0.387949886,10.4047376 0.866504622,10.4047376 C1.34505936,10.4047376 1.73300694,10.0167924 1.73300694,9.53823765 L1.73300694,5.49423765 C1.73353641,5.26430567 1.64250019,5.04362343 1.48000714,4.88094274 C1.31751409,4.71826206 1.09693706,4.62697121 0.867004622,4.62723707 L0.867004622,4.62823765 Z M10.6870046,4.62823765 C10.4570722,4.62797121 10.2364952,4.71926206 10.0740021,4.88194274 C9.91150905,5.04462343 9.82047283,5.26530567 9.8210023,5.49523765 L9.8210023,9.53823765 C9.8210023,10.0167924 10.2089499,10.4047376 10.6875046,10.4047376 C11.1660594,10.4047376 11.5540069,10.0167924 11.5540069,9.53823765 L11.5540069,5.49423765 C11.5545373,5.26413207 11.463364,5.04329761 11.3006543,4.88058796 C11.1379447,4.71787832 10.9171102,4.62670499 10.6870046,4.62723533 L10.6870046,4.62823765 Z M7.81700462,1.25323765 L8.57000462,0.499237647 C8.64738311,0.427522384 8.67935601,0.319238135 8.6533421,0.216994377 C8.62732819,0.114750619 8.54749165,0.0349140831 8.44524789,0.00890016922 C8.34300413,-0.0171137447 8.23471988,0.0148591565 8.16300462,0.092237647 L7.30800462,0.944237647 C6.34087286,0.463540973 5.20419506,0.464651733 4.23800462,0.947237647 L3.38000462,0.089237647 C3.30730107,0.0163554622 3.20123804,-0.0122035721 3.10176845,0.0143184562 C3.00229885,0.0408404846 2.92453449,0.118414248 2.89776844,0.217818461 C2.8710024,0.317222673 2.89930107,0.423355468 2.97200462,0.496237647 L3.72900462,1.25323765 C2.83823676,1.9068514 2.31172461,2.94539476 2.31100462,4.05023765 L9.24300462,4.05023765 C9.24300462,2.90023765 8.68000462,1.88323765 7.81600462,1.25323765 L7.81700462,1.25323765 Z M4.62100462,2.89323765 L4.04300462,2.89323765 L4.04300462,2.31623765 L4.62100462,2.31623765 L4.62100462,2.89323765 Z M7.50900462,2.89323765 L6.93100462,2.89323765 L6.93100462,2.31623765 L7.50900462,2.31623765 L7.50900462,2.89323765 Z" id="形状"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>H5</title>
<g id="企微客户端应用" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="发送游戏" transform="translate(-1144, -303)" fill-rule="nonzero">
<g id="4.数据展示/10.Popover气泡卡片/上左⬇️" transform="translate(1132, 287)">
<g id="H5" transform="translate(12, 16)">
<rect id="矩形" fill="#323335" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M2.133,1.333 L3.2,13.333 L7.993,14.667 L12.8,13.333 L13.867,1.333 L2.133,1.333 L2.133,1.333 Z M11.367,5.107 L5.7,5.107 L5.873,7.02 L11.2,7.02 L10.827,11.18 L8,11.96 L5.173,11.173 L5,9.227 L6.067,9.227 L6.167,10.36 L8,10.853 L8.053,10.84 L9.827,10.36 L10.027,8.093 L4.893,8.093 L4.533,4.04 L11.467,4.04 L11.367,5.107 L11.367,5.107 Z" id="形状" fill="#CC337D" opacity="0.99"></path>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>iOS</title>
<g id="企微客户端应用" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="发送游戏" transform="translate(-1144, -423)" fill-rule="nonzero">
<g id="4.数据展示/10.Popover气泡卡片/上左⬇️" transform="translate(1132, 287)">
<g id="编组-10备份-4" transform="translate(0, 120)">
<g id="iOS" transform="translate(12, 16)">
<rect id="矩形" fill="#323335" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M13.2677706,11.0140014 C13.2507706,11.0640014 12.9977706,11.9500014 12.3727706,12.8610014 C11.8327706,13.6470014 11.2737706,14.4390014 10.3917706,14.4530014 C9.52377064,14.4680014 9.24577064,13.9350014 8.25577064,13.9350014 C7.26577064,13.9350014 6.95577064,14.4390014 6.13577064,14.4680014 C5.28677064,14.5030014 4.63577064,13.6100014 4.09277064,12.8280014 C2.98377064,11.2250014 2.13077064,8.28700144 3.27577064,6.31800144 C3.83877064,5.33200144 4.85077064,4.70600144 5.95277064,4.68900144 C6.78277064,4.67300144 7.57377064,5.25200144 8.07877064,5.25200144 C8.59677064,5.25200144 9.55277064,4.55200144 10.5617706,4.65700144 C10.9837706,4.67500144 12.1647706,4.83300144 12.9257706,5.93700144 C12.8657706,5.97700144 11.5127706,6.77600144 11.5277706,8.40100144 C11.5477706,10.3640014 13.2487706,11.0070014 13.2677706,11.0140014 M9.90177064,3.60500144 C10.3557706,3.05100144 10.6547706,2.28800144 10.5767706,1.53100144 C9.92677064,1.55600144 9.13577064,1.96200144 8.66577064,2.51300144 C8.24977064,2.99500144 7.88377064,3.76900144 7.98577064,4.51800144 C8.70677064,4.56200144 9.44877064,4.14000144 9.90177064,3.60500144" id="形状" fill="#A995FF"></path>
</g>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle fill="#111" cx="8" cy="8" r="8"/><path d="M4.295 10.035a3.095 3.095 0 0 1 3.091-3.092v1.855c-.682 0-1.236.554-1.236 1.237 0 .607.328 1.26 1.05 1.26.048 0 1.176-.01 1.176-.97v-7.03h1.976c.002.923.75 1.67 1.673 1.671l-.001 1.852a3.433 3.433 0 0 1-1.793-.49v3.998c0 1.835-1.562 2.825-3.032 2.825-1.655 0-2.904-1.34-2.904-3.116Z" fill="#FF4040" fill-rule="nonzero"/><path d="M3.8 9.54a3.095 3.095 0 0 1 3.092-3.092v1.855A1.239 1.239 0 0 0 5.655 9.54c0 .607.329 1.261 1.05 1.261.048 0 1.176-.011 1.176-.97V2.8h1.977c.002.923.75 1.67 1.673 1.672l-.002 1.851a3.433 3.433 0 0 1-1.792-.489l-.001 3.997c0 1.835-1.562 2.825-3.031 2.825-1.656 0-2.905-1.34-2.905-3.116Z" fill="#00F5FF" fill-rule="nonzero"/><path d="M6.892 6.98c-1.996.254-3.575 2.8-1.937 5.09 1.513 1.238 4.697.519 4.785-2.16l-.002-4.074a3.52 3.52 0 0 0 1.796.486V4.889a1.822 1.822 0 0 1-.916-.689 1.7 1.7 0 0 1-.687-.91H8.377l-.002 7.056c0 .986-1.646 1.337-2.063.38-1.046-.5-.81-2.4.582-2.42L6.892 6.98Z" fill="#FFF" fill-rule="nonzero"/></g></svg>
\ No newline at end of file
<svg t="1718879942311" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4563" width="200" height="200"><path d="M256 960a128 128 0 0 1-128-128V466.645333A85.376 85.376 0 0 1 64 384v-106.666667a85.333333 85.333333 0 0 1 85.333333-85.333333h290.901334l-68.629334-68.629333a8.533333 8.533333 0 0 1 0-12.074667l33.194667-33.173333a8.533333 8.533333 0 0 1 12.053333 0l84.48 84.48 84.48-84.48a8.533333 8.533333 0 0 1 12.053334 0l33.194666 33.173333a8.533333 8.533333 0 0 1 0 12.074667l-68.650666 68.608L874.666667 192a85.333333 85.333333 0 0 1 85.333333 85.333333v106.666667a85.376 85.376 0 0 1-64 82.645333V832a128 128 0 0 1-128 128H256z m213.333333-490.666667H192v362.666667a64 64 0 0 0 60.245333 63.893333L256 896h213.333333V469.333333z m362.666667 0H533.333333v426.666667h234.666667a64 64 0 0 0 63.893333-60.245333L832 832V469.333333zM469.333333 256H149.333333a21.333333 21.333333 0 0 0-21.184 18.837333L128 277.333333v106.666667a21.333333 21.333333 0 0 0 18.837333 21.184L149.333333 405.333333h320v-149.333333z m405.333334 0H533.333333v149.333333h341.333334a21.333333 21.333333 0 0 0 21.184-18.837333L896 384v-106.666667a21.333333 21.333333 0 0 0-18.837333-21.184L874.666667 256z" fill="#8CA4BA" p-id="4564"></path></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="50px" height="54px" viewBox="0 0 50 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>三角形</title>
<desc>Created with Sketch.</desc>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="订单信息" transform="translate(-1762.000000, -133.000000)" fill="#FFE59A">
<g id="编组-4" transform="translate(1378.000000, 133.000000)">
<polygon id="三角形" transform="translate(409.151650, 26.961791) scale(-1, -1) translate(-409.151650, -26.961791) " points="384.3033 -9.09494702e-13 434 53.9235818 404.735562 53.9235818 384.3033 31.9235818"></polygon>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>VIP自助工具</title>
<g id="游戏" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="游戏-违规记录" transform="translate(-564, -468)" fill-rule="nonzero">
<g id="4.数据展示/10.Popover气泡卡片/上左⬇️" transform="translate(552, 452)">
<g id="VIP自助工具" transform="translate(12, 16)">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M7.99821895,14.321585 C7.56391795,14.321585 7.15856419,14.1264531 6.85937495,13.7875521 L1.12654628,7.17392176 C0.595742151,6.55774355 0.576419359,5.61294483 1.09759904,4.97622538 L3.26912253,2.31640027 C3.55866885,1.95695807 3.98330845,1.75156533 4.43691378,1.75156533 L11.5884714,1.75156533 C12.0324338,1.75156533 12.4570734,1.95697773 12.7466197,2.31640027 L14.9277861,4.97622538 C15.4489658,5.61294483 15.4393044,6.55774355 14.8988389,7.17392176 L9.12742004,13.7875521 C8.83787372,14.1264531 8.42285856,14.321585 7.99821895,14.321585 Z M7.41909807,13.0918286 C7.55918834,13.2618573 7.75534332,13.351868 7.9701627,13.351868 C8.17564989,13.351868 8.37178699,13.2618573 8.52124521,13.0918286 L14.1160422,6.65100913 C14.3775584,6.35097981 14.3868906,5.89091444 14.1347066,5.58087331 L12.0051415,2.99054812 C11.8650333,2.82051939 11.659564,2.72051599 11.4447268,2.72051599 L4.5142988,2.72051599 C4.29947942,2.72051599 4.09399224,2.82051939 3.95388409,2.99054812 L1.85231552,5.58087331 C1.60013158,5.89091444 1.60946378,6.35097981 1.8709978,6.65100913 L7.41908019,13.0918286 L7.41909807,13.0918286 Z M7.681625,9.94284375 L4.49796875,6.6775625 C4.37553125,6.54695312 4.37553125,6.342875 4.50614063,6.21225 C4.63675,6.0898125 4.84082813,6.0898125 4.97145313,6.22042187 L7.92654688,9.249 L11.03675,6.21226562 C11.167375,6.0898125 11.3714375,6.0898125 11.5020625,6.22042187 C11.6245,6.35104687 11.6245,6.55510937 11.4938906,6.68573437 L8.14695313,9.95104687 C8.08164063,10.0081719 8.00001563,10.0408281 7.91839063,10.0408281 C7.83673438,10.0408281 7.7469375,10.0081562 7.681625,9.94284375 Z" id="形状" fill="#889FB5"></path>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path data-follow-fill="#5BCA54" fill-rule="evenodd" fill="#5BCA54" d="M7.687.001c3.998-.062 6.559 2.18 7.726 4.952.79 1.875.779 4.28-.01 6.15-.954 2.258-2.757 3.88-5.235 4.614-.542.16-1.119.188-1.752.283l-.794-.01-.85-.077c-.65-.127-1.245-.284-1.784-.512-2.237-.944-3.862-2.622-4.614-5.05C.088 9.427-.13 8.043.091 6.858c.1-.533.192-1.05.37-1.513.9-2.333 2.471-3.964 4.8-4.865.467-.18.984-.287 1.523-.392l.903-.087ZM9.926 4c-.276.103-.579.082-.825.177-.931.36-1.538.965-1.747 2.034-.067.343.024.743-.036 1.124v2.081c.05.78-.033 1.435-.461 1.75-.877.646-1.967.314-2.317-.555-.053-.131-.134-.326-.097-.533.086-.488.455-.783.861-.957.267-.116.64-.225.74-.497.159-.428-.265-.828-.594-.887-.435-.079-1.041.326-1.286.485-.445.287-.857.611-1.068 1.123-.093.225-.121.562-.073.852.185 1.098.562 1.81 1.359 2.294 1.702 1.035 3.67-.125 4.21-1.467.133-.332.09-.74.157-1.17v-2.59c0-.328-.031-.71.049-.97.086-.28.266-.521.497-.662.845-.516 1.93-.192 2.244.556.055.128.137.323.097.532-.177.935-1.37.883-1.637 1.62-.053.146-.005.3.048.402.218.418.775.435 1.262.248 1.103-.423 2.151-1.993 1.48-3.512-.422-.954-1.45-1.482-2.863-1.478Z"/></svg>
<template>
<!-- 开发模式触发区域 - 隐藏的点击区域 -->
<div class="dev-mode-trigger" @click="handleDevModeClick" title="开发模式触发区域"></div>
<div class="debug-container">
<div class="dev-mode-console" @click="handleDevModeClick" title="开发模式触发区域"></div>
<div class="dev-mode-cookie" @click="cleanCookie" title="连续点击7次清除所有 cookie"></div>
</div>
</template>
<script>
import devModeManager from '@/utils/devMode'
export default {
name: 'Debug',
name: 'debug',
methods: {
handleDevModeClick() {
devModeManager.handleClick()
},
cleanCookie() {
// 调用 devModeManager 中的 handleCookieClearClick 方法
devModeManager.handleCookieClearClick()
}
}
}
</script>
<style lang="scss" scoped>
/* 开发模式触发区域 */
.dev-mode-trigger {
.debug-container{
width: 100%;
height: 50px;
position: absolute;
top: 5px;
left: 5px;
bottom: 0;
left: 0;
z-index: 999;
}
/* 开发模式触发区域 */
.dev-mode-console {
width: 50px;
height: 50px;
height: 20px;
background: transparent;
cursor: pointer;
z-index: 999;
user-select: none;
}
.dev-mode-cookie {
width: 50px;
height: 20px;
background: transparent;
cursor: pointer;
z-index: 999;
......@@ -29,8 +49,13 @@ export default {
}
/* 开发环境下显示边框提示 */
.dev-mode-trigger:hover {
.dev-mode-console:hover {
background: rgba(0, 191, 138, 0.1);
border-radius: 4px;
}
.dev-mode-cookie:hover {
background: rgba(138, 2, 162, 0.1);
border-radius: 4px;
}
</style>
\ No newline at end of file
/<template>
<div>
<div class="content">
<div style="margin-bottom:10px;">每行一个别名,多个按回车后添加</div>
<div>
<el-input
v-model="textarea"
type="textarea"
:autosize="{ minRows: 5, maxRows: 14}"
placeholder="请输入内容"
>
</el-input>
</div>
</div>
<div class="dialog-footer rowFlex">
<div class="btns">
<el-button
class="btn"
@click="close"
>取 消</el-button>
<el-button
:loading="loading"
class="btn"
type="primary"
@click="onSubmit"
> 确定</el-button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'inputTagsAlias',
props: {
rowInfo: {
type: Array,
default: () => []
}
},
data() {
return {
loading: false,
textarea: ''
}
},
mounted() {
this.rowInfo.forEach((item, index) => {
this.textarea += item + '\n'
})
},
methods: {
close() {
this.$emit('cancel')
},
onSubmit() {
let aliasList = this.textarea.split('\n')
aliasList = aliasList.filter((item) => {
return item
})
console.log(aliasList, '>>>>>>>>>>>>aliasList')
this.$emit('ok', aliasList)
}
}
}
</script>
<style lang="scss" scoped>
.content {
padding: 20px;
}
</style>
\ No newline at end of file
<template>
<div>
<el-form-item
class="inputItem"
:prop="ruleProp"
>
<template slot="label">
{{ labelText }}
</template>
<div class="alias">
<div
class="alias_list"
:class="disabled?'alias_list_disabled':''"
>
<span
v-for="(item, index) in inputSelectList"
:key="index"
class="el-tag el-tag--info el-tag--mini el-tag--light alias_item"
>
<span class="alias_item_text">{{ item }}</span>
<i
v-if="!disabled"
class="el-tag__close el-icon-close my_close"
@click="ondelalias(index)"
></i>
</span>
<input
v-model="aliasValue"
type="text"
class="alias_input"
:disabled="disabled"
:placeholder="disabled && inputSelectList.length>0?'':placeholder"
@keydown="onInputKey"
@blur="emitInputChange"
>
</div>
<!-- <div class="alias_add">
<el-button
type="text"
size="medium"
@click="editalias()"
>编辑</el-button>
</div> -->
</div>
</el-form-item>
<!-- <BiDrawer
v-model="alias.show"
:drawer-title="alias.title"
:row-info="alias.row"
:component="Alias"
:append-to-body="true"
drawer-size="600px"
@ok="aliasSubmit"
></BiDrawer> -->
</div>
</template>
<script>
import Alias from './alias.vue'
export default {
name: 'inputTags',
props: {
inputSelectList: {
type: Array,
default: () => []
},
labelText: {
type: String,
default: ''
},
ruleProp: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
Alias: Alias,
aliasValue: '',
alias: {
show: false,
title: '编辑',
row: {}
}
}
},
methods: {
aliasSubmit(aliasList) {
this.$emit('update:inputSelectList', aliasList)
},
onInputKey(e) {
if (e.key == 'Enter') {
if (!this.aliasValue) {
return
}
this.inputSelectList.push(this.aliasValue)
this.aliasValue = ''
}
},
emitInputChange() {
this.$emit('inputChange', this.inputSelectList)
},
ondelalias(index) {
this.inputSelectList.splice(index, 1)
this.emitInputChange()
},
editalias() {
this.alias = {
show: true,
row: this.inputSelectList,
title: '编辑'
}
}
}
}
</script>
<style lang="scss" scoped>
.alias {
display: flex;
align-content: center;
justify-content: space-between;
.alias_list {
min-height: 32px;
width: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-wrap: wrap;
align-items: center;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
// padding: 0 5px;
.alias_item {
margin-left: 5px;
max-width: none !important;
display: flex;
align-items: center;
.alias_item_text {
display: inline-block;
max-width: 350px;
white-space: nowrap; /* 禁止自动换行 */
overflow: hidden; /* 隐藏溢出的内容 */
text-overflow: ellipsis; /* 使用省略号表示溢出的文本 */
word-break: break-all;
}
/* 允许在任意字符间断开(可选,如果你想在任意字数换行) */
}
}
.alias_list_disabled {
background: #f5f7fa;
}
.alias_input {
border-style: none;
outline: none;
flex: 1;
height: 30px;
padding-left: 15px;
font-size: 13px;
border-color: #e4e7ed;
}
.alias_input:disabled {
// width: 100%;
// position: absolute;
// height: 100%;
// cursor: not-allowed;
background: #f5f7fa;
border-color: #e4e7ed;
color: #c0c4cc;
font-size: 13px;
cursor: not-allowed;
opacity: 0.7;
}
}
</style>
\ No newline at end of file
<template>
<div class="loading rowFlex allCenter">
<svg-icon icon-class="loading" class="loadingIcon" />
<p class="text">加载中</p>
</div>
</template>
<script>
export default {
name: 'loading',
data() {
return {
}
},
mounted() {},
methods: {}
}
</script>
<style lang="scss" scoped>
.loading{
position: absolute;
left: 50%;
transform: translateX(-50%);
top: 0;
.loadingIcon{
font-size: 24px;
animation: rotage linear 1s infinite;
}
.text{
color: #409EFF;
font-size: 14px;
margin-left: 5px;
}
}
@keyframes rotage {
0%{
transform: rotate(0deg);
}
100%{
transform: rotate(360deg);
}
}
</style>
\ No newline at end of file
......@@ -17,6 +17,7 @@
import { selectSearch } from '@/api/game'
import { searchcondition } from '@/api/pigeon'
export default {
name: 'mainGame',
// gameType:存在的时候取信鸽的游戏列表接口 gameDefaultList 编辑的时候 如果没有当前的游戏 id 主动加上去
props: ['defaultValue', 'width', 'label', 'disabled','gameType','gameDefaultList','isResize'],
data() {
......
......@@ -14,7 +14,7 @@
<script>
export default {
name: 'NoContent',
name: 'noContent',
props: {
// 主标题
title: {
......
......@@ -14,7 +14,7 @@
<script type="text/javascript">
export default {
name: 'page',
name: 'pageNum',
props: ['pageInfo'],
data() {
return {}
......
// 引入所有需要注册的全局组件
import noContent from '@/components/noContent.vue'
const globalComponents = [
noContent
]
export default {
install(Vue) {
globalComponents.forEach((component) => {
Vue.component(component.name, component)
})
}
}
<template>
<div class="search-item">
<div v-if="label && label.length<6 " class="item-label">{{ label }}</div>
<div v-else-if="label && label.length>=6 " class="item-label">
{{ label.slice(0,4) }} <br> {{ label.slice(4,label.length) }}
</div>
<div v-else class="item-label">{{ label }}</div>
<div class="item-content selectUser">
<el-select
v-model="resulte"
v-loadmore="loadMoreList"
filterable
:disabled="disabled"
remote
:remote-method="remoteMethod"
:placeholder="placeholder"
clearable
reserve-keyword
:loading="loading"
:style="{width:width}"
@change="selectChange"
>
<el-option
v-for="(item,index) in searchUserOption"
:key="index"
:value="item.external_userid+'¢'+item.user.userid"
:label="item.remark"
style="height:50px;"
>
<div class="rowFlex columnCenter selectItem">
<el-image
fit="fill"
:src="item.external_user.avatar"
class="tableImage"
></el-image>
<div class="infoSpan columnFlex rowCenter">
<p class="hidden">{{ item.remark &&item.remark!=''?item.remark:item.external_user.name }}</p>
<p class="rowFlex columnCenter">所属成员:<label
class="hidden"
style="max-width:120px;"
>{{ item.user.alias && item.user.alias!=''?item.user.alias:item.user.name }}</label></p>
</div>
</div>
</el-option>
</el-select>
</div>
</div>
</template>
<script>
import { remarkSearchSelect } from '@/api/works'
import { mapState } from 'vuex'
export default {
name: 'SearchSelectUser',
props: ['placeholder','corp_id', 'label', 'isResize', 'userid', 'disabled', 'width'],
// End of Selection
data() {
return {
loading: false,
noMore: false,
searchUserOption: [],
pageInfo: {
page: 1,
page_size: 20,
total: 0
},
resulte: ''
}
},
watch: {
// 监听是否重置
isResize(newVal, oldVal) {
if (newVal) {
this.resulte = ''
this.searchUserOption = []
this.pageInfo = {
page: 1,
page_size: 20,
total: 0
}
}
},
},
mounted() {
},
methods: {
loadMoreList() {
this.pageInfo.page++
if (!this.noMore) {
this.requestAccountList()
} else {
console.log('没有更新数据了')
}
},
selectChange(value) {
this.$emit('result', this.resulte.split('¢')[0], this.resulte.split('¢')[1])
},
requestAccountList() {
const data = {
remark: this.resulte.trim(),
...this.pageInfo,
corp_id: [this.corp_id]|| [],
userid: this.userid || '',
}
remarkSearchSelect(data).then((res) => {
this.loading = false
this.searchUserOption = this.searchUserOption.concat(res.data.data)
this.$forceUpdate()
if (res.data.data.length === 0) {
this.noMore = true
} else {
this.noMore = false
// this.pageInfo = res.data.page_info
}
})
},
// 删选过滤
remoteMethod(query) {
this.pageInfo = {
page: 1,
page_size: 20,
total: 0
}
this.resulte = query.trim()
if (this.resulte !== '') {
this.searchUserOption = []
this.pageInfo.page = 1
this.loading = true
this.noMore = false
this.requestAccountList()
} else {
return (this.searchUserOption = [])
}
}
}
}
</script>
<style lang="scss" scoped>
.selectItem {
height: 50px;
}
.infoSpan {
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
max-width: 250px;
height: 50px;
p {
font-size: 12px;
max-width: 100%;
line-height: 20px;
}
span {
color: #ffa81d;
}
}
.tableImage {
width: 30px;
height: 30px;
border-radius: 30px;
margin-right: 10px;
}
</style>
\ No newline at end of file
<template>
<div class="search-item">
<div v-if="label && label.length<6 || labelSplit " class="item-label">{{ label }}</div>
<div v-else-if="label && label.length>=6 " class="item-label">
{{ label.slice(0,4) }} <br> {{ label.slice(4,label.length) }}
</div>
<div v-else class="item-label">{{ label }}</div>
<div class="item-content selectUser">
<el-select
v-model="resulte"
:multiple="multiple"
:placeholder="placeholder"
:clearable="!clearable?true:false"
:filterable="filterable"
:disabled="disabled"
:collapse-tags="noTags=== false?false:true"
@change="selectChange"
>
<el-option
v-for="(item,index) in selectList"
:key="index"
:value="item.value"
:label="item.label"
>
<span style="float: left">{{ item.label }}</span>
</el-option>
</el-select>
</div>
</div>
</template>
<script>
export default {
name: 'Selece',
props: ['list', 'placeholder', 'label', 'width', 'isResize', 'defaultValue', 'noAll', 'multiple', 'close', 'filterable', 'clearable', 'noTags', 'disabled', 'labelSplit'],
data() {
return {
loading: false,
resulte: '',
selectList: []
}
},
watch: {
// 监听是否重置
isResize(newVal, oldVal) {
if (newVal) {
this.resulte = ''
}
},
defaultValue(newVal, oldVal) {
if ((newVal || newVal == 0)) {
this.resulte = newVal
}
},
list(newVal, oldVal) {
if (newVal && newVal.length > 0) {
const list = [{ label: '全部', value: '' }]
this.noAll ? this.selectList = newVal : this.selectList = list.concat(newVal)
} else {
this.selectList = []
this.resulte = ''
}
}
},
mounted() {
(this.defaultValue || this.defaultValue == 0) ? this.resulte = this.defaultValue : ''
if (this.list && this.list.length > 0) {
const list = [{ label: '全部', value: '' }]
this.noAll ? this.selectList = this.list : this.selectList = list.concat(this.list)
} else {
this.selectList = []
}
},
methods: {
selectChange(value) {
this.$emit('result', this.resulte)
}
}
}
</script>
<style lang="scss" scoped>
</style>
\ No newline at end of file
<template>
<div class="search-item">
<div class="date-picker-container">
<el-date-picker
v-model="value1"
:options="options"
:disabled="disabled"
:style="{'width': width || '100%'}"
:type="type?'datetimerange':'daterange'"
:format="type?'yyyy-MM-dd HH:mm:ss':'yyyy-MM-dd'"
:value-format="type?'yyyy-MM-dd HH:mm:ss':'yyyy-MM-dd'"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"
:clearable="noClearable ? !noClearable : true"
range-separator="~"
@change="handleDateChange"
/>
</div>
</div>
</template>
<script>
export default {
name: 'SelectDate',
// noClearable 默认 clearable : true 特殊的关闭
props: [ 'width', 'defaultValue', 'type', 'noClearable', 'disabled', 'options'],
data() {
return {
value1: []
}
},
watch: {
defaultValue: {
handler(newVal) {
if (newVal && Array.isArray(newVal) && newVal.length === 2) {
this.setDateValues(newVal)
} else {
this.value1 = []
}
},
immediate: true,
deep: true
}
},
methods: {
// 设置日期值
setDateValues(dateArray) {
this.$nextTick(() => {
this.value1 = dateArray
this.$forceUpdate()
})
},
// 处理日期范围变化
handleDateChange(value) {
this.value1 = value || []
this.$emit('result', this.value1)
}
}
}
</script>
<style lang="scss" scoped>
.date-picker-container {
width: 100%;
}
</style>
\ No newline at end of file
......@@ -34,7 +34,7 @@
title="查看大图"
append-to-body
center
top="50%"
top="80%"
:visible.sync="showScoleImage"
>
<div class="showScoleImageContent columnFlex allCenter" v-html="remark"></div>
......@@ -45,6 +45,7 @@
<script>
import { base64toFile } from '@/utils/index'
export default {
name: 'textEditor',
props: ['remark', 'contenteditable', 'domid'], // remark 原来的图文内容 contenteditable 是否可编辑 domid 编辑器的 DomId resultReamrk 方法吐出最后的编辑好的内容
data() {
return {
......@@ -177,7 +178,7 @@
}
.showScoleImageContent img{
max-width: 100%;
max-width: 80%;
margin-bottom: 20px;
}
</style>
<template>
<div class="uploadImage rowFlex flexWarp">
<div
v-if="imgUrl.length>0"
class="rowFlex"
>
<div
v-for="(item,index) in imgUrl"
:key="index"
class="rowFlex allCenter imageLoad"
@mouseenter="item.showLayer = true"
@mouseleave="item.showLayer = false"
>
<img
:src="item.src"
class="avatar"
>
<transition name="el-fade-in">
<div
v-if="item.showLayer"
class="iconsLayer rowFlex spaceAround columnCenter"
>
<i
class="el-icon-zoom-in icon"
@click="showBigImage(index)"
></i>
<i
class="el-icon-download icon"
@click="downImage(index)"
></i>
<i
class="el-icon-delete-solid icon"
@click="deleteImage(index)"
></i>
</div>
</transition>
</div>
</div>
<!-- 通过的上传图片组件包括图片裁剪 -->
<el-upload
class="avatar-uploader"
:http-request="uploadImg"
action="#"
:before-upload="fileSizeLimit"
:on-remove="removeImg"
:show-file-list="false"
>
<!-- 默认的样式 -->
<i class="el-icon-plus avatar-uploader-icon"></i>
<div
slot="tip"
class="el-upload__tip"
>{{ tip }}</div>
</el-upload>
<el-dialog
:visible.sync="dialogVisible"
title="查看大图"
:append-to-body="true"
>
<div class="rowFlex allCenter">
<el-image
style="width: 650px;height: 650px;"
:src="bigUrl"
fit="scale-down"
></el-image>
</div>
</el-dialog>
<!-- 裁剪功能 -->
<!-- <imageCropper
:show.sync="cropperVisible"
:file="file"
@confirm="onConfirm"
/> -->
</div>
</template>
<script type="text/javascript">
import { uploadImageBefore } from '@/utils/index'
// import imageCropper from './imageCorpper.vue'
export default {
name: 'uploadMultiple',
components: {
// imageCropper
},
props: ['tip', 'type', 'url', 'customStyle', 'fileList'], // index 第几个上传图片
data() {
return {
imgUrl: [],
bigUrl: '',
cropperVisible: false,
file: {
url: '',
name: ''
},
dialogVisible: false,
showLayer: false,
autoCropWidth: '400px',
autoCropHeight: '400px'
}
},
mounted() {
if (this.fileList && this.fileList.length > 0) {
this.imgUrl = this.fileList.map(item => {
return {
src: item.img || item,
showLayer: false
}
})
}
},
methods: {
showBigImage(index) {
this.dialogVisible = true
this.bigUrl = this.imgUrl[index].src
},
downImage(index) {
window.open(this.imgUrl[index].src || '')
},
deleteImage(index) {
this.imgUrl.splice(index, 1)
const images = this.imgUrl.map(item => item.src)
this.$emit('resultUpload', images)
},
getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
let imgResult = ''
reader.readAsDataURL(file)
reader.onload = function () {
imgResult = reader.result
}
reader.onerror = function (error) {
reject(error)
}
reader.onloadend = function () {
resolve(imgResult)
}
})
},
// 不需要裁剪图片
async uploadImg(params) {
const result = await this.uploading(params.file)
this.imgUrl.push({
src: result.data,
showLayer: false
})
const images = this.imgUrl.map(item => item.src)
this.$emit('resultUpload', images)
},
// 需要裁剪图片
// async uploadImg(params) {
// if (params.file) {
// this.file.url = await this.getBase64(params.file)
// this.file.name = params.file.name
// this.cropperVisible = true
// }
// },
// 上传之前
fileSizeLimit(file) {
return uploadImageBefore(file, this)
},
// 删除前
removeImg() { },
// 裁剪完成后返回图片地址
async onConfirm(file) {
const uploadConfig = {
dir: '/company_wx/service/avatars/'
}
this.cropperVisible = false
const result = await this.uploading(file, uploadConfig)
console.log(result, 'result')
this.imgUrl = result.data
this.$emit('resultUpload', this.imgUrl)
}
// 上传
}
}
</script>
<style lang="scss" scoped>
.uploadImage {
.imageLoad {
cursor: pointer;
position: relative;
background: #ffffff;
width: 100px;
height: 100px;
border: 1px solid #d9d9d9;
border-radius: 5px;
margin-right: 10px;
margin-bottom: 10px;
}
::v-deep .avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
::v-deep .avatar-uploader .el-upload:hover {
border-color: #409eff;
}
::v-deep .avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
line-height: 100px;
text-align: center;
}
::v-deep .avatar {
width: 100px;
height: 100px;
display: block;
border-radius: 6px;
}
::v-deep .el-upload__tip {
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #999999;
line-height: 20px;
}
.iconsLayer {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 10;
background: rgba(0, 0, 0, 0.4);
padding: 10px;
.icon {
font-size: 18px;
color: #ffffff;
cursor: pointer;
}
}
}
</style>
<style>
</style>
\ No newline at end of file
const clickagain = {
inserted: function (el, binding) {
let timer
el.addEventListener('click', () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
binding.value()
}, 1000)
})
}
}
export default clickagain
\ No newline at end of file
import clickagain from './clickagain'
const install = function(Vue) {
Vue.directive('clickagain', clickagain)
}
if (window.Vue) {
window.clickagain = clickagain
Vue.use(install); // eslint-disable-line
}
clickagain.install = install
export default clickagain
const debounce = {
inserted: function (el, binding) {
let timer
el.addEventListener('scroll', () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
binding.value()
}, 1000)
})
}
}
export default debounce
\ No newline at end of file
import debounce from './debounce'
const install = function(Vue) {
Vue.directive('debounce', debounce)
}
if (window.Vue) {
window.debounce = debounce
Vue.use(install); // eslint-disable-line
}
debounce.install = install
export default debounce
import loading from './loading'
const install = function(Vue) {
Vue.directive('loadingchat', loading)
}
if (window.Vue) {
window.loading = loading
Vue.use(install); // eslint-disable-line
}
loading.install = install
export default loading
import Vue from 'vue'
import loading from '@/components/common/loading'
const loadDirective = {
inserted: function (el, binding) {
const loadingCo = Vue.extend(loading)
// console.log('loadingCo', loadingCo)
const loadingComp = new loadingCo().$mount()
// console.log('loadingComp', loadingComp)
// 组件实例 挂到el元素上
el.loadingInstance = loadingComp
if (binding.value) {
el.appendChild(loadingComp.$el)
}
},
// 所在组件的 VNode 更新时调用
update(el, binding) {
// 当值改变
if (binding.value !== binding.oldValue) {
if (binding.value) { // v-loading true
el.appendChild(el.loadingInstance.$el)
} else { // v-loading false 删除节点
el.removeChild(el.loadingInstance.$el)
}
}
}
}
export default loadDirective
\ No newline at end of file
import loadMore from './loadmore'
const install = function(Vue) {
Vue.directive('loadmore', loadMore)
}
if (window.Vue) {
window.loadMore = loadMore
Vue.use(install); // eslint-disable-line
}
loadMore.install = install
export default loadMore
const loadmore = {
inserted: function (el, binding) {
// 获取element-ui定义好的scroll盒子
const SELECTWRAP_DOM = el.querySelector(
'.el-select-dropdown .el-select-dropdown__wrap'
)
SELECTWRAP_DOM.addEventListener('scroll', function() {
const CONDITION = this.scrollHeight - this.scrollTop <= this.clientHeight
if (CONDITION) {
binding.value()
}
})
}
}
export default loadmore
\ No newline at end of file
import permission from './permission'
const install = function(Vue) {
Vue.directive('permission', permission)
}
if (window.Vue) {
window['permission'] = permission
Vue.use(install) // eslint-disable-line
}
permission.install = install
export default permission
import store from '@/store'
function checkPermission(el, binding) {
let { arg } = binding
// 小说和游戏的权限控制
// store.state.user.systemRouter === 'game' ? arg = 'game-' + arg : ''
if (store.state.user.systemRouter === 'game') {
arg = 'game-' + arg
} else if (store.state.user.systemRouter === 'playlet') {
arg = 'playlet-' + arg
} else {
}
const roles = store.getters && store.getters.buttonInfo
const hasPermission = Object.prototype.hasOwnProperty.call(roles, arg) ? roles[arg] : false
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
export default {
inserted(el, binding) {
try {
checkPermission(el, binding)
} catch (error) {
console.log(error)
}
},
update(el, binding) {
try {
checkPermission(el, binding)
} catch (error) {
console.log(error)
}
}
}
# v-scroll 下拉加载更多指令
## 功能概述
`v-scroll` 是一个用于实现无限滚动加载的 Vue 自定义指令。当用户滚动到页面底部或接近底部时,自动触发加载更多数据的函数,适用于列表分页加载场景。
## 特性
- 自动检测滚动容器(支持窗口和自定义滚动容器)
- 使用节流控制滚动事件触发频率,提高性能
- 支持自定义触发距离
- 支持禁用功能
- 支持移动端触摸事件
- 自动处理内容不足一屏的情况
## 参数说明
### 指令值
- **类型**`Function`
- **必填**:是
- **说明**:滚动到底部时触发的加载更多函数
### 指令参数
- **语法**`v-scroll:distance`
- **类型**`Number`
- **默认值**`30`(单位:px)
- **说明**:距离底部多少像素时触发加载更多函数
### 修饰符
- **disabled**:禁用滚动加载功能
- **语法**`v-scroll.disabled`
- **说明**:设置后将不会触发加载更多函数
## 使用方法
### 基本用法
```vue
<template>
<div v-scroll="loadMore" class="list-container">
<div v-for="(item, index) in list" :key="index" class="list-item">
{{ item.title }}
</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-if="noMore" class="no-more">没有更多数据</div>
</div>
</template>
<script>
export default {
data() {
return {
list: [],
page: 1,
pageSize: 10,
loading: false,
noMore: false
}
},
mounted() {
// 初始加载第一页数据
this.getList()
},
methods: {
// 加载更多函数,将作为 v-scroll 指令的值
loadMore() {
// 如果正在加载或没有更多数据,则不执行
if (this.loading || this.noMore) return
this.page++
this.getList()
},
async getList() {
this.loading = true
try {
const res = await this.fetchData(this.page, this.pageSize)
if (res.data && res.data.length > 0) {
this.list = [...this.list, ...res.data]
} else {
this.noMore = true
}
} catch (error) {
console.error('加载数据失败', error)
} finally {
this.loading = false
}
},
fetchData(page, pageSize) {
// 实际项目中替换为真实 API 调用
return this.$api.getList({ page, pageSize })
}
}
}
</script>
<style scoped>
.list-container {
height: 500px; /* 设置容器高度以启用滚动 */
overflow-y: auto;
max-width: 360px; /* 适配企业微信侧边栏 */
margin: 0 auto;
}
</style>
```
### 自定义触发距离
通过参数设置距离底部多少像素时触发加载:
```html
<!-- 距离底部 50px 时触发加载更多 -->
<div v-scroll:50="loadMore" class="list-container">
<!-- 列表内容 -->
</div>
```
### 禁用滚动加载
使用 disabled 修饰符禁用滚动加载功能:
```html
<!-- 禁用滚动加载功能 -->
<div v-scroll.disabled="loadMore" class="list-container">
<!-- 列表内容 -->
</div>
```
### 动态控制启用/禁用
结合 Vue 的条件表达式动态控制滚动加载:
```html
<!-- 根据 isLoading 状态动态启用/禁用 -->
<div v-scroll:30="isLoading ? null : loadMore" class="list-container">
<!-- 列表内容 -->
</div>
<!-- 或使用对象语法 -->
<div v-scroll="{ loadMore: loadMoreFn, disabled: isLoading }" class="list-container">
<!-- 列表内容 -->
</div>
```
## 注意事项
1. **滚动容器设置**
- 确保滚动容器有固定高度或最大高度,并设置 `overflow-y: auto``overflow-y: scroll`
- 指令会自动检测最近的滚动容器,如果没有找到,则使用 window 作为滚动容器
2. **性能优化**
- 指令内部已使用节流函数控制滚动事件触发频率,默认为 200ms
- 大数据列表建议使用虚拟滚动结合本指令使用
3. **移动端适配**
- 已支持触摸事件,在移动端也能正常工作
- 在企业微信环境中,最大宽度建议设置为 360px
4. **加载状态处理**
- 在 loadMore 函数中应该有加载状态控制,防止重复触发
- 推荐使用 loading 和 noMore 两个状态分别控制加载中和无更多数据
5. **初始内容不足一屏**
- 指令会在初始化时检查一次是否需要加载更多,处理内容不足一屏的情况
- 初始检查会延迟 50ms 执行,确保 DOM 渲染完成
## 示例场景
### 商品列表无限加载
```vue
<template>
<div class="product-list" v-scroll="loadMoreProducts">
<product-card
v-for="product in products"
:key="product.id"
:product="product"
/>
<div v-if="loading" class="loading-indicator">
<i class="el-icon-loading"></i> 加载中...
</div>
<div v-if="noMore && !loading" class="no-more">
没有更多商品了
</div>
</div>
</template>
```
### 聊天记录上拉加载历史消息
```vue
<template>
<div class="chat-container" v-scroll:50="loadHistoryMessages">
<div class="message-list">
<message-item
v-for="msg in messages"
:key="msg.id"
:message="msg"
/>
</div>
</div>
</template>
```
## 技术实现
该指令基于以下核心实现:
1. 使用 `throttle` 节流函数控制滚动事件触发频率
2. 自动检测并绑定最近的滚动容器
3. 精确计算滚动位置,支持 window 和自定义容器两种情况
4. 完整支持 Vue 指令生命周期(inserted, update, unbind)
\ No newline at end of file
<template>
<div class="scroll-demo">
<h2 class="demo-title">下拉加载更多示例</h2>
<!-- 使用v-scroll指令的列表容器 -->
<div
v-scroll="loadMore"
class="list-container"
ref="listContainer"
>
<!-- 列表项 -->
<div
v-for="(item, index) in list"
:key="index"
class="list-item"
>
<div class="item-avatar">
<img :src="item.avatar" alt="头像">
</div>
<div class="item-content">
<div class="item-title">{{item.title}}</div>
<div class="item-desc">{{item.desc}}</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-status">
<i class="el-icon-loading"></i> 加载中...
</div>
<!-- 无更多数据提示 -->
<div v-if="noMore && !loading" class="no-more">
— 没有更多数据了 —
</div>
</div>
<!-- 控制面板 -->
<div class="control-panel">
<el-switch
v-model="disabled"
active-text="禁用加载"
inactive-text="启用加载"
/>
<el-button
@click="resetList"
type="primary"
size="small"
>
重置列表
</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'ScrollDemo',
data() {
return {
list: [],
page: 1,
pageSize: 10,
loading: false,
noMore: false,
disabled: false
}
},
computed: {
// 根据disabled状态动态计算加载函数
loadMoreHandler() {
return this.disabled ? null : this.loadMore
}
},
mounted() {
// 初始加载第一页数据
this.getList()
},
methods: {
// 加载更多数据
loadMore() {
// 如果正在加载、已禁用或没有更多数据,则不执行
if (this.loading || this.disabled || this.noMore) return
this.page++
this.getList()
},
// 获取列表数据
async getList() {
this.loading = true
try {
// 模拟API请求延迟
await this.sleep(800)
// 模拟API请求返回数据
const res = await this.mockApi(this.page, this.pageSize)
if (res.data && res.data.length > 0) {
// 追加新数据到列表
this.list = [...this.list, ...res.data]
} else {
// 设置无更多数据标记
this.noMore = true
}
} catch (error) {
console.error('加载数据失败', error)
this.$message.error('加载失败,请重试')
} finally {
this.loading = false
}
},
// 重置列表
resetList() {
this.list = []
this.page = 1
this.noMore = false
this.getList()
},
// 模拟API请求
mockApi(page, pageSize) {
return new Promise(resolve => {
// 假设只有5页数据
const hasMore = page <= 5
const data = hasMore
? Array(pageSize).fill().map((_, i) => ({
id: (page - 1) * pageSize + i + 1,
title: `用户${(page - 1) * pageSize + i + 1}`,
desc: `这是第${page}页的第${i + 1}条数据,总第${(page - 1) * pageSize + i + 1}条`,
avatar: `https://randomuser.me/api/portraits/men/${((page - 1) * pageSize + i) % 100}.jpg`
}))
: []
resolve({ data, total: 50 })
})
},
// 辅助方法:延时
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
}
</script>
<style scoped>
.scroll-demo {
max-width: 360px;
margin: 0 auto;
padding: 10px;
}
.demo-title {
text-align: center;
margin-bottom: 15px;
font-size: 18px;
}
.list-container {
height: 70vh;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 4px;
background-color: #fff;
}
.list-item {
display: flex;
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
}
.item-avatar {
width: 40px;
height: 40px;
margin-right: 12px;
}
.item-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.item-content {
flex: 1;
}
.item-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 4px;
}
.item-desc {
font-size: 13px;
color: #606266;
}
.loading-status, .no-more {
text-align: center;
padding: 15px;
color: #909399;
font-size: 14px;
}
.control-panel {
margin-top: 15px;
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f5f7fa;
border-radius: 4px;
}
</style>
\ No newline at end of file
// 表格吸顶
import scroll from './scroll'
const install = function(Vue) {
Vue.directive('scroll', scroll)
}
if (window.Vue) {
window.scroll = scroll
Vue.use(install); // eslint-disable-line
}
scroll.install = install
export default scroll
/*
* 我想封装这样的下拉加载更多的功能指令
* 1. 当页面滚动到底部或者接近底部的时候 触发接口请求请求下一页的数据
* 2.触发的时候 引入 debounce 防抖函数
*/
import { throttle } from '@/utils/index';
const InfiniteScroll = {
name: 'infinite-scroll',
inserted(el, binding, vnode) {
const options = parseOptions(el, binding);
el.__infinite_scroll_options = options;
// 使用节流控制滚动事件触发频率
const scrollHandler = createScrollHandler(el, binding, vnode);
el.__infinite_scroll_handler = throttle(scrollHandler, 200);
// 绑定滚动事件
el.__infinite_scroll_container = bindEvents(el, el.__infinite_scroll_handler);
// 初始检查一次(处理内容不足一屏的情况)
setTimeout(() => {
el.__infinite_scroll_handler();
}, 50);
},
update(el, binding) {
// 更新配置
el.__infinite_scroll_options = parseOptions(el, binding);
},
unbind(el) {
// 解绑事件
if (el.__infinite_scroll_container && el.__infinite_scroll_handler) {
unbindEvents(el, el.__infinite_scroll_container, el.__infinite_scroll_handler);
}
// 清理引用
delete el.__infinite_scroll_container;
delete el.__infinite_scroll_handler;
delete el.__infinite_scroll_options;
}
};
// 解析配置选项
function parseOptions(el, binding) {
return {
// 距离底部多远触发加载,默认30px
distance: binding.arg ? parseInt(binding.arg) : 30,
// 是否禁用
disabled: binding.modifiers.disabled,
// 加载函数
loadMore: binding.value
};
}
// 创建滚动处理函数
function createScrollHandler(el, binding, vnode) {
return function() {
const options = el.__infinite_scroll_options;
// 检查是否禁用
if (options && options.disabled) return;
const scrollContainer = el.__infinite_scroll_container;
const isBottom = isScrollBottom(scrollContainer, el, options.distance);
if (isBottom) {
// 调用加载函数
options.loadMore();
}
};
}
// 绑定事件
function bindEvents(el, handler) {
const scrollContainer = getScrollContainer(el);
scrollContainer.addEventListener('scroll', handler);
// 移动端支持
if ('ontouchend' in document) {
scrollContainer.addEventListener('touchend', handler);
}
return scrollContainer;
}
// 解绑事件
function unbindEvents(el, container, handler) {
container.removeEventListener('scroll', handler);
if ('ontouchend' in document) {
container.removeEventListener('touchend', handler);
}
}
// 获取滚动容器
function getScrollContainer(el) {
let container = el;
while (container && container !== document.body) {
const { overflowY } = window.getComputedStyle(container);
if (['auto', 'scroll', 'overlay'].includes(overflowY)) {
return container;
}
container = container.parentNode;
}
// 默认返回window
return window;
}
// 判断是否滚动到底部
function isScrollBottom(scrollContainer, el, distance = 30) {
// window情况
if (scrollContainer === window) {
return (
Math.ceil(window.innerHeight + window.pageYOffset) >=
document.documentElement.scrollHeight - distance
);
}
// DOM元素情况
return (
Math.ceil(scrollContainer.scrollTop + scrollContainer.clientHeight) >=
scrollContainer.scrollHeight - distance
);
}
export default InfiniteScroll;
\ No newline at end of file
# 使ElementUI的el-table表头自动吸顶,支持左右固定列。
## 引入
```js
import {tableSticky} from 'lp-vue'
Vue.directive('tableSticky', tableSticky)
```
## 注意
主要使用 CSS 属性 position: sticky 实现。
由于使用的是 sticky,所以要保证祖先元素不能设置 overflow:hidden 这类的样式,否则不生效。
使用
在 快速上手 中全局引用可以直接使用。
也可以按需使用:
```js
import {tableSticky} from '.lp-vue'
Vue.directive('tableSticky', tableSticky)
```
# 配置
## 基础使用
如果只是普通表格,不涉及固定列,直接使用即可。
```html
<el-table v-tableSticky>
<!-- 表格内容 -->
</el-table>
```
设置距离
```html
<el-table v-tableSticky="10">
<!-- 表格内容 -->
</el-table>
<el-table v-tableSticky="'.header'">
<!-- 表格内容 -->
</el-table>
```
|类型|说明| 默认值|
|---|---|---|
|number |表头距顶部距离 10|
|string |选择器,自动获取距目标元素高度位置|
## 设置固定列
当表格有固定列时,需要设置 fixed 修饰符。
```html
<el-table v-tableSticky.fixed="10">
<!-- 表格内容 -->
</el-table>
```
## 监听目标元素高度
在使用目标元素来决定吸顶高度时,随着页面的变化可能目标元素的高度会变高,那就有必要使用dom修饰符,如果高度固定就不需要监听;或者目标元素的宽小于等于表格的宽度,这样页面变化会触发表格的监听同样不需要这个监听。所以这个修饰符不是必要的。可以根据实际情况使用,如果发现表头吸顶位置不对,可以使用这个修饰符。
<el-table v-tableSticky.dom="'.header'">
<!-- 表格内容 -->
</el-table>
\ No newline at end of file
// 表格吸顶
import tableSticky from './tableSticky'
const install = function(Vue) {
Vue.directive('tableSticky', tableSticky)
}
if (window.Vue) {
window.tableSticky = tableSticky
Vue.use(install); // eslint-disable-line
}
tableSticky.install = install
export default tableSticky
'use strict'
exports.__esModule = true
function debounce(func, wait, immediate) {
var timeout = void 0
var args = void 0
var context = void 0
var timestamp = void 0
var result = void 0
var later = function later() {
// 据上一次触发时间间隔
var last = +new Date() - timestamp
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key]
}
context = this
timestamp = +new Date()
var callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}
// 头部吸顶
var stickyThead = function stickyThead(el, binging, vnode) {
var top = '0px'
if (!isNaN(Number(binging.value))) {
top = binging.value + 'px'
}
if (typeof binging.value === 'string' && document.querySelector(binging.value)) {
var rect = document.querySelector(binging.value).getBoundingClientRect()
top = rect ? rect.top + rect.height + 'px' : '0px'
}
el.style.overflow = 'visible'
var tHeader = el.querySelector('.el-table__header-wrapper')
tHeader.style.position = 'sticky'
tHeader.style.top = top
tHeader.style.zIndex = 20
// 移除is-hidden
var ths = tHeader.querySelectorAll('th.is-hidden')
ths.forEach(function (item) {
item.classList.remove('is-hidden')
})
// 找到实例
var table = vnode.context.$children.find(function (item) {
return item.$el === el
})
table.doLayout()
// 找到左边固定列
var leftFixed = table.fixedColumns
if (leftFixed && leftFixed.length) {
var leftFixedWidth = 0
leftFixed.forEach(function (item) {
var cell = tHeader.querySelector('th.' + item.id)
if (cell) {
var itemW = cell.getBoundingClientRect().width
cell.style.position = 'sticky'
cell.style.left = leftFixedWidth + 'px'
cell.style.top = top
cell.style.zIndex = 10
leftFixedWidth += itemW
}
})
}
// 找到右边固定列
var rightFixed = table.rightFixedColumns
if (rightFixed && rightFixed.length) {
var rightFixedWidth = 0
for (var i = rightFixed.length - 1; i >= 0; i--) {
var cell = tHeader.querySelector('th.' + rightFixed[i].id)
if (cell) {
var itemW = cell.getBoundingClientRect().width
cell.style.position = 'sticky'
cell.style.right = rightFixedWidth + 'px'
cell.style.top = top
cell.style.zIndex = 10
rightFixedWidth += itemW
}
}
}
}
// 简易吸顶
var sticky = function sticky(el, binging, vnode) {
var top = '0px'
if (!isNaN(Number(binging.value))) {
top = binging.value + 'px'
}
if (typeof binging.value === 'string' && document.querySelector(binging.value)) {
var rect = document.querySelector(binging.value).getBoundingClientRect()
top = rect ? rect.top + rect.height + 'px' : '0px'
}
el.style.overflow = 'visible'
var tHeader = el.querySelector('.el-table__header-wrapper')
tHeader.style.position = 'sticky'
tHeader.style.top = top
tHeader.style.zIndex = 20
}
var tableOb = null
var domOb = null
exports.default = {
inserted: function inserted(el, binging, vnode) {
if (binging.modifiers.fixed) {
// 监听表格变化
tableOb = new ResizeObserver(debounce(function () {
stickyThead(el, binging, vnode)
}, 500))
tableOb.observe(el)
} else {
sticky(el, binging, vnode)
}
if (binging.modifiers.dom) {
// 监听目标dom变化
if (typeof binging.value === 'string') {
var isDom = document.querySelector(binging.value)
if (isDom) {
domOb = new ResizeObserver(debounce(function () {
stickyThead(el, binging, vnode)
}, 500))
domOb.observe(isDom)
}
}
}
},
unbind: function unbind(el, binging, vnode) {
tableOb && tableOb.unobserve(el)
domOb && domOb.unobserve(el)
}
}
\ No newline at end of file
......@@ -2,45 +2,70 @@ import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// import ElementUI from 'element-ui';
import Cookies from 'js-cookie';
import _ from 'lodash';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import 'lib-flexible/flexible.js'
// Vue.use(ElementUI);
Vue.use(ElementUI);
// import '@/styles/element-theme-colors.css';
import '@/styles/index.scss';
import moment from 'moment'
// import VConsole from 'vconsole'; // 注释掉,使用 devMode.js 统一管理
// import 'bi-element-ui/lib/bi-element-ui.css'
import Element from 'bi-eleme'
// import 'bi-eleme/lib/theme-chalk/index.css'
import BiElementUi from 'bi-element-ui'
// import 'bi-element-ui/lib/bi-element-ui.css'
import VConsole from 'vconsole';
import uploading from '@/utils/cos-upload'
import 'element-ui/lib/theme-chalk/index.css';
import errorHandle from '@/utils/errorHandle'
import { getParams,deepClone } from '@/utils/index'
import * as ww from '@wecom/jssdk'
import globalComponent from '@/components/register.js'
import loadmore from '@/directive/loadmore/index.js' // 加载更多
import clickagain from './directive/clickagain'
import permission from '@/directive/permission/index.js' // 权限判断指令
import scroll from '@/directive/scroll' // 下拉加载更多指令
Vue.use(globalComponent).use(permission).use(clickagain).use(loadmore).use(scroll)
// 导入 VConsole 清理工具
import '@/utils/vconsoleCleanup'
// new VConsole();
// 开发环境下初始化 stagewise 工具栏
if (process.env.NODE_ENV === 'development') {
import('@stagewise/toolbar-vue').then(({ StagewiseToolbar }) => {
import('@stagewise-plugins/vue').then(({ VuePlugin }) => {
const stagewiseConfig = {
plugins: [VuePlugin]
};
// 动态创建并挂载 StagewiseToolbar 组件
const ToolbarConstructor = Vue.extend({
render(h) {
return h(StagewiseToolbar, { props: { config: stagewiseConfig } });
}
});
const toolbarInstance = new ToolbarConstructor();
toolbarInstance.$mount();
document.body.appendChild(toolbarInstance.$el);
});
}).catch(err => {
console.error('Failed to initialize stagewise toolbar:', err);
});
}
// 开发环境不收集日志
if (process.env.NODE_ENV !== 'development') {
errorHandle.onload()
}
// 测试一下
Vue.use(uploading)
Vue.use(BiElementUi, {
dev: true,
env: 'development',
system: null
})
Vue.use(Element, {
size: Cookies.get('size') || 'small' // set element-ui default size
// locale: enLang, // 如果使用中文,无需设置,请删除123132
})
// 配置和原型方法设置应该在创建Vue实例之前
Vue.config.productionTip = false
// 配置 Vue DevTools
Vue.config.devtools = process.env.NODE_ENV === 'development'
Vue.prototype.$cookies = Cookies;
Vue.prototype.$lodash = _;
Vue.prototype.$moment = moment;
Vue.prototype.$ww = ww;
Vue.prototype.$clone = deepClone // vue原型挂载递归拷贝方法
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
Vue.config.productionTip = false
Vue.prototype.$cookies = Cookies;
Vue.prototype.$lodash = _;
Vue.prototype.$moment = moment;
# 游戏日志收集功能使用说明
## 概述
`gameLogMixin` 是一个统一的游戏日志收集混入,用于自动监听 Vuex 中的 `send_game_log` 状态变化,并在发送消息成功后自动调用 `send_log_add` 接口记录日志。
## 功能特性
1. **自动监听**: 自动监听 Vuex 中 `send_game_log` 的变化
2. **自动记录**: 在 `sendChatMessage` 成功后自动记录游戏日志
3. **统一管理**: 提供统一的日志记录逻辑,避免重复代码
4. **兼容性**: 兼容 `chatUser``chatUserInfo` 两种用户信息格式
5. **错误处理**: 提供完善的错误处理机制
## 使用方法
### 1. 引入 mixin
```javascript
import gameLogMixin from '@/mixins/gameLogMixin'
export default {
mixins: [gameLogMixin],
// 其他组件配置...
}
```
### 2. 记录游戏日志
有两种方式记录游戏日志:
#### 方式一:手动调用 sendGameLog 方法
```javascript
// 在组件方法中调用
this.sendGameLog({
game_id: 123,
game_name: '游戏名称',
game_type: 1,
main_game_id: 456,
weixin_blongs_id: 789,
type: 2
})
// 然后发送消息
this.sendChatMessage('消息内容', 'text')
```
#### 方式二:直接使用 set_send_game_log
```javascript
this.set_send_game_log({
game_id: 123,
game_name: '游戏名称',
game_type: 1,
main_game_id: 456,
weixin_blongs_id: 789,
type: 2
})
// 然后发送消息
this.sendChatMessage('消息内容', 'text')
```
### 3. 发送消息
mixin 重写了 `sendChatMessage` 方法,支持自动日志记录:
```javascript
// 发送文本消息
this.sendChatMessage('游戏链接: https://example.com', 'text')
// 发送图片消息
this.sendChatMessage('https://example.com/image.jpg', 'image')
// 发送小程序消息
this.sendChatMessage({
appid: 'wx123',
title: '小程序标题',
// 其他小程序参数...
}, 'miniprogram')
```
## 参数说明
### sendGameLog 参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| game_id | String/Number | 是 | 游戏ID |
| game_name | String | 是 | 游戏名称 |
| game_type | String/Number | 否 | 游戏类型 |
| main_game_id | String/Number | 否 | 主游戏ID |
| weixin_blongs_id | String/Number | 否 | 微信归属ID |
| type | Number | 否 | 日志类型,默认为2 |
### sendChatMessage 参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| content | Any | 是 | 消息内容 |
| type | String | 是 | 消息类型:'text', 'image', 'miniprogram' 等 |
## 生命周期
1. 组件创建时,mixin 开始监听 `send_game_log` 变化
2. 调用 `sendGameLog``set_send_game_log` 时,将游戏信息存储到 `pendingGameLog`
3. 调用 `sendChatMessage` 发送消息
4. 消息发送成功后,自动调用 `send_log_add` 接口记录日志
5. 日志记录完成后,清空 `pendingGameLog` 和 Vuex 中的 `send_game_log`
## 注意事项
1. **组件必须有用户信息**: 确保组件中存在 `chatUser``chatUserInfo` 属性
2. **避免重复引入方法**: 使用 mixin 后不需要再手动引入 `sendChatMessage` 和相关 mutations
3. **错误处理**: 即使日志记录失败,也不会影响消息发送功能
4. **性能考虑**: 日志记录是异步进行的,不会阻塞用户操作
## 控制方法
### 禁用日志记录
```javascript
this.disableGameLogging()
```
### 启用日志记录
```javascript
this.enableGameLogging()
```
## 已应用的组件
- `sendGame.vue` - 主游戏发送组件
- `selectChannel.vue` - 渠道选择组件
- `sendSelectChannel.vue` - 发送选择渠道组件
- `SendTransWxGame.vue` - 微信小游戏转端组件
- `SendTransAppGame.vue` - App游戏转端组件
## 调试信息
mixin 会在控制台输出以下调试信息:
- `准备记录游戏日志:` - 当有新的游戏日志信息时
- `发送游戏日志:` - 发送日志到接口时
- `游戏日志记录成功` - 日志记录成功时
- `游戏日志记录失败:` - 日志记录失败时
## 故障排除
### 1. 日志没有记录
- 检查是否正确调用了 `sendGameLog` 方法
- 检查 `game_id``game_name` 是否有效
- 检查组件是否有用户信息(`chatUser``chatUserInfo`
### 2. 消息发送失败
- 检查网络连接
- 检查消息内容格式是否正确
- 查看控制台错误信息
### 3. 重复日志记录
- 确保没有重复调用 `sendGameLog` 方法
- 检查是否有多个地方调用了 `set_send_game_log`
\ No newline at end of file
import { mapState, mapMutations } from 'vuex'
import { send_log_add } from '@/api/works'
import { sendChatMessage as originalSendChatMessage } from '@/utils/index'
export default {
data() {
return {
pendingGameLog: null, // 待发送的游戏日志信息
isLoggingEnabled: true // 日志记录开关
}
},
computed: {
...mapState('game', ['send_game_log']),
// 统一处理chatUser,兼容chatUser和chatUserInfo
chatUserForLog() {
return this.chatUser || this.chatUserInfo || {}
}
},
watch: {
// 监听send_game_log变化,当有新的日志信息时准备记录
send_game_log: {
handler(newVal) {
if (newVal && this.isLoggingEnabled) {
this.pendingGameLog = { ...newVal }
console.log('准备记录游戏日志:', this.pendingGameLog)
}
},
immediate: false
}
},
methods: {
...mapMutations('game', ['set_send_game_log']),
/**
* 重写sendChatMessage方法,在发送成功后自动记录日志
* @param {*} content 消息内容
* @param {*} type 消息类型
*/
async sendChatMessage(content, type) {
// 开始发送
console.log(content,type,'content,type',this.pendingGameLog,this.isLoggingEnabled)
try {
// 调用原始的sendChatMessage方法
const result = await originalSendChatMessage(content, type)
console.log(result,'发送成功的回调')
// 如果有待记录的游戏日志,则记录
if (this.pendingGameLog && this.isLoggingEnabled) {
await this.recordGameLog(result, content, type)
}
return result
} catch (error) {
console.error('发送消息失败:', error)
throw error
}
},
/**
* 记录游戏日志
* @param {Object} messageInfo 发送消息的返回信息
* @param {*} content 消息内容
* @param {String} type 消息类型
*/
async recordGameLog(messageInfo, content, type) {
console.log(messageInfo, content, type,'进入记日志方法')
if (!this.pendingGameLog) {
return
}
try {
const chatUser = this.chatUserForLog
console.log(this.pendingGameLog,'pendingGameLog')
// 构建日志数据
const logData = {
game_id: this.pendingGameLog.game_id,
game_name: this.pendingGameLog.game_name,
game_type: this.pendingGameLog.game_type || '',
main_game_id: this.pendingGameLog.main_game_id || '',
weixin_blongs_id: this.pendingGameLog.weixin_blongs_id || '',
content: [this.formatLogContent(messageInfo?.message || content, type)],
type: this.pendingGameLog.type || 2,
frontend_message_id: messageInfo?.frontend_message_id || '',
session_id: messageInfo?.session_id || '',
userid: chatUser.userid || '',
external_userid: chatUser.external_userid || ''
}
console.log('发送游戏日志:', logData)
// 调用日志记录接口
const response = await send_log_add(logData)
if (response.status_code === 1) {
console.log('游戏日志记录成功')
} else {
console.warn('游戏日志记录失败:', response.msg)
}
// 清空待记录的日志信息
this.pendingGameLog = null
this.set_send_game_log(null)
} catch (error) {
console.error('记录游戏日志失败:', error)
// 即使日志记录失败,也要清空待记录信息,避免影响后续操作
this.pendingGameLog = null
this.set_send_game_log(null)
}
},
/**
* 格式化日志内容
* @param {*} content 消息内容
* @param {String} type 消息类型
* @returns {String} 格式化后的内容
*/
formatLogContent(content, type) {
if (type === 'text') {
return content
} else if (type === 'image') {
return `[图片] ${content}`
} else if (type === 'miniprogram') {
return `[小程序] ${content.title || ''}`
} else {
return `[${type}] ${JSON.stringify(content)}`
}
},
/**
* 手动记录游戏日志(兼容现有的sendGameLog方法)
* @param {Object} gameInfo 游戏信息
*/
sendGameLog(gameInfo) {
if (gameInfo && gameInfo.game_id && gameInfo.game_name) {
const logInfo = {
game_id: gameInfo.game_id,
game_name: gameInfo.game_name,
game_type: gameInfo.game_type || '',
main_game_id: gameInfo.main_game_id || '',
weixin_blongs_id: gameInfo.weixin_blongs_id || '',
type: gameInfo.type || 2
}
this.set_send_game_log(logInfo)
}
},
/**
* 禁用日志记录(在某些特殊情况下使用)
*/
disableGameLogging() {
this.isLoggingEnabled = false
},
/**
* 启用日志记录
*/
enableGameLogging() {
this.isLoggingEnabled = true
}
},
beforeDestroy() {
// 组件销毁前清理
this.pendingGameLog = null
}
}
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论