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