可以接收到咸鱼的订单创建连接
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
UPDATE orders
|
||||||
|
SET total_amount = total_amount * 100
|
||||||
|
WHERE total_amount != 0;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
UPDATE orders
|
||||||
|
SET platform = 'xianyu'
|
||||||
|
WHERE lower(trim(platform)) = 'aldsidle';
|
||||||
|
|
||||||
|
UPDATE webhook_events
|
||||||
|
SET platform = 'xianyu'
|
||||||
|
WHERE lower(trim(platform)) = 'aldsidle';
|
||||||
|
|
||||||
|
UPDATE message_deliveries
|
||||||
|
SET platform = 'xianyu'
|
||||||
|
WHERE lower(trim(platform)) = 'aldsidle';
|
||||||
@@ -7,6 +7,7 @@ import { getOrderById, listOrders } from '../repositories/order-repo.js'
|
|||||||
import { getTaskById, listTasks, listTasksByOrderId, updateTask } from '../repositories/task-repo.js'
|
import { getTaskById, listTasks, listTasksByOrderId, updateTask } from '../repositories/task-repo.js'
|
||||||
import { getWebhookEventById, listWebhookEvents, listWebhookEventsByOrderId } from '../repositories/webhook-event-repo.js'
|
import { getWebhookEventById, listWebhookEvents, listWebhookEventsByOrderId } from '../repositories/webhook-event-repo.js'
|
||||||
import { createHttpError } from '../utils/http.js'
|
import { createHttpError } from '../utils/http.js'
|
||||||
|
import { formatFenToAmount, normalizeFen, parseAmountToFen } from '../utils/money.js'
|
||||||
import { nowIso } from '../utils/time.js'
|
import { nowIso } from '../utils/time.js'
|
||||||
import { reserveCdkForTask } from './cdk-service.js'
|
import { reserveCdkForTask } from './cdk-service.js'
|
||||||
import { replayAgisoTradeWebhookEvent } from './webhook-service.js'
|
import { replayAgisoTradeWebhookEvent } from './webhook-service.js'
|
||||||
@@ -80,7 +81,8 @@ export function getAdminOrderDetail(orderId) {
|
|||||||
buyerId: order.buyer_id,
|
buyerId: order.buyer_id,
|
||||||
buyerName: order.buyer_name,
|
buyerName: order.buyer_name,
|
||||||
receiverContact: order.receiver_contact,
|
receiverContact: order.receiver_contact,
|
||||||
totalAmount: order.total_amount,
|
totalAmount: formatFenToAmount(order.total_amount),
|
||||||
|
totalAmountFen: normalizeFen(order.total_amount),
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
paidAt: order.paid_at,
|
paidAt: order.paid_at,
|
||||||
createdAt: order.created_at,
|
createdAt: order.created_at,
|
||||||
@@ -356,20 +358,7 @@ export function getAdminWebhookEvents(query = {}) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: items.map((item) => ({
|
items: items.map((item) => mapAdminWebhookEvent(item)),
|
||||||
eventId: item.id,
|
|
||||||
provider: item.provider || 'agiso',
|
|
||||||
platform: item.platform,
|
|
||||||
shopId: item.shop_id || '',
|
|
||||||
shopName: item.shop_name || '',
|
|
||||||
eventType: item.event_type,
|
|
||||||
eventKey: item.event_key,
|
|
||||||
signatureValid: Boolean(item.signature_valid),
|
|
||||||
processed: Boolean(item.processed),
|
|
||||||
processError: item.process_error,
|
|
||||||
relatedOrderId: item.related_order_id,
|
|
||||||
createdAt: item.created_at,
|
|
||||||
})),
|
|
||||||
pagination: { page, pageSize, total },
|
pagination: { page, pageSize, total },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,23 +373,7 @@ export function getAdminWebhookEventDetail(eventId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return mapAdminWebhookEvent(event, { includeRaw: true })
|
||||||
eventId: event.id,
|
|
||||||
provider: event.provider || 'agiso',
|
|
||||||
platform: event.platform,
|
|
||||||
shopId: event.shop_id || '',
|
|
||||||
shopName: event.shop_name || '',
|
|
||||||
eventType: event.event_type,
|
|
||||||
eventKey: event.event_key,
|
|
||||||
signatureValid: Boolean(event.signature_valid),
|
|
||||||
processed: Boolean(event.processed),
|
|
||||||
processError: event.process_error,
|
|
||||||
relatedOrderId: event.related_order_id,
|
|
||||||
createdAt: event.created_at,
|
|
||||||
headers: safeParseJson(event.headers_json),
|
|
||||||
query: safeParseJson(event.query_json),
|
|
||||||
body: safeParseJson(event.body_json),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function replayAdminWebhookEvent(eventId) {
|
export async function replayAdminWebhookEvent(eventId) {
|
||||||
@@ -607,6 +580,130 @@ function mapAdminTaskSummary(task) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapAdminWebhookEvent(item, { includeRaw = false } = {}) {
|
||||||
|
const headers = normalizeRecord(safeParseJson(item.headers_json))
|
||||||
|
const query = normalizeRecord(safeParseJson(item.query_json))
|
||||||
|
const body = normalizeRecord(safeParseJson(item.body_json))
|
||||||
|
const payload = extractWebhookPayload(body)
|
||||||
|
const itemSources = extractWebhookItemSources(payload)
|
||||||
|
const firstItem = itemSources[0] || {}
|
||||||
|
const rawAmount = pickFirstNonEmpty([
|
||||||
|
payload.total_fee,
|
||||||
|
payload.totalFee,
|
||||||
|
payload.TotalFee,
|
||||||
|
payload.pay_fee,
|
||||||
|
payload.payFee,
|
||||||
|
payload.Payment,
|
||||||
|
payload.payment,
|
||||||
|
payload.buy_amount,
|
||||||
|
payload.buyAmount,
|
||||||
|
])
|
||||||
|
const totalAmountFen = parseAmountToFen(rawAmount)
|
||||||
|
const taskCount = item.related_order_id ? listTasksByOrderId(item.related_order_id).length : 0
|
||||||
|
|
||||||
|
const mapped = {
|
||||||
|
eventId: item.id,
|
||||||
|
provider: item.provider || 'agiso',
|
||||||
|
platform: item.platform,
|
||||||
|
platformRaw: pickFirstNonEmpty([
|
||||||
|
query.fromPlatform,
|
||||||
|
query.from_platform,
|
||||||
|
body.fromPlatform,
|
||||||
|
body.from_platform,
|
||||||
|
payload.fromPlatform,
|
||||||
|
payload.FromPlatform,
|
||||||
|
payload.platform,
|
||||||
|
payload.Platform,
|
||||||
|
]),
|
||||||
|
shopId: item.shop_id || pickFirstNonEmpty([
|
||||||
|
payload.PlatformUserId,
|
||||||
|
payload.platformUserId,
|
||||||
|
payload.platform_user_id,
|
||||||
|
payload.seller_id,
|
||||||
|
payload.sellerId,
|
||||||
|
payload.SellerId,
|
||||||
|
payload.seller_nick,
|
||||||
|
payload.sellerNick,
|
||||||
|
payload.SellerNick,
|
||||||
|
]),
|
||||||
|
shopName: item.shop_name || pickFirstNonEmpty([
|
||||||
|
payload.shopName,
|
||||||
|
payload.shop_name,
|
||||||
|
payload.ShopName,
|
||||||
|
payload.seller_nick,
|
||||||
|
payload.sellerNick,
|
||||||
|
payload.SellerNick,
|
||||||
|
]),
|
||||||
|
eventType: item.event_type,
|
||||||
|
eventKey: item.event_key,
|
||||||
|
signatureValid: Boolean(item.signature_valid),
|
||||||
|
processed: Boolean(item.processed),
|
||||||
|
processError: item.process_error,
|
||||||
|
relatedOrderId: item.related_order_id,
|
||||||
|
createdAt: item.created_at,
|
||||||
|
platformOrderId: pickFirstNonEmpty([
|
||||||
|
payload.biz_order_id,
|
||||||
|
payload.bizOrderId,
|
||||||
|
payload.Tid,
|
||||||
|
payload.tid,
|
||||||
|
payload.Oid,
|
||||||
|
payload.oid,
|
||||||
|
payload.order_id,
|
||||||
|
payload.orderId,
|
||||||
|
firstItem.Oid,
|
||||||
|
firstItem.oid,
|
||||||
|
]),
|
||||||
|
buyerId: pickFirstNonEmpty([
|
||||||
|
payload.buyer_id,
|
||||||
|
payload.buyerId,
|
||||||
|
payload.BuyerId,
|
||||||
|
payload.openid,
|
||||||
|
payload.BuyerOpenUid,
|
||||||
|
payload.buyer_open_uid,
|
||||||
|
payload.buyerOpenUid,
|
||||||
|
]),
|
||||||
|
buyerName: pickFirstNonEmpty([
|
||||||
|
payload.buyer_name,
|
||||||
|
payload.buyerName,
|
||||||
|
payload.BuyerName,
|
||||||
|
payload.nick,
|
||||||
|
payload.BuyerNick,
|
||||||
|
payload.buyer_nick,
|
||||||
|
payload.buyerNick,
|
||||||
|
]),
|
||||||
|
totalAmount: rawAmount ? formatFenToAmount(totalAmountFen) : '',
|
||||||
|
totalAmountFen,
|
||||||
|
currency: pickFirstNonEmpty([payload.currency, payload.Currency, 'CNY']) || 'CNY',
|
||||||
|
itemCount: itemSources.length,
|
||||||
|
taskCount,
|
||||||
|
messageText: pickFirstNonEmpty([
|
||||||
|
payload.MsgTypeDes,
|
||||||
|
payload.msgTypeDes,
|
||||||
|
payload.msg_type_desc,
|
||||||
|
payload.message,
|
||||||
|
payload.msg,
|
||||||
|
]),
|
||||||
|
requestTimestamp: pickFirstNonEmpty([query.timestamp]),
|
||||||
|
requestSign: pickFirstNonEmpty([query.sign]),
|
||||||
|
aopic: pickFirstNonEmpty([query.aopic]),
|
||||||
|
sourceHost: pickFirstNonEmpty([headers['x-forwarded-host'], headers.host]),
|
||||||
|
sourceIp: pickFirstNonEmpty([headers['x-forwarded-for'], headers['x-real-ip']]),
|
||||||
|
userAgent: pickFirstNonEmpty([headers['user-agent'], headers['User-Agent']]),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!includeRaw) {
|
||||||
|
return mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mapped,
|
||||||
|
headers,
|
||||||
|
query,
|
||||||
|
body,
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mapAdminCdkListItem(item) {
|
function mapAdminCdkListItem(item) {
|
||||||
const task = item.reserved_by_task_id ? getTaskById(item.reserved_by_task_id) : null
|
const task = item.reserved_by_task_id ? getTaskById(item.reserved_by_task_id) : null
|
||||||
const order = task?.order_id ? getOrderById(task.order_id) : null
|
const order = task?.order_id ? getOrderById(task.order_id) : null
|
||||||
@@ -711,7 +808,8 @@ function mapAdminOrderListItem(item) {
|
|||||||
orderStatus: item.order_status,
|
orderStatus: item.order_status,
|
||||||
payStatus: item.pay_status,
|
payStatus: item.pay_status,
|
||||||
buyerName: item.buyer_name,
|
buyerName: item.buyer_name,
|
||||||
totalAmount: item.total_amount,
|
totalAmount: formatFenToAmount(item.total_amount),
|
||||||
|
totalAmountFen: normalizeFen(item.total_amount),
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
createdAt: item.created_at,
|
createdAt: item.created_at,
|
||||||
updatedAt: item.updated_at,
|
updatedAt: item.updated_at,
|
||||||
@@ -932,6 +1030,67 @@ function safeParseJson(rawText) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRecord(value) {
|
||||||
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractWebhookPayload(body) {
|
||||||
|
const normalizedBody = normalizeRecord(body)
|
||||||
|
const rawJson = String(normalizedBody.json || normalizedBody.JSON || '').trim()
|
||||||
|
|
||||||
|
if (rawJson) {
|
||||||
|
return normalizeRecord(safeParseJson(rawJson))
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedBody
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractWebhookItemSources(payload) {
|
||||||
|
const normalizedPayload = normalizeRecord(payload)
|
||||||
|
const candidates = [
|
||||||
|
normalizedPayload.orders,
|
||||||
|
normalizedPayload.Orders,
|
||||||
|
normalizedPayload.items,
|
||||||
|
normalizedPayload.Items,
|
||||||
|
normalizedPayload.order_items,
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (Array.isArray(candidate) && candidate.length > 0) {
|
||||||
|
return candidate.map((item) => normalizeRecord(item)).filter((item) => Object.keys(item).length > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pickFirstNonEmpty([
|
||||||
|
normalizedPayload.item_id,
|
||||||
|
normalizedPayload.itemId,
|
||||||
|
normalizedPayload.ItemId,
|
||||||
|
normalizedPayload.num_iid,
|
||||||
|
normalizedPayload.numIid,
|
||||||
|
normalizedPayload.NumIid,
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
return [normalizedPayload]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFirstNonEmpty(values) {
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
return value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
function maskCode(value) {
|
function maskCode(value) {
|
||||||
const text = String(value || '').trim()
|
const text = String(value || '').trim()
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { findTaskByClaimTokenId, updateTask } from '../repositories/task-repo.js
|
|||||||
import { getCdkById, markCdkDelivered, releaseReservedCdk } from '../repositories/cdk-repo.js'
|
import { getCdkById, markCdkDelivered, releaseReservedCdk } from '../repositories/cdk-repo.js'
|
||||||
import { buildClaimUrl } from './claim-service.js'
|
import { buildClaimUrl } from './claim-service.js'
|
||||||
import { createHttpError } from '../utils/http.js'
|
import { createHttpError } from '../utils/http.js'
|
||||||
|
import { formatFenToAmount, normalizeFen } from '../utils/money.js'
|
||||||
import { nowIso } from '../utils/time.js'
|
import { nowIso } from '../utils/time.js'
|
||||||
|
|
||||||
const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed'])
|
const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed'])
|
||||||
@@ -385,7 +386,8 @@ function buildClaimDetailPayload({ claimToken, task, order, orderItem, session }
|
|||||||
platformOrderId: order.platform_order_id,
|
platformOrderId: order.platform_order_id,
|
||||||
payStatus: order.pay_status,
|
payStatus: order.pay_status,
|
||||||
orderStatus: order.order_status,
|
orderStatus: order.order_status,
|
||||||
totalAmount: order.total_amount,
|
totalAmount: formatFenToAmount(order.total_amount),
|
||||||
|
totalAmountFen: normalizeFen(order.total_amount),
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
},
|
},
|
||||||
orderItem: {
|
orderItem: {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createWebhookEvent, updateWebhookEvent } from '../repositories/webhook-
|
|||||||
import { upsertOrderFromWebhook } from './order-service.js'
|
import { upsertOrderFromWebhook } from './order-service.js'
|
||||||
import { createHttpError } from '../utils/http.js'
|
import { createHttpError } from '../utils/http.js'
|
||||||
import { logWebhook } from '../utils/logger.js'
|
import { logWebhook } from '../utils/logger.js'
|
||||||
|
import { parseAmountToFen } from '../utils/money.js'
|
||||||
import { nowIso } from '../utils/time.js'
|
import { nowIso } from '../utils/time.js'
|
||||||
|
|
||||||
export async function processAgisoTradeWebhook(requestLike) {
|
export async function processAgisoTradeWebhook(requestLike) {
|
||||||
@@ -229,7 +230,7 @@ function parseAgisoTradeRequest(requestLike) {
|
|||||||
payload.receiver_mobile,
|
payload.receiver_mobile,
|
||||||
payload.receiverMobile,
|
payload.receiverMobile,
|
||||||
]),
|
]),
|
||||||
totalAmount: normalizeInteger(
|
totalAmount: parseAmountToFen(
|
||||||
pickFirstNonEmpty([
|
pickFirstNonEmpty([
|
||||||
payload.total_fee,
|
payload.total_fee,
|
||||||
payload.totalFee,
|
payload.totalFee,
|
||||||
@@ -524,6 +525,7 @@ function resolveBusinessPlatform(rawPlatform) {
|
|||||||
xianyu: 'xianyu',
|
xianyu: 'xianyu',
|
||||||
idlefish: 'xianyu',
|
idlefish: 'xianyu',
|
||||||
idle: 'xianyu',
|
idle: 'xianyu',
|
||||||
|
aldsidle: 'xianyu',
|
||||||
taobaoidle: 'xianyu',
|
taobaoidle: 'xianyu',
|
||||||
taobaoxianyu: 'xianyu',
|
taobaoxianyu: 'xianyu',
|
||||||
taobao: 'taobao',
|
taobao: 'taobao',
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
export function parseAmountToFen(value) {
|
||||||
|
const normalized = normalizeAmountInput(value)
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const sign = normalized.startsWith('-') ? -1 : 1
|
||||||
|
const unsigned = sign < 0 ? normalized.slice(1) : normalized
|
||||||
|
const match = unsigned.match(/^(\d+)(?:\.(\d+))?$/)
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const integerPart = Number(match[1] || '0')
|
||||||
|
|
||||||
|
if (!Number.isSafeInteger(integerPart)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const fractionRaw = String(match[2] || '')
|
||||||
|
const fractionForRound = `${fractionRaw}000`
|
||||||
|
let cents = integerPart * 100 + Number(fractionForRound.slice(0, 2))
|
||||||
|
|
||||||
|
if (Number(fractionForRound[2] || '0') >= 5) {
|
||||||
|
cents += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return cents * sign
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFenToAmount(value) {
|
||||||
|
const normalizedFen = normalizeFen(value)
|
||||||
|
const sign = normalizedFen < 0 ? '-' : ''
|
||||||
|
const absoluteFen = Math.abs(normalizedFen)
|
||||||
|
const integerPart = Math.floor(absoluteFen / 100)
|
||||||
|
const fractionPart = String(absoluteFen % 100).padStart(2, '0')
|
||||||
|
return `${sign}${integerPart}.${fractionPart}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeFen(value) {
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) ? Math.round(parsed) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAmountInput(value) {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.trim().replace(/,/g, '')
|
||||||
|
}
|
||||||
@@ -64,7 +64,8 @@ export interface AdminOrderListItem {
|
|||||||
orderStatus: string
|
orderStatus: string
|
||||||
payStatus: string
|
payStatus: string
|
||||||
buyerName: string
|
buyerName: string
|
||||||
totalAmount: number
|
totalAmount: string
|
||||||
|
totalAmountFen: number
|
||||||
currency: string
|
currency: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
@@ -124,7 +125,8 @@ export interface AdminOrderDetail {
|
|||||||
buyerId: string
|
buyerId: string
|
||||||
buyerName: string
|
buyerName: string
|
||||||
receiverContact: string
|
receiverContact: string
|
||||||
totalAmount: number
|
totalAmount: string
|
||||||
|
totalAmountFen: number
|
||||||
currency: string
|
currency: string
|
||||||
paidAt: string | null
|
paidAt: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
@@ -225,13 +227,32 @@ export interface AdminCdkListItem {
|
|||||||
|
|
||||||
export interface AdminWebhookEventListItem {
|
export interface AdminWebhookEventListItem {
|
||||||
eventId: number
|
eventId: number
|
||||||
|
provider: string
|
||||||
platform: string
|
platform: string
|
||||||
|
platformRaw: string
|
||||||
|
shopId: string
|
||||||
|
shopName: string
|
||||||
eventType: string
|
eventType: string
|
||||||
eventKey: string
|
eventKey: string
|
||||||
signatureValid: boolean
|
signatureValid: boolean
|
||||||
processed: boolean
|
processed: boolean
|
||||||
processError: string
|
processError: string
|
||||||
relatedOrderId: number | null
|
relatedOrderId: number | null
|
||||||
|
platformOrderId: string
|
||||||
|
buyerId: string
|
||||||
|
buyerName: string
|
||||||
|
totalAmount: string
|
||||||
|
totalAmountFen: number
|
||||||
|
currency: string
|
||||||
|
itemCount: number
|
||||||
|
taskCount: number
|
||||||
|
messageText: string
|
||||||
|
requestTimestamp: string
|
||||||
|
requestSign: string
|
||||||
|
aopic: string
|
||||||
|
sourceHost: string
|
||||||
|
sourceIp: string
|
||||||
|
userAgent: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +260,7 @@ export interface AdminWebhookEventDetail extends AdminWebhookEventListItem {
|
|||||||
headers: Record<string, unknown>
|
headers: Record<string, unknown>
|
||||||
query: Record<string, unknown>
|
query: Record<string, unknown>
|
||||||
body: Record<string, unknown>
|
body: Record<string, unknown>
|
||||||
|
payload: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminWebhookReplayResponse {
|
export interface AdminWebhookReplayResponse {
|
||||||
@@ -246,6 +268,11 @@ export interface AdminWebhookReplayResponse {
|
|||||||
replayed: boolean
|
replayed: boolean
|
||||||
result: {
|
result: {
|
||||||
accepted: boolean
|
accepted: boolean
|
||||||
|
provider: string
|
||||||
|
platform: string
|
||||||
|
platformRaw: string
|
||||||
|
shopId: string
|
||||||
|
shopName: string
|
||||||
eventType: string
|
eventType: string
|
||||||
platformOrderId: string
|
platformOrderId: string
|
||||||
orderId: number
|
orderId: number
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export interface ClaimOrderInfo {
|
|||||||
platformOrderId: string
|
platformOrderId: string
|
||||||
payStatus: string
|
payStatus: string
|
||||||
orderStatus: string
|
orderStatus: string
|
||||||
totalAmount: number
|
totalAmount: string
|
||||||
|
totalAmountFen: number
|
||||||
currency: string
|
currency: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { fetchAdminWebhookEventDetail, replayAdminWebhookEvent } from '@/service
|
|||||||
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
|
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
|
||||||
import type { AdminWebhookEventDetail, AdminWebhookReplayResponse } from '@/types/admin'
|
import type { AdminWebhookEventDetail, AdminWebhookReplayResponse } from '@/types/admin'
|
||||||
import { hasAdminRole } from '@/utils/admin-auth'
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
import { stringifyDisplayJson } from '@/utils/date-time'
|
import { stringifyDisplayJson } from '@/utils/date-time'
|
||||||
import { formatAdminWebhookEventType } from '@/utils/admin-webhook'
|
import { formatAdminWebhookEventType } from '@/utils/admin-webhook'
|
||||||
|
|
||||||
@@ -68,6 +69,21 @@ function prettyJson(value: Record<string, unknown>) {
|
|||||||
return stringifyDisplayJson(value, 2)
|
return stringifyDisplayJson(value, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRequestTimestamp(value: string) {
|
||||||
|
const normalized = String(value || '').trim()
|
||||||
|
|
||||||
|
if (!/^\d{10,13}$/.test(normalized)) {
|
||||||
|
return normalized || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const epochMs = normalized.length === 13 ? Number(normalized) : Number(normalized) * 1000
|
||||||
|
return formatAdminDateTime(new Date(epochMs).toISOString())
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(detail: AdminWebhookEventDetail) {
|
||||||
|
return detail.totalAmount ? `${detail.totalAmount} ${detail.currency}` : '-'
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(loadDetail)
|
onMounted(loadDetail)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -86,22 +102,86 @@ onMounted(loadDetail)
|
|||||||
|
|
||||||
<template v-else-if="detail">
|
<template v-else-if="detail">
|
||||||
<section class="info-card">
|
<section class="info-card">
|
||||||
<p>ID:{{ detail.eventId }} · 平台:{{ detail.platform }} · 类型:{{ formatAdminWebhookEventType(detail.eventType) }}</p>
|
<div class="overview-top">
|
||||||
<p>
|
<div>
|
||||||
验签:<AdminStatusTag :status="detail.signatureValid ? 'success' : 'failed'" />
|
<h3>{{ formatAdminWebhookEventType(detail.eventType) }}</h3>
|
||||||
· 订单:{{ detail.relatedOrderId || '-' }}
|
<p class="muted-copy">
|
||||||
</p>
|
#{{ detail.eventId }} · {{ detail.provider }} / {{ detail.platform }}
|
||||||
<p>
|
<template v-if="detail.platformRaw && detail.platformRaw !== detail.platform">
|
||||||
处理状态:
|
· 原始平台 {{ detail.platformRaw }}
|
||||||
<AdminStatusTag :status="detail.processed ? 'success' : 'failed'" />
|
</template>
|
||||||
<span v-if="!detail.processed && detail.processError" class="inline-error">{{ detail.processError }}</span>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
|
<div class="tag-row">
|
||||||
|
<AdminStatusTag :status="detail.signatureValid ? 'success' : 'failed'" />
|
||||||
|
<AdminStatusTag :status="detail.processed ? 'success' : 'failed'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">店铺</span>
|
||||||
|
<strong>{{ detail.shopName || detail.shopId || '-' }}</strong>
|
||||||
|
<span class="summary-note">ID: {{ detail.shopId || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">平台订单</span>
|
||||||
|
<strong>{{ detail.platformOrderId || '-' }}</strong>
|
||||||
|
<span class="summary-note">aopic: {{ detail.aopic || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">关联订单</span>
|
||||||
|
<strong v-if="detail.relatedOrderId">
|
||||||
|
<RouterLink class="inline-link" :to="`/admin/orders/${detail.relatedOrderId}`">{{ detail.relatedOrderId }}</RouterLink>
|
||||||
|
</strong>
|
||||||
|
<strong v-else>-</strong>
|
||||||
|
<span class="summary-note">任务数: {{ detail.taskCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">买家</span>
|
||||||
|
<strong>{{ detail.buyerName || detail.buyerId || '-' }}</strong>
|
||||||
|
<span class="summary-note">买家 ID: {{ detail.buyerId || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">金额</span>
|
||||||
|
<strong>{{ formatAmount(detail) }}</strong>
|
||||||
|
<span class="summary-note">商品数: {{ detail.itemCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">请求时间</span>
|
||||||
|
<strong>{{ formatRequestTimestamp(detail.requestTimestamp) }}</strong>
|
||||||
|
<span class="summary-note">入库时间: {{ formatAdminDateTime(detail.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">来源 Host</span>
|
||||||
|
<strong>{{ detail.sourceHost || '-' }}</strong>
|
||||||
|
<span class="summary-note">来源 IP: {{ detail.sourceIp || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">消息说明</span>
|
||||||
|
<strong>{{ detail.messageText || '-' }}</strong>
|
||||||
|
<span class="summary-note">签名: {{ detail.requestSign || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="event-key-block">
|
||||||
|
<span class="summary-label">事件 Key</span>
|
||||||
|
<code>{{ detail.eventKey }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="detail.processError" class="inline-error">{{ detail.processError }}</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="lastReplayResult" class="table-card">
|
<section v-if="lastReplayResult" class="table-card">
|
||||||
<h3>最近一次重放结果</h3>
|
<h3>最近一次重放结果</h3>
|
||||||
<p class="result-copy">
|
<p class="result-copy">
|
||||||
平台订单:{{ lastReplayResult.platformOrderId }} · 事件:{{ formatAdminWebhookEventType(lastReplayResult.eventType) }} · 任务数:{{ lastReplayResult.taskCount }}
|
{{ lastReplayResult.provider }} / {{ lastReplayResult.platform }}
|
||||||
|
<template v-if="lastReplayResult.platformRaw && lastReplayResult.platformRaw !== lastReplayResult.platform">
|
||||||
|
· 原始平台 {{ lastReplayResult.platformRaw }}
|
||||||
|
</template>
|
||||||
|
· 平台订单 {{ lastReplayResult.platformOrderId }}
|
||||||
|
· 事件 {{ formatAdminWebhookEventType(lastReplayResult.eventType) }}
|
||||||
|
· 任务数 {{ lastReplayResult.taskCount }}
|
||||||
</p>
|
</p>
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -121,6 +201,11 @@ onMounted(loadDetail)
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="table-card">
|
||||||
|
<h3>解析后的 Payload</h3>
|
||||||
|
<pre class="json-block">{{ prettyJson(detail.payload) }}</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="table-card">
|
<section class="table-card">
|
||||||
<h3>Headers</h3>
|
<h3>Headers</h3>
|
||||||
<pre class="json-block">{{ prettyJson(detail.headers) }}</pre>
|
<pre class="json-block">{{ prettyJson(detail.headers) }}</pre>
|
||||||
@@ -147,11 +232,69 @@ onMounted(loadDetail)
|
|||||||
.info-card,.table-card,.empty-block,.error-copy {
|
.info-card,.table-card,.empty-block,.error-copy {
|
||||||
padding: 18px; border-radius: 20px; background: rgba(255,255,255,.94); border: 1px solid rgba(86,108,138,.1);
|
padding: 18px; border-radius: 20px; background: rgba(255,255,255,.94); border: 1px solid rgba(86,108,138,.1);
|
||||||
}
|
}
|
||||||
|
.overview-top { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; }
|
||||||
|
.overview-top h3 { margin: 0; color: #0f172a; }
|
||||||
|
.muted-copy { margin: 8px 0 0; color: #64748b; }
|
||||||
|
.tag-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.summary-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
}
|
||||||
|
.summary-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.summary-item strong {
|
||||||
|
color: #0f172a;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.summary-note {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.event-key-block {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
}
|
||||||
|
.event-key-block code {
|
||||||
|
color: #334155;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
.error-copy { color: #b42318; }
|
.error-copy { color: #b42318; }
|
||||||
.inline-error { margin-left: 8px; color: #b42318; }
|
.inline-error {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: #b42318;
|
||||||
|
background: #fef3f2;
|
||||||
|
border: 1px solid #fecdca;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
.result-copy { margin: 0 0 12px; color: #64748b; }
|
.result-copy { margin: 0 0 12px; color: #64748b; }
|
||||||
.data-table { width: 100%; border-collapse: collapse; }
|
.data-table { width: 100%; border-collapse: collapse; }
|
||||||
.data-table th,.data-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid rgba(86,108,138,.08); color: #334155; }
|
.data-table th,.data-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid rgba(86,108,138,.08); color: #334155; }
|
||||||
|
.inline-link { color: #175cd3; }
|
||||||
.json-block {
|
.json-block {
|
||||||
margin: 12px 0 0;
|
margin: 12px 0 0;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
@@ -162,4 +305,13 @@ onMounted(loadDetail)
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.overview-top {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const loading = ref(true)
|
|||||||
const actionLoadingId = ref<number | null>(null)
|
const actionLoadingId = ref<number | null>(null)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const items = ref<AdminWebhookEventListItem[]>([])
|
const items = ref<AdminWebhookEventListItem[]>([])
|
||||||
|
const provider = ref('')
|
||||||
const platform = ref('')
|
const platform = ref('')
|
||||||
const processed = ref('')
|
const processed = ref('')
|
||||||
const relatedOrderId = ref('')
|
const relatedOrderId = ref('')
|
||||||
@@ -34,6 +35,7 @@ async function loadEvents(page = pagination.value.page) {
|
|||||||
const response = await fetchAdminWebhookEvents({
|
const response = await fetchAdminWebhookEvents({
|
||||||
page,
|
page,
|
||||||
pageSize: pagination.value.pageSize,
|
pageSize: pagination.value.pageSize,
|
||||||
|
provider: provider.value.trim(),
|
||||||
platform: platform.value.trim(),
|
platform: platform.value.trim(),
|
||||||
processed: processed.value.trim(),
|
processed: processed.value.trim(),
|
||||||
relatedOrderId: relatedOrderId.value.trim(),
|
relatedOrderId: relatedOrderId.value.trim(),
|
||||||
@@ -77,6 +79,21 @@ async function submitReplay(item: AdminWebhookEventListItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRequestTimestamp(value: string) {
|
||||||
|
const normalized = String(value || '').trim()
|
||||||
|
|
||||||
|
if (!/^\d{10,13}$/.test(normalized)) {
|
||||||
|
return normalized || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const epochMs = normalized.length === 13 ? Number(normalized) : Number(normalized) * 1000
|
||||||
|
return formatAdminDateTime(new Date(epochMs).toISOString())
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(item: AdminWebhookEventListItem) {
|
||||||
|
return item.totalAmount ? `${item.totalAmount} ${item.currency}` : '-'
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(loadEvents)
|
onMounted(loadEvents)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -90,7 +107,8 @@ onMounted(loadEvents)
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="filter-bar">
|
<section class="filter-bar">
|
||||||
<input v-model="platform" class="text-input" placeholder="平台,如 agiso" />
|
<input v-model="provider" class="text-input" placeholder="服务商,如 agiso" />
|
||||||
|
<input v-model="platform" class="text-input" placeholder="业务平台,如 xianyu / taobao" />
|
||||||
<select v-model="processed" class="text-input select-input">
|
<select v-model="processed" class="text-input select-input">
|
||||||
<option v-for="option in adminWebhookProcessedOptions" :key="option.value" :value="option.value">
|
<option v-for="option in adminWebhookProcessedOptions" :key="option.value" :value="option.value">
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
@@ -113,11 +131,11 @@ onMounted(loadEvents)
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>平台</th>
|
<th>来源</th>
|
||||||
<th>事件类型</th>
|
<th>事件摘要</th>
|
||||||
<th>验签</th>
|
<th>订单与店铺</th>
|
||||||
<th>处理结果</th>
|
<th>验签与处理</th>
|
||||||
<th>订单</th>
|
<th>请求来源</th>
|
||||||
<th>时间</th>
|
<th>时间</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -127,15 +145,70 @@ onMounted(loadEvents)
|
|||||||
<td>
|
<td>
|
||||||
<RouterLink :to="`/admin/webhook-events/${item.eventId}`">{{ item.eventId }}</RouterLink>
|
<RouterLink :to="`/admin/webhook-events/${item.eventId}`">{{ item.eventId }}</RouterLink>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.platform }}</td>
|
|
||||||
<td>{{ formatAdminWebhookEventType(item.eventType) }}</td>
|
|
||||||
<td><AdminStatusTag :status="item.signatureValid ? 'success' : 'failed'" /></td>
|
|
||||||
<td>
|
<td>
|
||||||
<AdminStatusTag :status="item.processed ? 'success' : 'failed'" />
|
<div class="cell-stack">
|
||||||
<span v-if="!item.processed && item.processError" class="error-inline">{{ item.processError }}</span>
|
<div class="cell-title">{{ item.provider }} / {{ item.platform }}</div>
|
||||||
|
<div v-if="item.platformRaw && item.platformRaw !== item.platform" class="cell-subtle">
|
||||||
|
原始平台:{{ item.platformRaw }}
|
||||||
|
</div>
|
||||||
|
<div class="meta-chip-row">
|
||||||
|
<span class="meta-chip">店铺 {{ item.shopName || item.shopId || '-' }}</span>
|
||||||
|
<span v-if="item.shopId" class="meta-chip">ID {{ item.shopId }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-stack">
|
||||||
|
<div class="cell-title">{{ formatAdminWebhookEventType(item.eventType) }}</div>
|
||||||
|
<div v-if="item.messageText" class="cell-subtle">{{ item.messageText }}</div>
|
||||||
|
<div class="meta-chip-row">
|
||||||
|
<span v-if="item.aopic" class="meta-chip">aopic {{ item.aopic }}</span>
|
||||||
|
<span v-if="item.itemCount" class="meta-chip">商品 {{ item.itemCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mono-inline">Key: {{ item.eventKey }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-stack">
|
||||||
|
<div class="cell-title">平台单:{{ item.platformOrderId || '-' }}</div>
|
||||||
|
<div class="cell-subtle">买家:{{ item.buyerName || item.buyerId || '-' }}</div>
|
||||||
|
<div class="cell-subtle">金额:{{ formatAmount(item) }}</div>
|
||||||
|
<div class="meta-chip-row">
|
||||||
|
<span class="meta-chip">
|
||||||
|
关联订单
|
||||||
|
<template v-if="item.relatedOrderId">
|
||||||
|
<RouterLink class="inline-link" :to="`/admin/orders/${item.relatedOrderId}`">{{ item.relatedOrderId }}</RouterLink>
|
||||||
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</span>
|
||||||
|
<span class="meta-chip">任务 {{ item.taskCount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-stack">
|
||||||
|
<div class="tag-row">
|
||||||
|
<AdminStatusTag :status="item.signatureValid ? 'success' : 'failed'" />
|
||||||
|
<AdminStatusTag :status="item.processed ? 'success' : 'failed'" />
|
||||||
|
</div>
|
||||||
|
<div class="cell-subtle">签名:{{ item.requestSign || '-' }}</div>
|
||||||
|
<div v-if="item.processError" class="error-inline">{{ item.processError }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-stack">
|
||||||
|
<div class="cell-title">{{ item.sourceHost || '-' }}</div>
|
||||||
|
<div class="cell-subtle">IP:{{ item.sourceIp || '-' }}</div>
|
||||||
|
<div class="cell-subtle line-clamp-2">UA:{{ item.userAgent || '-' }}</div>
|
||||||
|
<div class="cell-subtle">请求时间:{{ formatRequestTimestamp(item.requestTimestamp) }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cell-stack">
|
||||||
|
<div class="cell-title">{{ formatAdminDateTime(item.createdAt) }}</div>
|
||||||
|
<div class="cell-subtle">事件 ID #{{ item.eventId }}</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.relatedOrderId || '-' }}</td>
|
|
||||||
<td>{{ formatAdminDateTime(item.createdAt) }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="hasAdminRole('admin')"
|
v-if="hasAdminRole('admin')"
|
||||||
@@ -183,6 +256,49 @@ onMounted(loadEvents)
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-title {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-subtle {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-chip-row,
|
||||||
|
.tag-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #475467;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-link {
|
||||||
|
color: #175cd3;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.table-card,
|
.table-card,
|
||||||
.empty-block,
|
.empty-block,
|
||||||
.error-copy {
|
.error-copy {
|
||||||
@@ -198,9 +314,9 @@ onMounted(loadEvents)
|
|||||||
|
|
||||||
.error-inline {
|
.error-inline {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 6px;
|
|
||||||
color: #b42318;
|
color: #b42318;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-input {
|
.text-input {
|
||||||
@@ -234,4 +350,18 @@ onMounted(loadEvents)
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mono-inline {
|
||||||
|
color: #475467;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user