提交 1afeb6e9 作者: 施汉文

新增异步对话框组件及相关逻辑,支持高风险用户确认发送操作

上级 01abd744
import Vue from 'vue'
import AsyncDialog from './index.vue'
let instance = null
let mountNode = null
function createInstance() {
if (instance && !instance._isDestroyed) {
return instance
}
const ComponentConstructor = Vue.extend(AsyncDialog)
mountNode = document.createElement('div')
document.body.appendChild(mountNode)
instance = new ComponentConstructor()
instance.$mount(mountNode)
return instance
}
export function getAsyncDialogInstance() {
return createInstance()
}
export function openAsyncDialog(options = {}) {
return createInstance().open(options)
}
export function closeAsyncDialog(action = 'close') {
if (!instance || instance._isDestroyed) {
return
}
instance.close(action)
}
export function destroyAsyncDialog() {
if (!instance) {
return
}
instance.$destroy()
if (instance.$el && instance.$el.parentNode) {
instance.$el.parentNode.removeChild(instance.$el)
}
if (mountNode && mountNode.parentNode) {
mountNode.parentNode.removeChild(mountNode)
}
instance = null
mountNode = null
}
export default openAsyncDialog
<template>
<el-dialog
v-bind="dialogAttrs"
:visible.sync="visible"
:before-close="handleBeforeClose"
@close="handleDialogClose"
@closed="handleDialogClosed"
custom-class="async-dialog-custom"
>
<slot v-if="hasDefaultSlot" v-bind="slotScope"></slot>
<div
v-else-if="dialogMessage"
class="async-dialog__body-content"
>
<i class="el-icon-warning-fill async-dialog__warning-icon"></i>
<div
class="async-dialog__message"
:class="{ 'is-html': dialogOptions.dangerouslyUseHTMLString }"
v-html="dialogContent"
></div>
</div>
<span v-if="showFooter" slot="footer" class="dialog-footer">
<slot name="footer" v-bind="slotScope">
<el-button
v-if="showCancelButton"
v-bind="cancelButtonProps"
:disabled="confirmLoading"
@click="handleCancel"
>
{{ cancelText }}
</el-button>
<el-button
v-if="showConfirmButton"
v-bind="confirmButtonProps"
:disabled="confirmDisabled"
:loading="confirmLoading"
@click="handleConfirm"
>
{{ confirmText }}
</el-button>
</slot>
</span>
</el-dialog>
</template>
<script>
const CUSTOM_OPTION_KEYS = [
'payload',
'message',
'dangerouslyUseHTMLString',
'countdown',
'confirmText',
'cancelText',
'showFooter',
'showCancelButton',
'showConfirmButton',
'confirmButtonProps',
'cancelButtonProps',
'onConfirm',
'onCancel',
'onClosed',
'beforeClose'
]
export default {
name: 'AsyncDialog',
inheritAttrs: false,
data() {
return {
visible: false,
confirmLoading: false,
confirmCountdown: 0,
countdownTimer: null,
dialogOptions: this.getDefaultOptions(),
dialogHooks: this.getDefaultHooks(),
promiseHandlers: null,
closingAction: '',
closingResult: undefined
}
},
computed: {
hasDefaultSlot() {
return Boolean(this.$scopedSlots.default || this.$slots.default)
},
dialogMessage() {
return this.dialogOptions.message
},
dialogContent() {
if (this.dialogOptions.dangerouslyUseHTMLString) {
return this.dialogMessage || ''
}
return this.escapeHtml(this.dialogMessage || '')
},
dialogAttrs() {
const dialogAttrs = {
...this.$attrs
}
Object.keys(this.dialogOptions).forEach((key) => {
if (!CUSTOM_OPTION_KEYS.includes(key)) {
dialogAttrs[key] = this.dialogOptions[key]
}
})
return {
title: '',
width: '335px',
appendToBody: true,
closeOnClickModal: false,
closeOnPressEscape: false,
destroyOnClose: true,
...dialogAttrs
}
},
showFooter() {
return this.dialogOptions.showFooter !== false
},
showCancelButton() {
return this.dialogOptions.showCancelButton !== false
},
showConfirmButton() {
return this.dialogOptions.showConfirmButton !== false
},
confirmText() {
const text = this.dialogOptions.confirmText || '确 定'
if (this.confirmCountdown > 0) {
return `${text}(${this.confirmCountdown}s)`
}
return text
},
cancelText() {
return this.dialogOptions.cancelText || '取 消'
},
confirmDisabled() {
return this.confirmLoading || this.confirmCountdown > 0
},
confirmButtonProps() {
return {
type: 'primary',
...(this.dialogOptions.confirmButtonProps || {})
}
},
cancelButtonProps() {
return {
...(this.dialogOptions.cancelButtonProps || {})
}
},
slotScope() {
return {
visible: this.visible,
loading: this.confirmLoading,
countdown: this.confirmCountdown,
payload: this.dialogOptions.payload,
options: this.dialogOptions,
confirm: this.handleConfirm,
cancel: this.handleCancel,
close: this.close
}
}
},
beforeDestroy() {
this.clearCountdown()
this.finishPromise('destroy')
},
methods: {
getDefaultOptions() {
return {
payload: undefined,
message: '',
dangerouslyUseHTMLString: false,
countdown: 0,
title: '',
width: '335px',
appendToBody: true,
closeOnClickModal: false,
closeOnPressEscape: false,
destroyOnClose: true,
showFooter: true,
showCancelButton: true,
showConfirmButton: true,
confirmText: '确 定',
cancelText: '取 消',
confirmButtonProps: null,
cancelButtonProps: null
}
},
getDefaultHooks() {
return {
onConfirm: null,
onCancel: null,
onClosed: null,
beforeClose: null
}
},
open(options = {}) {
if (this.promiseHandlers) {
return Promise.reject({
action: 'busy',
message: 'AsyncDialog 正在显示中'
})
}
const mergedOptions = {
...this.getDefaultOptions(),
...options
}
this.dialogOptions = mergedOptions
this.dialogHooks = {
onConfirm: mergedOptions.onConfirm,
onCancel: mergedOptions.onCancel,
onClosed: mergedOptions.onClosed,
beforeClose: mergedOptions.beforeClose
}
this.closingAction = ''
this.closingResult = undefined
this.confirmLoading = false
this.startCountdown(mergedOptions.countdown)
this.visible = true
return new Promise((resolve, reject) => {
this.promiseHandlers = {
resolve,
reject
}
})
},
async handleConfirm() {
if (this.confirmDisabled || !this.promiseHandlers) {
return
}
try {
this.confirmLoading = true
let result
if (typeof this.dialogHooks.onConfirm === 'function') {
result = await this.dialogHooks.onConfirm(this.getActionContext('confirm'))
}
if (result === false) {
return
}
this.closingAction = 'confirm'
this.closingResult = result
this.visible = false
} catch (error) {
return
} finally {
this.confirmLoading = false
}
},
async handleCancel() {
if (!this.promiseHandlers || this.confirmLoading) {
return
}
try {
if (typeof this.dialogHooks.onCancel === 'function') {
const result = await this.dialogHooks.onCancel(this.getActionContext('cancel'))
if (result === false) {
return
}
}
this.closingAction = 'cancel'
this.closingResult = undefined
this.visible = false
} catch (error) {
return
}
},
startCountdown(countdown) {
this.clearCountdown()
const countdownSeconds = Number(countdown) || 0
if (countdownSeconds <= 0) {
this.confirmCountdown = 0
return
}
this.confirmCountdown = Math.ceil(countdownSeconds)
this.countdownTimer = setInterval(() => {
if (this.confirmCountdown <= 1) {
this.confirmCountdown = 0
this.clearCountdown()
return
}
this.confirmCountdown -= 1
}, 1000)
},
clearCountdown() {
if (this.countdownTimer) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
}
this.confirmCountdown = 0
},
close(action = 'close') {
if (!this.promiseHandlers) {
return
}
this.closingAction = this.closingAction || action
this.visible = false
},
handleClose() {
this.close('close')
},
handleDialogClose() {
if (!this.promiseHandlers) {
return
}
this.closingAction = this.closingAction || 'close'
},
async handleBeforeClose(done) {
const beforeClose = this.dialogHooks.beforeClose
if (typeof beforeClose !== 'function') {
done()
return
}
let doneCalled = false
const wrappedDone = () => {
doneCalled = true
done()
}
try {
const result = await beforeClose(wrappedDone, this.getActionContext(this.closingAction || 'close'))
if (result !== false && !doneCalled) {
done()
}
} catch (error) {
return
}
},
async handleDialogClosed() {
const action = this.closingAction || 'close'
const closeResult = this.buildResult(action)
try {
if (typeof this.dialogHooks.onClosed === 'function') {
await this.dialogHooks.onClosed(closeResult)
}
} finally {
this.finishPromise(action, closeResult)
this.resetState()
}
},
finishPromise(action, result) {
if (!this.promiseHandlers) {
return
}
const { resolve, reject } = this.promiseHandlers
this.promiseHandlers = null
if (action === 'confirm') {
resolve(result || this.buildResult(action))
return
}
reject(result || this.buildResult(action))
},
buildResult(action) {
return {
action,
payload: this.dialogOptions.payload,
result: this.closingResult
}
},
getActionContext(action) {
return {
action,
payload: this.dialogOptions.payload,
options: this.dialogOptions,
close: this.handleClose,
cancel: this.handleCancel,
confirm: this.handleConfirm
}
},
resetState() {
this.clearCountdown()
this.visible = false
this.confirmLoading = false
this.dialogOptions = this.getDefaultOptions()
this.dialogHooks = this.getDefaultHooks()
this.closingAction = ''
this.closingResult = undefined
},
escapeHtml(text) {
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
}
}
</script>
<style scoped>
.async-dialog__message {
line-height: 22px;
word-break: break-word;
white-space: pre-wrap;
}
.async-dialog__message.is-html {
white-space: normal;
}
/* 消息内容区:图标 + 文字横排 */
.async-dialog__body-content {
display: flex;
align-items: flex-start;
gap: 8px;
}
.async-dialog__warning-icon {
font-size: 20px;
color: #FF7D00;
flex-shrink: 0;
line-height: 22px;
}
</style>
<style>
/* 遮罩层样式 */
.el-overlay.is-dialog {
background-color: rgba(0, 0, 0, 0.5);
}
/* 弹窗整体 */
.async-dialog-custom {
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.async-dialog-custom.el-dialog {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 头部 */
.async-dialog-custom .el-dialog__header {
padding: 12px 20px;
margin: 0;
display: flex;
justify-content: space-between;
align-items: center;
border: none;
}
.async-dialog-custom .el-dialog__title {
font-family: PingFang SC;
font-weight: 600;
font-size: 16px;
line-height: 24px;
color: #323335;
margin: 0;
}
.async-dialog-custom .el-dialog__headerbtn {
top: 12px;
right: 20px;
}
.async-dialog-custom .el-dialog__headerbtn .el-dialog__close {
font-size: 16px;
width: 16px;
height: 16px;
color: #86909C;
line-height: 16px;
}
.async-dialog-custom .el-dialog__headerbtn:hover .el-dialog__close {
color: #323335;
}
/* 内容区 */
.async-dialog-custom .el-dialog__body {
padding: 12px 20px;
color: #4E5969;
font-family: PingFang SC;
font-weight: 400;
font-size: 14px;
line-height: 22px;
border: none !important;
border-top: none !important;
border-bottom: none !important;
}
/* 底部 */
.async-dialog-custom .el-dialog__footer {
padding: 12px 20px;
border: none !important;
border-top: none !important;
}
/* 按钮样式 */
.async-dialog-custom .el-button {
padding: 4px 16px;
height: 32px;
border-radius: 4px;
font-family: PingFang SC;
font-weight: 400;
font-size: 14px;
line-height: 22px;
margin-left: 0 !important;
}
.async-dialog-custom .el-button--default {
border: 1px solid #D9D9D9;
color: #323335;
background: #FFFFFF;
}
.async-dialog-custom .el-button--default:hover {
color: #5A6675;
border-color: #BFBFBF;
}
.async-dialog-custom .el-button--primary {
background: #00BF8A;
border: 1px solid #00BF8A;
color: #FFFFFF;
}
.async-dialog-custom .el-button--primary:hover,
.async-dialog-custom .el-button--primary:focus {
background: #00a979;
border: 1px solid #00a979;
}
/* 倒计时期间按钮置灰,结束后恢复主题色 */
.async-dialog-custom .el-button--primary.is-disabled {
background: #6B7785;
border-color: #6B7785;
color: #FFFFFF;
opacity: 1;
cursor: not-allowed;
}
.async-dialog-custom .el-button.is-disabled {
opacity: 0.5;
}
/* 底部按钮容器 */
.async-dialog-custom .dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
width: 100%;
border: none;
}
.async-dialog-custom .dialog-footer .el-button {
margin: 0;
}
</style>
......@@ -463,6 +463,7 @@ import {
getLandingPageConfig,
getMemberTransStatus,
memberRegGameCloneLink,
getMemberLabel,
} from '@/api/game';
import {
getRecentSendLog,
......@@ -483,6 +484,7 @@ import sendSelectChannel from './sendGame/sendSelectChannel.vue';
import gameLogMixin from '@/mixins/gameLogMixin';
import { sendChatMessage } from '@/utils/index';
import QrcodeVue from 'qrcode.vue';
import { openAsyncDialog } from '@/components/AsyncDialog';
export default {
name: 'sendGame',
mixins: [gameLogMixin],
......@@ -532,6 +534,11 @@ export default {
h5CloneGameInfo: {},
qrCodeValue: '', // 二维码内容
qrCodeSize: 200, // 二维码大小
transRiskMemberId: '',
transRiskInfo: {
isHighRisk: false,
isPhishing: false,
},
};
},
mounted() {
......@@ -553,6 +560,11 @@ export default {
accountSelect(newVal, oldVal) {
// 切换 w 账号的时候清空 conversionGameList 数据
this.conversionGameList = [];
this.transRiskMemberId = '';
this.transRiskInfo = {
isHighRisk: false,
isPhishing: false,
};
this.getMemberTransStatus();
if (newVal && newVal !== '' && this.bindGameUserList.length > 0) {
this.disabled = false;
......@@ -1049,11 +1061,102 @@ export default {
return false;
}
},
sendLink: throttle(function (item, type) {
isTruthyFlag(value) {
return value === true || value === 1 || value === '1';
},
async getTransRiskInfo() {
const localRiskInfo = {
isHighRisk:
Boolean(this.gameUserInfo && this.gameUserInfo.exp_ip) ||
this.isTruthyFlag(this.gameUserInfo && this.gameUserInfo.change_risk),
isPhishing:
this.isTruthyFlag(
this.chatUserInfo && this.chatUserInfo.is_phishing_account,
) ||
this.isTruthyFlag(
this.chatUserInfo && this.chatUserInfo.change_appraisal,
) ||
this.isTruthyFlag(
this.gameUserInfo && this.gameUserInfo.change_appraisal,
),
};
if (!this.accountSelect) {
return localRiskInfo;
}
if (this.transRiskMemberId === this.accountSelect) {
return {
isHighRisk:
localRiskInfo.isHighRisk || this.transRiskInfo.isHighRisk,
isPhishing:
localRiskInfo.isPhishing || this.transRiskInfo.isPhishing,
};
}
try {
const res = await getMemberLabel({
member_id: this.accountSelect,
label_type: [4, 6],
});
const memberLabelList = res?.data?.data || [];
const riskLabel = memberLabelList.find(
(item) => item.label_type == 4,
);
const phishingLabel = memberLabelList.find(
(item) => item.label_type == 6,
);
this.transRiskMemberId = this.accountSelect;
this.transRiskInfo = {
isHighRisk: this.isTruthyFlag(riskLabel && riskLabel.label_value),
isPhishing: this.isTruthyFlag(
phishingLabel && phishingLabel.label_value,
),
};
} catch (error) {
console.log(error);
this.transRiskMemberId = this.accountSelect;
this.transRiskInfo = {
isHighRisk: false,
isPhishing: false,
};
}
return {
isHighRisk: localRiskInfo.isHighRisk || this.transRiskInfo.isHighRisk,
isPhishing: localRiskInfo.isPhishing || this.transRiskInfo.isPhishing,
};
},
async confirmTransRiskBeforeSend() {
const riskInfo = await this.getTransRiskInfo();
if (!riskInfo.isHighRisk && !riskInfo.isPhishing) {
return true;
}
try {
await openAsyncDialog({
title: '确认发送',
message:
'当前用户为钓鱼号/高风险用户,请与组长确认是否发送转端链接',
countdown: 3,
confirmText: '确 定',
cancelText: '取 消',
});
return true;
} catch (error) {
return false;
}
},
sendLink: throttle(async function (item, type) {
if (!this.transMemberStatus) {
this.$message.warning('当前w账号不满足转端要求,请联系组长处理');
return;
}
if (!(await this.confirmTransRiskBeforeSend())) {
return;
}
console.log(item, '转端发送仅发送链接');
const result = this.handleAccount();
if (!result) {
......@@ -1077,11 +1180,14 @@ export default {
item.type = 1;
this.sendGameLog(item);
}, 500),
sendPassword: throttle(function (item, type) {
sendPassword: throttle(async function (item, type) {
if (!this.transMemberStatus) {
this.$message.warning('当前w账号不满足转端要求,请联系组长处理');
return;
}
if (!(await this.confirmTransRiskBeforeSend())) {
return;
}
console.log(item, '转端仅发送账号密码');
const result = this.handleAccount();
if (!result) {
......@@ -1110,11 +1216,14 @@ export default {
console.log(err);
});
}, 500),
sendMessage: throttle(function (item, type) {
sendMessage: throttle(async function (item, type) {
if (!this.transMemberStatus) {
this.$message.warning('当前w账号不满足转端要求,请联系组长处理');
return;
}
if (!(await this.confirmTransRiskBeforeSend())) {
return;
}
const result = this.handleAccount();
if (!result) {
this.$message.warning('请稍后再试');
......@@ -1170,11 +1279,14 @@ export default {
this.getMediaId(value, 'image');
},
// 转端发送落地页面
sendDownLoadPage: throttleStart(function (items, type, index) {
sendDownLoadPage: throttleStart(async function (items, type, index) {
if (!this.transMemberStatus) {
this.$message.warning('当前w账号不满足转端要求,请联系组长处理');
return;
}
if (!(await this.confirmTransRiskBeforeSend())) {
return;
}
this.$set(
this.conversionGameList[index],
'send_trans_page_id',
......@@ -1184,6 +1296,13 @@ export default {
}, 500),
// 转端发送游戏分身包 h5 安卓游戏 IOS游戏 发送分身包
async sendTransferCloneGame(type, items) {
if (!this.transMemberStatus) {
this.$message.warning('当前w账号不满足转端要求,请联系组长处理');
return;
}
if (!(await this.confirmTransRiskBeforeSend())) {
return;
}
if (!this.h5CloneGameInfo?.data?.h5_download_url) {
this.h5CloneGameInfo =
(await memberRegGameCloneLink({
......@@ -1239,6 +1358,9 @@ export default {
this.$message.warning('当前w账号不满足转端要求,请联系组长处理');
return;
}
if (!(await this.confirmTransRiskBeforeSend())) {
return;
}
const result = this.handleAccount();
if (!result) {
this.$message.warning('请稍后再试');
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论