45 changed files with 1539 additions and 1456 deletions
@ -1,8 +1,15 @@ |
|||||||
# 端口号 |
# 端口号 |
||||||
VITE_PORT = 3100 |
VITE_PORT = 80 |
||||||
|
|
||||||
# 网站标题 |
# 网站标题 |
||||||
VITE_GLOB_APP_TITLE = Vben Admin |
VITE_GLOB_APP_TITLE = 芋道管理系统 |
||||||
|
|
||||||
# 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符 |
# 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符 |
||||||
VITE_GLOB_APP_SHORT_NAME = vue_vben_admin |
VITE_GLOB_APP_SHORT_NAME = Vben |
||||||
|
|
||||||
|
# 租户开关 |
||||||
|
VITE_GLOB_APP_TENANT_ENABLE = true |
||||||
|
|
||||||
|
# 验证码的开关 |
||||||
|
VITE_GLOB_APP_CAPTCHA_ENABLE = true |
||||||
|
|
||||||
|
@ -0,0 +1,24 @@ |
|||||||
|
# 本地开发环境 |
||||||
|
NODE_ENV=development |
||||||
|
|
||||||
|
# 资源公共路径,需要以 /开头和结尾 |
||||||
|
VITE_PUBLIC_PATH = / |
||||||
|
|
||||||
|
# 本地开发代理,可以解决跨域及多地址代理 |
||||||
|
# 如果接口地址匹配到,则会转发到http://localhost:3000,防止本地出现跨域问题 |
||||||
|
# 可以有多个,注意多个不能换行,否则代理将会失效 |
||||||
|
VITE_PROXY = [["/dev-api","http://api-dashboard.yudao.iocoder.cn"],["/upload","http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload"]] |
||||||
|
# VITE_PROXY=[["/api","http://vben.xingyuv.com/test"]] |
||||||
|
|
||||||
|
# 是否删除Console.log |
||||||
|
VITE_DROP_CONSOLE = false |
||||||
|
|
||||||
|
# 接口地址 |
||||||
|
# 如果没有跨域问题,直接在这里配置即可 |
||||||
|
VITE_GLOB_API_URL = "http://localhost:48080/admin-api" |
||||||
|
|
||||||
|
# 文件上传接口 可选 |
||||||
|
VITE_GLOB_UPLOAD_URL = /upload |
||||||
|
|
||||||
|
# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换 |
||||||
|
VITE_GLOB_API_URL_PREFIX = |
@ -1,4 +0,0 @@ |
|||||||
import { withInstall } from '@/utils' |
|
||||||
import flowChart from './src/FlowChart.vue' |
|
||||||
|
|
||||||
export const FlowChart = withInstall(flowChart) |
|
@ -1,142 +0,0 @@ |
|||||||
<template> |
|
||||||
<div class="h-full" :class="prefixCls"> |
|
||||||
<FlowChartToolbar :prefixCls="prefixCls" v-if="toolbar" @view-data="handlePreview" /> |
|
||||||
<div ref="lfElRef" class="h-full"></div> |
|
||||||
<BasicModal @register="register" title="流程数据" width="50%"> |
|
||||||
<JsonPreview :data="graphData" /> |
|
||||||
</BasicModal> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
<script lang="ts" setup name="FlowChart"> |
|
||||||
import type { Ref } from 'vue' |
|
||||||
import { computed, nextTick, onMounted, ref, unref, watch } from 'vue' |
|
||||||
import type { Definition } from '@logicflow/core' |
|
||||||
import LogicFlow from '@logicflow/core' |
|
||||||
import FlowChartToolbar from './FlowChartToolbar.vue' |
|
||||||
import { BpmnElement, DndPanel, Menu, SelectionSelect, Snapshot } from '@logicflow/extension' |
|
||||||
import { useDesign } from '@/hooks/web/useDesign' |
|
||||||
import { useAppStore } from '@/store/modules/app' |
|
||||||
import { createFlowChartContext } from './useFlowContext' |
|
||||||
import { toLogicFlowData } from './adpterForTurbo' |
|
||||||
import { BasicModal, useModal } from '@/components/Modal' |
|
||||||
import { JsonPreview } from '@/components/CodeEditor' |
|
||||||
import { configDefaultDndPanel } from './config' |
|
||||||
import '@logicflow/core/dist/style/index.css' |
|
||||||
import '@logicflow/extension/lib/style/index.css' |
|
||||||
|
|
||||||
const props = defineProps({ |
|
||||||
flowOptions: { |
|
||||||
type: Object as PropType<Definition>, |
|
||||||
default: () => ({}) |
|
||||||
}, |
|
||||||
data: { |
|
||||||
type: Object as PropType<any>, |
|
||||||
default: () => ({}) |
|
||||||
}, |
|
||||||
toolbar: { |
|
||||||
type: Boolean, |
|
||||||
default: true |
|
||||||
}, |
|
||||||
patternItems: { |
|
||||||
type: Array |
|
||||||
} |
|
||||||
}) |
|
||||||
const lfElRef = ref(null) |
|
||||||
const graphData = ref({}) |
|
||||||
|
|
||||||
const lfInstance = ref(null) as Ref<LogicFlow | null> |
|
||||||
|
|
||||||
const { prefixCls } = useDesign('flow-chart') |
|
||||||
const appStore = useAppStore() |
|
||||||
const [register, { openModal }] = useModal() |
|
||||||
createFlowChartContext({ |
|
||||||
logicFlow: lfInstance as unknown as LogicFlow |
|
||||||
}) |
|
||||||
|
|
||||||
const getFlowOptions = computed(() => { |
|
||||||
const { flowOptions } = props |
|
||||||
|
|
||||||
const defaultOptions: Partial<Definition> = { |
|
||||||
grid: true, |
|
||||||
background: { |
|
||||||
color: appStore.getDarkMode === 'light' ? '#f7f9ff' : '#151515' |
|
||||||
}, |
|
||||||
keyboard: { |
|
||||||
enabled: true |
|
||||||
}, |
|
||||||
...flowOptions |
|
||||||
} |
|
||||||
return defaultOptions as Definition |
|
||||||
}) |
|
||||||
|
|
||||||
watch( |
|
||||||
() => props.data, |
|
||||||
() => { |
|
||||||
onRender() |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
// TODO |
|
||||||
// watch( |
|
||||||
// () => appStore.getDarkMode, |
|
||||||
// () => { |
|
||||||
// init(); |
|
||||||
// } |
|
||||||
// ); |
|
||||||
|
|
||||||
watch( |
|
||||||
() => unref(getFlowOptions), |
|
||||||
(options) => { |
|
||||||
unref(lfInstance)?.updateEditConfig(options) |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
// init logicFlow |
|
||||||
async function init() { |
|
||||||
await nextTick() |
|
||||||
|
|
||||||
const lfEl = unref(lfElRef) |
|
||||||
if (!lfEl) { |
|
||||||
return |
|
||||||
} |
|
||||||
LogicFlow.use(DndPanel) |
|
||||||
|
|
||||||
// Canvas configuration |
|
||||||
LogicFlow.use(Snapshot) |
|
||||||
// Use the bpmn plug-in to introduce bpmn elements, which can be used after conversion in turbo |
|
||||||
LogicFlow.use(BpmnElement) |
|
||||||
// Start the right-click menu |
|
||||||
LogicFlow.use(Menu) |
|
||||||
LogicFlow.use(SelectionSelect) |
|
||||||
|
|
||||||
lfInstance.value = new LogicFlow({ |
|
||||||
...unref(getFlowOptions), |
|
||||||
container: lfEl |
|
||||||
}) |
|
||||||
const lf = unref(lfInstance)! |
|
||||||
lf?.setDefaultEdgeType('line') |
|
||||||
await onRender() |
|
||||||
lf?.setPatternItems(props.patternItems || configDefaultDndPanel(lf)) |
|
||||||
} |
|
||||||
|
|
||||||
async function onRender() { |
|
||||||
await nextTick() |
|
||||||
const lf = unref(lfInstance) |
|
||||||
if (!lf) { |
|
||||||
return |
|
||||||
} |
|
||||||
const lFData = toLogicFlowData(props.data) |
|
||||||
lf.render(lFData) |
|
||||||
} |
|
||||||
|
|
||||||
function handlePreview() { |
|
||||||
const lf = unref(lfInstance) |
|
||||||
if (!lf) { |
|
||||||
return |
|
||||||
} |
|
||||||
graphData.value = unref(lf).getGraphData() |
|
||||||
openModal() |
|
||||||
} |
|
||||||
|
|
||||||
onMounted(init) |
|
||||||
</script> |
|
@ -1,153 +0,0 @@ |
|||||||
<template> |
|
||||||
<div :class="`${prefixCls}-toolbar`" class="flex items-center px-2 py-1"> |
|
||||||
<template v-for="item in toolbarItemList" :key="item.type"> |
|
||||||
<Tooltip placement="bottom" v-bind="item.disabled ? { visible: false } : {}"> |
|
||||||
<template #title>{{ item.tooltip }}</template> |
|
||||||
<span :class="`${prefixCls}-toolbar__icon`" v-if="item.icon" @click="onControl(item)"> |
|
||||||
<Icon :icon="item.icon" :class="item.disabled ? 'cursor-not-allowed disabeld' : 'cursor-pointer'" /> |
|
||||||
</span> |
|
||||||
</Tooltip> |
|
||||||
<Divider v-if="item.separate" type="vertical" /> |
|
||||||
</template> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
<script lang="ts" setup name="FlowChartToolbar"> |
|
||||||
import type { ToolbarConfig } from './types' |
|
||||||
|
|
||||||
import { nextTick, onUnmounted, ref, unref, watchEffect } from 'vue' |
|
||||||
import { Divider, Tooltip } from 'ant-design-vue' |
|
||||||
import { Icon } from '@/components/Icon' |
|
||||||
|
|
||||||
import { useFlowChartContext } from './useFlowContext' |
|
||||||
import { ToolbarTypeEnum } from './enum' |
|
||||||
|
|
||||||
defineProps({ |
|
||||||
prefixCls: String |
|
||||||
}) |
|
||||||
const emit = defineEmits(['view-data']) |
|
||||||
const toolbarItemList = ref<ToolbarConfig[]>([ |
|
||||||
{ |
|
||||||
type: ToolbarTypeEnum.ZOOM_IN, |
|
||||||
icon: 'codicon:zoom-out', |
|
||||||
tooltip: '缩小' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: ToolbarTypeEnum.ZOOM_OUT, |
|
||||||
icon: 'codicon:zoom-in', |
|
||||||
tooltip: '放大' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: ToolbarTypeEnum.RESET_ZOOM, |
|
||||||
icon: 'codicon:screen-normal', |
|
||||||
tooltip: '重置比例' |
|
||||||
}, |
|
||||||
{ separate: true }, |
|
||||||
{ |
|
||||||
type: ToolbarTypeEnum.UNDO, |
|
||||||
icon: 'ion:arrow-undo-outline', |
|
||||||
tooltip: '后退', |
|
||||||
disabled: true |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: ToolbarTypeEnum.REDO, |
|
||||||
icon: 'ion:arrow-redo-outline', |
|
||||||
tooltip: '前进', |
|
||||||
disabled: true |
|
||||||
}, |
|
||||||
{ separate: true }, |
|
||||||
{ |
|
||||||
type: ToolbarTypeEnum.SNAPSHOT, |
|
||||||
icon: 'ion:download-outline', |
|
||||||
tooltip: '下载' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: ToolbarTypeEnum.VIEW_DATA, |
|
||||||
icon: 'carbon:document-view', |
|
||||||
tooltip: '查看数据' |
|
||||||
} |
|
||||||
]) |
|
||||||
|
|
||||||
const { logicFlow } = useFlowChartContext() |
|
||||||
|
|
||||||
function onHistoryChange({ data: { undoAble, redoAble } }) { |
|
||||||
const itemsList = unref(toolbarItemList) |
|
||||||
const undoIndex = itemsList.findIndex((item) => item.type === ToolbarTypeEnum.UNDO) |
|
||||||
const redoIndex = itemsList.findIndex((item) => item.type === ToolbarTypeEnum.REDO) |
|
||||||
if (undoIndex !== -1) { |
|
||||||
unref(toolbarItemList)[undoIndex].disabled = !undoAble |
|
||||||
} |
|
||||||
if (redoIndex !== -1) { |
|
||||||
unref(toolbarItemList)[redoIndex].disabled = !redoAble |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const onControl = (item) => { |
|
||||||
const lf = unref(logicFlow) |
|
||||||
if (!lf) { |
|
||||||
return |
|
||||||
} |
|
||||||
switch (item.type) { |
|
||||||
case ToolbarTypeEnum.ZOOM_IN: |
|
||||||
lf.zoom() |
|
||||||
break |
|
||||||
case ToolbarTypeEnum.ZOOM_OUT: |
|
||||||
lf.zoom(true) |
|
||||||
break |
|
||||||
case ToolbarTypeEnum.RESET_ZOOM: |
|
||||||
lf.resetZoom() |
|
||||||
break |
|
||||||
case ToolbarTypeEnum.UNDO: |
|
||||||
lf.undo() |
|
||||||
break |
|
||||||
case ToolbarTypeEnum.REDO: |
|
||||||
lf.redo() |
|
||||||
break |
|
||||||
case ToolbarTypeEnum.SNAPSHOT: |
|
||||||
lf.getSnapshot() |
|
||||||
break |
|
||||||
case ToolbarTypeEnum.VIEW_DATA: |
|
||||||
emit('view-data') |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
watchEffect(async () => { |
|
||||||
if (unref(logicFlow)) { |
|
||||||
await nextTick() |
|
||||||
unref(logicFlow)?.on('history:change', onHistoryChange) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
onUnmounted(() => { |
|
||||||
unref(logicFlow)?.off('history:change', onHistoryChange) |
|
||||||
}) |
|
||||||
</script> |
|
||||||
<style lang="less"> |
|
||||||
@prefix-cls: ~'@{namespace}-flow-chart-toolbar'; |
|
||||||
|
|
||||||
html[data-theme='dark'] { |
|
||||||
.lf-dnd { |
|
||||||
background: #080808; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.@{prefix-cls} { |
|
||||||
height: 36px; |
|
||||||
background-color: @app-content-background; |
|
||||||
border-bottom: 1px solid @border-color-base; |
|
||||||
|
|
||||||
.disabeld { |
|
||||||
color: @disabled-color; |
|
||||||
} |
|
||||||
|
|
||||||
&__icon { |
|
||||||
display: inline-block; |
|
||||||
padding: 2px 4px; |
|
||||||
margin-right: 10px; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
color: @primary-color; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
|
@ -1,75 +0,0 @@ |
|||||||
const TurboType = { |
|
||||||
SEQUENCE_FLOW: 1, |
|
||||||
START_EVENT: 2, |
|
||||||
END_EVENT: 3, |
|
||||||
USER_TASK: 4, |
|
||||||
SERVICE_TASK: 5, |
|
||||||
EXCLUSIVE_GATEWAY: 6 |
|
||||||
} |
|
||||||
|
|
||||||
function convertFlowElementToEdge(element) { |
|
||||||
const { incoming, outgoing, properties, key } = element |
|
||||||
const { text, startPoint, endPoint, pointsList, logicFlowType } = properties |
|
||||||
const edge = { |
|
||||||
id: key, |
|
||||||
type: logicFlowType, |
|
||||||
sourceNodeId: incoming[0], |
|
||||||
targetNodeId: outgoing[0], |
|
||||||
text, |
|
||||||
startPoint, |
|
||||||
endPoint, |
|
||||||
pointsList, |
|
||||||
properties: {} |
|
||||||
} |
|
||||||
const excludeProperties = ['startPoint', 'endPoint', 'pointsList', 'text', 'logicFlowType'] |
|
||||||
Object.keys(element.properties).forEach((property) => { |
|
||||||
if (excludeProperties.indexOf(property) === -1) { |
|
||||||
edge.properties[property] = element.properties[property] |
|
||||||
} |
|
||||||
}) |
|
||||||
return edge |
|
||||||
} |
|
||||||
|
|
||||||
function convertFlowElementToNode(element) { |
|
||||||
const { properties, key } = element |
|
||||||
const { x, y, text, logicFlowType } = properties |
|
||||||
const node = { |
|
||||||
id: key, |
|
||||||
type: logicFlowType, |
|
||||||
x, |
|
||||||
y, |
|
||||||
text, |
|
||||||
properties: {} |
|
||||||
} |
|
||||||
const excludeProperties = ['x', 'y', 'text', 'logicFlowType'] |
|
||||||
Object.keys(element.properties).forEach((property) => { |
|
||||||
if (excludeProperties.indexOf(property) === -1) { |
|
||||||
node.properties[property] = element.properties[property] |
|
||||||
} |
|
||||||
}) |
|
||||||
return node |
|
||||||
} |
|
||||||
|
|
||||||
export function toLogicFlowData(data) { |
|
||||||
const lfData: { |
|
||||||
// TODO type
|
|
||||||
nodes: any[] |
|
||||||
edges: any[] |
|
||||||
} = { |
|
||||||
nodes: [], |
|
||||||
edges: [] |
|
||||||
} |
|
||||||
const list = data.flowElementList |
|
||||||
list && |
|
||||||
list.length > 0 && |
|
||||||
list.forEach((element) => { |
|
||||||
if (element.type === TurboType.SEQUENCE_FLOW) { |
|
||||||
const edge = convertFlowElementToEdge(element) |
|
||||||
lfData.edges.push(edge) |
|
||||||
} else { |
|
||||||
const node = convertFlowElementToNode(element) |
|
||||||
lfData.nodes.push(node) |
|
||||||
} |
|
||||||
}) |
|
||||||
return lfData |
|
||||||
} |
|
@ -1,96 +0,0 @@ |
|||||||
export const nodeList = [ |
|
||||||
{ |
|
||||||
text: '开始', |
|
||||||
type: 'start', |
|
||||||
class: 'node-start' |
|
||||||
}, |
|
||||||
{ |
|
||||||
text: '矩形', |
|
||||||
type: 'rect', |
|
||||||
class: 'node-rect' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'user', |
|
||||||
text: '用户', |
|
||||||
class: 'node-user' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'push', |
|
||||||
text: '推送', |
|
||||||
class: 'node-push' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'download', |
|
||||||
text: '位置', |
|
||||||
class: 'node-download' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'end', |
|
||||||
text: '结束', |
|
||||||
class: 'node-end' |
|
||||||
} |
|
||||||
] |
|
||||||
|
|
||||||
export const BpmnNode = [ |
|
||||||
{ |
|
||||||
type: 'bpmn:startEvent', |
|
||||||
text: '开始', |
|
||||||
class: 'bpmn-start' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'bpmn:endEvent', |
|
||||||
text: '结束', |
|
||||||
class: 'bpmn-end' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'bpmn:exclusiveGateway', |
|
||||||
text: '网关', |
|
||||||
class: 'bpmn-exclusiveGateway' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'bpmn:userTask', |
|
||||||
text: '用户', |
|
||||||
class: 'bpmn-user' |
|
||||||
} |
|
||||||
] |
|
||||||
|
|
||||||
export function configDefaultDndPanel(lf) { |
|
||||||
return [ |
|
||||||
{ |
|
||||||
text: '选区', |
|
||||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAOVJREFUOBGtVMENwzAIjKP++2026ETdpv10iy7WFbqFyyW6GBywLCv5gI+Dw2Bluj1znuSjhb99Gkn6QILDY2imo60p8nsnc9bEo3+QJ+AKHfMdZHnl78wyTnyHZD53Zzx73MRSgYvnqgCUHj6gwdck7Zsp1VOrz0Uz8NbKunzAW+Gu4fYW28bUYutYlzSa7B84Fh7d1kjLwhcSdYAYrdkMQVpsBr5XgDGuXwQfQr0y9zwLda+DUYXLaGKdd2ZTtvbolaO87pdo24hP7ov16N0zArH1ur3iwJpXxm+v7oAJNR4JEP8DoAuSFEkYH7cAAAAASUVORK5CYII=', |
|
||||||
callback: () => { |
|
||||||
lf.updateEditConfig({ |
|
||||||
stopMoveGraph: true |
|
||||||
}) |
|
||||||
} |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'circle', |
|
||||||
text: '开始', |
|
||||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg==' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'rect', |
|
||||||
text: '用户任务', |
|
||||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==', |
|
||||||
cls: 'important-node' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'rect', |
|
||||||
text: '系统任务', |
|
||||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==', |
|
||||||
cls: 'import_icon' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'diamond', |
|
||||||
text: '条件判断', |
|
||||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAAHeEJUAAAAABGdBTUEAALGPC/xhBQAAAvVJREFUOBGNVEFrE0EU/mY3bQoiFlOkaUJrQUQoWMGePLX24EH0IIoHKQiCV0G8iE1covgLiqA/QTzVm1JPogc9tIJYFaQtlhQxqYjSpunu+L7JvmUTU3AgmTfvffPNN++9WSA1DO182f6xwILzD5btfAoQmwL5KJEwiQyVbSVZ0IgRyV6PTpIJ81E5ZvqfHQR0HUOBHW4L5Et2kQ6Zf7iAOhTFAA8s0pEP7AXO1uAA52SbqGk6h/6J45LaLhO64ByfcUzM39V7ZiAdS2yCePPEIQYvTUHqM/n7dgQNfBKWPjpF4ISk8q3J4nB11qw6X8l+FsF3EhlkEMfrjIer3wJTLwS2aCNcj4DbGxXTw00JmAuO+Ni6bBxVUCvS5d9aa04+so4pHW5jLTywuXAL7jJ+D06sl82Sgl2JuVBQn498zkc2bGKxULHjCnSMadBKYDYYHAtsby1EQ5lNGrQd4Y3v4Zo0XdGEmDno46yCM9Tk+RiJmUYHS/aXHPNTcjxcbTFna000PFJHIVZ5lFRqRpJWk9/+QtlOUYJj9HG5pVFEU7zqIYDVsw2s+AJaD8wTd2umgSCCyUxgGsS1Y6TBwXQQTFuZaHcd8gAGioE90hlsY+wMcs30RduYtxanjMGal8H5dMW67dmT1JFtYUEe8LiQLRsPZ6IIc7A4J5tqco3T0pnv/4u0kyzrYUq7gASuEyI8VXKvB9Odytv6jS/PNaZBln0nioJG/AVQRZvApOdhjj3Jt8QC8Im09SafwdBdvIpztpxWxpeKCC+EsFdS8DCyuCn2munFpL7ctHKp+Xc5cMybeIyMAN33SPL3ZR9QV1XVwLyzHm6Iv0/yeUuUb7PPlZC4D4HZkeu6dpF4v9j9MreGtMbxMMRLIcjJic9yHi7WQ3yVKzZVWUr5UrViJvn1FfUlwe/KYVfYyWRLSGNu16hR01U9IacajXPei0wx/5BqgInvJN+MMNtNme7ReU9SBbgntovn0kKHpFg7UogZvaZiOue/q1SBo9ktHzQAAAAASUVORK5CYII=' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'circle', |
|
||||||
text: '结束', |
|
||||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAA1BJREFUOBFtVE1IVUEYPXOf+tq40Y3vPcmFIdSjIorWoRG0ERWUgnb5FwVhYQSl72oUoZAboxKNFtWiwKRN0M+jpfSzqJAQclHo001tKkjl3emc8V69igP3znzfnO/M9zcDcKT67azmjYWTwl9Vn7Vumeqzj1DVb6cleQY4oAVnIOPb+mKAGxQmKI5CWNJ2aLPatxWa3aB9K7/fB+/Z0jUF6TmMlFLQqrkECWQzOZxYGjTlOl8eeKaIY5yHnFn486xBustDjWT6dG7pmjHOJd+33t0iitTPkK6tEvjxq4h2MozQ6WFSX/LkDUGfFwfhEZj1Auz/U4pyAi5Sznd7uKzznXeVHlI/Aywmk6j7fsUsEuCGADrWARXXwjxWQsUbIupDHJI7kF5dRktg0eN81IbiZXiTESic50iwS+t1oJgL83jAiBupLDCQqwziaWSoAFSeIR3P5Xv5az00wyIn35QRYTwdSYbz8pH8fxUUAtxnFvYmEmgI0wYXUXcCCSpeEVpXlsRhBnCEATxWylL9+EKCAYhe1NGstUa6356kS9NVvt3DU2fd+Wtbm/+lSbylJqsqkSm9CRhvoJVlvKPvF1RKY/FcPn5j4UfIMLn8D4UYb54BNsilTDXKnF4CfTobA0FpoW/LSp306wkXM+XaOJhZaFkcNM82ASNAWMrhrUbRfmyeI1FvRBTpN06WKxa9BK0o2E4Pd3zfBBEwPsv9sQBnmLVbLEIZ/Xe9LYwJu/Er17W6HYVBc7vmuk0xUQ+pqxdom5Fnp55SiytXLPYoMXNM4u4SNSCFWnrVIzKG3EGyMXo6n/BQOe+bX3FClY4PwydVhthOZ9NnS+ntiLh0fxtlUJHAuGaFoVmttpVMeum0p3WEXbcll94l1wM/gZ0Ccczop77VvN2I7TlsZCsuXf1WHvWEhjO8DPtyOVg2/mvK9QqboEth+7pD6NUQC1HN/TwvydGBARi9MZSzLE4b8Ru3XhX2PBxf8E1er2A6516o0w4sIA+lwURhAON82Kwe2iDAC1Watq4XHaGQ7skLcFOtI5lDxuM2gZe6WFIotPAhbaeYlU4to5cuarF1QrcZ/lwrLaCJl66JBocYZnrNlvm2+MBCTmUymPrYZVbjdlr/BxlMjmNmNI3SAAAAAElFTkSuQmCC' |
|
||||||
} |
|
||||||
] |
|
||||||
} |
|
@ -1,11 +0,0 @@ |
|||||||
export enum ToolbarTypeEnum { |
|
||||||
ZOOM_IN = 'zoomIn', |
|
||||||
ZOOM_OUT = 'zoomOut', |
|
||||||
RESET_ZOOM = 'resetZoom', |
|
||||||
|
|
||||||
UNDO = 'undo', |
|
||||||
REDO = 'redo', |
|
||||||
|
|
||||||
SNAPSHOT = 'snapshot', |
|
||||||
VIEW_DATA = 'viewData' |
|
||||||
} |
|
@ -1,14 +0,0 @@ |
|||||||
import { NodeConfig } from '@logicflow/core' |
|
||||||
import { ToolbarTypeEnum } from './enum' |
|
||||||
|
|
||||||
export interface NodeItem extends NodeConfig { |
|
||||||
icon: string |
|
||||||
} |
|
||||||
|
|
||||||
export interface ToolbarConfig { |
|
||||||
type?: string | ToolbarTypeEnum |
|
||||||
tooltip?: string | boolean |
|
||||||
icon?: string |
|
||||||
disabled?: boolean |
|
||||||
separate?: boolean |
|
||||||
} |
|
@ -1,17 +0,0 @@ |
|||||||
import type LogicFlow from '@logicflow/core' |
|
||||||
|
|
||||||
import { provide, inject } from 'vue' |
|
||||||
|
|
||||||
const key = Symbol('flow-chart') |
|
||||||
|
|
||||||
type Instance = { |
|
||||||
logicFlow: LogicFlow |
|
||||||
} |
|
||||||
|
|
||||||
export function createFlowChartContext(instance: Instance) { |
|
||||||
provide(key, instance) |
|
||||||
} |
|
||||||
|
|
||||||
export function useFlowChartContext(): Instance { |
|
||||||
return inject(key) as Instance |
|
||||||
} |
|
@ -0,0 +1,4 @@ |
|||||||
|
import { withInstall } from '@/utils/index' |
||||||
|
import verify from './src/Verify.vue' |
||||||
|
|
||||||
|
export const Verify = withInstall(verify) |
File diff suppressed because one or more lines are too long
@ -0,0 +1,250 @@ |
|||||||
|
<template> |
||||||
|
<div style="position: relative"> |
||||||
|
<div class="verify-img-out"> |
||||||
|
<div |
||||||
|
class="verify-img-panel" |
||||||
|
:style="{ |
||||||
|
width: setSize.imgWidth, |
||||||
|
height: setSize.imgHeight, |
||||||
|
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight, |
||||||
|
'margin-bottom': vSpace + 'px' |
||||||
|
}" |
||||||
|
> |
||||||
|
<div class="verify-refresh" style="z-index: 3" @click="refresh" v-show="showRefresh"> |
||||||
|
<i class="iconfont icon-refresh"></i> |
||||||
|
</div> |
||||||
|
<img |
||||||
|
:src="'data:image/png;base64,' + pointBackImgBase" |
||||||
|
ref="canvas" |
||||||
|
alt="" |
||||||
|
style="width: 100%; height: 100%; display: block" |
||||||
|
@click="bindingClick ? canvasClick($event) : undefined" |
||||||
|
/> |
||||||
|
|
||||||
|
<div |
||||||
|
v-for="(tempPoint, index) in tempPoints" |
||||||
|
:key="index" |
||||||
|
class="point-area" |
||||||
|
:style="{ |
||||||
|
'background-color': '#1abd6c', |
||||||
|
color: '#fff', |
||||||
|
'z-index': 9999, |
||||||
|
width: '20px', |
||||||
|
height: '20px', |
||||||
|
'text-align': 'center', |
||||||
|
'line-height': '20px', |
||||||
|
'border-radius': '50%', |
||||||
|
position: 'absolute', |
||||||
|
top: parseInt(tempPoint.y - 10) + 'px', |
||||||
|
left: parseInt(tempPoint.x - 10) + 'px' |
||||||
|
}" |
||||||
|
> |
||||||
|
{{ index + 1 }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- 'height': this.barSize.height, --> |
||||||
|
<div |
||||||
|
class="verify-bar-area" |
||||||
|
:style="{ |
||||||
|
width: setSize.imgWidth, |
||||||
|
color: barAreaColor, |
||||||
|
'border-color': barAreaBorderColor, |
||||||
|
'line-height': barSize.height |
||||||
|
}" |
||||||
|
> |
||||||
|
<span class="verify-msg">{{ text }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<script type="text/babel" setup> |
||||||
|
/** |
||||||
|
* VerifyPoints |
||||||
|
* @description 点选 |
||||||
|
* */ |
||||||
|
import { resetSize } from './../utils/util' |
||||||
|
import { aesEncrypt } from './../utils/ase' |
||||||
|
import { getCaptcha, checkCaptcha } from '@/api/base/login' |
||||||
|
import { onMounted, reactive, ref, nextTick, toRefs, getCurrentInstance } from 'vue' |
||||||
|
import { useI18n } from '@/hooks/web/useI18n' |
||||||
|
|
||||||
|
const props = defineProps({ |
||||||
|
//弹出式pop,固定fixed |
||||||
|
mode: { |
||||||
|
type: String, |
||||||
|
default: 'fixed' |
||||||
|
}, |
||||||
|
captchaType: { |
||||||
|
type: String |
||||||
|
}, |
||||||
|
//间隔 |
||||||
|
vSpace: { |
||||||
|
type: Number, |
||||||
|
default: 5 |
||||||
|
}, |
||||||
|
imgSize: { |
||||||
|
type: Object, |
||||||
|
default() { |
||||||
|
return { |
||||||
|
width: '310px', |
||||||
|
height: '155px' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
barSize: { |
||||||
|
type: Object, |
||||||
|
default() { |
||||||
|
return { |
||||||
|
width: '310px', |
||||||
|
height: '40px' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const { t } = useI18n() |
||||||
|
const { mode, captchaType } = toRefs(props) |
||||||
|
const { proxy } = getCurrentInstance() |
||||||
|
let secretKey = ref(''), //后端返回的ase加密秘钥 |
||||||
|
checkNum = ref(3), //默认需要点击的字数 |
||||||
|
fontPos = reactive([]), //选中的坐标信息 |
||||||
|
checkPosArr = reactive([]), //用户点击的坐标 |
||||||
|
num = ref(1), //点击的记数 |
||||||
|
pointBackImgBase = ref(''), //后端获取到的背景图片 |
||||||
|
poinTextList = reactive([]), //后端返回的点击字体顺序 |
||||||
|
backToken = ref(''), //后端返回的token值 |
||||||
|
setSize = reactive({ |
||||||
|
imgHeight: 0, |
||||||
|
imgWidth: 0, |
||||||
|
barHeight: 0, |
||||||
|
barWidth: 0 |
||||||
|
}), |
||||||
|
tempPoints = reactive([]), |
||||||
|
text = ref(''), |
||||||
|
barAreaColor = ref(undefined), |
||||||
|
barAreaBorderColor = ref(undefined), |
||||||
|
showRefresh = ref(true), |
||||||
|
bindingClick = ref(true) |
||||||
|
|
||||||
|
const init = () => { |
||||||
|
//加载页面 |
||||||
|
fontPos.splice(0, fontPos.length) |
||||||
|
checkPosArr.splice(0, checkPosArr.length) |
||||||
|
num.value = 1 |
||||||
|
getPictrue() |
||||||
|
nextTick(() => { |
||||||
|
let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy) |
||||||
|
setSize.imgHeight = imgHeight |
||||||
|
setSize.imgWidth = imgWidth |
||||||
|
setSize.barHeight = barHeight |
||||||
|
setSize.barWidth = barWidth |
||||||
|
proxy.$parent.$emit('ready', proxy) |
||||||
|
}) |
||||||
|
} |
||||||
|
onMounted(() => { |
||||||
|
// 禁止拖拽 |
||||||
|
init() |
||||||
|
proxy.$el.onselectstart = function () { |
||||||
|
return false |
||||||
|
} |
||||||
|
}) |
||||||
|
const canvas = ref(null) |
||||||
|
const canvasClick = (e) => { |
||||||
|
checkPosArr.push(getMousePos(canvas, e)) |
||||||
|
if (num.value == checkNum.value) { |
||||||
|
num.value = createPoint(getMousePos(canvas, e)) |
||||||
|
//按比例转换坐标值 |
||||||
|
let arr = pointTransfrom(checkPosArr, setSize) |
||||||
|
checkPosArr.length = 0 |
||||||
|
checkPosArr.push(...arr) |
||||||
|
//等创建坐标执行完 |
||||||
|
setTimeout(() => { |
||||||
|
// var flag = this.comparePos(this.fontPos, this.checkPosArr); |
||||||
|
//发送后端请求 |
||||||
|
var captchaVerification = secretKey.value |
||||||
|
? aesEncrypt(backToken.value + '---' + JSON.stringify(checkPosArr), secretKey.value) |
||||||
|
: backToken.value + '---' + JSON.stringify(checkPosArr) |
||||||
|
let data = { |
||||||
|
captchaType: captchaType.value, |
||||||
|
pointJson: secretKey.value ? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value) : JSON.stringify(checkPosArr), |
||||||
|
token: backToken.value |
||||||
|
} |
||||||
|
checkCaptcha(data).then((response) => { |
||||||
|
const res = response.data |
||||||
|
if (res.repCode == '0000') { |
||||||
|
barAreaColor.value = '#4cae4c' |
||||||
|
barAreaBorderColor.value = '#5cb85c' |
||||||
|
text.value = t('component.captcha.success') |
||||||
|
bindingClick.value = false |
||||||
|
if (mode.value == 'pop') { |
||||||
|
setTimeout(() => { |
||||||
|
proxy.$parent.clickShow = false |
||||||
|
refresh() |
||||||
|
}, 1500) |
||||||
|
} |
||||||
|
proxy.$parent.$emit('success', { captchaVerification }) |
||||||
|
} else { |
||||||
|
proxy.$parent.$emit('error', proxy) |
||||||
|
barAreaColor.value = '#d9534f' |
||||||
|
barAreaBorderColor.value = '#d9534f' |
||||||
|
text.value = t('component.captcha.fail') |
||||||
|
setTimeout(() => { |
||||||
|
refresh() |
||||||
|
}, 700) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, 400) |
||||||
|
} |
||||||
|
if (num.value < checkNum.value) { |
||||||
|
num.value = createPoint(getMousePos(canvas, e)) |
||||||
|
} |
||||||
|
} |
||||||
|
//获取坐标 |
||||||
|
const getMousePos = function (obj, e) { |
||||||
|
var x = e.offsetX |
||||||
|
var y = e.offsetY |
||||||
|
return { x, y } |
||||||
|
} |
||||||
|
//创建坐标点 |
||||||
|
const createPoint = function (pos) { |
||||||
|
tempPoints.push(Object.assign({}, pos)) |
||||||
|
return num.value + 1 |
||||||
|
} |
||||||
|
const refresh = async function () { |
||||||
|
tempPoints.splice(0, tempPoints.length) |
||||||
|
barAreaColor.value = '#000' |
||||||
|
barAreaBorderColor.value = '#ddd' |
||||||
|
bindingClick.value = true |
||||||
|
fontPos.splice(0, fontPos.length) |
||||||
|
checkPosArr.splice(0, checkPosArr.length) |
||||||
|
num.value = 1 |
||||||
|
await getPictrue() |
||||||
|
showRefresh.value = true |
||||||
|
} |
||||||
|
|
||||||
|
// 请求背景图片和验证图片 |
||||||
|
const getPictrue = async () => { |
||||||
|
let data = { |
||||||
|
captchaType: captchaType.value |
||||||
|
} |
||||||
|
const res = await getCaptcha(data) |
||||||
|
if (res.data.repCode == '0000') { |
||||||
|
pointBackImgBase.value = res.data.repData.originalImageBase64 |
||||||
|
backToken.value = res.data.repData.token |
||||||
|
secretKey.value = res.data.repData.secretKey |
||||||
|
poinTextList.value = res.data.repData.wordList |
||||||
|
text.value = t('component.captcha.point') + '【' + poinTextList.value.join(',') + '】' |
||||||
|
} else { |
||||||
|
text.value = res.data.repMsg |
||||||
|
} |
||||||
|
} |
||||||
|
//坐标转换函数 |
||||||
|
const pointTransfrom = function (pointArr, imgSize) { |
||||||
|
var newPointArr = pointArr.map((p) => { |
||||||
|
let x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth)) |
||||||
|
let y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight)) |
||||||
|
return { x, y } |
||||||
|
}) |
||||||
|
return newPointArr |
||||||
|
} |
||||||
|
</script> |
@ -0,0 +1,357 @@ |
|||||||
|
<template> |
||||||
|
<div style="position: relative"> |
||||||
|
<div v-if="type === '2'" class="verify-img-out" :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }"> |
||||||
|
<div class="verify-img-panel" :style="{ width: setSize.imgWidth, height: setSize.imgHeight }"> |
||||||
|
<img :src="'data:image/png;base64,' + backImgBase" alt="" style="width: 100%; height: 100%; display: block" /> |
||||||
|
<div class="verify-refresh" @click="refresh" v-show="showRefresh"> |
||||||
|
<i class="iconfont icon-refresh"></i> |
||||||
|
</div> |
||||||
|
<transition name="tips"> |
||||||
|
<span class="verify-tips" v-if="tipWords" :class="passFlag ? 'suc-bg' : 'err-bg'"> |
||||||
|
{{ tipWords }} |
||||||
|
</span> |
||||||
|
</transition> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- 公共部分 --> |
||||||
|
<div class="verify-bar-area" :style="{ width: setSize.imgWidth, height: barSize.height, 'line-height': barSize.height }"> |
||||||
|
<span class="verify-msg" v-text="text"></span> |
||||||
|
<div |
||||||
|
class="verify-left-bar" |
||||||
|
:style="{ |
||||||
|
width: leftBarWidth !== undefined ? leftBarWidth : barSize.height, |
||||||
|
height: barSize.height, |
||||||
|
'border-color': leftBarBorderColor, |
||||||
|
transaction: transitionWidth |
||||||
|
}" |
||||||
|
> |
||||||
|
<span class="verify-msg" v-text="finishText"></span> |
||||||
|
<div |
||||||
|
class="verify-move-block" |
||||||
|
@touchstart="start" |
||||||
|
@mousedown="start" |
||||||
|
:style="{ |
||||||
|
width: barSize.height, |
||||||
|
height: barSize.height, |
||||||
|
'background-color': moveBlockBackgroundColor, |
||||||
|
left: moveBlockLeft, |
||||||
|
transition: transitionLeft |
||||||
|
}" |
||||||
|
> |
||||||
|
<i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor }"></i> |
||||||
|
<div |
||||||
|
v-if="type === '2'" |
||||||
|
class="verify-sub-block" |
||||||
|
:style="{ |
||||||
|
width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px', |
||||||
|
height: setSize.imgHeight, |
||||||
|
top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px', |
||||||
|
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight |
||||||
|
}" |
||||||
|
> |
||||||
|
<img :src="blockBackImgBase" alt="" style="width: 100%; height: 100%; display: block; -webkit-user-drag: none" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<script type="text/babel" setup> |
||||||
|
/** |
||||||
|
* VerifySlide |
||||||
|
* @description 滑块 |
||||||
|
* */ |
||||||
|
import { aesEncrypt } from './../utils/ase' |
||||||
|
import { resetSize } from './../utils/util' |
||||||
|
import { getCaptcha, checkCaptcha } from '@/api/base/login' |
||||||
|
import { useI18n } from '@/hooks/web/useI18n' |
||||||
|
import { computed, onMounted, reactive, ref, watch, nextTick, toRefs, getCurrentInstance } from 'vue' |
||||||
|
|
||||||
|
const props = defineProps({ |
||||||
|
captchaType: { |
||||||
|
type: String |
||||||
|
}, |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
default: '1' |
||||||
|
}, |
||||||
|
//弹出式pop,固定fixed |
||||||
|
mode: { |
||||||
|
type: String, |
||||||
|
default: 'fixed' |
||||||
|
}, |
||||||
|
vSpace: { |
||||||
|
type: Number, |
||||||
|
default: 5 |
||||||
|
}, |
||||||
|
explain: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
imgSize: { |
||||||
|
type: Object, |
||||||
|
default() { |
||||||
|
return { |
||||||
|
width: '310px', |
||||||
|
height: '155px' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
blockSize: { |
||||||
|
type: Object, |
||||||
|
default() { |
||||||
|
return { |
||||||
|
width: '50px', |
||||||
|
height: '50px' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
barSize: { |
||||||
|
type: Object, |
||||||
|
default() { |
||||||
|
return { |
||||||
|
width: '310px', |
||||||
|
height: '30px' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const { t } = useI18n() |
||||||
|
const { mode, captchaType, type, blockSize, explain } = toRefs(props) |
||||||
|
const { proxy } = getCurrentInstance() |
||||||
|
const secretKey = ref('') //后端返回的ase加密秘钥 |
||||||
|
const passFlag = ref('') //是否通过的标识 |
||||||
|
const backImgBase = ref('') //验证码背景图片 |
||||||
|
const blockBackImgBase = ref('') //验证滑块的背景图片 |
||||||
|
const backToken = ref('') //后端返回的唯一token值 |
||||||
|
const startMoveTime = ref('') //移动开始的时间 |
||||||
|
const endMovetime = ref('') //移动结束的时间 |
||||||
|
const tipWords = ref('') |
||||||
|
const text = ref('') |
||||||
|
const finishText = ref('') |
||||||
|
const setSize = reactive({ |
||||||
|
imgHeight: 0, |
||||||
|
imgWidth: 0, |
||||||
|
barHeight: 0, |
||||||
|
barWidth: 0 |
||||||
|
}) |
||||||
|
const moveBlockLeft = ref(undefined) |
||||||
|
const leftBarWidth = ref(undefined) |
||||||
|
// 移动中样式 |
||||||
|
const moveBlockBackgroundColor = ref(undefined) |
||||||
|
const leftBarBorderColor = ref('#ddd') |
||||||
|
const iconColor = ref(undefined) |
||||||
|
const iconClass = ref('icon-right') |
||||||
|
const status = ref(false) //鼠标状态 |
||||||
|
const isEnd = ref(false) //是够验证完成 |
||||||
|
const showRefresh = ref(true) |
||||||
|
const transitionLeft = ref('') |
||||||
|
const transitionWidth = ref('') |
||||||
|
const startLeft = ref(0) |
||||||
|
|
||||||
|
const barArea = computed(() => { |
||||||
|
return proxy.$el.querySelector('.verify-bar-area') |
||||||
|
}) |
||||||
|
const init = () => { |
||||||
|
if (explain.value === '') { |
||||||
|
text.value = t('component.captcha.slide') |
||||||
|
} else { |
||||||
|
text.value = explain.value |
||||||
|
} |
||||||
|
getPictrue() |
||||||
|
nextTick(() => { |
||||||
|
let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy) |
||||||
|
setSize.imgHeight = imgHeight |
||||||
|
setSize.imgWidth = imgWidth |
||||||
|
setSize.barHeight = barHeight |
||||||
|
setSize.barWidth = barWidth |
||||||
|
proxy.$parent.$emit('ready', proxy) |
||||||
|
}) |
||||||
|
|
||||||
|
window.removeEventListener('touchmove', function (e) { |
||||||
|
move(e) |
||||||
|
}) |
||||||
|
window.removeEventListener('mousemove', function (e) { |
||||||
|
move(e) |
||||||
|
}) |
||||||
|
|
||||||
|
//鼠标松开 |
||||||
|
window.removeEventListener('touchend', function () { |
||||||
|
end() |
||||||
|
}) |
||||||
|
window.removeEventListener('mouseup', function () { |
||||||
|
end() |
||||||
|
}) |
||||||
|
|
||||||
|
window.addEventListener('touchmove', function (e) { |
||||||
|
move(e) |
||||||
|
}) |
||||||
|
window.addEventListener('mousemove', function (e) { |
||||||
|
move(e) |
||||||
|
}) |
||||||
|
|
||||||
|
//鼠标松开 |
||||||
|
window.addEventListener('touchend', function () { |
||||||
|
end() |
||||||
|
}) |
||||||
|
window.addEventListener('mouseup', function () { |
||||||
|
end() |
||||||
|
}) |
||||||
|
} |
||||||
|
watch(type, () => { |
||||||
|
init() |
||||||
|
}) |
||||||
|
onMounted(() => { |
||||||
|
// 禁止拖拽 |
||||||
|
init() |
||||||
|
proxy.$el.onselectstart = function () { |
||||||
|
return false |
||||||
|
} |
||||||
|
}) |
||||||
|
//鼠标按下 |
||||||
|
const start = (e) => { |
||||||
|
e = e || window.event |
||||||
|
if (!e.touches) { |
||||||
|
//兼容PC端 |
||||||
|
var x = e.clientX |
||||||
|
} else { |
||||||
|
//兼容移动端 |
||||||
|
var x = e.touches[0].pageX |
||||||
|
} |
||||||
|
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left) |
||||||
|
startMoveTime.value = +new Date() //开始滑动的时间 |
||||||
|
if (isEnd.value == false) { |
||||||
|
text.value = '' |
||||||
|
moveBlockBackgroundColor.value = '#337ab7' |
||||||
|
leftBarBorderColor.value = '#337AB7' |
||||||
|
iconColor.value = '#fff' |
||||||
|
e.stopPropagation() |
||||||
|
status.value = true |
||||||
|
} |
||||||
|
} |
||||||
|
//鼠标移动 |
||||||
|
const move = (e) => { |
||||||
|
e = e || window.event |
||||||
|
if (status.value && isEnd.value == false) { |
||||||
|
if (!e.touches) { |
||||||
|
//兼容PC端 |
||||||
|
var x = e.clientX |
||||||
|
} else { |
||||||
|
//兼容移动端 |
||||||
|
var x = e.touches[0].pageX |
||||||
|
} |
||||||
|
var bar_area_left = barArea.value.getBoundingClientRect().left |
||||||
|
var move_block_left = x - bar_area_left //小方块相对于父元素的left值 |
||||||
|
if (move_block_left >= barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2) { |
||||||
|
move_block_left = barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2 |
||||||
|
} |
||||||
|
if (move_block_left <= 0) { |
||||||
|
move_block_left = parseInt(parseInt(blockSize.value.width) / 2) |
||||||
|
} |
||||||
|
//拖动后小方块的left值 |
||||||
|
moveBlockLeft.value = move_block_left - startLeft.value + 'px' |
||||||
|
leftBarWidth.value = move_block_left - startLeft.value + 'px' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//鼠标松开 |
||||||
|
const end = () => { |
||||||
|
endMovetime.value = +new Date() |
||||||
|
//判断是否重合 |
||||||
|
if (status.value && isEnd.value == false) { |
||||||
|
var moveLeftDistance = parseInt((moveBlockLeft.value || '').replace('px', '')) |
||||||
|
moveLeftDistance = (moveLeftDistance * 310) / parseInt(setSize.imgWidth) |
||||||
|
let data = { |
||||||
|
captchaType: captchaType.value, |
||||||
|
pointJson: secretKey.value |
||||||
|
? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value) |
||||||
|
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }), |
||||||
|
token: backToken.value |
||||||
|
} |
||||||
|
checkCaptcha(data).then((response) => { |
||||||
|
const res = response.data |
||||||
|
if (res.repCode == '0000') { |
||||||
|
moveBlockBackgroundColor.value = '#5cb85c' |
||||||
|
leftBarBorderColor.value = '#5cb85c' |
||||||
|
iconColor.value = '#fff' |
||||||
|
iconClass.value = 'icon-check' |
||||||
|
showRefresh.value = false |
||||||
|
isEnd.value = true |
||||||
|
if (mode.value == 'pop') { |
||||||
|
setTimeout(() => { |
||||||
|
proxy.$parent.clickShow = false |
||||||
|
refresh() |
||||||
|
}, 1500) |
||||||
|
} |
||||||
|
passFlag.value = true |
||||||
|
tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s |
||||||
|
${t('component.captcha.success')}` |
||||||
|
const captchaVerification = secretKey.value |
||||||
|
? aesEncrypt(backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value) |
||||||
|
: backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }) |
||||||
|
setTimeout(() => { |
||||||
|
tipWords.value = '' |
||||||
|
proxy.$parent.closeBox() |
||||||
|
proxy.$parent.$emit('success', { captchaVerification }) |
||||||
|
}, 1000) |
||||||
|
} else { |
||||||
|
moveBlockBackgroundColor.value = '#d9534f' |
||||||
|
leftBarBorderColor.value = '#d9534f' |
||||||
|
iconColor.value = '#fff' |
||||||
|
iconClass.value = 'icon-close' |
||||||
|
passFlag.value = false |
||||||
|
setTimeout(function () { |
||||||
|
refresh() |
||||||
|
}, 1000) |
||||||
|
proxy.$parent.$emit('error', proxy) |
||||||
|
tipWords.value = t('component.captcha.fail') |
||||||
|
setTimeout(() => { |
||||||
|
tipWords.value = '' |
||||||
|
}, 1000) |
||||||
|
} |
||||||
|
}) |
||||||
|
status.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const refresh = async () => { |
||||||
|
showRefresh.value = true |
||||||
|
finishText.value = '' |
||||||
|
|
||||||
|
transitionLeft.value = 'left .3s' |
||||||
|
moveBlockLeft.value = 0 |
||||||
|
|
||||||
|
leftBarWidth.value = undefined |
||||||
|
transitionWidth.value = 'width .3s' |
||||||
|
|
||||||
|
leftBarBorderColor.value = '#ddd' |
||||||
|
moveBlockBackgroundColor.value = '#fff' |
||||||
|
iconColor.value = '#000' |
||||||
|
iconClass.value = 'icon-right' |
||||||
|
isEnd.value = false |
||||||
|
|
||||||
|
await getPictrue() |
||||||
|
setTimeout(() => { |
||||||
|
transitionWidth.value = '' |
||||||
|
transitionLeft.value = '' |
||||||
|
text.value = explain.value |
||||||
|
}, 300) |
||||||
|
} |
||||||
|
|
||||||
|
// 请求背景图片和验证图片 |
||||||
|
const getPictrue = async () => { |
||||||
|
let data = { |
||||||
|
captchaType: captchaType.value |
||||||
|
} |
||||||
|
const res = await getCaptcha(data) |
||||||
|
if (res.data.repCode == '0000') { |
||||||
|
backImgBase.value = res.data.repData.originalImageBase64 |
||||||
|
blockBackImgBase.value = 'data:image/png;base64,' + res.data.repData.jigsawImageBase64 |
||||||
|
backToken.value = res.data.repData.token |
||||||
|
secretKey.value = res.data.repData.secretKey |
||||||
|
} else { |
||||||
|
tipWords.value = res.data.repMsg |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
@ -0,0 +1,4 @@ |
|||||||
|
import VerifySlide from './VerifySlide.vue' |
||||||
|
import VerifyPoints from './VerifyPoints.vue' |
||||||
|
|
||||||
|
export { VerifySlide, VerifyPoints } |
@ -0,0 +1,14 @@ |
|||||||
|
import CryptoJS from 'crypto-js' |
||||||
|
/** |
||||||
|
* @word 要加密的内容 |
||||||
|
* @keyWord String 服务器随机返回的关键字 |
||||||
|
* */ |
||||||
|
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') { |
||||||
|
const key = CryptoJS.enc.Utf8.parse(keyWord) |
||||||
|
const srcs = CryptoJS.enc.Utf8.parse(word) |
||||||
|
const encrypted = CryptoJS.AES.encrypt(srcs, key, { |
||||||
|
mode: CryptoJS.mode.ECB, |
||||||
|
padding: CryptoJS.pad.Pkcs7 |
||||||
|
}) |
||||||
|
return encrypted.toString() |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
export function resetSize(vm) { |
||||||
|
let img_width, img_height, bar_width, bar_height //图片的宽度、高度,移动条的宽度、高度
|
||||||
|
const EmployeeWindow = window as any |
||||||
|
const parentWidth = vm.$el.parentNode.offsetWidth || EmployeeWindow.offsetWidth |
||||||
|
const parentHeight = vm.$el.parentNode.offsetHeight || EmployeeWindow.offsetHeight |
||||||
|
if (vm.imgSize.width.indexOf('%') != -1) { |
||||||
|
img_width = (parseInt(vm.imgSize.width) / 100) * parentWidth + 'px' |
||||||
|
} else { |
||||||
|
img_width = vm.imgSize.width |
||||||
|
} |
||||||
|
|
||||||
|
if (vm.imgSize.height.indexOf('%') != -1) { |
||||||
|
img_height = (parseInt(vm.imgSize.height) / 100) * parentHeight + 'px' |
||||||
|
} else { |
||||||
|
img_height = vm.imgSize.height |
||||||
|
} |
||||||
|
|
||||||
|
if (vm.barSize.width.indexOf('%') != -1) { |
||||||
|
bar_width = (parseInt(vm.barSize.width) / 100) * parentWidth + 'px' |
||||||
|
} else { |
||||||
|
bar_width = vm.barSize.width |
||||||
|
} |
||||||
|
|
||||||
|
if (vm.barSize.height.indexOf('%') != -1) { |
||||||
|
bar_height = (parseInt(vm.barSize.height) / 100) * parentHeight + 'px' |
||||||
|
} else { |
||||||
|
bar_height = vm.barSize.height |
||||||
|
} |
||||||
|
|
||||||
|
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height } |
||||||
|
} |
||||||
|
|
||||||
|
export const _code_chars = [ |
||||||
|
1, |
||||||
|
2, |
||||||
|
3, |
||||||
|
4, |
||||||
|
5, |
||||||
|
6, |
||||||
|
7, |
||||||
|
8, |
||||||
|
9, |
||||||
|
'a', |
||||||
|
'b', |
||||||
|
'c', |
||||||
|
'd', |
||||||
|
'e', |
||||||
|
'f', |
||||||
|
'g', |
||||||
|
'h', |
||||||
|
'i', |
||||||
|
'j', |
||||||
|
'k', |
||||||
|
'l', |
||||||
|
'm', |
||||||
|
'n', |
||||||
|
'o', |
||||||
|
'p', |
||||||
|
'q', |
||||||
|
'r', |
||||||
|
's', |
||||||
|
't', |
||||||
|
'u', |
||||||
|
'v', |
||||||
|
'w', |
||||||
|
'x', |
||||||
|
'y', |
||||||
|
'z', |
||||||
|
'A', |
||||||
|
'B', |
||||||
|
'C', |
||||||
|
'D', |
||||||
|
'E', |
||||||
|
'F', |
||||||
|
'G', |
||||||
|
'H', |
||||||
|
'I', |
||||||
|
'J', |
||||||
|
'K', |
||||||
|
'L', |
||||||
|
'M', |
||||||
|
'N', |
||||||
|
'O', |
||||||
|
'P', |
||||||
|
'Q', |
||||||
|
'R', |
||||||
|
'S', |
||||||
|
'T', |
||||||
|
'U', |
||||||
|
'V', |
||||||
|
'W', |
||||||
|
'X', |
||||||
|
'Y', |
||||||
|
'Z' |
||||||
|
] |
||||||
|
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0'] |
||||||
|
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC'] |
@ -1,7 +0,0 @@ |
|||||||
import { withInstall } from '@/utils' |
|
||||||
import basicDragVerify from './src/DragVerify.vue' |
|
||||||
import rotateDragVerify from './src/ImgRotate.vue' |
|
||||||
|
|
||||||
export const BasicDragVerify = withInstall(basicDragVerify) |
|
||||||
export const RotateDragVerify = withInstall(rotateDragVerify) |
|
||||||
export * from './src/typing' |
|
@ -1,365 +0,0 @@ |
|||||||
<script lang="tsx"> |
|
||||||
import type { Ref } from 'vue' |
|
||||||
import { defineComponent, ref, computed, unref, reactive, watch, watchEffect } from 'vue' |
|
||||||
import { useTimeoutFn } from '@/hooks/core/useTimeout' |
|
||||||
import { useEventListener } from '@/hooks/event/useEventListener' |
|
||||||
import { basicProps } from './props' |
|
||||||
import { getSlot } from '@/utils/helper/tsxHelper' |
|
||||||
import { CheckOutlined, DoubleRightOutlined } from '@ant-design/icons-vue' |
|
||||||
|
|
||||||
export default defineComponent({ |
|
||||||
name: 'BaseDargVerify', |
|
||||||
props: basicProps, |
|
||||||
emits: ['success', 'update:value', 'change', 'start', 'move', 'end'], |
|
||||||
setup(props, { emit, slots, expose }) { |
|
||||||
const state = reactive({ |
|
||||||
isMoving: false, |
|
||||||
isPassing: false, |
|
||||||
moveDistance: 0, |
|
||||||
toLeft: false, |
|
||||||
startTime: 0, |
|
||||||
endTime: 0 |
|
||||||
}) |
|
||||||
|
|
||||||
const wrapElRef = ref<HTMLDivElement | null>(null) |
|
||||||
const barElRef = ref<HTMLDivElement | null>(null) |
|
||||||
const contentElRef = ref<HTMLDivElement | null>(null) |
|
||||||
const actionElRef = ref(null) as Ref<HTMLDivElement | null> |
|
||||||
|
|
||||||
useEventListener({ |
|
||||||
el: document, |
|
||||||
name: 'mouseup', |
|
||||||
listener: () => { |
|
||||||
if (state.isMoving) { |
|
||||||
resume() |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
const getActionStyleRef = computed(() => { |
|
||||||
const { height, actionStyle } = props |
|
||||||
const h = `${parseInt(height as string)}px` |
|
||||||
return { |
|
||||||
left: 0, |
|
||||||
width: h, |
|
||||||
height: h, |
|
||||||
...actionStyle |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
const getWrapStyleRef = computed(() => { |
|
||||||
const { height, width, circle, wrapStyle } = props |
|
||||||
const h = parseInt(height as string) |
|
||||||
const w = `${parseInt(width as string)}px` |
|
||||||
return { |
|
||||||
width: w, |
|
||||||
height: `${h}px`, |
|
||||||
lineHeight: `${h}px`, |
|
||||||
borderRadius: circle ? h / 2 + 'px' : 0, |
|
||||||
...wrapStyle |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
const getBarStyleRef = computed(() => { |
|
||||||
const { height, circle, barStyle } = props |
|
||||||
const h = parseInt(height as string) |
|
||||||
return { |
|
||||||
height: `${h}px`, |
|
||||||
borderRadius: circle ? h / 2 + 'px 0 0 ' + h / 2 + 'px' : 0, |
|
||||||
...barStyle |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
const getContentStyleRef = computed(() => { |
|
||||||
const { height, width, contentStyle } = props |
|
||||||
const h = `${parseInt(height as string)}px` |
|
||||||
const w = `${parseInt(width as string)}px` |
|
||||||
|
|
||||||
return { |
|
||||||
height: h, |
|
||||||
width: w, |
|
||||||
...contentStyle |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
watch( |
|
||||||
() => state.isPassing, |
|
||||||
(isPassing) => { |
|
||||||
if (isPassing) { |
|
||||||
const { startTime, endTime } = state |
|
||||||
const time = (endTime - startTime) / 1000 |
|
||||||
emit('success', { isPassing, time: time.toFixed(1) }) |
|
||||||
emit('update:value', isPassing) |
|
||||||
emit('change', isPassing) |
|
||||||
} |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
watchEffect(() => { |
|
||||||
state.isPassing = !!props.value |
|
||||||
}) |
|
||||||
|
|
||||||
function getEventPageX(e: MouseEvent | TouchEvent) { |
|
||||||
return (e as MouseEvent).pageX || (e as TouchEvent).touches[0].pageX |
|
||||||
} |
|
||||||
|
|
||||||
function handleDragStart(e: MouseEvent | TouchEvent) { |
|
||||||
if (state.isPassing) { |
|
||||||
return |
|
||||||
} |
|
||||||
const actionEl = unref(actionElRef) |
|
||||||
if (!actionEl) return |
|
||||||
emit('start', e) |
|
||||||
state.moveDistance = getEventPageX(e) - parseInt(actionEl.style.left.replace('px', ''), 10) |
|
||||||
state.startTime = new Date().getTime() |
|
||||||
state.isMoving = true |
|
||||||
} |
|
||||||
|
|
||||||
function getOffset(el: HTMLDivElement) { |
|
||||||
const actionWidth = parseInt(el.style.width) |
|
||||||
const { width } = props |
|
||||||
const widthNum = parseInt(width as string) |
|
||||||
const offset = widthNum - actionWidth - 6 |
|
||||||
return { offset, widthNum, actionWidth } |
|
||||||
} |
|
||||||
|
|
||||||
function handleDragMoving(e: MouseEvent | TouchEvent) { |
|
||||||
const { isMoving, moveDistance } = state |
|
||||||
if (isMoving) { |
|
||||||
const actionEl = unref(actionElRef) |
|
||||||
const barEl = unref(barElRef) |
|
||||||
if (!actionEl || !barEl) return |
|
||||||
const { offset, widthNum, actionWidth } = getOffset(actionEl) |
|
||||||
const moveX = getEventPageX(e) - moveDistance |
|
||||||
|
|
||||||
emit('move', { |
|
||||||
event: e, |
|
||||||
moveDistance, |
|
||||||
moveX |
|
||||||
}) |
|
||||||
if (moveX > 0 && moveX <= offset) { |
|
||||||
actionEl.style.left = `${moveX}px` |
|
||||||
barEl.style.width = `${moveX + actionWidth / 2}px` |
|
||||||
} else if (moveX > offset) { |
|
||||||
actionEl.style.left = `${widthNum - actionWidth}px` |
|
||||||
barEl.style.width = `${widthNum - actionWidth / 2}px` |
|
||||||
if (!props.isSlot) { |
|
||||||
checkPass() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function handleDragOver(e: MouseEvent | TouchEvent) { |
|
||||||
const { isMoving, isPassing, moveDistance } = state |
|
||||||
if (isMoving && !isPassing) { |
|
||||||
emit('end', e) |
|
||||||
const actionEl = unref(actionElRef) |
|
||||||
const barEl = unref(barElRef) |
|
||||||
if (!actionEl || !barEl) return |
|
||||||
const moveX = getEventPageX(e) - moveDistance |
|
||||||
const { offset, widthNum, actionWidth } = getOffset(actionEl) |
|
||||||
if (moveX < offset) { |
|
||||||
if (!props.isSlot) { |
|
||||||
resume() |
|
||||||
} else { |
|
||||||
setTimeout(() => { |
|
||||||
if (!props.value) { |
|
||||||
resume() |
|
||||||
} else { |
|
||||||
const contentEl = unref(contentElRef) |
|
||||||
if (contentEl) { |
|
||||||
contentEl.style.width = `${parseInt(barEl.style.width)}px` |
|
||||||
} |
|
||||||
} |
|
||||||
}, 0) |
|
||||||
} |
|
||||||
} else { |
|
||||||
actionEl.style.left = `${widthNum - actionWidth}px` |
|
||||||
barEl.style.width = `${widthNum - actionWidth / 2}px` |
|
||||||
checkPass() |
|
||||||
} |
|
||||||
state.isMoving = false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function checkPass() { |
|
||||||
if (props.isSlot) { |
|
||||||
resume() |
|
||||||
return |
|
||||||
} |
|
||||||
state.endTime = new Date().getTime() |
|
||||||
state.isPassing = true |
|
||||||
state.isMoving = false |
|
||||||
} |
|
||||||
|
|
||||||
function resume() { |
|
||||||
state.isMoving = false |
|
||||||
state.isPassing = false |
|
||||||
state.moveDistance = 0 |
|
||||||
state.toLeft = false |
|
||||||
state.startTime = 0 |
|
||||||
state.endTime = 0 |
|
||||||
const actionEl = unref(actionElRef) |
|
||||||
const barEl = unref(barElRef) |
|
||||||
const contentEl = unref(contentElRef) |
|
||||||
if (!actionEl || !barEl || !contentEl) return |
|
||||||
state.toLeft = true |
|
||||||
useTimeoutFn(() => { |
|
||||||
state.toLeft = false |
|
||||||
actionEl.style.left = '0' |
|
||||||
barEl.style.width = '0' |
|
||||||
// The time is consistent with the animation time |
|
||||||
}, 300) |
|
||||||
contentEl.style.width = unref(getContentStyleRef).width |
|
||||||
} |
|
||||||
|
|
||||||
expose({ |
|
||||||
resume |
|
||||||
}) |
|
||||||
|
|
||||||
return () => { |
|
||||||
const renderBar = () => { |
|
||||||
const cls = [`darg-verify-bar`] |
|
||||||
if (state.toLeft) { |
|
||||||
cls.push('to-left') |
|
||||||
} |
|
||||||
return <div class={cls} ref={barElRef} style={unref(getBarStyleRef)} /> |
|
||||||
} |
|
||||||
|
|
||||||
const renderContent = () => { |
|
||||||
const cls = [`darg-verify-content`] |
|
||||||
const { isPassing } = state |
|
||||||
const { text, successText } = props |
|
||||||
|
|
||||||
isPassing && cls.push('success') |
|
||||||
|
|
||||||
return ( |
|
||||||
<div class={cls} ref={contentElRef} style={unref(getContentStyleRef)}> |
|
||||||
{getSlot(slots, 'text', isPassing) || (isPassing ? successText : text)} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
const renderAction = () => { |
|
||||||
const cls = [`darg-verify-action`] |
|
||||||
const { toLeft, isPassing } = state |
|
||||||
if (toLeft) { |
|
||||||
cls.push('to-left') |
|
||||||
} |
|
||||||
return ( |
|
||||||
<div class={cls} onMousedown={handleDragStart} onTouchstart={handleDragStart} style={unref(getActionStyleRef)} ref={actionElRef}> |
|
||||||
{getSlot(slots, 'actionIcon', isPassing) || |
|
||||||
(isPassing ? ( |
|
||||||
<CheckOutlined class={`darg-verify-action__icon`} /> |
|
||||||
) : ( |
|
||||||
<DoubleRightOutlined class={`darg-verify-action__icon`} /> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<div |
|
||||||
class="darg-verify" |
|
||||||
ref={wrapElRef} |
|
||||||
style={unref(getWrapStyleRef)} |
|
||||||
onMousemove={handleDragMoving} |
|
||||||
onTouchmove={handleDragMoving} |
|
||||||
onMouseleave={handleDragOver} |
|
||||||
onMouseup={handleDragOver} |
|
||||||
onTouchend={handleDragOver} |
|
||||||
> |
|
||||||
{renderBar()} |
|
||||||
{renderContent()} |
|
||||||
{renderAction()} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
</script> |
|
||||||
<style lang="less"> |
|
||||||
@radius: 4px; |
|
||||||
|
|
||||||
.darg-verify { |
|
||||||
position: relative; |
|
||||||
overflow: hidden; |
|
||||||
text-align: center; |
|
||||||
background-color: rgb(238 238 238); |
|
||||||
border: 1px solid #ddd; |
|
||||||
border-radius: @radius; |
|
||||||
|
|
||||||
&-bar { |
|
||||||
position: absolute; |
|
||||||
width: 0; |
|
||||||
height: 36px; |
|
||||||
background-color: @success-color; |
|
||||||
border-radius: @radius; |
|
||||||
|
|
||||||
&.to-left { |
|
||||||
width: 0 !important; |
|
||||||
transition: width 0.3s; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&-content { |
|
||||||
position: absolute; |
|
||||||
top: 0; |
|
||||||
font-size: 12px; |
|
||||||
text-size-adjust: none; |
|
||||||
background-color: -webkit-gradient( |
|
||||||
linear, |
|
||||||
left top, |
|
||||||
right top, |
|
||||||
color-stop(0, #333), |
|
||||||
color-stop(0.4, #333), |
|
||||||
color-stop(0.5, #fff), |
|
||||||
color-stop(0.6, #333), |
|
||||||
color-stop(1, #333) |
|
||||||
); |
|
||||||
animation: slidetounlock 3s infinite; |
|
||||||
background-clip: text; |
|
||||||
user-select: none; |
|
||||||
|
|
||||||
&.success { |
|
||||||
-webkit-text-fill-color: @white; |
|
||||||
} |
|
||||||
|
|
||||||
& > * { |
|
||||||
-webkit-text-fill-color: #333; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&-action { |
|
||||||
position: absolute; |
|
||||||
top: 0; |
|
||||||
left: 0; |
|
||||||
display: flex; |
|
||||||
cursor: move; |
|
||||||
background-color: @white; |
|
||||||
border-radius: @radius; |
|
||||||
justify-content: center; |
|
||||||
align-items: center; |
|
||||||
|
|
||||||
&__icon { |
|
||||||
cursor: inherit; |
|
||||||
} |
|
||||||
|
|
||||||
&.to-left { |
|
||||||
left: 0 !important; |
|
||||||
transition: left 0.3s; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@keyframes slidetounlock { |
|
||||||
0% { |
|
||||||
background-position: -120px 0; |
|
||||||
} |
|
||||||
|
|
||||||
100% { |
|
||||||
background-position: 120px 0; |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
|
@ -1,214 +0,0 @@ |
|||||||
<script lang="tsx"> |
|
||||||
import type { MoveData, DragVerifyActionType } from './typing' |
|
||||||
import { defineComponent, computed, unref, reactive, watch, ref } from 'vue' |
|
||||||
import { useTimeoutFn } from '@/hooks/core/useTimeout' |
|
||||||
import BasicDragVerify from './DragVerify.vue' |
|
||||||
import { hackCss } from '@/utils/domUtils' |
|
||||||
import { rotateProps } from './props' |
|
||||||
import { useI18n } from '@/hooks/web/useI18n' |
|
||||||
|
|
||||||
export default defineComponent({ |
|
||||||
name: 'ImgRotateDragVerify', |
|
||||||
inheritAttrs: false, |
|
||||||
props: rotateProps, |
|
||||||
emits: ['success', 'change', 'update:value'], |
|
||||||
setup(props, { emit, attrs, expose }) { |
|
||||||
const basicRef = ref<Nullable<DragVerifyActionType>>(null) |
|
||||||
const state = reactive({ |
|
||||||
showTip: false, |
|
||||||
isPassing: false, |
|
||||||
imgStyle: {}, |
|
||||||
randomRotate: 0, |
|
||||||
currentRotate: 0, |
|
||||||
toOrigin: false, |
|
||||||
startTime: 0, |
|
||||||
endTime: 0, |
|
||||||
draged: false |
|
||||||
}) |
|
||||||
const { t } = useI18n() |
|
||||||
|
|
||||||
watch( |
|
||||||
() => state.isPassing, |
|
||||||
(isPassing) => { |
|
||||||
if (isPassing) { |
|
||||||
const { startTime, endTime } = state |
|
||||||
const time = (endTime - startTime) / 1000 |
|
||||||
emit('success', { isPassing, time: time.toFixed(1) }) |
|
||||||
emit('change', isPassing) |
|
||||||
emit('update:value', isPassing) |
|
||||||
} |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
const getImgWrapStyleRef = computed(() => { |
|
||||||
const { imgWrapStyle, imgWidth } = props |
|
||||||
return { |
|
||||||
width: `${imgWidth}px`, |
|
||||||
height: `${imgWidth}px`, |
|
||||||
...imgWrapStyle |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
const getFactorRef = computed(() => { |
|
||||||
const { minDegree, maxDegree } = props |
|
||||||
if (minDegree === maxDegree) { |
|
||||||
return Math.floor(1 + Math.random() * 1) / 10 + 1 |
|
||||||
} |
|
||||||
return 1 |
|
||||||
}) |
|
||||||
function handleStart() { |
|
||||||
state.startTime = new Date().getTime() |
|
||||||
} |
|
||||||
|
|
||||||
function handleDragBarMove(data: MoveData) { |
|
||||||
state.draged = true |
|
||||||
const { imgWidth, height, maxDegree } = props |
|
||||||
const { moveX } = data |
|
||||||
const currentRotate = Math.ceil((moveX / (imgWidth! - parseInt(height as string))) * maxDegree! * unref(getFactorRef)) |
|
||||||
state.currentRotate = currentRotate |
|
||||||
state.imgStyle = hackCss('transform', `rotateZ(${state.randomRotate - currentRotate}deg)`) |
|
||||||
} |
|
||||||
|
|
||||||
function handleImgOnLoad() { |
|
||||||
const { minDegree, maxDegree } = props |
|
||||||
const ranRotate = Math.floor(minDegree! + Math.random() * (maxDegree! - minDegree!)) // 生成随机角度 |
|
||||||
state.randomRotate = ranRotate |
|
||||||
state.imgStyle = hackCss('transform', `rotateZ(${ranRotate}deg)`) |
|
||||||
} |
|
||||||
|
|
||||||
function handleDragEnd() { |
|
||||||
const { randomRotate, currentRotate } = state |
|
||||||
const { diffDegree } = props |
|
||||||
|
|
||||||
if (Math.abs(randomRotate - currentRotate) >= (diffDegree || 20)) { |
|
||||||
state.imgStyle = hackCss('transform', `rotateZ(${randomRotate}deg)`) |
|
||||||
state.toOrigin = true |
|
||||||
useTimeoutFn(() => { |
|
||||||
state.toOrigin = false |
|
||||||
state.showTip = true |
|
||||||
// 时间与动画时间保持一致 |
|
||||||
}, 300) |
|
||||||
} else { |
|
||||||
checkPass() |
|
||||||
} |
|
||||||
state.showTip = true |
|
||||||
} |
|
||||||
function checkPass() { |
|
||||||
state.isPassing = true |
|
||||||
state.endTime = new Date().getTime() |
|
||||||
} |
|
||||||
|
|
||||||
function resume() { |
|
||||||
state.showTip = false |
|
||||||
const basicEl = unref(basicRef) |
|
||||||
if (!basicEl) { |
|
||||||
return |
|
||||||
} |
|
||||||
state.isPassing = false |
|
||||||
|
|
||||||
basicEl.resume() |
|
||||||
handleImgOnLoad() |
|
||||||
} |
|
||||||
|
|
||||||
expose({ resume }) |
|
||||||
|
|
||||||
// handleImgOnLoad(); |
|
||||||
return () => { |
|
||||||
const { src } = props |
|
||||||
const { toOrigin, isPassing, startTime, endTime } = state |
|
||||||
const imgCls: string[] = [] |
|
||||||
if (toOrigin) { |
|
||||||
imgCls.push('to-origin') |
|
||||||
} |
|
||||||
const time = (endTime - startTime) / 1000 |
|
||||||
|
|
||||||
return ( |
|
||||||
<div class="ir-dv"> |
|
||||||
<div class={`ir-dv-img__wrap`} style={unref(getImgWrapStyleRef)}> |
|
||||||
<img |
|
||||||
src={src} |
|
||||||
onLoad={handleImgOnLoad} |
|
||||||
width={parseInt(props.width as string)} |
|
||||||
class={imgCls} |
|
||||||
style={state.imgStyle} |
|
||||||
onClick={() => { |
|
||||||
resume() |
|
||||||
}} |
|
||||||
alt="verify" |
|
||||||
/> |
|
||||||
{state.showTip && ( |
|
||||||
<span class={[`ir-dv-img__tip`, state.isPassing ? 'success' : 'error']}> |
|
||||||
{state.isPassing ? t('component.verify.time', { time: time.toFixed(1) }) : t('component.verify.error')} |
|
||||||
</span> |
|
||||||
)} |
|
||||||
{!state.showTip && !state.draged && <span class={[`ir-dv-img__tip`, 'normal']}>{t('component.verify.redoTip')}</span>} |
|
||||||
</div> |
|
||||||
<BasicDragVerify |
|
||||||
class={`ir-dv-drag__bar`} |
|
||||||
onMove={handleDragBarMove} |
|
||||||
onEnd={handleDragEnd} |
|
||||||
onStart={handleStart} |
|
||||||
ref={basicRef} |
|
||||||
{...{ ...attrs, ...props }} |
|
||||||
value={isPassing} |
|
||||||
isSlot={true} |
|
||||||
/> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
</script> |
|
||||||
<style lang="less"> |
|
||||||
.ir-dv { |
|
||||||
position: relative; |
|
||||||
display: flex; |
|
||||||
flex-direction: column; |
|
||||||
align-items: center; |
|
||||||
|
|
||||||
&-img__wrap { |
|
||||||
position: relative; |
|
||||||
overflow: hidden; |
|
||||||
border-radius: 50%; |
|
||||||
|
|
||||||
img { |
|
||||||
width: 100%; |
|
||||||
border-radius: 50%; |
|
||||||
|
|
||||||
&.to-origin { |
|
||||||
transition: transform 0.3s; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&-img__tip { |
|
||||||
position: absolute; |
|
||||||
bottom: 10px; |
|
||||||
left: 0; |
|
||||||
z-index: 1; |
|
||||||
display: block; |
|
||||||
width: 100%; |
|
||||||
height: 30px; |
|
||||||
font-size: 12px; |
|
||||||
line-height: 30px; |
|
||||||
color: @white; |
|
||||||
text-align: center; |
|
||||||
|
|
||||||
&.success { |
|
||||||
background-color: fade(@success-color, 60%); |
|
||||||
} |
|
||||||
|
|
||||||
&.error { |
|
||||||
background-color: fade(@error-color, 60%); |
|
||||||
} |
|
||||||
|
|
||||||
&.normal { |
|
||||||
background-color: rgb(0 0 0 / 30%); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&-drag__bar { |
|
||||||
margin-top: 20px; |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
|
@ -1,86 +0,0 @@ |
|||||||
import { useI18n } from '@/hooks/web/useI18n' |
|
||||||
|
|
||||||
const { t } = useI18n() |
|
||||||
export const basicProps = { |
|
||||||
value: { |
|
||||||
type: Boolean as PropType<boolean>, |
|
||||||
default: false |
|
||||||
}, |
|
||||||
|
|
||||||
isSlot: { |
|
||||||
type: Boolean as PropType<boolean>, |
|
||||||
default: false |
|
||||||
}, |
|
||||||
|
|
||||||
text: { |
|
||||||
type: [String] as PropType<string>, |
|
||||||
default: t('component.verify.dragText') |
|
||||||
}, |
|
||||||
successText: { |
|
||||||
type: [String] as PropType<string>, |
|
||||||
default: t('component.verify.successText') |
|
||||||
}, |
|
||||||
height: { |
|
||||||
type: [Number, String] as PropType<number | string>, |
|
||||||
default: 40 |
|
||||||
}, |
|
||||||
|
|
||||||
width: { |
|
||||||
type: [Number, String] as PropType<number | string>, |
|
||||||
default: 220 |
|
||||||
}, |
|
||||||
|
|
||||||
circle: { |
|
||||||
type: Boolean as PropType<boolean>, |
|
||||||
default: false |
|
||||||
}, |
|
||||||
|
|
||||||
wrapStyle: { |
|
||||||
type: Object as PropType<any>, |
|
||||||
default: () => ({}) |
|
||||||
}, |
|
||||||
contentStyle: { |
|
||||||
type: Object as PropType<any>, |
|
||||||
default: () => ({}) |
|
||||||
}, |
|
||||||
barStyle: { |
|
||||||
type: Object as PropType<any>, |
|
||||||
default: () => ({}) |
|
||||||
}, |
|
||||||
actionStyle: { |
|
||||||
type: Object as PropType<any>, |
|
||||||
default: () => ({}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export const rotateProps = { |
|
||||||
...basicProps, |
|
||||||
src: { |
|
||||||
type: String as PropType<string> |
|
||||||
}, |
|
||||||
|
|
||||||
imgWidth: { |
|
||||||
type: Number as PropType<number>, |
|
||||||
default: 260 |
|
||||||
}, |
|
||||||
|
|
||||||
imgWrapStyle: { |
|
||||||
type: Object as PropType<any>, |
|
||||||
default: () => ({}) |
|
||||||
}, |
|
||||||
|
|
||||||
minDegree: { |
|
||||||
type: Number as PropType<number>, |
|
||||||
default: 90 |
|
||||||
}, |
|
||||||
|
|
||||||
maxDegree: { |
|
||||||
type: Number as PropType<number>, |
|
||||||
default: 270 |
|
||||||
}, |
|
||||||
|
|
||||||
diffDegree: { |
|
||||||
type: Number as PropType<number>, |
|
||||||
default: 20 |
|
||||||
} |
|
||||||
} |
|
@ -1,14 +0,0 @@ |
|||||||
export interface DragVerifyActionType { |
|
||||||
resume: () => void |
|
||||||
} |
|
||||||
|
|
||||||
export interface PassingData { |
|
||||||
isPassing: boolean |
|
||||||
time: number |
|
||||||
} |
|
||||||
|
|
||||||
export interface MoveData { |
|
||||||
event: MouseEvent | TouchEvent |
|
||||||
moveDistance: number |
|
||||||
moveX: number |
|
||||||
} |
|
Loading…
Reference in new issue