提交 bdcdf05f 作者: 毛细亚

Merge branch 'main'

---
description:
globs:
alwaysApply: false
---
# 项目结构说明
这是一个 H5的移动端页面 这是一个 H5的移动端页面 最大宽度是 380px 最小宽度是 360px 样所以需要用到 css 自适应 弹性伸缩
本项目为基于 Vue.js 的前端应用,主要目录结构如下:
- [src/](mdc:src):主源码目录,包括:
- [main.js](mdc:src/main.js):应用入口文件。
- [App.vue](mdc:src/App.vue):根组件,负责全局认证逻辑。
- [router/](mdc:src/router):路由配置,主文件为 [index.js](mdc:src/router/index.js)。
- [store/](mdc:src/store):Vuex 状态管理,主文件为 [index.js](mdc:src/store/index.js)。
- [api/](mdc:src/api):接口请求封装,示例:[user.js](mdc:src/api/user.js)。
- [utils/](mdc:src/utils):工具函数和请求封装,如 [request.js](mdc:src/utils/request.js)。
- [mixins/](mdc:src/mixins):Vue 混入逻辑,如 [external_userid.js](mdc:src/mixins/external_userid.js)。
- [views/](mdc:src/views):页面组件,如 [HomeView.vue](mdc:src/views/HomeView.vue)、[AboutView.vue](mdc:src/views/AboutView.vue)。
- [styles/](mdc:src/styles):全局和局部样式文件。
- [components/](mdc:src/components):可复用组件(当前为空)。
- [assets/](mdc:src/assets):静态资源(如图片等)。
- @docs: 文档内容
- [public/](mdc:public):静态资源目录,供打包时复制到最终输出。
- [package.json](mdc:package.json):项目依赖和脚本配置。
- [README.md](mdc:README.md):项目说明文档。
如需详细了解某一目录或文件,可参考上述路径。
# 技术库
- 前端框架 vue 2.6
- 脚手架 用 vue-cli 5.0.0 创建
- 路由 使用 vue-router 3.5.1 管理路由
- 状态管理 使用 vuex 3.6.2 管理状态
- 使用 element-ui 2.15.6 作为 UI 库
- axios 请求 api
- 基于企业微信 新版本 jssdk 来进行开发 企业微信的变量是 `ww`
# 开发规范
- 组件命名规范
- Props类型声明 Props 尽量用规范的写法 并且声明默认值
- 响应式最佳实践
- 生命周期规范
- 事件处理规范
- 样式作用域
- 使用 try/catch 块处理异步操作
- 使用组件化的概念 尽量按照功能切分成多个组件 一个组件的代码不要太多
工程化要求:
- ESLint + Prettier
- 性能优化
- 代码分割
- 懒加载实现
# UI 规范
因为 企业微信侧边栏 宽度有限 而且是一个 H5 移动端的页面 最大宽度是 380px 最小宽度 是 360px 所以尽量使用 自适应的样式来进行开发
---
description:
globs:
alwaysApply: false
---
......@@ -5,5 +5,7 @@ VUE_APP_BASE_API = '/dev-api'
VUE_APP_URL = 'http://localhost:9528'
# 掌权登录跳转url
NODE_ENV = 'development'
ENV = 'development'
VUE_APP_ZQLOGIN_URL = 'http://zq.zwwlkj03.top'
isTestWx = true
\ No newline at end of file
# just a flag
ENV = 'production'
NODE_ENV = 'production'
# base api
VUE_APP_BASE_API = '/api/'
# 掌权登录跳转url
VUE_APP_ZQLOGIN_URL = 'http://zq.wozhangwan.com'
# build src
VUE_APP_BUILD_DIR = 'dist'
# just a flag
NODE_ENV = 'staging'
ENV = 'staging'
# base api
VUE_APP_BASE_API = '/api/'
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
parserOptions: {
parser: '@babel/eslint-parser'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}
.DS_Store
node_modules
/dist
/company_app
# local env files
......@@ -21,3 +22,4 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
# cebianlan
# 基于企业微信客户端开发 企业微信侧边栏H5 网页应用
基于企业微信客户端 开发侧边栏功能 企业微信的文档 [企业微信文档]([链接地址](https://developer.work.weixin.qq.com/document/path/94352) )
## 项目介绍
这是一个 H5的移动端页面 这是一个 H5的移动端页面 最大宽度是 380px 最小宽度是 360px 样所以需要用到 css 自适应 弹性伸缩 企微微信中内嵌掌微第三方页面 来支持开发企业微信侧边栏功能
## 项目结构
## Project setup
```
pnpm install
```
- @src/:主源码目录,包括:
- @main.js:应用入口文件。
- @App.vue:根组件,负责全局认证逻辑。
- @router/:路由配置,主文件为 @index.js。
- @store/:Vuex 状态管理,主文件为 @index.js。
- @api/:接口请求封装,示例:@user.js。
- @utils/:工具函数和请求封装,如 @request.js。
- @mixins/:Vue 混入逻辑,如 @external_userid.js。
- @views/:页面组件,如 @HomeView.vue、@AboutView.vue。
- @styles/:全局和局部样式文件。
- @components/:可复用组件(当前为空)。
- @assets/:静态资源(如图片等)。
- @docs: 文档内容
- @public/:静态资源目录,供打包时复制到最终输出。
- @package.json:项目依赖和脚本配置。
- @README.md:项目说明文档。
### Compiles and hot-reloads for development
```
pnpm run serve
```
### Compiles and minifies for production
```
pnpm run build
```
如需详细了解某一目录或文件,可参考上述路径。
### Lints and fixes files
```
pnpm run lint
```
## 技术库
- 前端框架 vue 2.6
- 脚手架 用 vue-cli 5.0.0 创建
- 路由 使用 vue-router 3.5.1 管理路由
- 状态管理 使用 vuex 3.6.2 管理状态
- 使用 element-ui 2.15.6 作为 UI 库
- axios 请求 api
- 基于企业微信 新版本 jssdk 来进行开发 企业微信的变量是 `ww`
## 开发规范
- 组件命名规范
- Props类型声明 Props 尽量用规范的写法 并且声明默认值
- 响应式最佳实践
- 生命周期规范
- 事件处理规范
- 样式作用域
- 使用 try/catch 块处理异步操作
- 使用组件化的概念 尽量按照功能切分成多个组件 一个组件的代码不要太多
工程化要求:
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
- ESLint + Prettier
- 性能优化
- 代码分割
- 懒加载实现
## UI 规范
因为当前页面是在企微的侧边栏中打开的 所以需要适配企微的侧边栏样式 企微侧边栏 最大的宽度是 320px 所有页面需要适配320px的宽度,必须要使用自适应的方法 来开发页面 不能写死超过 320px 的宽度 宽度的样式一切都是可伸缩的
\ 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.0">
<link rel="icon" href="favicon.ico">
<title>company_app</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>
<script defer src="static/js/chunk-vendors.js"></script><script defer src="static/js/app.js"></script></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>
{
"name": "cebianlan",
"name": "company_app",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"build:prod": "vue-cli-service build --mode prod",
"build:stage": "vue-cli-service build --mode staging",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@wecom/jssdk": "^2.3.1",
"axios": "^1.9.0",
"babel-plugin-component": "^1.1.1",
"bi-eleme": "^2.4.4",
"bi-element-ui": "^1.5.2",
"clipboard": "^2.0.11",
"core-js": "^3.8.3",
"cos-js-sdk-v5": "^1.10.1",
"dingtalk-jsapi": "^3.1.0",
"element-ui": "^2.15.14",
"js-cookie": "^3.0.5",
"lib-flexible": "^0.3.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"postcss-plugin-px2rem": "^0.8.1",
"sass": "^1.89.0",
"sass-loader": "^16.0.5",
"vconsole": "^3.15.1",
"vue": "^2.6.14",
"vue-router": "^3.5.1",
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"element-theme-chalk": "^2.15.14",
"vue-template-compiler": "^2.6.14"
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -6,12 +6,13 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<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>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<template>
<div id="app">
<router-view/>
<div id="app" class="mobile-app-wrapper">
<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"
>
<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>
<!-- 绑定的 w 账号 -->
<bindUserList />
</div>
<div class="mobile-content">
<router-view></router-view>
</div>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
<script>
import bindUserList from '@/views/components/bindGameAccount/bindUserList.vue'
import { getToken } from '@/utils/auth'
import { mapState } from 'vuex'
import Cookies from 'js-cookie'
export default {
name: 'App',
components: {
bindUserList
},
data() {
return {
menuList: [
{
label: '客户信息',
path: '/userInfo'
},
// {
// label: '快捷回复',
// path: '/quickReply'
// },
// {
// label: '礼包记录',
// path: '/giftRecord'
// },
// {
// label: '申请记录',
// path: '/applyRecord'
// },
// {
// label: '快捷发送',
// path: '/quickSend'
// },
// {
// label: '通讯录',
// path: '/addressBook'
// },
],
selectedPath: '/userInfo',
showMemberId: false,
}
},
computed:{
...mapState('user',['external_userid','token']),
},
watch: {
'$route.path'(val) {
// 处理各种可能的路径情况
if (val === '/' || val === '/index.html') {
this.selectedPath = '/userInfo'
} else {
this.selectedPath = val
}
console.log('路由变化:', val, '选中路径:', this.selectedPath)
},
// 监听 external_userid 的变化,确保界面及时更新
external_userid: {
handler(newVal) {
if (newVal) {
this.$nextTick(() => {
this.showMemberId = true
console.log('external_userid 已设置:', newVal, window.location.href, this.token,Cookies.get('token'))
// 强制更新组件
this.$forceUpdate()
})
}
},
immediate: true
}
},
mounted() {
// 初始化时处理路径
const currentPath = this.$route.path
if (currentPath === '/' || currentPath === '' || currentPath === '/index.html') {
this.selectedPath = '/userInfo'
} else {
this.selectedPath = currentPath
}
console.log('创建时路径:', currentPath, '选中路径:', this.selectedPath)
},
methods:{
handleSelect(key, keyPath) {
console.log('菜单选择:', key, keyPath, window.location.href)
}
}
}
</script>
nav {
padding: 30px;
<style scoped>
#app{
width: 100%;
height: 100%;
background: #fff;
}
nav a {
.mobile-app-wrapper {
width: 100%;
margin: 0 auto;
background: #f0f2f5;
min-height: 100vh;
display: flex;
flex-direction: column;
box-shadow: 0 0 12px rgba(0,0,0,0.04);
}
.mobile-menu-bar {
background: #fff;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 2px 8px #f0f1f2;
z-index: 10;
}
.mobile-el-menu {
border: none;
background: #fff;
display: flex;
justify-content: flex-start;
padding-left: 16px;
}
.mobile-menu-item {
font-size: 14px ;
padding: 0 16px !important;
min-width: 0;
flex: none;
width: auto;
max-width: 120px;
text-align: center;
margin-right: 8px;
transition: all 0.3s ease;
}
.mobile-el-menu .el-menu-item.is-active {
font-weight: bold;
color: #2c3e50;
}
nav a.router-link-exact-active {
color: #42b983;
.mobile-content {
flex: 1;
overflow-y: auto;
}
.mobile-content > div {
background: #fff;
border-radius: 8px;
min-height: 60vh;
padding: 10px;
}
.el-menu--horizontal>.el-menu-item{
height: 50px;
line-height: 50px;
}
body {
background: #f0f2f5;
}
</style>
import request from "@/utils/request";
// 游戏业务所属的接口信息
// 所属分组下拉
export function cross_systemRequest(data) {
data.api = "/api" + data.api;
data.params
? (data.params.corp_id = window.localStorage.getItem("corp_id") || "")
: "";
return request({
url: "/admin/cross_system/request",
method: "post",
data,
});
}
// 搜索下拉
export function searchcondition(data) {
return new Promise((resovle, reject) => {
cross_systemRequest({
system: "pigeon",
api: "/api/searchcondition/index",
params: data,
}).then((res) => {
resovle(res);
});
});
}
\ No newline at end of file
import request from '@/utils/request'
import axios from 'axios'
// import { getSignatureData } from '@wecom/jssdk'
export function cross_systemRequest(data) {
!data.noApi ? data.api = '/api' + data.api : ''
return request({
url: '/api/cross_system/request',
method: 'post',
headers:{
"Corp-Id":"wweaefe716636df3d1"
},
data
})
}
// 获取 企微登录的信息
export function getAuthUser(data) {
return request({
url: '/api/agent/getAuthUser',
url: '/api/sidebar_login/workWeiXin',
method: 'post',
data
})
}
// 获取组织列表
export function getOrganization(data) {
return axios.post('https://zq.zwwlkj03.top/api/login/organization',data)
}
// 获取签名信息
export function getSignature(data) {
return request({
url: '/sidebar/work_wei_xin/signature',
method: 'post',
data
})
}
// 上传接口
export function uploadCos(params) {
return request({
url: '/api/common/getCosSts',
method: 'get',
params
})
}
// 请求企业配置
export function companyviewConfig(data) {
return request({
url: '/api/corp/viewConfig',
method: 'post',
data
})
}
// 下线
export function logout(data) {
return request({
url: '/sidebar/work_wei_xin/logout',
method: 'post',
data
})
}
// 客户绑定掌游账号列表
import request from '@/utils/request'
function returnApi(api) {
return '/sidebar' + api
}
// w 账号绑定列表
export function zyouBindMember(data) {
return request({
url: returnApi('/external_user/zyouBindMember'),
method: 'post',
data
})
}
// 检查账号绑定
export function checkZyouBind(data) {
return request({
url: returnApi('/external_user/checkZyouBind'),
method: 'post',
data
})
}
// 绑定掌游账号
// 绑定掌游账号
export function zyouBind(data) {
return request({
url: returnApi('/external_user/zyouBind'),
method: 'post',
data
})
}
export function detailsInfoRequest(data) {
return request({
url: returnApi('/external_user/info'),
method: 'post',
data
})
}
// 是否转端
export function toTransfer(data) {
return request({
url: returnApi('/external_user/toTransfer'),
method: 'post',
data
})
}
// 同步智能标签
export function syncSessionIntelTag(data) {
return request({
url: '/sidebar/group_tag_detail/syncSessionIntelTag',
method: 'post',
data
})
}
// 关联客服
export function memberBindCser(data) {
return request({
url: returnApi('/external_user/memberBindCser'),
method: 'post',
data
})
}
// 修改用户信息
export function editUser(data) {
return request({
url: returnApi('/external_user/edit'),
method: 'post',
data
})
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="50px" height="50px" viewBox="0 0 50 50" 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(-1850, -240)">
<g id="5.反馈/3.Modal弹窗/展示/3空" transform="translate(1296, 0)">
<g id="已通过备份" transform="translate(554, 240)">
<rect id="矩形" fill="#000000" fill-rule="nonzero" opacity="0" x="0" y="0" width="50" height="50"></rect>
<path d="M48.9604167,18.1563095 L41.587381,6.53083335 C40.9435714,5.51708335 39.5982143,5.21714287 38.5814286,5.85535713 L1.78476191,29.1335714 C0.770773828,29.777381 0.471071436,31.1227381 1.10791665,32.1336905 L8.48095239,43.7606548 C9.12482144,44.7718453 10.470119,45.0715477 11.4841667,44.4333929 L48.2822024,21.1567857 C49.2989881,20.512619 49.6044643,19.1689286 48.9604167,18.1563095 Z M47.4918452,19.907381 L10.6938095,43.1870238 C10.3683333,43.3920833 9.94232144,43.2960714 9.73726191,42.9719643 L2.36422617,31.3450595 C2.15910713,31.0195238 2.25517856,30.5949405 2.58065479,30.3870833 L39.380119,7.10886904 C39.7058929,6.90517856 40.1332143,7.00142856 40.3352976,7.32553569 L47.711369,18.9522619 C47.9092262,19.2707738 47.8117857,19.6981548 47.4918452,19.907381 Z M15.7707738,12.5301786 L16.3818452,13.9957143 L17.1735714,12.6204167 L18.7553571,12.4927381 L17.6897619,11.313869 L18.0526786,9.77208335 L16.6069048,10.4219643 L15.2488095,9.6 L15.4208333,11.1790476 L14.2161905,12.2060714 L15.7707738,12.5301786 L15.7707738,12.5301786 Z M25.1401786,8.16726191 L24.3800595,6.77196431 L23.73625,8.22470239 L22.1730357,8.52303569 L23.3532738,9.58142856 L23.1495833,11.1546429 L24.5263095,10.3586905 L25.9620833,11.04 L25.6307738,9.48541665 L26.7191667,8.33232144 L25.1401786,8.16726191 Z M39.4247619,31.540119 L38.1668452,30.5733333 L38.16125,32.1581548 L36.8475595,33.0489286 L38.3605357,33.5479762 L38.8064881,35.0695238 L39.7444643,33.7888095 L41.3320833,33.8332738 L40.3999405,32.5455357 L40.9377381,31.0581548 L39.4247619,31.540119 L39.4247619,31.540119 Z M34.2903571,37.8589286 L33.6909524,36.3889881 L32.8861905,37.75875 L31.3057738,37.8734524 L32.361369,39.0580357 L31.9855952,40.5982143 L33.4397619,39.9629762 L34.7923214,40.8032738 L34.6330357,39.2228571 L35.8376786,38.1974405 L34.2903571,37.8589286 L34.2903571,37.8589286 Z M24.884881,42.1558333 L25.6380357,43.5569643 L26.294881,42.1083929 L27.8580952,41.8289286 L26.6848214,40.758869 L26.900119,39.1870833 L25.5175595,39.9688095 L24.0889881,39.2775595 L24.4073214,40.8291071 L23.3030952,41.9780357 L24.884881,42.1558333 L24.884881,42.1558333 Z M10.7956548,18.6727381 L12.046369,19.655119 L12.0649405,18.0689286 L13.3844643,17.1825595 L11.879881,16.6733333 L11.4452976,15.1445833 L10.4959524,16.4166667 L8.90833335,16.3592262 L9.82767856,17.6529167 L9.27964287,19.1445238 L10.7956548,18.6727381 L10.7956548,18.6727381 Z M5.20952383,25.5207738 C5.04327383,18.8059524 8.30892861,12.1672024 14.3926786,8.31934521 C20.4777976,4.47142856 27.8752976,4.36238091 33.8844048,7.37720239 L35.3357738,6.45922617 C33.5575,5.46541665 31.6443453,4.73815474 29.6294048,4.28636904 C26.9373214,3.68113096 24.1952977,3.60529761 21.4845833,4.05130952 C18.6779167,4.51583335 16.0247024,5.52845239 13.6009524,7.06446431 C11.178631,8.6004167 9.12482144,10.562381 7.50416665,12.900119 C5.93517856,15.1545833 4.83238096,17.6659524 4.22571431,20.3535714 C3.77392861,22.3655357 3.61327383,24.4049405 3.75392856,26.437381 L5.20952383,25.5207738 Z M44.7254167,24.4939881 C44.898869,31.2088095 41.6332143,37.8475595 35.5494643,41.6940476 C29.4657143,45.546131 22.0669048,45.652381 16.0591071,42.637619 L14.6049405,43.5569643 C16.3832143,44.5479167 18.2977381,45.2767262 20.3129762,45.7283929 C23.0047619,46.333631 25.7468452,46.4125 28.4575,45.9634524 C31.2641667,45.498869 33.9174405,44.4876786 36.3397619,42.9516667 C38.7620833,41.4156547 40.8158929,39.4536905 42.4379167,37.117381 C44.001131,34.860119 45.1039286,32.3503571 45.7091667,29.6641071 C46.1623214,27.6505357 46.3202381,25.6097619 46.1795238,23.577381 L44.7254167,24.4939881 Z M18.781131,15.243631 C20.485,14.1680357 22.3593453,13.5941667 24.2410714,13.4795238 L26.4869048,12.0595833 C23.5195238,11.7096429 20.5480357,12.3780953 17.9838095,13.9973214 C15.4266071,15.6149405 13.5520833,18.0172619 12.6013095,20.8453571 L14.8457738,19.4256548 C15.7579762,17.7675596 17.0789286,16.3220238 18.781131,15.243631 Z M31.1595238,34.7697619 C29.4556548,35.8483929 27.5827381,36.4220238 25.7010119,36.5366667 L23.4564881,37.9566071 C26.4225,38.3065476 29.392619,37.6366667 31.9582143,36.0174405 C34.5154167,34.40125 36.3899405,31.9975 37.3392857,29.1694048 L35.0961905,30.5921429 C34.1841071,32.2427977 32.8617857,33.6941667 31.1595238,34.7697619 Z" id="形状" fill="#00BF8A" fill-rule="nonzero"></path>
<text id="已完成" transform="translate(25, 24.5) rotate(-32) translate(-25, -24.5)" font-family="STSongti-SC-Black, Songti SC" font-size="12" font-weight="700" fill="#00BF8A">
<tspan x="7" y="29">已完成</tspan>
</text>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<template>
<div class="search-item">
<div class="item-label">{{ label }}</div>
<div class="item-content">
<el-select v-model.trim="main_game_id" filterable remote clearable :style="{ 'width': width || '' }"
reserve-keyword placeholder="请输入主游戏名" :remote-method="remoteMethod" :loading="loading" :disabled='disabled'
@change="returnSelectResult" @focus="gameNameList = optionsList">
<el-option v-for="item in gameNameList" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</div>
</div>
</template>
<script>
import { selectSearch } from '@/api/game'
import { searchcondition } from '@/api/pigeon'
export default {
// gameType:存在的时候取信鸽的游戏列表接口 gameDefaultList 编辑的时候 如果没有当前的游戏 id 主动加上去
props: ['defaultValue', 'width', 'label', 'disabled','gameType','gameDefaultList','isResize'],
data() {
return {
loading: false,
main_game_id: '',
gameNameList: [],
optionsList: []
}
},
watch: {
defaultValue(newVal) {
if (newVal) {
this.gameType?this.main_game_id = newVal:this.main_game_id = Number(newVal)
} else {
return ''
}
},
isResize(newVal, oldVal) {
if (newVal) {
this.main_game_id = ''
this.$emit('result', this.main_game_id, '')
}
},
},
mounted() {
this.defaultValue && this.gameType ? this.main_game_id = Number(this.defaultValue) : this.main_game_id = this.defaultValue
this.switchGameList()
},
methods: {
switchGameList() {
if(this.gameType){
this.requestPigeonGameList()
}else{
this.requestGameList()
}
},
requestGameList() {
const data = {
type: 'mainGameList',
value: '',
weixin_blong_id: ''
}
selectSearch(data).then(res => {
this.loading = false
if (res.status_code == 1) {
this.gameNameList = this.optionsList = res.data.data
}
})
},
requestPigeonGameList() {
const data = {
corp_gift_package_main_games:1,
}
searchcondition(data).then(res => {
this.loading = false
if (res.status_code == 1 && res.data.corp_gift_package_main_games?.length > 0) {
this.gameNameList = this.optionsList = res.data.corp_gift_package_main_games
if(this.gameDefaultList.length > 0){
const gameItem = this.gameNameList.find(item=>item.value === this.gameDefaultList[0].value)
if(!gameItem){
this.gameNameList = this.optionsList = this.gameNameList.concat(this.gameDefaultList)
}
}
}
})
},
remoteMethod(query) {
if (query !== '') {
this.gameNameList = this.optionsList.filter(item => {
return item.label.toLowerCase()
.indexOf(query.toLowerCase()) > -1
})
} else {
this.gameNameList = []
}
},
returnSelectResult(value) {
const label = this.gameNameList.find(item => item.value == this.main_game_id)
label ? this.$emit('result', this.main_game_id, label.label) : this.$emit('result', this.main_game_id, '')
}
}
}
</script>
<style lang="scss" scoped>
.newPage {
width: 100%;
height: 100%;
.el-select {
width: 100%;
}
}
</style>
\ No newline at end of file
<template>
<div class="no-content-container">
<div class="no-content-box">
<!-- 自定义 SVG 图标 -->
<!-- <i class=""></i> -->
<!-- 文字内容 -->
<div class="content">
<h3 class="title">{{ title }}</h3>
<p class="description">{{ description }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'NoContent',
props: {
// 主标题
title: {
type: String,
default: '暂无数据'
},
// 描述文字
description: {
type: String,
default: '当前没有任何数据,请稍后再试或联系管理员'
},
// 是否显示操作区域
showAction: {
type: Boolean,
default: false
},
// 是否显示刷新按钮
showRefresh: {
type: Boolean,
default: false
},
// 图标颜色主题
iconTheme: {
type: String,
default: 'default', // default, primary, success, warning, danger
validator: (value) => ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
}
},
methods: {
handleRefresh() {
this.$emit('refresh');
}
}
}
</script>
<style lang="scss" scoped>
.no-content-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
padding: 40px 20px;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 12px;
position: relative;
overflow: hidden;
// 背景装饰
&::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(0, 191, 138, 0.05) 0%, transparent 50%);
pointer-events: none;
}
}
.no-content-box {
text-align: center;
max-width: 400px;
position: relative;
z-index: 1;
}
.icon-wrapper {
position: relative;
margin-bottom: 32px;
display: inline-block;
}
.no-data-icon {
width: 120px;
height: 120px;
color: #6c757d;
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.1));
animation: float 3s ease-in-out infinite;
}
// 图标主题色
.no-content-container[data-theme="primary"] .no-data-icon {
color: #00BF8A;
}
.no-content-container[data-theme="success"] .no-data-icon {
color: #67c23a;
}
.no-content-container[data-theme="warning"] .no-data-icon {
color: #e6a23c;
}
.no-content-container[data-theme="danger"] .no-data-icon {
color: #f56c6c;
}
// 装饰圆点
.decorative-dots {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
}
.dot {
position: absolute;
width: 6px;
height: 6px;
background: #00BF8A;
border-radius: 50%;
opacity: 0.6;
}
.dot-1 {
top: 20%;
left: 15%;
animation: pulse 2s ease-in-out infinite;
}
.dot-2 {
top: 30%;
right: 20%;
animation: pulse 2s ease-in-out infinite 0.5s;
}
.dot-3 {
bottom: 25%;
left: 25%;
animation: pulse 2s ease-in-out infinite 1s;
}
// 文字内容
.content {
.title {
font-size: 24px;
font-weight: 600;
color: #2c3e50;
margin: 0 0 12px 0;
line-height: 1.3;
}
.description {
font-size: 14px;
color: #6c757d;
line-height: 1.6;
margin: 0 0 24px 0;
max-width: 300px;
margin-left: auto;
margin-right: auto;
}
}
.action-area {
margin-top: 32px;
.el-button {
padding: 12px 24px;
border-radius: 6px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0, 191, 138, 0.3);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 191, 138, 0.4);
}
i {
margin-right: 6px;
}
}
}
// 动画效果
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
@keyframes pulse {
0%, 100% {
opacity: 0.6;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
}
// 响应式设计
@media (max-width: 768px) {
.no-content-container {
min-height: 250px;
padding: 30px 15px;
}
.no-data-icon {
width: 80px;
height: 80px;
}
.content {
.title {
font-size: 20px;
}
.description {
font-size: 13px;
}
}
.action-area {
margin-top: 24px;
.el-button {
padding: 10px 20px;
font-size: 14px;
}
}
}
// 暗色主题支持
@media (prefers-color-scheme: dark) {
.no-content-container {
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
&::before {
background: radial-gradient(circle, rgba(0, 191, 138, 0.1) 0%, transparent 50%);
}
}
.content {
.title {
color: #ecf0f1;
}
.description {
color: #bdc3c7;
}
}
}
</style>
\ No newline at end of file
<template>
<div class="newPage rowFlex allCenter">
<el-pagination
:total="Number(pageInfo.total)"
:current-page.sync="pageInfo.page"
:page-size="Number(pageInfo.page_size)"
background
layout="total,prev, pager, next, jumper"
@current-change="handleCurrentChange"
>
</el-pagination>
</div>
</template>
<script type="text/javascript">
export default {
name: 'page',
props: ['pageInfo'],
data() {
return {}
},
mounted() {
},
methods: {
handleCurrentChange(newPage) {
this.pageInfo.page = newPage
this.$emit('requestNextPage', this.pageInfo)
}
}
}
</script>
<style lang="scss" scoped>
.newPage {
width: 100%;
height: 60px;
background: #fff;
border-radius: 0 0 5px 5px;
padding-bottom: 20px;
}
</style>
\ No newline at end of file
<template>
<!-- 简单版的图文编辑器 可以上传 截图上传图片 能实现简单的图文编辑功能 -->
<div class="textEditor">
<div
class="rowFlex columnCenter textEditorTitle"
@click.stop="watchImage"
>
<el-image
class="watchImage"
src="https://wanxiaomeng-1255977238.cos.ap-shanghai.myqcloud.com/mxy/web/eye.png"
> </el-image>
<p class="watchImageText">查看大图</p>
</div>
<!-- 只展示不编辑 -->
<div
v-if="!contenteditable"
:id="domid"
class="qualityRemark noActive"
>
<div v-html="remark"></div>
</div>
<!-- 可以编辑 -->
<div
v-else
:id="domid"
class="qualityRemark"
:class="contenteditable ? 'active' : 'noActive'"
:contenteditable="contenteditable"
@blur.stop="handleBlurEvent"
@input="handleInputEvent"
></div>
<!-- 查看大图 -->
<el-dialog
title="查看大图"
append-to-body
center
:visible.sync="showScoleImage"
>
<div class="showScoleImageContent columnFlex allCenter" v-html="remark"></div>
</el-dialog>
</div>
</template>
<script>
import { base64toFile } from '@/utils/index'
export default {
props: ['remark', 'contenteditable', 'domid'], // remark 原来的图文内容 contenteditable 是否可编辑 domid 编辑器的 DomId resultReamrk 方法吐出最后的编辑好的内容
data() {
return {
srcList: [],
chatMessage: {},
imgList: [],
htmlContent: '',
showScoleImage: false
}
},
mounted() {
this.$nextTick(() => {
this.chatMessage = document.getElementById(`${this.domid}`)
this.remark && this.remark.length > 0 && this.contenteditable ? this.chatMessage.innerHTML = this.remark : ''
})
},
methods: {
handleBlurEvent() {
// this.handleImage()
},
handleInputEvent() {
this.handleImage()
},
async watchImage() {
if (this.remark.trim().length > 0) {
this.chatMessage.innerHTML = this.remark
this.srcList = []
const imgList = this.chatMessage.querySelectorAll('img')
if (imgList && imgList.length > 0) {
for (let index = 0; index < imgList.length; index++) {
this.srcList.push(imgList[index].src)
}
}
this.showScoleImage = true
} else {
this.$message.warning('内容为空')
}
},
async handleImage() {
this.srcList = []
const imgListElement = this.chatMessage.querySelectorAll('img')
let imgList = []
if (imgListElement.length > 0) {
for (let i = 0; i < imgListElement.length; i++) {
const nodeItem = imgListElement[i]
if (nodeItem.src.indexOf('data:image') !== -1) {
imgList.push(nodeItem)
}
}
} else {
imgList = []
}
if (imgList && imgList.length > 0) {
for (let index = 0; index < imgList.length; index++) {
if (imgList[index].src.indexOf('http:') !== -1) {
this.srcList.push(imgList[index].src)
} else {
const img = base64toFile(imgList[index].src)
const uploadConfig = {
dir: '/company_wx/service/avatars/'
}
this.srcList.push(imgList[index].src)
const result = await this.uploading(img, uploadConfig)
imgList[index].src = result.data
}
}
this.$emit('resultReamrk', this.chatMessage.innerHTML)
this.$emit('update:remark', this.chatMessage.innerHTML)
} else {
this.$emit('resultReamrk', this.chatMessage.innerHTML)
this.$emit('update:remark', this.chatMessage.innerHTML)
}
}
}
}
</script>
<style lang="scss" scoped>
.watchImage {
width: 20px;
height: 20px;
margin-right: 5px;
cursor: pointer;
}
.textEditorTitle{
width: 100px;
}
.watchImageText{
color: #00bf8a;
cursor: pointer;
}
.noActive {
background: #f5f7fa;
color: #c0c4cc;
}
.active {
background: #fff;
color: #333;
}
.showScoleImageContent{
width: 100%;
line-height: 16px;
margin-top: 30px;
img{
max-width: 80%;
}
}
</style>
<style>
.qualityRemark {
width: 100%;
height: 150px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 10px;
overflow: auto;
line-height: 20px;
}
.qualityRemark img {
width: 80px !important;
height: auto !important;
margin-right: 5px;
margin-top:10px;
margin-right: 10px;
}
.showScoleImageContent{
width: 100%;
overflow: auto;
line-height: 16px;
}
.showScoleImageContent img{
max-width: 100%;
margin-bottom: 20px;
}
</style>
\ No newline at end of file
# 企业微信客户端侧边栏应用开发
基于企业微信客户端 开发侧边栏功能 企业微信的文档 [企业微信文档]([链接地址](https://developer.work.weixin.qq.com/document/path/94352) ) 首先需要调用微信的 授权接口 进行授权拿到 用户的 code 等信息
以下是初始化页面需要完成的功能
# 需求流程
## 1.企微授权
每次进去页面都需要去进行企微授权 授权成功后 通过接口 获取 当前客服号的 userid 和 企业微信签名的一些信息 去校验签名 然后调用 企业微信的 js sdk 来获取 企业微信的一些api 然后再进行 侧边栏的开发
主要页面的功能在 `@/App.vue` 中 这个逻辑只是暂时写在 App.vue 中 需要你帮我重构一下 重新规划页面逻辑处理
`@/App.vue` 中 的方法和功能如下
- `Authorize` 方法: 如果当前页面没有授权过 则重定向到企业微信的授权页面
- `handleAuthCode` 方法: 每次进入页面都要重新调用这个方法 先查看 当前 url 中 是否有 code 参数 如果有 code 参数 则调用 `getAuthUser` 方法 去获取 用户的信息 然后再进行 企业微信的签名校验 然后再调用 企业微信的 js sdk 来获取 企业微信的一些api 然后再进行 侧边栏的开发
- `getAuthUser` 方法:通过 code 获取当前登录企业微信的用户信息 userid 这个userid 要储存在 cookie 中 过期时间 为 7 天
- `getSignature` 方法: 通过接口获取 企业微信的签名信息 然后调用 企业微信的 js sdk 来获取 企业微信的一些api 然后再进行 侧边栏的开发 这个接口的调用应该在 钉钉授权成功以后 调用 如果没有钉钉授权成功 则不调用 说明使用人 没有登录 不需要调用企微微信的一些功能
- `register`方法: 钉钉登录成功以后 回调到当前页面地址 需要 用之前获取的签名信息 去校验 企业签名和应用签名 通过后就可以使用 企业微信的 js sdk 来获取一些 api 方法
## 2. 在企业微信jssdk 初始化完成后 钉钉扫码界面的开发
### 2.1 钉钉扫码界面的开发
进入侧边栏页面需要钉钉扫码登录 所以需要先进入钉钉扫码登录页面 所有的页面都需要先登录 扫码登录页面是 `@/views/ddLogin.vue` 这个页面的逻辑如下
- `initOrganization` 方法: 获取当前的组织架构列表 可以通过 选择组织架构来决定获取
- `handleLogin` 方法: 通过钉钉的扫码登录 扫码成功后钉钉会重定向一个后端地址 会携带钉钉的 code 参数 后端获取到 code 参数 后 会重定向当前的前端页面 url 上 会携带 token 参数 然后把 token 存在 cookie 中 过期时间 为 7 天
- 每次刷新重新进入页面的时候 都要检查一下 cookie 中是否有 token 如果有 说明已经登录过了 不需要重新钉钉扫码登录 如果没有 则需要重新钉钉扫码登录
### 2.2 钉钉扫码登录成功以后
1. 钉钉扫码登录成功以后 才可以查看其他的页面 如果钉钉没有扫码登录 则无法查看其他的页面 页面只展示钉钉扫码登录的页面
2. 钉钉扫码登录成功以后 会把 token 存在 cookie 中 过期时间 为 7 天
3. 登录成功以后会有一个退出的按钮 点击退出按钮 会把 cookie 中的 token 删除 清除 token 然后跳转到钉钉扫码登录页面 重新钉钉扫码登录
4. 需要把钉钉扫码成功后的 token 每次请求接口的时候 放在请求头中 每个接口需要放上 token 才能获取到数据 后端需要校验 请你帮我完成这部分工作
### 3.cookie 中需要缓存 通过企业微信获取的 userid 和 钉钉扫码成功后 获取的 token
### 4.关于回调地址
1. 本项目中会经历两次页面回调 一次是企微的授权验证回调 一次是 钉钉登录成功的回调 回调页面必须在首页完成 因为这个回调地址已经配置过了 不能更改 至于写在 app.vue 中还是 vuex 中 或者 另外的 js 中 你自己决定 只需要保证 在用户登录的时候 能够拿到 token 就可以了
2. 回调的时候 引入
```js
import {getParams} from '@/utils/index.js'
```
获取 url 后面的参数 储存在 vuex 中
本项目中会经历两次页面回调 一次是企微的授权验证回调 一次是 钉钉登录成功的回调 回调页面必须在首页完成 因为这个回调地址已经配置过了 钉钉回调成功后 页面 url 上会带一个 type=ding 的参数 需要根据这个参数来判断 是钉钉回调还是企微回调 如果是钉钉的回调 表明 微信授权的逻辑已经完成了 可以直接去 首页了
\ No newline at end of file
# 企业微信开发接入文档
# 企业微信开发接入文档
## 一、前期准备
### 1.1 开发者平台配置
1. 登录 企业微信开发者平台
2. 创建应用并获取以下信息:
- corpid(企业ID)
- agentid(应用ID)
- Secret(应用密钥)
3. 配置可信域名
4. 开通相关接口权限
## 二、项目配置
在配置签名的时候 后端签名时候的 url 应该是 重定向回来的那个 url 带 code 和 state 参数 而不是 在企微后台设置的那个 url 就是 后端签名的 url 要和 授权的时候 当前的那个页面的 url 一样 但是不包含 # 和 # 后面的url 内容
应该是 https://companywx.jianshuwenhua.com/company_app/index.html?code=-aXitj4Tt8UNLq_evPRgECSVS6zHpP6-r3ofYzk0BeM&state=STATE 带上参数去签名
### 2.1 引入企业微信 JS-SDK
在项目的 index.html 中添加:
老版本引入 方式:
```
<script src="https://res.wx.qq.com/open/js/
jweixin-1.2.0.js"></script>
```
新版本引入方法
```
<script src="https://wwcdn.weixin.qq.com/node/open/js/wecom-jssdk-2.0.2.js"></script>
```
### 2.2 基础配置示例
查看文档 [文档](https://developer.work.weixin.qq.com/document/path/94325)
以下是老版本的配置内容
先配置 企业应用 id 签名 然后 在配置 应用id 签名 必须先配置企业 id的签名人 然后再配置应用id的签名
```
wx.config({
  beta: true, // 必须这么写,否则wx.invoke调用形式
  的jsapi会有问题
  debug: false, // 开启调试模式
  appId: corp_id, // 必填,企业微信的corpID
  timestamp: time, // 必填,生成签名的时间戳
  nonceStr: nonceStr, // 必填,生成签名的随机串
  signature: signature, // 必填,签名
  jsApiList: ["getContext", 
  "getCurExternalContact"] // 必填,需要使用的JS
  接口列表
});
```
### 2.3 应用配置示例
```
wx.agentConfig({
  corpid: corp_id, // 必填,企业微信的corpid
  agentid: agentid, // 必填,企业微信的应用id
  timestamp: time, // 必填,生成签名的时间戳
  nonceStr: nonceStr, // 必填,生成签名的随机串
  signature: signature, // 必填,签名
  jsApiList: ["getContext", 
  "getCurExternalContact"], // 必填,需要使用的JS
  接口列表
  success: function(res) {
    // 回调
  },
  fail: function(res) {
    if (res.errMsg.indexOf('function not 
    exist') > -1) {
      alert('版本过低请升级')
    }
  }
});
```
新版本的配置
同时配置企业签名 和 应用签名 配置完成调用 wx的方法即可
```
ww.register({
corpId: that.userInfo.corp_id, // 必填,当前用户企业所属企业ID
agentId: 1000013, // 必填,当前应用的AgentID
jsApiList: jsApiList, // 必填,需要使用的JSAPI列表
getConfigSignature:function(){ // // 必填,根据url生成企业签名的回调函数
return new Promise((resolve, reject) => {
resolve({
nonceStr: that.userInfo.nonceStr,
timestamp: that.userInfo.time,
signature: that.userInfo.corp_signature,
})
})
},
getAgentConfigSignature:function(){ // 必填,根据url生成应用签名的回调函数
return new Promise((resolve, reject) => {
resolve({
nonceStr: that.userInfo.nonceStr,
timestamp: that.userInfo.time,
signature: that.userInfo.agent_signature,
})
})
}
})
```
## 三、常用接口
### 3.1 获取外部联系人信息
```
wx.invoke('getCurExternalContact', {
}, function(res) {
  if (res.err_msg == 
  "getCurExternalContact:ok") {
    var userId = res.userId; //返回当前外部联系人
    userId
  } else {
    //错误处理
  }
});
```
## 四、环境配置
### 4.1 开发环境配置
```
// .env.development
VUE_APP_CORPID=your_development_corpid
VUE_APP_AGENTID=your_development_agentid
```
### 4.2 生产环境配置
```
// .env.production
VUE_APP_CORPID=your_production_corpid
VUE_APP_AGENTID=your_production_agentid
```
## 六、常见问题
### 6.1 接口调用失败
- 检查域名配置
- 验证 corpid 和 agentid 正确性
- 确认环境是否支持 HTTPS
- 检查签名是否正确
- 后端授权时的 url 是否和当前授权页面的 url 是否一直 包含参数和是否一直 但是不包含 # 和 # 后面的url 内容
# 1.后端签名时候的 url 应该是 重定向回来的那个 url 带 code 和 state 参数 而不是 在企微后台设置的那个 url
应该是 https://companywx.jianshuwenhua.com/company_app/index.html?code=-aXitj4Tt8UNLq_evPRgECSVS6zHpP6-r3ofYzk0BeM&state=STATE 带上参数去签名
# 1.掌权扫码登录的时候需要在 钉钉应用后台添加授权跳转地址 由于每个主体的域名不一样 所以所有的主体的地址都需要添加 新增加的主体也需要在钉钉应用后台添加
# 2.这里用 hash 模式 因为本项目是在企微 服务器下的一个二级页面 如果用 history 模式 需要修改 nginx 配置 在 nginx 配置中 需要添加
```
location /company_app/ {
root /usr/share/nginx/html; # 注意这里是html,不是html/company_app
try_files $uri $uri/ /company_app/index.html;
}
```
这个配置是 如果访问的 url 是 /company_app/ 那么就访问 /company_app/index.html 这个文件 用 hash 模式不用修改 nginx 配置 直接用就行了
如果用 history 需要在 vue.config.js 中配置 historyApiFallback: true, 解决本地开发404问题
# 3. 在域名的二级目录下 重新上传一个 带路由的项目时 需要在 router.js 中设置 base 目录 本地不需要设置
```
base:process.env.NODE_ENV === 'production' ? '/company_app' : '/',
```
# 4.打包到测试环境的时候 用 npm run build:stage 打包到正式环境的时候 用 npm run build:prod
因为企微侧边栏是跟当前登录的主体的域名下的 html 文件 主体域名下 没有测试和正式的域名 只有一个域名
例如 简书 的域名 https://companywx.jianshuwenhua.com/ 不管在 正式环境还是在测试环境的掌微中 都是跳转这个域名 所以 在配置钉钉回调的时候 回调地址有测试和正式的域名 但是 在企微侧边栏打开的页面只有一个 域名 companywx.jianshuwenhua.com 所以在打包到测试环境的时候
用 npm run build:stage 打包到正式环境的时候 用 npm run build:prod
# 5. 谨记 当打包上线到正式环境的时候 一定要 用npm run build:prod 重新打包一次 要不钉钉扫码登录会有问题 回调地址会有错误
# 6. 只用到企业微信应用的 api 的时候 可以只注册 企业微信应用的签名 当用到 企业的api 的时候 需要注册企业的签名 当用到什么 api 的时候 就用那个的签名 具体看官方文档
\ No newline at end of file
# 钉钉扫码登录接入文档
# 钉钉扫码登录接入文档
## 一、前期准备
### 1.1 开发者平台配置
1. 登录 钉钉开放平台
2. 创建应用并获取以下信息:
- AppID(应用ID)
- AppSecret(应用密钥)
3. 配置应用回调域名
4. 开通"扫码登录"功能权限
## 二、项目配置
### 2.1 引入钉钉 SDK
在项目的 index.html 中添加:
```
<script src="https://g.alicdn.com/dingding/
dingtalk-jsapi/2.13.42/dingtalk.open.js"></
script>
```
### 2.2 创建登录组件
```
<template>
  <div class="DDlogin">
    <h2>钉钉扫码登录</h2>
    <div id="login_container"></div>
  </div>
</template>
<script>
export default {
  name: "DDLogin",
  data() {
    return {
      appid: process.env.
      VUE_APP_DINGTALK_APPID,
      dingRedirect_uri: encodeURIComponent
      (window.location.origin + '/login'),
      dingCodeConfig: {
        id: "login_container",
        style: "border:none;
        background-color:#FFFFFF;",
        width: "400",
        height: "400",
      }
    }
  },
  methods: {
    initDingLogin() {
      window.DDLogin(this.getDingCodeConfig);
    },
    addDingListener() {
      const handleMessage = (event) => {
        if (event.origin === "https://login.
        dingtalk.com") {
          this.handleLoginTmpCode(event.data);
        }
      };
      window.addEventListener("message", 
      handleMessage, false);
    }
  }
}
</script>
```
## 三、登录流程
1. 用户访问登录页面
2. 初始化钉钉扫码组件
3. 用户使用钉钉APP扫描二维码
4. 获取临时授权码(loginTmpCode)
5. 重定向到回调地址并携带 code
6. 后端验证 code 并返回登录态
## 四、环境配置
### 4.1 开发环境配置
```
// .env.development
VUE_APP_DINGTALK_APPID=your_development_appid
VUE_APP_REDIRECT_URI=http://localhost:8080/
login
```
### 4.2 生产环境配置
```
// .env.production
VUE_APP_DINGTALK_APPID=your_production_appid
VUE_APP_REDIRECT_URI=https://your-domain.com/
login
```
## 五、注意事项
### 5.1 安全性
1. AppSecret 禁止在前端暴露
2. 必须使用 HTTPS 协议
3. 注意防范 CSRF 攻击
### 5.2 开发建议
1. 建议本地开发使用 HTTPS
2. 区分开发和生产环境配置
3. 做好错误处理和异常提示
## 六、常见问题
### 6.1 扫码无法回调
- 检查回调域名配置
- 验证 AppID 正确性
- 确认环境是否支持 HTTPS
### 6.2 登录失败
- 检查网络连接
- 验证应用配置
- 查看控制台错误信息
## 七、上线检查清单
- 更新正式环境 AppID
- 配置正确的回调域名
- 确认 HTTPS 证书有效
- 测试完整登录流程
- 验证错误处理机制
- 检查日志记录
## 八、参考资料
- 钉钉开放平台文档
- 扫码登录接入指南
\ No newline at end of file
......@@ -2,11 +2,42 @@ import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// import '@/styles/index.scss' // global css
Vue.config.productionTip = false
// import ElementUI from 'element-ui';
import Cookies from 'js-cookie';
import _ from 'lodash';
import 'lib-flexible/flexible.js'
// Vue.use(ElementUI);
import '@/styles/element-theme-colors.css';
import '@/styles/index.scss';
import moment from 'moment'
import VConsole from 'vconsole';
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 uploading from '@/utils/cos-upload'
if(process.env.NODE_ENV !== 'production'){
new VConsole();
}
Vue.use(uploading)
Vue.use(BiElementUi, {
dev: process.env.NODE_ENV !== 'production',
env: process.env.NODE_ENV,
system: null
})
Vue.use(Element, {
size: Cookies.get('size') || 'small' // set element-ui default size
// locale: enLang, // 如果使用中文,无需设置,请删除
})
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;
import { debounce } from '@/utils'
import { mapState } from 'vuex'
export default {
watch: {
accountSelect: {
handler:debounce(function(newVal, oldVal) {
console.log(12313,'出发了')
if (newVal && newVal !== "" && newVal !== oldVal) {
this.memberChange()
}
},300)
}
},
computed: {
...mapState('game', ['accountSelect'])
},
}
\ No newline at end of file
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import {getAuthUser} from '@/api/user'
import userInfo from '../views/userInfo/userInfo.vue'
import quickReply from '../views/quickReply.vue'
import giftRecord from '../views/giftRecord.vue'
import applyRecord from '../views/applyRecord.vue'
import quickSend from '../views/quickSend.vue'
import addressBook from '../views/addressBook.vue'
import Cookies from 'js-cookie'
import store from '@/store'
Vue.use(VueRouter)
function Authorize() {
// 先获取企微配置信息
let originUrl = location.href; originUrl;
let redirectUrl = originUrl.split('?')[0];
let returnUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=wweaefe716636df3d1&redirect_uri=${encodeURIComponent(redirectUrl)}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`
window.location.href = returnUrl;
}
// 通过oAuth2授权获取当前用户userId
function getWxUserInfo(to, from, next, authCode,corp_id) {
getAuthUser({code:authCode,corp_id:'wweaefe716636df3d1'}).then((res) => {
let { code, data, message } = res || {};
if (code === 200) {
let { userId } = data || {};
if (userId) {
localStorage.setItem('userId', userId);
next();
} else {
Authorize(to)
}
} else {
console.log(message);
// PC端侧边栏tab页面切换后再切回来, 页面地址还是上次重定向后的带code的,此时code已经消费过一次,再调接口获取用户信息会失败, 所以加上重新授权的逻辑
Authorize(to)
}
}).catch(() => {
Authorize(to)
})
}
import { getParams } from '@/utils/index'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
redirect: '/userInfo'
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue')
}
path: '/index.html',
redirect: '/userInfo'
},
{
path: '/userInfo',
name: 'userInfo',
component: userInfo
},
{
path: '/quickReply',
name: 'quickReply',
component: quickReply
},
{
path: '/giftRecord',
name: 'giftRecord',
component: giftRecord
},
{
path: '/applyRecord',
name: 'applyRecord',
component: applyRecord
},
{
path: '/quickSend',
name: 'quickSend',
component: quickSend
},
{
path: '/addressBook',
name: 'addressBook',
component: addressBook
},
{
path: '/login',
name: 'login',
component: () => import('../views/login.vue')
},
{
path: '/testWx',
name: 'testWx',
component: () => import('../views/testWx.vue')
},
]
const router = new VueRouter({
mode: 'hash',
base: process.env.BASE_URL,
/* 这里用 hash 模式 因为本项目是在企微 服务器下的一个二级页面 如果用 history 模式 需要修改 nginx 配置 在 nginx 配置中 需要添加 location /company_app/ {
root /usr/share/nginx/html; # 注意这里是html,不是html/company_app
try_files $uri $uri/ /company_app/index.html;
}
这个配置是 如果访问的 url 是 /company_app/ 那么就访问 /company_app/index.html 这个文件
用 hash 模式不用修改 nginx 配置 直接用就行了
如果用 history 需要在 vue.config.js 中配置 historyApiFallback: true, 解决404问题
*/
mode: 'history',
base:process.env.NODE_ENV === 'development' ? '/' : '/company_app',
routes
})
// router/index.js
router.beforeEach((to, from, next) => {
// 判断是否是微信浏览器打开
let ua = navigator.userAgent.toLowerCase();
let isWeixin = ua.indexOf('micromessenger') != -1;
if (!isWeixin) {
// 需要登录态的页面
const needAuth = !['/login'].includes(to.path)
// 检查登录信息
const wecomUserId = Cookies.get('userid')
const cser_id = Cookies.get('cser_id')
const token = Cookies.get('token')
const urlParams = getParams();
// 本地测试数据
if(Cookies.get('external_userid')){
store.state.user.external_userid = Cookies.get('external_userid')
}
if (needAuth) {
if (wecomUserId && token && store.state.user.external_userid) {
// 登录信息齐全,允许进入
next()
return
} else {
// 缺少登录信息,跳转到登录页
// 跳转到 login 的时候带上当前 url 的参数
next({
path: '/login',
query: urlParams
})
// next()
}
// 企微环境获取当前用户userId和外部客户externalUserId
let code = to.query.code || '';
let corp_id = to.query.corp_id || '';
if (code) {
// 有code说明是授权过的,重定向地址, 调接口获取当前用户信息
getWxUserInfo(to, from, next, code,corp_id);
} else {
// 获取授权
Authorize(to);
// 登录页、回调页等不需要校验
next()
}
})
export default router
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import game from './modules/game'
Vue.use(Vuex)
export default new Vuex.Store({
......@@ -13,5 +14,7 @@ export default new Vuex.Store({
actions: {
},
modules: {
user,
game
}
})
// 和游戏相关的逻辑处理
// 管理公共的store
import { zyouBindMember } from '@/api/works'
const state = {
accountSelect: '', // 当前选中的用户的member_id
bindGameUserList: [], // 用户绑定的游戏角色
changeSelectWindow: false, // 切换客服窗口的时候 下发通知
gameUserInfo:{},
chatUserInfo: {}, // 当前选中的用户的详情
}
const mutations = {
set_accountSelect(state, data) {
state.accountSelect = data
},
set_bindGameUserList(state, data) {
state.bindGameUserList = data
},
set_chatUserInfo(state, data) {
state.chatUserInfo = data
},
set_gameUserInfo(state, data) {
state.gameUserInfo = data
}
}
const actions = {
// 游戏绑定用户
gameBindUser({ dispatch, commit }, data) {
return new Promise((resolve, reject) => {
zyouBindMember(data).then(async (res) => {
if (res.status_code == 1) {
res.data.data.map((item) => {
item.member_id = item.member_id + ''
})
if (res.data.data && res.data.data.length > 0) {
res.data.data.map((item) => {
item.label = item.username
item.value = item.member_id
})
}
commit('set_bindGameUserList', res.data.data)
resolve(res.data.data)
}
})
})
},
}
export default {
namespaced: true,
state,
mutations,
actions
}
import Cookies from 'js-cookie'
import { companyviewConfig } from '@/api/user'
const state = {
userInfo: {
"userid": "JinDuoXia",
"corp_id": "wweaefe716636df3d1",
"nonceStr": "xL1ZHTEt8d5GrjMG",
"agent_signature": "05a47ef848266c48ff28f52e7749ba8b70adcc14",
"corp_signature": "29e61720786b3c6dac31f1041c70878b4819842e",
"time": 1747726636,
external_userid:''
},
userid:Cookies.get('userid'),
corp_id:'',
external_userid:'',
token:'',
cser_info:{
cser_id:'',
cser_name:''
},
cser_id:'',
cser_name:'',
signData:{
corp_id:'',
agent_signature:'',
corp_signature:'',
time:''
},
weixin_blongs_id_list:[],
external_userid:''
// 六子的 用户id wm5rUgMgAAjqjOcqp8i3lEhFZDQieWug
// 我的 userid JinDuoXia cser_id 4090 corp_id wweaefe716636df3d1 cser_id 4090 token token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJpc3MiOjQwOTAsImlhdCI6MTc0NzgxMjMxMiwiZXhwIjoxNzQ4NDE3MTEyLCJuYmYiOjE3NDc4MTIzMTIsInN1YiI6InRva2Vu6K6k6K-BIiwianRpIjoiMjBkOTY3MDZiYzI1MDdmY2MxOWI2MjU1YTM0YWQ3M2YifQ.yX7E7QHV7x2ubpa8iK3Avy794EiHNCaW2CtB4A4UQWo
}
const mutations = {
set_userInfo(state,userInfo){
state.userInfo = userInfo
},
set_userid(state,userid){
state.userid = userid
},
set_corp_id(state,corp_id){
state.corp_id = corp_id
},
set_external_userid(state,external_userid){
state.external_userid = external_userid
},
set_token(state,token){
state.token = token
},
set_cser_info(state,cser_info){
state.cser_info = cser_info
},
set_signData(state,signData){
state.signData = signData
},
set_cser_id(state,cser_id){
state.cser_id = cser_id
},
set_cser_name(state,cser_name){
state.cser_name = cser_name
},
set_weixin_blongs_id_list(state, data) {
if (data.length === 0) {
state.weixin_blongs_id_list = []
} else {
state.weixin_blongs_id_list = data
}
},
}
const actions = {
async requestCompanyviewConfig({ commit, state }, data) {
const res = await companyviewConfig(data)
let blongsList = []
let returnList = []
if (res.data.weixin_blongs_name && Object.keys(res.data.weixin_blongs_name).length > 0) {
blongsList = res.data.weixin_blongs_name
returnList = Object.keys(blongsList).map(key => ({
label: blongsList[key],
value: key
}))
commit('set_weixin_blongs_id_list', returnList)
}
},
}
export default {
namespaced: true,
state,
mutations,
actions
}
\ No newline at end of file
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type='file'] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
position: relative;
margin: 0 auto;
}
// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
// dropdown
.el-dropdown-menu {
a {
display: block;
}
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}
.el-radio-button:focus {
box-shadow: none !important;
}
.el-drawer__header > span,
.el-tabs__item {
outline: none;
}
.el-drawer__body {
position: relative;
}
.error-message {
z-index: 5000 !important;
}
.el-drawer__header>span,.el-tabs__item{
outline: none;
}
.el-icon-circle-close {
color: #fff;
}
\ No newline at end of file
// 全局函数和变量
$color: #00BF8A; // 全局颜色
@function vw($px) {
@return ($px / 1920) * 100vw;
}
@function vh($px) {
@return ($px / 1080) * 100vw;
}
.flex_x {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
}
.flex_y {
display: flex;
flex-flow: column;
justify-content: space-between;
}
.flex_wrap {
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: space-between;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-content {
.el-select--small {
.el-tag:first-child {
max-width: 64% !important;
}
}
}
.BiInput_single_clear {
.el-input__suffix {
display: none !important;
}
}
.display_none_dialog {
.el-drawer__body::-webkit-scrollbar {
/*滚动条整体样式*/
width: 0;
background: #fff;
border-radius: 3px;
}
.el-drawer__body::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
width: 0;
border-radius: 3px;
background: rgba(221, 221, 221, 1);
}
}
.radio {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}
.el-input__count {
vertical-align: top;
}
.el-range-separator {
margin-right: 10px !important;
}
:root {
--primary-color: #2563eb;
--primary-hover: #1d4ed8;
--bg-color: #f8fafc;
--chat-bg: #ffffff;
--user-msg-bg: #2563eb;
--ai-msg-bg: #f1f5f9;
--border-color: #e2e8f0;
--text-primary: #1e293b;
--text-secondary: #64748b;
}
\ No newline at end of file
@import "./variables.scss";
@import "./mixin.scss";
@import "./transition.scss";
@import "./element-ui.scss";
@import "./sidebar.scss";
@import "./ui.scss";
@import 'uistyle.scss';
@import './reset.scss';
// @tailwind base; // 重设的 css 代码
// @tailwind components;
@tailwind utilities;
// @import "./variables.scss";
// @import "./element-variables.scss";
// @import "./index.css";
// @import "./element-theme-colors.css";
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
......@@ -18,6 +12,8 @@ body {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, Arial, sans-serif;
font-size: 14px;
margin: 0 ;
padding: 0;
}
label {
......@@ -412,6 +408,7 @@ li {
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 20px;
padding: 15px;
}
.el-table__cell {
......@@ -582,6 +579,13 @@ li {
margin-bottom: 5px;
}
}
.el-form-item{
margin-bottom: 10px;
.el-input__inner{
height: 32px;
line-height: 32px;
}
}
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
.success {
background-color: #67c23a;
}
// .info {
// background-color: #909399;
// }
.primary {
background-color: #409eff;
}
.danger {
background-color: #f56c6c;
}
.warning {
background-color: #e6a23c;
}
.content-outer {
.bi-tabs-box {
background-color: #fff;
margin-bottom: 10px;
overflow-y: hidden;
.el-tabs__nav-wrap.is-top::after {
content: none;
}
.el-tabs__active-bar.is-top {
top: 0;
}
.el-tabs__header.is-top {
margin-bottom: 0;
}
.el-tabs__content {
padding: 0;
display: none;
overflow: initial;
}
.el-tabs__item.is-top {
min-width: 100px;
text-align: center;
}
// .el-tabs--border-card {
// border: none;
// box-shadow: none;
// .el-tabs__nav {
// display: flex;
// }
// & > .el-tabs__header {
// border-bottom: none;
// .el-tabs__item {
// margin-top: 0;
// border: none;
// display: block;
// }
// }
// }
}
& > .content-head.container {
padding-bottom: 0 !important;
background-color: white;
}
.content-table {
margin-top: 10px;
background-color: white;
padding: 20px;
.action-box {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.table-table {
padding-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
}
}
.drawer-container {
// padding: 20px;
}
}
.bi-column-container {
display: flex;
justify-content: space-between;
.column-container {
margin-left: 20px;
width: 230px;
flex: 0 0 230px;
}
}
.colorStatus {
display: inline-block;
> p {
display: inline;
}
.all {
padding: 2px 8px;
height: 20px;
line-height: 16px;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #3491fa;
border: 1px solid #c3e7fe;
background: #e8f7ff;
}
// 成功
.success {
padding: 2px 8px;
height: 20px;
line-height: 16px;
background: #e1fff0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #00bf8a;
}
.indeter {
padding: 2px 8px;
height: 20px;
line-height: 16px;
background: #fffae0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
border-radius: 4px;
border: 1px solid #ffe4ba;
color: #ff7d00;
}
.fail {
padding: 2px 8px;
height: 20px;
line-height: 16px;
background: #ffece8;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #f53f3f;
border: 1px solid #fdcdc5;
}
.normal {
padding: 2px 8px;
height: 20px;
line-height: 16px;
background: #f4f4f4;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #86909c;
}
.dianFail {
display: inline-block;
width: 8px;
height: 8px;
background: #f45454;
border-radius: 5px;
}
.dianLineup {
display: inline-block;
width: 8px;
height: 8px;
background: #c9cdd4;
border-radius: 5px;
}
.dianLoading {
display: inline-block;
width: 8px;
height: 8px;
background: #f45454;
border-radius: 5px;
}
.dianSuccess {
display: inline-block;
width: 8px;
height: 8px;
background: #00bf8a;
border-radius: 5px;
}
.dianWait {
display: inline-block;
width: 8px;
height: 8px;
background: #ff9d02;
border-radius: 5px;
}
.dianNormal {
display: inline-block;
width: 8px;
height: 8px;
background: #c9cdd4;
border-radius: 5px;
}
.dianOk {
display: inline-block;
width: 8px;
height: 8px;
background: #0fc6c2;
border-radius: 5px;
}
.dianKou {
display: inline-block;
width: 8px;
height: 8px;
background: #3491fa;
border-radius: 5px;
}
}
.el-dialog {
.dialog-footer {
position: relative;
}
}
// 弹窗的按钮位置
.dialog-footer {
width: calc(100%);
position: absolute;
bottom: 0px;
right: 0;
padding-top: 20px;
padding-bottom: 20px;
padding-right: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
justify-content: flex-end;
overflow: hidden;
background: #fff;
z-index: 10;
.btn {
width: 84px;
height: 32px;
}
}
.tableheader-popper-editdialog {
.dialog-footer {
padding-top: 0;
padding-right: 0;
}
}
.el-drawer__header {
padding: 0 20px !important;
line-height: 58px;
height: 58px;
// padding-bottom: 0;
margin-bottom: 0;
font-size: 16px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
// border: 1px rgba(0, 0, 0, 0.06) solid;
// box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
}
// 带background的title
.baseInfo {
width: 100%;
height: auto;
margin-bottom: 10px;
border: 1px solid #f1f5fd;
background: #fff;
.content{
padding-top: 0!important;
}
.title {
width: 100%;
height: 40px;
background: #f7f8fa;
font-size: 14px;
font-weight: 500;
color: #323335;
padding-left: 10px;
line-height: 40px;
margin-bottom: 20px;
}
}
.drawer-container-title{
width: 100%;
height: auto;
font-weight: 500;
font-size: 16px;
color: #323335;
line-height: 22px;
text-align: left;
border-left: 3px solid #00BF8A;
text-indent: 10px;
font-weight: 500;
line-height: 16px;
margin-bottom: 20px;
}
// 弹窗里面的确定 取消按钮暂时固定选择
.layerBtns {
display: flex;
justify-content: flex-end;
.btn {
width: auto;
height: 32px;
}
.el-button {
font-weight: 500 !important;
}
}
.el-image__inner {
border-radius: 5px;
}
p {
margin: 0;
}
//
.infoText {
color: #86909c;
font-size: 12px;
}
// .el-drawer__body{
// position: relative;
// }
.el-dialog__header {
min-height: 40px;
}
.avatar-uploader .el-upload:hover {
// border:1px dashed #00BF8A !important;
}
// form 里面label换行的时候的样式
.lineHeight {
.el-form-item__label {
line-height: 20px !important;
}
}
// 应用icon样式
.miniappIcon {
width: 30px;
height: 30px;
border-radius: 30px;
margin-right: 10px;
}
.miniappIconText {
position: relative;
top: -10px;
}
.textInfo {
color: rgb(153, 153, 153);
font-size: 12px;
}
.textHidden {
word-break: break-all;
}
.redText {
font-size: 14px;
font-weight: 400;
color: #ff4d4f;
}
//注意样式
.carefulText {
width: 100%;
height: 40px;
background: #fff7e8;
border-radius: 4px;
font-size: 14px;
font-weight: 400;
color: #323335;
padding-left: 10px;
margin-top: 0 !important;
margin-bottom: 10px;
display: flex;
align-items: center;
.icon {
color: #ff7d00 !important;
font-size: 14px;
margin: 0 10px;
}
i {
color: #ff7d00 !important;
font-size: 14px;
}
}
.search-form {
.el-select__tags {
max-width: 200px !important;
}
}
.el-form-item__label {
font-weight: 600;
}
.el-table__header .cell {
color: #323335;
font-weight: 500;
}
.el-tooltip {
max-width: 200px;
}
.tooltipWidth {
max-width: 250px;
line-height: 18px !important;
}
.el-tag {
margin-right: 5px !important;
}
.el-date-range-picker__header {
border: none !important;
}
.el-tabs__header {
margin-bottom: 15px;
}
.el-icon-question {
color: #86909c;
}
.id_color {
font-size: 14px;
color: #86909c;
}
#app {
.main-container {
min-height: 100%;
padding-top: 0px !important;
transition: margin-left .28s;
margin-left: $sideBarWidth;
position: relative;
}
.sidebar-container {
transition: width 0.28s;
width: $sideBarWidth !important;
background-color: $menuBg;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
box-shadow: 0 1px 4px #00152914;
// reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 90px);
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
width: 22px;
height: 22px;
background-size: contain;
color: #8893aa;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
// menu hover
.submenu-title-noDropdown,
.el-submenu__title {
&:hover {
background-color: $menuHover !important;
}
}
.is-active > .el-submenu__title,
.is-active > .el-submenu__title svg,
.is-active.submenu-title-noDropdown,
.is-active.submenu-title-noDropdown svg {
color: $subMenuActiveText !important;
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: $sideBarWidth !important;
// background-color: $subMenuBg !important;
&:hover {
background-color: $subMenuHover !important;
}
}
}
.hideSidebar {
.sidebar-container {
width: 54px !important;
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
.svg-icon {
margin-left: 15px;
}
.sub-el-icon {
margin-left: 19px;
}
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding: 0 !important;
.svg-icon {
margin-left: 15px;
}
.sub-el-icon {
margin-left: 19px;
}
.el-submenu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: $sideBarWidth !important;
}
// mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: $sideBarWidth !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$sideBarWidth, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
// when menu collapsed
.el-menu--vertical {
&>.el-menu {
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
}
.nest-menu .el-submenu>.el-submenu__title,
.el-menu-item {
&:hover {
// you can use $subMenuHover
background-color: $menuHover !important;
}
}
// the scroll bar appears when the subMenu is too long
>.el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}
.el-menu-item{
position: relative;
&::after{
content:' ';
position: absolute;
width: 3px;
height: 0;
right: 0;
top: 0;
transition: .3s;
}
&.is-active::after{
height: 100%
}
}
.el-submenu .el-menu-item{
padding-left: 60px !important;
}
.el-submenu__title{
font-size: 16px !important;
font-weight: 500 !important;
color: #323335 !important;
}
.submenu-title-noDropdown:not(.el-submenu .submenu-title-noDropdown){
font-size: 16px !important;
font-weight: 500 !important;
color: #323335 !important;
}
\ No newline at end of file
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
// rotage-transform
.rotage-transform-leave-active,
.rotage-transform-enter-active {
transition: all 1s;
}
.rotage-transform-enter {
transform: rotate(180deg);
}
.rotage-transform-leave-to {
transform: rotate(0deg);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all .5s;
}
.breadcrumb-leave-active {
position: absolute;
}
$themeColor: #00BF8A;
$sidebarWidthOpen: 190px;
$sidebarWidthClosed: 56px;
$navbarHeight: 50px;
:export {
themeColor: $themeColor;
sidebarWidthOpen: $sidebarWidthOpen;
sidebarWidthClosed: $sidebarWidthClosed;
navbarHeight: $navbarHeight;
}
// @import './ui-variables.scss';
@import './ui-variables.scss';
/* bi-ui-style */
// $appmainWidth
/*
*Font
*默认为font-size: 14px;line-height: 20px;
*/
body {
font-size: 14px;
line-height: 20px;
color: #323335;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
#app {
height: 100%;
background-color: #e5e5e5;
.navbar {
height: $navbarHeight;
.navbar-box {
height: $navbarHeight;
line-height: $navbarHeight;
.app-breadcrumb {
padding: 10px 0;
}
}
}
.app-main {
min-height: calc(100vh - #{$navbarHeight});
}
}
.app-main {
.text {
&-12 {
font-size: 12px;
line-height: 18px;
}
&-14 {
font-size: 14px;
line-height: 20px;
}
&-16 {
font-size: 16px;
line-height: 22px;
}
&-24 {
font-size: 16px;
line-height: 34px;
}
}
.container {
padding: 20px;
background: #fff;
border-radius: 4px;
& + .container {
margin-top: 10px;
}
}
}
.search-form {
display: flex;
flex-wrap: wrap;
& + * {
margin-top: 15px !important;
}
.search-item {
min-width: 330px !important;
padding-right: 20px;
padding-bottom: 20px;
margin-bottom: 0;
margin-right: 0;
margin-left: 0;
margin-top: 0;
display: flex;
flex: 0 0 auto;
.item-label,
.select-label {
flex-wrap: 0;
flex-shrink: 0;
padding-right: 10px;
margin-bottom: 0;
margin-right: 0;
margin-left: 0;
margin-top: 0;
text-align: right;
line-height: 16px;
display: flex;
align-items: center;
flex-direction: row-reverse;
max-height: 32px;
overflow: hidden;
}
.select-value {
.el-input__inner {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.item-label {
width: 80px;
}
.select-label {
width: 100px;
margin-right: -1px;
text-align: right;
padding-right: 0;
.is-focus .el-input__inner {
z-index: 1;
}
.el-input__inner {
position: relative;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:focus {
z-index: 1;
}
}
}
.item-content,
.select-content {
flex-grow: 1;
& > * {
width: 100%;
}
}
.select-content {
height: 32px;
.input-number-range {
position: relative;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
&.is-focus {
z-index: 1;
}
}
&:not(.el-select) {
.is-focus .el-input__inner {
z-index: 1;
}
.el-input__inner {
position: relative;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:focus {
z-index: 1;
}
}
}
&.el-select {
.el-input .el-input__inner {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}
}
}
.hideSidebar {
.search-form {
@media screen and (max-width: 1103px) {
.search-item {
width: 50%;
// &:nth-last-child(3) ~ .search-item {
// padding-bottom: 0;
// }
}
}
@media screen and (min-width: 1104px) {
.search-item {
width: 33.3%;
// &:nth-last-child(4) ~ .search-item {
// padding-bottom: 0;
// }
}
}
@media screen and (min-width: 1434px) {
.search-item {
width: 25%;
// &:nth-last-child(5) ~ .search-item {
// padding-bottom: 0;
// }
}
}
@media screen and (min-width: 1764px) {
.search-item {
width: 20%;
// &:nth-last-child(6) ~ .search-item {
// padding-bottom: 0;
// }
}
}
}
}
.openSidebar {
.search-form {
@media screen and (max-width: 910px) {
.search-item {
width: 50%;
// &:nth-last-child(3) ~ .search-item {
// padding-bottom: 0;
// }
}
}
@media screen and (min-width: 1240px) {
.search-item {
width: 33.3%;
// &:nth-last-child(4) ~ .search-item {
// padding-bottom: 0;
// }
}
}
@media screen and (min-width: 1570px) {
.search-item {
width: 25%;
// &:nth-last-child(5) ~ .search-item {
// padding-bottom: 0;
// }
}
}
@media screen and (min-width: 1900px) {
.search-item {
width: 20%;
// &:nth-last-child(6) ~ .search-item {
// padding-bottom: 0;
// }
}
}
}
}
/* element-ui */
.el-select,
.el-cascader {
.el-input__inner {
padding-right: 20px;
}
.el-input__suffix {
z-index: 1;
width: 20px;
}
.el-cascader__tags {
width: 210px;
flex-wrap: nowrap;
display: flex;
align-items: center;
}
.el-cascader__search-input {
min-width: 10px;
margin-left: 10px;
}
}
.el-date-editor {
.el-range__close-icon {
width: 15px;
}
}
.el-button + .el-button {
margin-left: 10px;
}
// .el-dialog__wrapper {
// display: flex;
// align-items: center;
// .el-dialog {
// min-width: 480px;
// margin-top: 50px;
// margin-bottom: 50px;
// max-height: calc(100vh - 100px);
// .el-dialog__header {
// padding: 16px 20px;
// line-height: 20px;
// font-size: 16px;
// color: rgba(0, 0, 0, 0.85);
// border-bottom: 1px solid rgba(0, 0, 0, 0.06);
// }
// }
// }
#app .sidebar-container .nest-menu .el-submenu > .el-submenu__title,
#app .sidebar-container .el-submenu .el-menu-item {
// color: #606266;
}
#app .sidebar-container .el-menu .el-menu-item .svg-icon + span {
font-weight: 500;
font-size: 16px;
// color: #606266;
}
#app .sidebar-container .el-menu .el-submenu__title .svg-icon + span {
font-size: 16px;
font-weight: 500;
}
#app .sidebar-container .svg-icon {
color: #8893aa;
}
#app .is-active .svg-icon {
color: #00bf8a;
}
#app .el-table thead th.el-table__cell .cell {
display: inline-block;
}
.el-table--enable-row-hover .el-table__body tr:hover > td.el-table__cell {
// background-color: #fff9f5;
opacity: 1;
}
.el-picker-panel.el-date-range-picker.el-popper.has-sidebar.has-time.my-popper {
z-index: 200000 !important;
}
.input-number-right-aligned .el-input-number__increase,
.input-number-right-aligned .el-input-number__decrease {
display: inline-block;
width: auto;
}
.input-number-right-aligned .el-input {
text-align: left;
}
.input-number-right-aligned .el-input-number__inner {
text-align: left;
}
// .rowFlex .el-input-number .el-input__inner {
// text-align: left !important;
// }
// 框架样式更改 重新规划
#app {
.app-main {
background: #eeeff5;
padding: 0 15px;
.dashboard {
width: calc(100% - 30px);
}
.content-head.container {
border-radius: 4px;
}
// 表格样式
.content-table {
border-radius: 4px;
margin-top: 15px;
}
.el-table {
// 表格里面的选择框
.el-table-column--selection .cell {
padding: 10px 16px !important;
}
.el-table__fixed-right {
background: #fff;
}
}
}
// 面包屑
.app-breadcrumb.el-breadcrumb {
padding: 14px 0;
.no-redirect {
font-size: 14px;
color: #323335;
font-weight: 700;
}
}
.content-head.container {
border-radius: 4px;
}
// 表格样式
.content-table {
border-radius: 4px;
margin-top: 15px;
}
.el-table {
// 表格里面的选择框
.el-table-column--selection .cell {
padding: 10px 16px !important;
}
.el-table__fixed-right {
background: #fff;
}
}
}
// 选择应用
.selectRouter {
.el-input__suffix {
height: 32px;
}
}
.el-tooltip__popper {
max-width: 300px;
}
// .app-main{
// // 图表的icon颜色
// i,.icon{
// color: #86909C;
// }
// }
.tableContent {
color: #323335 !important;
font-weight: 400 !important;
.tableHeader {
background: #f2f3f5;
}
}
.goRight {
width: 28px;
height: 27px;
background: #00bf8a;
border-radius: 14px;
color: #ffffff;
font-size: 16px;
margin: 0 20px;
cursor: pointer;
border-radius: 14px;
i {
color: #ffffff;
font-size: 16px;
margin: 0 20px;
cursor: pointer;
border-radius: 14px;
i {
color: #ffffff;
}
}
}
// 暂无数据
.noContent {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
i {
font-size: 42px;
color: #86909c;
}
.text {
font-size: 14px;
font-weight: 400;
color: #c9cdd4;
margin-top: 5px;
}
}
// 修改 tabs 全局样式
.el-tabs {
.el-tabs__header {
height: 50px;
border-radius: 4px;
}
.el-tabs__nav-scroll {
height: 50px;
}
.el-tabs__item {
height: 50px;
line-height: 50px;
font-size: 16px;
color: #4e5969;
}
.el-tabs__item.is-active{
color: #00bf8a;
}
}
// 表格样式
.content-outer {
.table-table {
padding-bottom: 20px !important;
}
}
.imageIcon {
position: relative;
}
// 统一标签样式
.checkTagsView {
width: 100%;
.tagsItem {
margin-right: 5px;
margin-bottom: 5px;
}
}
// toast 样式
.el-message{
z-index: 999999 !important;
}
// form 表单类型
.el-form-item .el-form-item, .el-form-item .el-form-item.el-form-item--small{
margin-bottom: 16px;
}
// 描述文字样式
.infoTextColor{
color: #86909c;
}
.infoSpan {
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
max-width: 250px;
p {
font-size: 12px;
max-width: 100%;
line-height: 20px;
color: #86909c;
}
span {
color: #ffa81d;
}
}
// tab 样式 .tabs__content 默认 overflow 为 hidden 为 hidden 的时候 表格表头吸顶功能异常 需要 overflow 设置为 initial
.el-tabs__content {
overflow: initial;
}
\ No newline at end of file
......@@ -7,7 +7,11 @@ $green: #30b08f;
$tiffany: #4ab7bd;
$yellow: #fec171;
$panGreen: #30b08f;
$themeColor: #00BF8A;
$sidebarWidthOpen: 190px;
$sidebarWidthClosed: 56px;
$navbarHeight: 50px;
// sidebar
$menuText: #606266;
$menuActiveText: #00BF8A;
......@@ -30,6 +34,10 @@ $sideBarWidth: 190px;
menuBg: $menuBg;
menuHover: $menuHover;
subMenuBg: $subMenuBg;
themeColor: $themeColor;
sidebarWidthOpen: $sidebarWidthOpen;
sidebarWidthClosed: $sidebarWidthClosed;
navbarHeight: $navbarHeight;
subMenuHover: $subMenuHover;
sideBarWidth: $sideBarWidth;
}
\ No newline at end of file
import Cookies from 'js-cookie'
const TokenKey = 'authorization'
import store from '@/store/index'
const TokenKey = 'token'
const time = new Date(new Date().getTime() + 10 * 12 * 30 * 24 * 60 * 60 * 1000)
export function getToken() {
return Cookies.get(TokenKey)
return Cookies.get(TokenKey) || store.state.user.token
}
export function setToken(token) {
store.state.user.token = token
return Cookies.set(TokenKey, token, { expires: time })
}
export function removeToken() {
store.state.user.token = null
return Cookies.remove(TokenKey)
}
/*
* @Author: your name
* @Date: 2021-12-30 17:11:51
* @LastEditTime: 2022-01-07 12:49:00
* @LastEditors: Please set LastEditors
* @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
* @FilePath: \zhangyou_frontend\src\utils\cos-upload.js
*/
import COS from 'cos-js-sdk-v5'
import { randomStr } from '@/utils/randomStr'
// import { getUpload } from '@/api/public'
import { uploadCos } from '@/api/user'
import moment from 'moment'
const configImg = {
dir: '/company_wx/',
bucket: 'companywx-1300623068',
Region: 'ap-nanjing',
file: null,
date: moment().format('YYYYMMDD'),
str: randomStr(),
ext: null
}
// 默认获取
const getOptions = function (params) {
return new Promise(function (reject, resolve) {
uploadCos(params).then((res) => {
if (res.status_code === 1) {
reject(res)
} else {
resolve(res)
}
})
})
}
function uploading(file, config, callbackApi = getOptions) {
const _config = Object.assign({}, configImg, config)
_config.file = file
_config.ext = file.name.slice(file.name.lastIndexOf('.') + 1)
//
return new Promise(function (resolve, reject) {
const cos = new COS({
// 后端获取签名
getAuthorization: function (options, callback) {
//
callbackApi({ bucket: _config.bucket, region: _config.Region }).then(
(res) => {
var credentials = res.data.data.credentials
if (!credentials) return
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
XCosSecurityToken: credentials.sessionToken,
// 建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误
StartTime: res.data.data.startTime, // 时间戳,单位秒,如:1580000000
ExpiredTime: res.data.data.expiredTime // 时间戳,单位秒,如:1580000900
})
}
)
}
})
const accessConfig = {
Bucket: _config.bucket,
Region: _config.Region,
Key:
_config.dir +
_config.date +
'/' +
_config.str +
new Date().valueOf() +
'.' +
_config.ext,
StorageClass: 'STANDARD',
Body: _config.file, // 上传文件对象
onProgress: function (progressData) {}
}
//
cos.putObject(accessConfig, function (err, data) {
if (err) {
reject({
status_code: -1,
msg: '上传失败',
data: err,
config: _config
})
// resolve({
// status_code: -1,
// msg: '上传失败',
// data: err,
// config: _config
// })
}
if (data && data.Location) {
const urlList = data.Location.split('/')
urlList[0] = 'companywxcdn.zwnet.cn'
resolve({
status_code: 1,
msg: '上传成功',
data: 'https://' + urlList.join('/'),
config: _config
})
}
})
})
}
// export default uploading
function _install(Vue) {
if (Vue && Vue.uploading === undefined) {
Vue.prototype.uploading = uploading
} else {
return
}
}
export default _install
......@@ -10,7 +10,6 @@
*/
import store from '@/store'
import Clipboard from 'clipboard'
import * as imageConversion from 'image-conversion'
export function parseTime(time, cFormat) {
if (arguments.length === 0 || !time) {
return null
......@@ -103,7 +102,8 @@ export function formatTime(time, option) {
* @param {string} url
* @returns {Object}
*/
export function param2Obj(url) {
export function getParams(url) {
url = url || window.location.href;
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) {
return {}
......
const jsApiList = [
'getCurExternalContact',
]
export default jsApiList
\ No newline at end of file
export function randomStr(len) {
var le = len || 32
var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' // ****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****
var maxPos = $chars.length
var str = ''
for (var i = 0; i < le; i++) {
str += $chars.charAt(Math.floor(Math.random() * maxPos))
}
return str
}
\ No newline at end of file
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store/index'
import { getToken } from '@/utils/auth'
import { getToken,removeToken } from '@/utils/auth'
// 常量定义
const CancelToken = axios.CancelToken
const STATUS_CODE_SUCCESS = 1
const TOKEN_ERROR_CODES = [50008, 50012, 50014] // Illegal token, Other clients logged in, Token expired
import Cookies from 'js-cookie'
/**
......@@ -31,16 +31,19 @@ service.interceptors.request.use(
const url = config.data
? config.url + JSON.stringify(config.data)
: config.url
store.state.user.axiosCancelList.push({
url,
cancel: c
})
})
const authToken = getToken()
// 设置认证token
if (store.getters.token) {
config.headers['authToken'] = getToken()
if (authToken) {
config.headers['Authtoken'] = authToken
}
const corp_id = store.state.user.corp_id || Cookies.get('corp_id')
const userid = Cookies.get('userid')
if (corp_id) {
config.headers['Corp-Id'] = corp_id
}
if (userid) {
config.headers['User-Id'] = userid
}
return config
},
......@@ -50,6 +53,7 @@ service.interceptors.request.use(
}
)
// 响应拦截器
service.interceptors.response.use(
/**
......@@ -65,33 +69,23 @@ service.interceptors.response.use(
(response) => {
const res = response.data
// cancelPending(response.config)
// 如果状态码不是成功,则判断为错误
if (res.status_code !== STATUS_CODE_SUCCESS) {
// if (res.status_code === -1) {
// return res
// }
Message({
message: res.msg || 'Error',
type: 'error',
duration: 5 * 1000
})
// 处理token相关错误: 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (TOKEN_ERROR_CODES.includes(res.status_code)) {
// to re-login
MessageBox.confirm('登录过期,请重新登录', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
console.log('重新登录')
duration: 2 * 1000
})
if (res.status_code === -100) {
// 登录 过期 重新去登录
setTimeout(() => {
removeToken()
window.location.href = window.location.origin +'/company_app/index.html?corp_id='+Cookies.get('corp_id')
}, 2000);
return res
}
return Promise.reject(new Error(res.msg || 'Error'))
}
return res
},
(error) => {
......@@ -101,7 +95,6 @@ service.interceptors.response.use(
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
......
<template>
<div class="about">
<h1>about</h1>
</div>
</template>
<template>
<div class="home">
<h1>home</h1>
</div>
</template>
<script>
export default {
name: 'HomeView',
components: {
}
}
</script>
<template>
<div class="addressBook-content">
通讯录
</div>
</template>
<script>
export default {
name: 'AddressBook',
}
</script>
<style lang="scss" scoped>
.addressBook-content{
width: 100%;
height: 100%;
background: #fff;
}
</style>
\ No newline at end of file
<template>
<div class="applyRecord-content">
申请记录
</div>
</template>
<script>
export default {
name: 'ApplyRecord',
}
</script>
<style lang="scss" scoped>
.applyRecord-content{
width: 100%;
height: 100%;
background: #fff;
}
</style>
\ No newline at end of file
<template>
<div class="bindUserList rowFlex columnCenter">
<div class="select">
<el-select v-model="bindAccount" placeholder="请选择关联账号" :clearable="false" @change="handleChange">
<el-option label="新增关联账号" value="add" @click="addNewUser">
</el-option>
<el-option v-for="(item, index) in bindGameUserList" :key="index" :label="item.username"
:value="item.member_id">
</el-option>
</el-select>
</div>
<p v-if="bindGameUserList.length > 0" class="num">
总共{{ bindGameUserList.length }}个账号
</p>
<el-button type="danger" style="margin-left: 10px;" size="mini" @click="logout">下线</el-button>
<addUser
:show.sync="showLayer"
title="选择玩家"
width="60%"
/>
</div>
</template>
<script type="text/javascript">
import { detailsInfoRequest } from '@/api/works'
import {memberView} from '@/api/game'
import { logout } from '@/api/user'
import { mapState, mapMutations, mapActions } from 'vuex'
import addUser from './addUser.vue'
import { getToken,removeToken } from '@/utils/auth'
// 更新代码
export default {
components: {
addUser
},
data() {
return {
showLayer: false,
accountValue: '',
bindAccount:'',
memberCheckList:[], // 自定义列
}
},
computed: {
...mapState('game', [
'bindGameUserList',
'accountSelect',
'gameUserInfo'
]),
...mapState('user', [
'userid',
'corp_id',
'external_userid'
]),
},
watch: {
accountSelect(newVal,oldVal) {
if(newVal){
console.log(newVal,'hahhaha')
this.gameMemberView()
this.bindAccount = newVal
}
}
},
async mounted() {
console.log(this.external_userid,'external_userid')
this.bindUserList()
this.requestDetails()
},
methods: {
...mapMutations('game', [
'set_accountSelect',
'set_chatUserInfo',
'set_gameUserInfo'
]),
...mapActions('game', ['gameBindUser']),
handleChange(value) {
if (value == 'add') {
this.showLayer = true
} else {
this.set_accountSelect(value)
}
},
gameMemberView(item) {
if (this.accountSelect && this.accountSelect !== '') {
const data = { member_id: this.accountSelect, need_channel: 1, need_roleInfo: 1, need_banned: 1 }
memberView(data).then((res) => {
if (res.status_code === 1) {
this.set_gameUserInfo(res.data)
} else {
this.set_gameUserInfo({})
}
}, (err) => {
this.set_gameUserInfo({})
})
}
},
logout(){
this.$confirm('确定下线吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.userLogout()
}).catch(() => {
this.$message({
type: 'info',
message: '已取消'
})
})
},
async userLogout(){
const data = {
userid: this.userid,
}
const res = await logout(data)
if(res.status_code === 1){
this.$message({
type: 'success',
message: '下线成功'
})
removeToken()
window.location.href = window.location.origin +'/company_app/index.html?corp_id='+this.corp_id
}else{
this.$message({
type: 'error',
message: '下线失败'
})
}
},
addNewUser() {
console.log(11)
},
async requestDetails() {
const data = {
userid: this.userid,
external_userid: this.external_userid
}
const res = await detailsInfoRequest(data)
if (res.data && res.data.userid) {
console.log(res.data,'1231')
this.chatUserDetails = res.data
this.set_chatUserInfo(this.chatUserDetails) // 设置云端信息
console.log(this.chatUserDetails,'1231')
if (this.chatUserDetails.self_defined_columns && this.chatUserDetails.self_defined_columns.length > 0) {
this.memberCheckList =
this.chatUserDetails.self_defined_columns.map(
(item) => item.name
)
} else {
this.memberCheckList = []
}
} else {
this.chatUserDetails = {}
}
// 获取到用户的详情 储存在缓存里面
},
// 绑定列表
async bindUserList() {
const data = {
userid: this.userid,
external_userid: this.external_userid
}
const res = await this.gameBindUser(data)
if (res.length > 0) {
this.set_accountSelect(res[0].member_id)
this.bindAccount = res[0].member_id
} else {
this.set_accountSelect('')
this.bindAccount = ''
}
}
}
}
</script>
<style lang="scss" scoped>
.bindUserList {
margin: 10px;
.select {
::v-deep .el-input--small .el-input__inner {
// border-radius: 16px;
border: none;
// color: $color;
min-width: 200px;
height: 30px;
line-height: 30px;
background: #ecfff6;
color: #46c988;
}
::v-deep .el-input__suffix {
// display: none;
color: #46c988;
}
}
.num {
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 600;
margin-left: 10px;
white-space: nowrap;
color: #F53F3F;
}
}
</style>
\ No newline at end of file
<template>
<el-table
ref="multipleTable"
:data="list"
tooltip-effect="dark"
style="width: 100%"
size="medium"
highlight-current-row
:header-cell-style="{
background: '#F7F8FA',
color: '#333333',
fontWeight: 500,
}"
@current-change="handleCurrent"
>
<el-table-column width="50px">
<template v-slot="scope">
<!-- label值要与el-table数据id实现绑定 -->
<el-radio
v-model="unitInfo.role_id"
style="margin-left:10px;"
:label="scope.row.role_id"
>{{ "" }}</el-radio>
</template>
</el-table-column>
<el-table-column
label="w账号"
prop="username"
>
</el-table-column>
<el-table-column
label="角色名"
prop="role_name"
>
</el-table-column>
<el-table-column
label="区服"
prop="server_name"
>
</el-table-column>
<el-table-column
label="充值金额"
prop="recharge_total"
>
<template v-slot="scope">
{{ scope.row.recharge_total }}
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
props: ['list'],
data() {
return {
currentRow: [],
unitInfo: {
userId: ''
}
}
},
watch: {
list(newVal, oldVal) {
this.unitInfo = { userId: '' }
}
},
methods: {
handleCurrent(val) {
console.log(val)
if (val) {
this.unitInfo = val
this.$emit('checkedTag', this.unitInfo)
}
}
}
}
</script>
<style scoped lang='scss'>
.el-dropdown {
margin-left: 10px;
}
.tags {
background: #ecfff6;
border-radius: 4px;
border: 1px solid #c5ffe2;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #46c988;
padding: 0 5px;
max-width: 100%;
font-size: 12px;
}
.tagItem {
display: inline-block;
}
.allTags {
.tags {
margin-left: 10px;
}
}
.el-dropdown-link {
cursor: pointer;
}
.qrImage {
width: vw(140);
height: vw(140);
}
.tableImage {
width: 30px;
height: 30px;
border-radius: 30px;
margin-right: 10px;
}
.infoSpan {
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #999999;
max-width: 120px;
span {
color: #ffa81d;
}
}
::v-deep .el-table__header-wrapper .el-checkbox {
display: none;
}
</style>
\ No newline at end of file
<template>
<div class="giftRecord-content">
礼包记录
</div>
</template>
<script>
export default {
name: 'GiftRecord',
}
</script>
<style lang="scss" scoped>
.giftRecord-content{
width: 100%;
height: 100%;
background: #fff;
}
</style>
\ No newline at end of file
<template>
<div class="quickReply-content">
快捷回复
</div>
</template>
<script>
export default {
name: 'QuickReply',
}
</script>
<style lang="scss" scoped>
.quickReply-content{
width: 100%;
height: 100%;
background: #fff;
}
</style>
\ No newline at end of file
<template>
<div class="quickSend-content">
快捷发送
</div>
</template>
<script>
export default {
name: 'QuickSend',
}
</script>
<style lang="scss" scoped>
.quickSend-content{
width: 100%;
height: 100%;
background: #fff;
}
</style>
\ No newline at end of file
<template>
<h1>1231312312312312321</h1>
</template>
<script>
import * as ww from '@wecom/jssdk'
import { getSignature } from '@/api/user'
export default {
name: 'testWx',
data() {
return {
signData: {},
}
},
mounted() {
this.getSignature()
console.log(window.location.href,'window.location.href')
},
methods: {
async getSignature(){
console.log('获取签名',window.location.href)
const res = await getSignature({ corp_id: 'wweaefe716636df3d1', path: window.location.href });
if(res.status_code === 1){
this.signData = res.data
try{
this.registerWeComSDK();
}catch(err){
console.log(err,'初始化sdk 失败')
}
}
},
registerWeComSDK() {
ww.register({
corpId: 'wweaefe716636df3d1',
agentId: this.signData.agent_id,
jsApiList: ['getExternalUserInfo'],
getConfigSignature: () => Promise.resolve({
nonceStr: this.signData.nonce_str,
timestamp: this.signData.signature_time,
signature: this.signData.corp_signature,
}),
// 只用到应用的 api 可以只进行应用的签名
getAgentConfigSignature: () => Promise.resolve({
nonceStr: this.signData.nonce_str,
timestamp: this.signData.signature_time,
signature: this.signData.agent_signature,
}),
onAgentConfigSuccess: (res) => {
console.log('注册成功可以调用企微 js-sdk',res)
this.getCurExternalContact()
// 注册成功后不立即获取外部联系人,等钉钉扫码后再获取
},
onAgentConfigFail: (err) => {
console.log('注册失败不能使用企微js-sdk',err)
// 错误处理
}
});
},
getCurExternalContact() {
ww.getCurExternalContact({
success: (res) => {
if (res.err_msg === "getCurExternalContact:ok") {
console.log(res,'获取企微外部联系人')
}
},
fail: (err) => {
console.log(err,'获取企微外部联系人失败')
// 错误处理
}
});
},
}
}
</script>
\ No newline at end of file
<template>
<div class="roleTab">
<el-tabs v-model="roleActive" @tab-click="handleClick">
<el-tab-pane label="角色信息" name="role">
<userInfo v-if="roleActive==='role'" />
</el-tab-pane>
<!-- <el-tab-pane label="举报记录" name="report">
<report v-show="roleActive==='report'" />
</el-tab-pane>
-->
<el-tab-pane label="申诉记录" name="approval">
<approval v-if="roleActive==='approval'" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import userInfo from './roleInfo/userInfo.vue'
// import report from './roleInfo/report.vue'
import approval from './roleInfo/approval.vue'
export default {
components: {
userInfo,
approval,
// report
},
props: {
// report_is_send: {
// type: Boolean,
// default: false
// }
},
data() {
return {
roleActive: 'role'
}
},
methods: {
handleClick(value) {
}
}
}
</script>
<style lang="scss" scoped>
.roleTab{
width: 100%;
height: 100%;
background: #fff;
}
</style>
\ No newline at end of file
<template>
<div class="details columnFlex">
<el-form
v-loading="loading"
class="content"
label-width="100px"
>
<div v-if="violationList.length > 0">
<div
v-for="(item, index) in violationList"
:key="index"
class="contentItem"
>
<el-form-item label="违规时间:">
<p>{{ item.violation_time }}</p>
</el-form-item>
<el-form-item label="违规操作:">
<p>{{ item.violation_type_name || "" }}</p>
</el-form-item>
<el-form-item label="封禁时间:">
<p>{{ item.create_time }}</p>
</el-form-item>
<el-form-item label="角色名称:">
<p>{{ item.server_name }} - {{ item.role_name }}</p>
</el-form-item>
<el-form-item label="封禁方式:">
<p :class="item.banned_time_type == 3 ? 'error' : ''">
{{ item.banned_type_text }}
</p>
</el-form-item>
<el-form-item label="是否允许申诉:">
<p class="error">{{ item.appeal_name }}</p>
</el-form-item>
<el-form-item
v-if="item.remake != ''"
label="详情:"
>
<!-- AI自动封禁 -->
<div
class="remarkType"
v-if="item.information_type === 6"
>
<p>
<span class="label">所属项目:</span><span>{{ item.newRemake.project || "" }}</span>
</p>
<p>
<span class="label">角色ID:</span><span>{{ item.newRemake.cp_role_id || "" }}</span>
</p>
<p>
<span class="label">名单类型:</span><span>{{ item.newRemake.list_type || "" }}</span>
</p>
<p>
<span class="label">当前命中:</span><span>{{ item.newRemake.current_hit || "" }}</span>
</p>
<p>
<span class="label">累计命中:</span><span>{{ item.newRemake.hit_total || "" }}</span>
</p>
<p>
<span class="label">释放时间:</span><span>{{ item.newRemake.release_time || "" }}</span>
</p>
<p>
<span class="label">操作用户:</span><span>{{ item.newRemake.handel_user || "" }}</span>
</p>
<p>
<span class="label">备注信息:</span><span>{{ item.newRemake.remark || "" }}</span>
</p>
</div>
<!-- 其他类型 -->
<div
class="remarkType"
v-else
>
<div
v-if="item.remake.indexOf('src=') !== -1"
class="remakeImage"
>
<p class="watchDetails">
<el-button
type="text"
icon="el-icon-view"
size="medium"
style="z-index: 1; position: relative; margin-left: 5px"
@click="showRemake(item.remake)"
>查看大图</el-button>
</p>
<p v-html="item.remake"></p>
</div>
<div
v-else
class="remakeImage"
>
<p v-html="item.remake"></p>
</div>
</div>
</el-form-item>
<div v-if="item && item.newRemake && item.newRemake.hit">
<div
class="title"
style="font-weight: 600; margin-bottom: 10px"
>
命中统计
</div>
<BiTable
:data="item.newRemake.hit"
size="medium"
:column="tableColums"
>
<template v-slot:time="{ row }">
<p>{{ $moment(row.time).format("YYYY-MM-DD HH:mm:ss") }}</p>
</template>
</BiTable>
</div>
</div>
</div>
<div
v-if="!loading && violationList.length == 0"
class="noContent rowFlex allCenter"
>
<noContent title="暂无数据" description="当前没有任何数据,请稍后再试或联系管理员" />
</div>
</el-form>
<el-dialog
title="查看大图"
:visible.sync="imageLayer"
width="50%"
append-to-body
fit="contain"
@close="imageLayer = false"
>
<div
v-html="imageSrc"
class="layerImage"
></div>
</el-dialog>
</div>
</template>
<script>
import { mapState } from "vuex";
import { violationList } from "@/api/game";
import noContent from "@/components/noContent.vue";
import { debounce } from '@/utils'
import watchMember from '@/mixins/watchMember'
export default {
components: {
noContent,
},
data() {
return {
imageSrc: [],
violationList: [],
tableColums: [
{
label: "文本内容",
prop: "content",
},
{
label: "命中类型",
prop: "type",
},
{
label: "关键字",
prop: "key",
},
{
label: "时间",
prop: "time",
width: 120,
slotScope: true
},
],
loading: false,
imageLayer: false,
};
},
computed: {
...mapState("game", ["accountSelect", "gameTabActive"]),
},
mixins: [watchMember],
mounted() {
this.requestViolationList();
},
methods: {
memberChange() {
this.requestViolationList()
},
handleRemark(remark) {
try {
const remarkObj = JSON.parse(JSON.parse(remark.replace(/\r\n\t/g, "")));
console.log(remarkObj, "remarkObj");
return remarkObj;
} catch (error) {
return remark;
}
},
showRemake(remake) {
this.imageSrc = remake;
this.imageLayer = true;
},
requestViolationList() {
const data = {
account_type: 1,
member_id: this.accountSelect,
};
this.loading = true;
violationList(data).then(
(res) => {
if (res.status_code == 1 && res.data.data.length > 0) {
this.violationList = res.data.data;
this.violationList.map((item) => {
if (item.information_type == 6) {
item.newRemake = this.handleRemark(item.remake);
}
});
} else {
this.violationList = [];
}
this.loading = false;
},
(err) => {
this.loading = false;
}
);
},
},
};
</script>
<style lang="scss">
.remakeImage {
img {
max-width: 200px;
height: auto;
border-radius: 5px;
margin-top: 10px;
}
}
.layerImage {
img {
max-width: 800px;
}
}
</style>
<style lang="scss" scoped>
.details {
width:100%;
height: calc(100vh - 150px);
background: #fff;
margin-left: 2px;
.content {
width: 100%;
padding: vw(20);
height: 100%;
overflow: auto;
}
.contentMain {
}
.contentItem {
border-bottom: 1px dashed #ebeef5;
margin-top: 10px;
}
.remarkType {
width: auto;
margin-top: 4px;
p {
line-height: 25px;
}
.label {
color: #99a3b4;
margin-right: 5px;
}
.value {
}
}
.error {
color: #ff5959;
}
.foulImage {
max-width: 300px;
height: auto;
}
::v-deep .el-collapse-item {
margin-bottom: 20px;
}
::v-deep .el-collapse-item__content {
padding-bottom: 10px;
}
::v-deep .el-collapse {
border: none;
}
::v-deep .el-collapse-item__header {
width: 100%;
height: 44px;
background: #f9faff;
color: #333333;
padding-left: 80px;
font-size: 14px;
font-weight: 400;
}
::v-deep .el-form-item {
margin-bottom: 10px;
}
}
.remakeDetails {
::v-deep img {
max-width: 200px;
max-height: 200px;
}
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论