完善虎牙短信登录与图形安全验证
This commit is contained in:
@@ -12,17 +12,23 @@ import { useParams } from 'react-router'
|
||||
|
||||
import {
|
||||
fetchCustomerFulfillment,
|
||||
loginCustomerHuyaSms,
|
||||
loginCustomerWeiboSms,
|
||||
markCustomerFulfillmentOpened,
|
||||
pollCustomerLogin,
|
||||
pollCustomerWeiboQrLogin,
|
||||
sendCustomerHuyaSmsCode,
|
||||
sendCustomerWeiboSmsCode,
|
||||
startCustomerLogin,
|
||||
startCustomerWeiboQrLogin,
|
||||
verifyCustomerHuyaSmsChallenge,
|
||||
type CustomerHuyaChallengeSolution,
|
||||
type CustomerHuyaSmsChallenge,
|
||||
} from '@/services/customer-fulfillment'
|
||||
import type { CustomerLoginInfo, CustomerSemiAutoTask } from '@/types/customer-fulfillment'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { runWeiboYidunChallenge } from '@/utils/weibo-yidun'
|
||||
import HuyaChallengeModal from './HuyaChallengeModal'
|
||||
|
||||
const POLL_INTERVAL_MS = 2000
|
||||
|
||||
@@ -43,12 +49,15 @@ export default function CustomerFulfillmentPage() {
|
||||
const [smsLogging, setSmsLogging] = useState(false)
|
||||
const [smsCountdown, setSmsCountdown] = useState(0)
|
||||
const [smsMfaRequired, setSmsMfaRequired] = useState(false)
|
||||
const [huyaChallenge, setHuyaChallenge] = useState<CustomerHuyaSmsChallenge | null>(null)
|
||||
const [huyaChallengeLoading, setHuyaChallengeLoading] = useState(false)
|
||||
|
||||
const selectedTask = useMemo(
|
||||
() => tasks.find((task) => task.id === selectedTaskId) || tasks[0] || null,
|
||||
[selectedTaskId, tasks],
|
||||
)
|
||||
const isWeibo = selectedTask?.platform === 'weibo'
|
||||
const isHuya = selectedTask?.platform === 'huya'
|
||||
|
||||
const load = useCallback(
|
||||
async (silent = false) => {
|
||||
@@ -83,6 +92,16 @@ export default function CustomerFulfillmentPage() {
|
||||
void load()
|
||||
}, [load])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask) return
|
||||
setLoginMode('sms')
|
||||
setMobile('')
|
||||
setSmsCode('')
|
||||
setSmsMfaRequired(false)
|
||||
setSmsCountdown(0)
|
||||
setHuyaChallenge(null)
|
||||
}, [selectedTask?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask) return
|
||||
void markCustomerFulfillmentOpened(orderNo, selectedTask.id).catch(() => undefined)
|
||||
@@ -109,6 +128,7 @@ export default function CustomerFulfillmentPage() {
|
||||
setSmsCode('')
|
||||
setSmsMfaRequired(false)
|
||||
setSmsCountdown(0)
|
||||
setHuyaChallenge(null)
|
||||
}, [selectedTask?.id, selectedTask?.loginStatus])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -177,66 +197,83 @@ export default function CustomerFulfillmentPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSendWeiboSms() {
|
||||
async function handleSendSms() {
|
||||
if (!selectedTask) return
|
||||
const phone = mobile.trim()
|
||||
if (!isValidWeiboMobile(phone)) {
|
||||
if (!isValidMainlandMobile(phone)) {
|
||||
message.warning('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
setSmsSending(true)
|
||||
try {
|
||||
let response = await sendCustomerWeiboSmsCode(orderNo, selectedTask.id, phone, '86')
|
||||
if (response.data.mfaRequired) {
|
||||
setSmsMfaRequired(true)
|
||||
if (!response.data.mfaId || !response.data.captchaId) {
|
||||
throw new Error('微博返回的安全验证参数不完整')
|
||||
}
|
||||
await runWeiboYidunChallenge(response.data.mfaId, response.data.captchaId)
|
||||
response = await sendCustomerWeiboSmsCode(
|
||||
orderNo,
|
||||
selectedTask.id,
|
||||
phone,
|
||||
'86',
|
||||
response.data.mfaId,
|
||||
)
|
||||
if (selectedTask.platform === 'weibo') {
|
||||
let response = await sendCustomerWeiboSmsCode(orderNo, selectedTask.id, phone, '86')
|
||||
if (response.data.mfaRequired) {
|
||||
throw new Error('微博安全验证未通过,请重新获取验证码')
|
||||
setSmsMfaRequired(true)
|
||||
if (!response.data.mfaId || !response.data.captchaId) {
|
||||
throw new Error('微博返回的安全验证参数不完整')
|
||||
}
|
||||
await runWeiboYidunChallenge(response.data.mfaId, response.data.captchaId)
|
||||
response = await sendCustomerWeiboSmsCode(
|
||||
orderNo,
|
||||
selectedTask.id,
|
||||
phone,
|
||||
'86',
|
||||
response.data.mfaId,
|
||||
)
|
||||
if (response.data.mfaRequired) {
|
||||
throw new Error('微博安全验证未通过,请重新获取验证码')
|
||||
}
|
||||
}
|
||||
message.success(response.data.message || '验证码已发送,请注意查收')
|
||||
} else {
|
||||
const response = await sendCustomerHuyaSmsCode(orderNo, selectedTask.id, phone)
|
||||
if (response.data.challenge) {
|
||||
setHuyaChallenge(response.data.challenge)
|
||||
message.info('请先完成滑块安全验证')
|
||||
return
|
||||
}
|
||||
message.success(response.data.message || '验证码已发送,请注意查收')
|
||||
}
|
||||
message.success(response.data.message || '验证码已发送,请注意查收')
|
||||
setSmsMfaRequired(false)
|
||||
setSmsCountdown(60)
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '发送微博验证码失败')
|
||||
message.error(error instanceof Error ? error.message : '发送验证码失败')
|
||||
} finally {
|
||||
setSmsSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoginWeiboSms() {
|
||||
async function handleLoginSms() {
|
||||
if (!selectedTask) return
|
||||
const phone = mobile.trim()
|
||||
if (!isValidWeiboMobile(phone)) {
|
||||
if (!isValidMainlandMobile(phone)) {
|
||||
message.warning('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
if (!smsCode.trim()) {
|
||||
message.warning('请输入微博短信验证码')
|
||||
message.warning('请输入短信验证码')
|
||||
return
|
||||
}
|
||||
setSmsLogging(true)
|
||||
try {
|
||||
const response = await loginCustomerWeiboSms(orderNo, selectedTask.id, {
|
||||
mobile: phone,
|
||||
code: smsCode.trim(),
|
||||
countryCode: '86',
|
||||
})
|
||||
const response =
|
||||
selectedTask.platform === 'weibo'
|
||||
? await loginCustomerWeiboSms(orderNo, selectedTask.id, {
|
||||
mobile: phone,
|
||||
code: smsCode.trim(),
|
||||
countryCode: '86',
|
||||
})
|
||||
: await loginCustomerHuyaSms(orderNo, selectedTask.id, {
|
||||
mobile: phone,
|
||||
code: smsCode.trim(),
|
||||
})
|
||||
setTasks((current) =>
|
||||
current.map((task) => (task.id === response.data.task.id ? response.data.task : task)),
|
||||
)
|
||||
setLoginInfo(response.data.login)
|
||||
message.success('微博登录成功,等待客服处理')
|
||||
setHuyaChallenge(null)
|
||||
message.success(`${isWeibo ? '微博' : '虎牙'}登录成功,等待客服处理`)
|
||||
} catch (error) {
|
||||
const errorCode = (error as { errorCode?: string } | null)?.errorCode || ''
|
||||
if (errorCode === 'weibo_sms_mfa_required') {
|
||||
@@ -244,12 +281,32 @@ export default function CustomerFulfillmentPage() {
|
||||
message.warning('微博要求二次验证,请改用扫码登录')
|
||||
return
|
||||
}
|
||||
message.error(error instanceof Error ? error.message : '微博短信登录失败')
|
||||
message.error(error instanceof Error ? error.message : '短信登录失败')
|
||||
} finally {
|
||||
setSmsLogging(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVerifyHuyaChallenge(solution: CustomerHuyaChallengeSolution) {
|
||||
if (!selectedTask || !huyaChallenge) return
|
||||
setHuyaChallengeLoading(true)
|
||||
try {
|
||||
const response = await verifyCustomerHuyaSmsChallenge(orderNo, selectedTask.id, solution)
|
||||
if (response.data.challenge) {
|
||||
setHuyaChallenge(response.data.challenge)
|
||||
message.warning('安全验证未通过,请重新完成')
|
||||
return
|
||||
}
|
||||
setHuyaChallenge(null)
|
||||
setSmsCountdown(60)
|
||||
message.success(response.data.message || '验证码已发送,请注意查收')
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '虎牙安全验证失败')
|
||||
} finally {
|
||||
setHuyaChallengeLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const config = selectedTask?.semiAutoConfig || {}
|
||||
const displayTitle = isWeibo ? '微博账号登录授权' : '虎牙账号登录授权'
|
||||
const loginQrVisible =
|
||||
@@ -332,96 +389,88 @@ export default function CustomerFulfillmentPage() {
|
||||
selectedTask?.loginStatus === 'failed' ||
|
||||
selectedTask?.loginStatus === 'expired' ? (
|
||||
<section className="customer-auth-section">
|
||||
{isWeibo ? (
|
||||
<Tabs
|
||||
className="customer-auth-tabs"
|
||||
activeKey={loginMode}
|
||||
onChange={(key) => setLoginMode(key as 'sms' | 'qr')}
|
||||
items={[
|
||||
{
|
||||
key: 'sms',
|
||||
label: '手机号登录',
|
||||
children: (
|
||||
<div className="customer-auth-form">
|
||||
<div className="customer-auth-field customer-auth-phone-field">
|
||||
<span className="customer-auth-country-prefix">+86</span>
|
||||
<Input
|
||||
value={mobile}
|
||||
onChange={(event) => setMobile(event.target.value)}
|
||||
placeholder="请输入微博绑定手机号"
|
||||
maxLength={11}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
<Button
|
||||
className="customer-auth-send-code"
|
||||
type="primary"
|
||||
loading={smsSending}
|
||||
disabled={smsCountdown > 0}
|
||||
onClick={() => void handleSendWeiboSms()}
|
||||
>
|
||||
{smsCountdown > 0 ? `${smsCountdown}s` : '发送验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
{smsMfaRequired ? (
|
||||
<div className="customer-auth-note">
|
||||
请在安全验证弹窗中完成验证,通过后会继续发送短信验证码。
|
||||
</div>
|
||||
) : null}
|
||||
<div className="customer-auth-field customer-auth-code-field">
|
||||
<Input
|
||||
value={smsCode}
|
||||
onChange={(event) => setSmsCode(event.target.value)}
|
||||
placeholder="请输入短信验证码"
|
||||
maxLength={8}
|
||||
prefix={<SafetyCertificateOutlined />}
|
||||
onPressEnter={() => void handleLoginWeiboSms()}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
className="customer-auth-code-submit"
|
||||
loading={smsLogging}
|
||||
onClick={() => void handleLoginWeiboSms()}
|
||||
>
|
||||
登录微博
|
||||
</Button>
|
||||
<Tabs
|
||||
className="customer-auth-tabs"
|
||||
activeKey={loginMode}
|
||||
onChange={(key) => setLoginMode(key as 'sms' | 'qr')}
|
||||
items={[
|
||||
{
|
||||
key: 'sms',
|
||||
label: '手机号登录',
|
||||
children: (
|
||||
<div className="customer-auth-form">
|
||||
<div className="customer-auth-field customer-auth-phone-field">
|
||||
<span className="customer-auth-country-prefix">+86</span>
|
||||
<Input
|
||||
value={mobile}
|
||||
onChange={(event) => setMobile(event.target.value)}
|
||||
placeholder={`请输入${isHuya ? '虎牙' : '微博'}绑定手机号`}
|
||||
maxLength={11}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
<Button
|
||||
className="customer-auth-send-code"
|
||||
type="primary"
|
||||
loading={smsSending}
|
||||
disabled={smsCountdown > 0}
|
||||
onClick={() => void handleSendSms()}
|
||||
>
|
||||
{smsCountdown > 0 ? `${smsCountdown}s` : '发送验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
{isWeibo && smsMfaRequired ? (
|
||||
<div className="customer-auth-note">
|
||||
请在安全验证弹窗中完成验证,通过后会继续发送短信验证码。
|
||||
</div>
|
||||
) : null}
|
||||
<div className="customer-auth-field customer-auth-code-field">
|
||||
<Input
|
||||
value={smsCode}
|
||||
onChange={(event) => setSmsCode(event.target.value)}
|
||||
placeholder="请输入短信验证码"
|
||||
maxLength={8}
|
||||
prefix={<SafetyCertificateOutlined />}
|
||||
onPressEnter={() => void handleLoginSms()}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
className="customer-auth-code-submit"
|
||||
loading={smsLogging}
|
||||
onClick={() => void handleLoginSms()}
|
||||
>
|
||||
登录{isHuya ? '虎牙' : '微博'}
|
||||
</Button>
|
||||
</div>
|
||||
{isWeibo ? (
|
||||
<span className="customer-auth-form-hint">
|
||||
如安全验证弹窗未出现,请检查浏览器是否拦截脚本。
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'qr',
|
||||
label: '扫码登录',
|
||||
children: (
|
||||
<div className="customer-auth-form">
|
||||
<p className="customer-auth-form-copy">
|
||||
点击下方按钮后,使用微博 App 扫码并确认登录。
|
||||
</p>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
onClick={() => void handleStartLogin()}
|
||||
>
|
||||
获取登录二维码
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<div className="customer-auth-form">
|
||||
<p className="customer-auth-form-copy">
|
||||
{config.loginInstructions || '使用虎牙 App 扫码登录,请确认扫码账号属于本人。'}
|
||||
</p>
|
||||
<Button type="primary" size="large" block onClick={() => void handleStartLogin()}>
|
||||
获取登录二维码
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'qr',
|
||||
label: '扫码登录',
|
||||
children: (
|
||||
<div className="customer-auth-form">
|
||||
<p className="customer-auth-form-copy">
|
||||
{config.loginInstructions ||
|
||||
`点击下方按钮后,使用${isHuya ? '虎牙' : '微博'} App 扫码并确认登录。`}
|
||||
</p>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
onClick={() => void handleStartLogin()}
|
||||
>
|
||||
获取登录二维码
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
@@ -486,6 +535,16 @@ export default function CustomerFulfillmentPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<HuyaChallengeModal
|
||||
open={Boolean(huyaChallenge)}
|
||||
challenge={huyaChallenge}
|
||||
loading={huyaChallengeLoading}
|
||||
onCancel={() => {
|
||||
if (!huyaChallengeLoading) setHuyaChallenge(null)
|
||||
}}
|
||||
onSubmit={(solution) => void handleVerifyHuyaChallenge(solution)}
|
||||
/>
|
||||
|
||||
<CustomerPrivacyNote />
|
||||
</div>
|
||||
</CustomerAuthFrame>
|
||||
@@ -576,7 +635,7 @@ function CustomerPrivacyNote() {
|
||||
)
|
||||
}
|
||||
|
||||
function isValidWeiboMobile(value: string) {
|
||||
function isValidMainlandMobile(value: string) {
|
||||
const digits = value.replace(/\D/g, '')
|
||||
if (!digits) return false
|
||||
return /^1\d{10}$/.test(digits)
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { Button, Modal, Slider } from 'antd'
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
} from 'react'
|
||||
|
||||
import type {
|
||||
CustomerHuyaChallengeSolution,
|
||||
CustomerHuyaSmsChallenge,
|
||||
} from '@/services/customer-fulfillment'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
challenge: CustomerHuyaSmsChallenge | null
|
||||
loading: boolean
|
||||
onCancel: () => void
|
||||
onSubmit: (solution: CustomerHuyaChallengeSolution) => void
|
||||
}
|
||||
|
||||
const IMAGE_LEFT = 10
|
||||
|
||||
export default function HuyaChallengeModal(input: Props) {
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [angle, setAngle] = useState(0)
|
||||
const [clickPoints, setClickPoints] = useState<Array<{ x: number; y: number }>>([])
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const dragRef = useRef({ startX: 0, startOffset: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
if (!input.open) return
|
||||
setOffset(0)
|
||||
setAngle(0)
|
||||
setClickPoints([])
|
||||
setDragging(false)
|
||||
const updateWidth = () => {
|
||||
setContainerWidth(containerRef.current?.clientWidth || 0)
|
||||
}
|
||||
updateWidth()
|
||||
const observer = new ResizeObserver(updateWidth)
|
||||
if (containerRef.current) observer.observe(containerRef.current)
|
||||
window.addEventListener('resize', updateWidth)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
window.removeEventListener('resize', updateWidth)
|
||||
}
|
||||
}, [input.open, input.challenge?.image, input.challenge?.type])
|
||||
|
||||
const challenge = input.challenge
|
||||
const scale = challenge && containerWidth > 0 ? containerWidth / challenge.width : 1
|
||||
const maxOffset = useMemo(() => {
|
||||
if (!challenge || challenge.type !== 'slider') return 0
|
||||
return Math.max(0, (challenge.width - IMAGE_LEFT - Math.max(1, challenge.puzzleWidth)) * scale)
|
||||
}, [challenge, scale])
|
||||
const distance = Math.round(IMAGE_LEFT + offset / Math.max(scale, 0.0001) || 0)
|
||||
const clickLimit = challenge?.type === 'click' ? Math.max(1, challenge.wordSize) : 0
|
||||
const canSubmit =
|
||||
challenge?.type === 'slider'
|
||||
? offset >= 1
|
||||
: challenge?.type === 'click'
|
||||
? clickPoints.length > 0
|
||||
: Boolean(challenge)
|
||||
|
||||
function handlePointerDown(event: ReactPointerEvent<HTMLButtonElement>) {
|
||||
if (input.loading) return
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
dragRef.current = { startX: event.clientX, startOffset: offset }
|
||||
setDragging(true)
|
||||
}
|
||||
|
||||
function handlePointerMove(event: ReactPointerEvent<HTMLButtonElement>) {
|
||||
if (!dragging || input.loading) return
|
||||
const next = dragRef.current.startOffset + event.clientX - dragRef.current.startX
|
||||
setOffset(Math.max(0, Math.min(maxOffset, next)))
|
||||
}
|
||||
|
||||
function finishDrag(event: ReactPointerEvent<HTMLButtonElement>) {
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
setDragging(false)
|
||||
}
|
||||
|
||||
function handleStageClick(event: ReactMouseEvent<HTMLDivElement>) {
|
||||
if (!challenge || challenge.type !== 'click' || input.loading) return
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
const x = Math.max(0, Math.min(challenge.width, (event.clientX - rect.left) / scale))
|
||||
const y = Math.max(0, Math.min(challenge.height, (event.clientY - rect.top) / scale))
|
||||
setClickPoints((current) => {
|
||||
if (current.length >= clickLimit) return [{ x, y }]
|
||||
return [...current, { x, y }]
|
||||
})
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!challenge) return
|
||||
if (challenge.type === 'slider') {
|
||||
input.onSubmit({ distance })
|
||||
return
|
||||
}
|
||||
if (challenge.type === 'rotate') {
|
||||
input.onSubmit({ angle })
|
||||
return
|
||||
}
|
||||
input.onSubmit({ points: clickPoints })
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={input.open}
|
||||
title="完成虎牙安全验证"
|
||||
width={430}
|
||||
centered
|
||||
maskClosable={false}
|
||||
keyboard={!input.loading}
|
||||
closable={!input.loading}
|
||||
onCancel={input.onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" disabled={input.loading} onClick={input.onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={input.loading}
|
||||
disabled={!challenge || !canSubmit}
|
||||
onClick={submit}
|
||||
>
|
||||
完成验证并发送验证码
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div className="huya-slider-challenge">
|
||||
{challenge?.type === 'slider' ? <p>拖动下方滑块,将拼图对齐到缺口后发送验证码。</p> : null}
|
||||
{challenge?.type === 'rotate' ? (
|
||||
<p>拖动角度滑块,把图片方向调整正确后发送验证码。</p>
|
||||
) : null}
|
||||
{challenge?.type === 'click' ? (
|
||||
<p>
|
||||
{challenge.question || `请按顺序点击图中的 ${clickLimit} 个目标`}
|
||||
<span className="huya-challenge-progress">
|
||||
已选择 {clickPoints.length}/{clickLimit}
|
||||
</span>
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{challenge ? (
|
||||
<>
|
||||
{challenge.type === 'click' && challenge.tipImage ? (
|
||||
<img className="huya-click-tip" src={challenge.tipImage} alt="" draggable={false} />
|
||||
) : null}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`huya-slider-stage ${challenge.type === 'click' ? 'is-click' : ''}`}
|
||||
style={{ aspectRatio: `${challenge.width} / ${challenge.height}` }}
|
||||
onClick={handleStageClick}
|
||||
>
|
||||
<img
|
||||
className="huya-slider-background"
|
||||
src={challenge.image}
|
||||
alt=""
|
||||
draggable={false}
|
||||
/>
|
||||
{challenge.type === 'slider' ? (
|
||||
<img
|
||||
className="huya-slider-puzzle"
|
||||
src={challenge.puzzleImage}
|
||||
alt=""
|
||||
draggable={false}
|
||||
style={{
|
||||
left: IMAGE_LEFT * scale + offset,
|
||||
top: challenge.puzzleY * scale,
|
||||
width: Math.max(1, challenge.puzzleWidth) * scale,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{challenge.type === 'rotate' ? (
|
||||
<img
|
||||
className="huya-slider-puzzle is-rotate"
|
||||
src={challenge.puzzleImage}
|
||||
alt=""
|
||||
draggable={false}
|
||||
style={{
|
||||
left: challenge.puzzleX * scale,
|
||||
top: challenge.puzzleY * scale,
|
||||
width: challenge.puzzleWidth * scale,
|
||||
height: challenge.puzzleHeight * scale,
|
||||
transform: `rotate(${angle}deg)`,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{challenge.type === 'click'
|
||||
? clickPoints.map((point, index) => (
|
||||
<span
|
||||
key={`${point.x}-${point.y}-${index}`}
|
||||
className="huya-click-point"
|
||||
style={{ left: point.x * scale, top: point.y * scale }}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{challenge.type === 'slider' ? (
|
||||
<div className="huya-slider-track">
|
||||
<div className="huya-slider-track-fill" style={{ width: offset + 28 }} />
|
||||
<button
|
||||
type="button"
|
||||
className="huya-slider-handle"
|
||||
style={{ transform: `translateX(${offset}px)` }}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={finishDrag}
|
||||
onPointerCancel={finishDrag}
|
||||
>
|
||||
拖动
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{challenge.type === 'rotate' ? (
|
||||
<div className="huya-rotate-control">
|
||||
<Slider min={0} max={359} value={angle} onChange={setAngle} />
|
||||
<span>{angle}°</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{challenge.type === 'click' && clickPoints.length > 0 ? (
|
||||
<Button size="small" onClick={() => setClickPoints([])}>
|
||||
重新点选
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user