点“关闭任务”,就会达到你要的效果:链接失效,预占释放。
This commit is contained in:
@@ -20,5 +20,5 @@ TENCENT_SESSION_DEBUG=false
|
|||||||
ADMIN_SESSION_SECRET=xm4BDpbdHJvU4RIlpGqD0nvQTZtIidvQMk6LSP1d
|
ADMIN_SESSION_SECRET=xm4BDpbdHJvU4RIlpGqD0nvQTZtIidvQMk6LSP1d
|
||||||
ADMIN_DEFAULT_USERS_JSON=[{"username":"admin","password":"Mzl5iG6pkdrzLs73COiB","role":"admin"},{"username":"operator","password":"d9tvmbPPMRdLySEhORzk","role":"operator"}]
|
ADMIN_DEFAULT_USERS_JSON=[{"username":"admin","password":"Mzl5iG6pkdrzLs73COiB","role":"admin"},{"username":"operator","password":"d9tvmbPPMRdLySEhORzk","role":"operator"}]
|
||||||
|
|
||||||
AGISO_APP_SECRET=9fcb0f688aab713f9af3e85d29ba8b15
|
AGISO_APP_SECRET=tccxk5c7ppy7xpr43rvastceyydskfha
|
||||||
AGISO_MESSAGING_ENABLED=false
|
AGISO_MESSAGING_ENABLED=false
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { confirmClaimRoleForAdminTask, redeemClaimTaskForAdminTask } from '../cl
|
|||||||
import { reserveInventoryForTask } from '../order/inventory-service.js'
|
import { reserveInventoryForTask } from '../order/inventory-service.js'
|
||||||
import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js'
|
import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js'
|
||||||
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
|
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
|
||||||
|
import { closeTencentBrowserSession } from '../session/session.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import {
|
import {
|
||||||
@@ -394,7 +395,25 @@ export async function redeemAdminTaskAssisted(taskId, session = null) {
|
|||||||
/** @returns {Promise<AdminTaskActionResponse>} */
|
/** @returns {Promise<AdminTaskActionResponse>} */
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
export async function closeAdminTask(taskId) {
|
export async function closeAdminTask(taskId) {
|
||||||
const task = await getRequiredTask(taskId)
|
return closeAdminTaskWithDeps(taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {Promise<AdminTaskActionResponse>} */
|
||||||
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
|
export async function closeAdminTaskWithDeps(
|
||||||
|
taskId,
|
||||||
|
{
|
||||||
|
getRequiredTask: getTask = getRequiredTask,
|
||||||
|
listTaskInventoryBindingsByTaskId: listBindings = listTaskInventoryBindingsByTaskId,
|
||||||
|
releaseReservedInventoryItem: releaseReserved = releaseReservedInventoryItem,
|
||||||
|
updateClaimToken: updateToken = updateClaimToken,
|
||||||
|
updateTask: updateTaskRecord = updateTask,
|
||||||
|
createTaskEvent: createEvent = createTaskEvent,
|
||||||
|
closeTencentBrowserSession: closeSession = closeTencentBrowserSession,
|
||||||
|
nowIso: getNowIso = nowIso,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const task = await getTask(taskId)
|
||||||
|
|
||||||
if (task.task_status === 'redeemed') {
|
if (task.task_status === 'redeemed') {
|
||||||
throw createHttpError('已兑换任务不能关闭', {
|
throw createHttpError('已兑换任务不能关闭', {
|
||||||
@@ -403,12 +422,62 @@ export async function closeAdminTask(taskId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedTask = await updateTask(task.id, {
|
const now = getNowIso()
|
||||||
task_status: 'closed',
|
const bindings = await listBindings(task.id)
|
||||||
delivery_status: 'closed',
|
const reservedBindings = bindings.filter((binding) => String(binding.binding_status || '').trim() === 'reserved')
|
||||||
last_error: task.last_error || '已手动关闭任务',
|
const releasedInventoryItemIds = Array.from(new Set(reservedBindings.map((binding) => Number(binding.inventory_item_id)).filter((id) => id > 0)))
|
||||||
updated_at: nowIso(),
|
const hasConsumedBindings = bindings.some((binding) => String(binding.binding_status || '').trim() === 'consumed')
|
||||||
|
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
|
||||||
|
let browserSessionClosed = false
|
||||||
|
|
||||||
|
if (primaryClaimTokenId) {
|
||||||
|
await updateToken(primaryClaimTokenId, {
|
||||||
|
status: 'revoked',
|
||||||
|
expired_at: now,
|
||||||
|
updated_at: now,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.browser_session_id) {
|
||||||
|
try {
|
||||||
|
await closeSession(task.browser_session_id)
|
||||||
|
browserSessionClosed = true
|
||||||
|
} catch (error) {
|
||||||
|
if (!isRecoverableTaskSessionCloseError(error)) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const inventoryItemId of releasedInventoryItemIds) {
|
||||||
|
await releaseReserved(inventoryItemId, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeReasonParts = ['已手动关闭任务']
|
||||||
|
if (primaryClaimTokenId) {
|
||||||
|
closeReasonParts.push('领取链接已失效')
|
||||||
|
}
|
||||||
|
if (releasedInventoryItemIds.length > 0) {
|
||||||
|
closeReasonParts.push('预占库存已释放')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTask = await updateTaskRecord(task.id, {
|
||||||
|
task_status: 'closed',
|
||||||
|
inventory_status: hasConsumedBindings ? 'consumed' : 'pending',
|
||||||
|
delivery_status: 'closed',
|
||||||
|
user_action_status: 'closed',
|
||||||
|
claim_expires_at: primaryClaimTokenId ? now : task.claim_expires_at,
|
||||||
|
browser_session_id: '',
|
||||||
|
last_error: task.last_error || closeReasonParts.join(','),
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
await createEvent(task.id, 'task_closed', {
|
||||||
|
claimTokenRevoked: Boolean(primaryClaimTokenId),
|
||||||
|
releasedInventoryItemIds,
|
||||||
|
releasedInventoryCount: releasedInventoryItemIds.length,
|
||||||
|
browserSessionClosed,
|
||||||
|
}, now)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
task: mapTaskActionPayload(updatedTask),
|
task: mapTaskActionPayload(updatedTask),
|
||||||
@@ -697,6 +766,11 @@ function ensureViewerCanOperateAssistedTask(task, viewerContext, action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRecoverableTaskSessionCloseError(error) {
|
||||||
|
const errorCode = String(error?.errorCode || error?.code || '').trim()
|
||||||
|
return errorCode === 'session_not_found' || errorCode === 'session_closed'
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeManualDispatchOutcome(value) {
|
function normalizeManualDispatchOutcome(value) {
|
||||||
const normalized = String(value || '').trim().toLowerCase()
|
const normalized = String(value || '').trim().toLowerCase()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
import { closeAdminTaskWithDeps } from './admin-write-service.js'
|
||||||
|
|
||||||
|
test('closeAdminTaskWithDeps revokes claim link, releases reserved inventory, and closes browser session', async () => {
|
||||||
|
const calls = {
|
||||||
|
updateToken: [],
|
||||||
|
releaseReserved: [],
|
||||||
|
closeSession: [],
|
||||||
|
updateTask: [],
|
||||||
|
createEvent: [],
|
||||||
|
}
|
||||||
|
const task = {
|
||||||
|
id: 30,
|
||||||
|
task_status: 'link_generated',
|
||||||
|
delivery_status: 'pending',
|
||||||
|
inventory_status: 'reserved',
|
||||||
|
user_action_status: 'pending_claim',
|
||||||
|
browser_session_id: 'browser-session-30',
|
||||||
|
primary_claim_token_id: 9,
|
||||||
|
claim_expires_at: '2026-04-15T10:00:00.000Z',
|
||||||
|
last_error: '',
|
||||||
|
updated_at: '2026-04-14T10:00:00.000Z',
|
||||||
|
}
|
||||||
|
const now = '2026-04-14T10:16:18.000Z'
|
||||||
|
|
||||||
|
const result = await closeAdminTaskWithDeps(task.id, {
|
||||||
|
getRequiredTask: async () => task,
|
||||||
|
listTaskInventoryBindingsByTaskId: async () => ([
|
||||||
|
{ inventory_item_id: 17, binding_status: 'reserved' },
|
||||||
|
{ inventory_item_id: 18, binding_status: 'released' },
|
||||||
|
{ inventory_item_id: 17, binding_status: 'reserved' },
|
||||||
|
]),
|
||||||
|
releaseReservedInventoryItem: async (inventoryItemId, updatedAt) => {
|
||||||
|
calls.releaseReserved.push({ inventoryItemId, updatedAt })
|
||||||
|
return { id: inventoryItemId }
|
||||||
|
},
|
||||||
|
updateClaimToken: async (tokenId, patch) => {
|
||||||
|
calls.updateToken.push({ tokenId, patch })
|
||||||
|
return { id: tokenId, ...patch }
|
||||||
|
},
|
||||||
|
closeTencentBrowserSession: async (sessionId) => {
|
||||||
|
calls.closeSession.push(sessionId)
|
||||||
|
return { sessionId, closed: true }
|
||||||
|
},
|
||||||
|
updateTask: async (taskId, patch) => {
|
||||||
|
calls.updateTask.push({ taskId, patch })
|
||||||
|
return { ...task, ...patch }
|
||||||
|
},
|
||||||
|
createTaskEvent: async (taskId, eventType, payload, createdAt) => {
|
||||||
|
calls.createEvent.push({ taskId, eventType, payload, createdAt })
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
nowIso: () => now,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(calls.updateToken.length, 1)
|
||||||
|
assert.deepEqual(calls.updateToken[0], {
|
||||||
|
tokenId: 9,
|
||||||
|
patch: {
|
||||||
|
status: 'revoked',
|
||||||
|
expired_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.deepEqual(calls.releaseReserved, [
|
||||||
|
{ inventoryItemId: 17, updatedAt: now },
|
||||||
|
])
|
||||||
|
assert.deepEqual(calls.closeSession, ['browser-session-30'])
|
||||||
|
assert.equal(calls.updateTask.length, 1)
|
||||||
|
assert.equal(calls.updateTask[0].patch.task_status, 'closed')
|
||||||
|
assert.equal(calls.updateTask[0].patch.delivery_status, 'closed')
|
||||||
|
assert.equal(calls.updateTask[0].patch.inventory_status, 'pending')
|
||||||
|
assert.equal(calls.updateTask[0].patch.user_action_status, 'closed')
|
||||||
|
assert.equal(calls.updateTask[0].patch.browser_session_id, '')
|
||||||
|
assert.equal(calls.updateTask[0].patch.claim_expires_at, now)
|
||||||
|
assert.match(calls.updateTask[0].patch.last_error, /领取链接已失效/)
|
||||||
|
assert.match(calls.updateTask[0].patch.last_error, /预占库存已释放/)
|
||||||
|
assert.equal(calls.createEvent.length, 1)
|
||||||
|
assert.deepEqual(calls.createEvent[0], {
|
||||||
|
taskId: 30,
|
||||||
|
eventType: 'task_closed',
|
||||||
|
payload: {
|
||||||
|
claimTokenRevoked: true,
|
||||||
|
releasedInventoryItemIds: [17],
|
||||||
|
releasedInventoryCount: 1,
|
||||||
|
browserSessionClosed: true,
|
||||||
|
},
|
||||||
|
createdAt: now,
|
||||||
|
})
|
||||||
|
assert.equal(result.task.status, 'closed')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('closeAdminTaskWithDeps rejects redeemed tasks', async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
() => closeAdminTaskWithDeps(99, {
|
||||||
|
getRequiredTask: async () => ({
|
||||||
|
id: 99,
|
||||||
|
task_status: 'redeemed',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
/已兑换任务不能关闭/,
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -73,14 +73,17 @@ export async function upsertOrderFromWebhook(event) {
|
|||||||
rawPayloadJson: JSON.stringify(event.rawPayload),
|
rawPayloadJson: JSON.stringify(event.rawPayload),
|
||||||
paidAt: event.paidAt,
|
paidAt: event.paidAt,
|
||||||
}
|
}
|
||||||
|
const mergedPayload = mergeWebhookOrderState(existing, basePayload)
|
||||||
|
|
||||||
const order = existing
|
const order = existing
|
||||||
? await updateOrder(existing.id, {
|
? await updateOrder(existing.id, {
|
||||||
...basePayload,
|
...basePayload,
|
||||||
|
...mergedPayload,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
: await createOrder({
|
: await createOrder({
|
||||||
...basePayload,
|
...basePayload,
|
||||||
|
...mergedPayload,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
@@ -147,3 +150,90 @@ export async function upsertOrderFromWebhook(event) {
|
|||||||
messageDeliveries,
|
messageDeliveries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ORDER_STATUS_PRIORITY = {
|
||||||
|
created: 0,
|
||||||
|
paid: 1,
|
||||||
|
closed: 2,
|
||||||
|
refunded: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAY_STATUS_PRIORITY = {
|
||||||
|
unpaid: 0,
|
||||||
|
failed: 0,
|
||||||
|
paid: 1,
|
||||||
|
refunded: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeWebhookOrderState(existing, incoming) {
|
||||||
|
const orderStatus = chooseHigherPriorityStatus(
|
||||||
|
normalizeStateValue(existing?.order_status ?? existing?.orderStatus),
|
||||||
|
normalizeStateValue(incoming?.orderStatus ?? incoming?.order_status),
|
||||||
|
ORDER_STATUS_PRIORITY,
|
||||||
|
) || normalizeStateValue(incoming?.orderStatus ?? incoming?.order_status) || 'created'
|
||||||
|
|
||||||
|
const payStatus = chooseHigherPriorityStatus(
|
||||||
|
normalizeStateValue(existing?.pay_status ?? existing?.payStatus),
|
||||||
|
normalizeStateValue(incoming?.payStatus ?? incoming?.pay_status),
|
||||||
|
PAY_STATUS_PRIORITY,
|
||||||
|
) || normalizeStateValue(incoming?.payStatus ?? incoming?.pay_status) || 'unpaid'
|
||||||
|
|
||||||
|
return {
|
||||||
|
orderStatus,
|
||||||
|
payStatus,
|
||||||
|
paidAt: resolveMergedPaidAt({
|
||||||
|
existingPaidAt: existing?.paid_at ?? existing?.paidAt ?? null,
|
||||||
|
incomingPaidAt: incoming?.paidAt ?? incoming?.paid_at ?? null,
|
||||||
|
payStatus,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseHigherPriorityStatus(existingValue, incomingValue, priorityMap) {
|
||||||
|
const existingPriority = resolveStatusPriority(existingValue, priorityMap)
|
||||||
|
const incomingPriority = resolveStatusPriority(incomingValue, priorityMap)
|
||||||
|
|
||||||
|
if (incomingPriority > existingPriority) {
|
||||||
|
return incomingValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingPriority >= incomingPriority) {
|
||||||
|
return existingValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return incomingValue || existingValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStatusPriority(value, priorityMap) {
|
||||||
|
return Number(priorityMap[normalizeStateValue(value)] ?? -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStateValue(value) {
|
||||||
|
return String(value || '').trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMergedPaidAt({ existingPaidAt, incomingPaidAt, payStatus }) {
|
||||||
|
if (!['paid', 'refunded'].includes(normalizeStateValue(payStatus))) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingTime = parseDateValue(existingPaidAt)
|
||||||
|
const incomingTime = parseDateValue(incomingPaidAt)
|
||||||
|
|
||||||
|
if (existingTime && incomingTime) {
|
||||||
|
return existingTime <= incomingTime ? existingPaidAt : incomingPaidAt
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingPaidAt || incomingPaidAt || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateValue(value) {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.parse(text)
|
||||||
|
return Number.isFinite(timestamp) ? timestamp : null
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
import { mergeWebhookOrderState } from './order-service.js'
|
||||||
|
|
||||||
|
test('mergeWebhookOrderState keeps paid state when a late trade_create webhook arrives', () => {
|
||||||
|
const merged = mergeWebhookOrderState(
|
||||||
|
{
|
||||||
|
order_status: 'paid',
|
||||||
|
pay_status: 'paid',
|
||||||
|
paid_at: '2026-04-14T10:06:10.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orderStatus: 'created',
|
||||||
|
payStatus: 'unpaid',
|
||||||
|
paidAt: null,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(merged.orderStatus, 'paid')
|
||||||
|
assert.equal(merged.payStatus, 'paid')
|
||||||
|
assert.equal(merged.paidAt, '2026-04-14T10:06:10.000Z')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('mergeWebhookOrderState upgrades unpaid orders when payment webhook arrives later', () => {
|
||||||
|
const merged = mergeWebhookOrderState(
|
||||||
|
{
|
||||||
|
order_status: 'created',
|
||||||
|
pay_status: 'unpaid',
|
||||||
|
paid_at: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orderStatus: 'paid',
|
||||||
|
payStatus: 'paid',
|
||||||
|
paidAt: '2026-04-14T10:06:12.000Z',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(merged.orderStatus, 'paid')
|
||||||
|
assert.equal(merged.payStatus, 'paid')
|
||||||
|
assert.equal(merged.paidAt, '2026-04-14T10:06:12.000Z')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('mergeWebhookOrderState preserves refunded status over older paid webhook', () => {
|
||||||
|
const merged = mergeWebhookOrderState(
|
||||||
|
{
|
||||||
|
order_status: 'refunded',
|
||||||
|
pay_status: 'refunded',
|
||||||
|
paid_at: '2026-04-14T10:06:10.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orderStatus: 'paid',
|
||||||
|
payStatus: 'paid',
|
||||||
|
paidAt: '2026-04-14T10:06:09.000Z',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(merged.orderStatus, 'refunded')
|
||||||
|
assert.equal(merged.payStatus, 'refunded')
|
||||||
|
assert.equal(merged.paidAt, '2026-04-14T10:06:09.000Z')
|
||||||
|
})
|
||||||
@@ -35,7 +35,20 @@ const manualDispatchForm = reactive({
|
|||||||
})
|
})
|
||||||
const canOperateTasks = computed(() => hasAdminRole('operator'))
|
const canOperateTasks = computed(() => hasAdminRole('operator'))
|
||||||
const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData))
|
const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData))
|
||||||
const claimUrl = computed(() => lastClaimUrl.value || detail.value?.claimToken?.claimUrl || '')
|
const claimUrl = computed(() => {
|
||||||
|
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
|
||||||
|
const taskStatus = String(detail.value?.task.status || '').trim()
|
||||||
|
|
||||||
|
if (tokenStatus !== 'active' || ['closed', 'expired'].includes(taskStatus)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastClaimUrl.value || detail.value?.claimToken?.claimUrl || ''
|
||||||
|
})
|
||||||
|
const claimLinkInvalid = computed(() => {
|
||||||
|
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
|
||||||
|
return Boolean(detail.value?.claimToken?.claimUrl) && tokenStatus && tokenStatus !== 'active'
|
||||||
|
})
|
||||||
const screenshotSectionTitle = computed(() => {
|
const screenshotSectionTitle = computed(() => {
|
||||||
if (detail.value?.task.status === 'redeemed') {
|
if (detail.value?.task.status === 'redeemed') {
|
||||||
return '结果截图'
|
return '结果截图'
|
||||||
@@ -286,6 +299,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
领取链接:
|
领取链接:
|
||||||
<a class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
|
<a class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p v-else-if="claimLinkInvalid">领取链接:已失效</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="action-row">
|
<section class="action-row">
|
||||||
@@ -394,6 +408,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
<th>领取链接</th>
|
<th>领取链接</th>
|
||||||
<td>
|
<td>
|
||||||
<a v-if="claimUrl" class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
|
<a v-if="claimUrl" class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
|
||||||
|
<span v-else-if="claimLinkInvalid">已失效</span>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user