快手小店-核销--ok

This commit is contained in:
yml
2026-05-03 16:34:39 +08:00
parent d7c61ed20f
commit b568d70e64
17 changed files with 2108 additions and 8 deletions
@@ -5,21 +5,26 @@ import { showError, showSuccess } from '@/lib/feedback'
import {
appointAdminCloudtentaclesVn,
buyAdminCloudtentaclesSku,
consumeAdminKuaishouEticket,
fetchAdminCloudtentaclesAsset,
fetchAdminCloudtentaclesBindUrl,
fetchAdminCloudtentaclesCategories,
fetchAdminCloudtentaclesKnapsack,
fetchAdminAgisoShopConfigs,
fetchAdminCloudtentaclesSourceConfig,
fetchAdminKuaishouEticketSourceConfig,
fetchAdminCloudtentaclesSkuList,
fetchAdminCloudtentaclesVnCode,
fetchAdminCloudtentaclesVnList,
fetchAdminKhhaoSourceConfig,
generateAdminCloudtentaclesVnLoginCode,
queryAdminKuaishouEticketDetail,
queryAdminKuaishouEticketShopInfo,
queryAdminKhhaoOrders,
runAdminCloudtentaclesFullFlow,
saveAdminCloudtentaclesSourceConfig,
saveAdminAgisoShopConfigs,
saveAdminKuaishouEticketSourceConfig,
saveAdminKhhaoSourceConfig,
sendAdminCloudtentaclesSmsCode,
syncAdminKhhaoOrders,
@@ -41,11 +46,16 @@ import type {
AdminKhhaoOrderQueryResult,
AdminKhhaoSyncState,
AdminKhhaoOrderSyncResult,
AdminKuaishouEticketShopConfigItem,
AdminKuaishouEticketShopInfoResult,
AdminKuaishouEticketConsumeResult,
AdminKuaishouEticketDetailResult,
AdminKuaishouEticketSourceConfig,
} from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
type PlatformTab = 'agiso' | 'khhao' | 'cloudtentacles'
type PlatformTab = 'agiso' | 'khhao' | 'kuaishouEticket' | 'cloudtentacles'
type EditableDefaults = {
messageTemplate: string
@@ -62,6 +72,15 @@ type EditableShop = {
autoDeliveryMessageTemplate: string
}
type EditableKuaishouEticketShop = {
id: string
shopId: string
kshopName: string
cookie: string
userAvatar: string
enabled: boolean
}
const loading = ref(true)
const isDevMode = import.meta.env.DEV
const saving = ref(false)
@@ -73,6 +92,7 @@ const activePlatform = ref<PlatformTab>('agiso')
const agisoFilePath = ref('')
const khhaoFilePath = ref('')
const khhaoStateFilePath = ref('')
const kuaishouEticketFilePath = ref('')
const cloudtentaclesFilePath = ref('')
const cloudtentaclesSessionFilePath = ref('')
const defaults = ref<EditableDefaults>(createEmptyDefaults())
@@ -100,6 +120,26 @@ const khhaoLoginResult = ref<AdminKhhaoLoginTestResult | null>(null)
const khhaoQueryResult = ref<AdminKhhaoOrderQueryResult | null>(null)
const khhaoSyncResult = ref<AdminKhhaoOrderSyncResult | null>(null)
const khhaoAutoSyncState = ref<AdminKhhaoSyncState | null>(null)
const kuaishouEticketShops = ref<EditableKuaishouEticketShop[]>([])
const expandedKuaishouEticketShopIds = ref<string[]>([])
const kuaishouEticketForm = ref({
baseUrl: 'https://s.kwaixiaodian.com',
selectedShopId: '',
queryTicketCode: '',
eTicketId: '',
oid: '',
formToken: '',
num: 1,
storeId: '0',
})
const kuaishouEticketSaving = ref(false)
const kuaishouEticketResolvingShopId = ref('')
const kuaishouEticketDetailing = ref(false)
const kuaishouEticketConsuming = ref(false)
const kuaishouEticketResultError = ref('')
const kuaishouEticketShopInfoResult = ref<AdminKuaishouEticketShopInfoResult | null>(null)
const kuaishouEticketDetailResult = ref<AdminKuaishouEticketDetailResult | null>(null)
const kuaishouEticketConsumeResult = ref<AdminKuaishouEticketConsumeResult | null>(null)
const cloudtentaclesForm = ref({
baseUrl: 'https://123.207.217.176',
username: '',
@@ -138,6 +178,18 @@ const khhaoStats = computed(() => ({
ignoredCount: khhaoAutoSyncState.value?.ignoredCount || khhaoSyncResult.value?.ignoredCount || 0,
}))
const activeKuaishouEticketShop = computed(() => (
kuaishouEticketShops.value.find((item) => item.id === kuaishouEticketForm.value.selectedShopId) || null
))
const kuaishouEticketStats = computed(() => ({
configuredShopCount: kuaishouEticketShops.value.length,
cookieReadyCount: kuaishouEticketShops.value.filter((item) => item.cookie.trim()).length,
selectedShopReady: Boolean(activeKuaishouEticketShop.value?.cookie.trim()),
detailReady: Boolean(kuaishouEticketDetailResult.value?.detail),
consumed: Boolean(kuaishouEticketConsumeResult.value?.consumed),
}))
const cloudtentaclesStats = computed(() => ({
hasCredential: Boolean(
cloudtentaclesForm.value.username.trim()
@@ -186,6 +238,28 @@ function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop {
}
}
function createEmptyKuaishouEticketShop(): EditableKuaishouEticketShop {
return {
id: crypto.randomUUID(),
shopId: '',
kshopName: '',
cookie: '',
userAvatar: '',
enabled: true,
}
}
function mapEditableKuaishouEticketShop(item: AdminKuaishouEticketShopConfigItem): EditableKuaishouEticketShop {
return {
id: crypto.randomUUID(),
shopId: item.shopId,
kshopName: item.kshopName,
cookie: item.cookie,
userAvatar: item.userAvatar,
enabled: item.enabled !== false,
}
}
async function loadConfigs() {
if (!hasAdminRole('admin')) {
loading.value = false
@@ -196,9 +270,10 @@ async function loadConfigs() {
errorMessage.value = ''
try {
const [agisoResponse, khhaoResponse, cloudtentaclesResponse] = await Promise.all([
const [agisoResponse, khhaoResponse, kuaishouEticketResponse, cloudtentaclesResponse] = await Promise.all([
fetchAdminAgisoShopConfigs(),
fetchAdminKhhaoSourceConfig(),
fetchAdminKuaishouEticketSourceConfig(),
fetchAdminCloudtentaclesSourceConfig(),
])
@@ -224,6 +299,21 @@ async function loadConfigs() {
autoSyncPageSize: khhaoResponse.data.source.autoSync?.pageSize || 20,
}
kuaishouEticketFilePath.value = kuaishouEticketResponse.data.filePath
kuaishouEticketShops.value = kuaishouEticketResponse.data.source.shops.map(mapEditableKuaishouEticketShop)
kuaishouEticketForm.value = {
baseUrl: kuaishouEticketResponse.data.source.baseUrl || 'https://s.kwaixiaodian.com',
selectedShopId: '',
queryTicketCode: '',
eTicketId: '',
oid: '',
formToken: '',
num: 1,
storeId: '0',
}
kuaishouEticketForm.value.selectedShopId = kuaishouEticketShops.value[0]?.id || ''
expandedKuaishouEticketShopIds.value = kuaishouEticketShops.value[0]?.id ? [kuaishouEticketShops.value[0].id] : []
cloudtentaclesFilePath.value = cloudtentaclesResponse.data.filePath
cloudtentaclesSessionFilePath.value = cloudtentaclesResponse.data.sessionFilePath
cloudtentaclesPersistedSession.value = cloudtentaclesResponse.data.session
@@ -468,6 +558,256 @@ async function handleKhhaoSyncOrders() {
}
}
function buildKuaishouEticketConfigPayload(): AdminKuaishouEticketSourceConfig {
return {
enabled: true,
baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com',
shops: kuaishouEticketShops.value.map((item) => ({
shopId: item.shopId.trim(),
kshopName: item.kshopName.trim(),
cookie: item.cookie.trim(),
cookieMasked: '',
hasCookie: Boolean(item.cookie.trim()),
userAvatar: item.userAvatar.trim(),
enabled: item.enabled,
})),
}
}
function addKuaishouEticketShop() {
const shop = createEmptyKuaishouEticketShop()
kuaishouEticketShops.value.unshift(shop)
setKuaishouEticketDebugShop(shop.id)
}
function removeKuaishouEticketShop(id: string) {
kuaishouEticketShops.value = kuaishouEticketShops.value.filter((item) => item.id !== id)
collapseKuaishouEticketShop(id)
if (kuaishouEticketForm.value.selectedShopId === id) {
kuaishouEticketForm.value.selectedShopId = kuaishouEticketShops.value[0]?.id || ''
}
}
function isKuaishouEticketShopExpanded(id: string) {
return expandedKuaishouEticketShopIds.value.includes(id)
}
function expandKuaishouEticketShop(id: string) {
if (expandedKuaishouEticketShopIds.value.includes(id)) {
return
}
expandedKuaishouEticketShopIds.value = [id, ...expandedKuaishouEticketShopIds.value]
}
function collapseKuaishouEticketShop(id: string) {
expandedKuaishouEticketShopIds.value = expandedKuaishouEticketShopIds.value.filter((item) => item !== id)
}
function toggleKuaishouEticketShop(id: string) {
if (isKuaishouEticketShopExpanded(id)) {
collapseKuaishouEticketShop(id)
return
}
expandKuaishouEticketShop(id)
}
function expandAllKuaishouEticketShops() {
expandedKuaishouEticketShopIds.value = kuaishouEticketShops.value.map((item) => item.id)
}
function collapseAllKuaishouEticketShops() {
expandedKuaishouEticketShopIds.value = []
}
function setKuaishouEticketDebugShop(id: string) {
kuaishouEticketForm.value.selectedShopId = id
expandKuaishouEticketShop(id)
}
function describeKuaishouEticketShop(shop: EditableKuaishouEticketShop) {
const status = shop.cookie.trim() ? 'Cookie 已就绪' : '待补 Cookie'
const identity = shop.shopId.trim() ? '已识别店铺' : '待读取信息'
return `${status} · ${identity}`
}
function getKuaishouEticketShopInitial(shop: EditableKuaishouEticketShop) {
const label = shop.kshopName.trim() || shop.shopId.trim() || '店'
return label.slice(0, 1).toUpperCase()
}
function ensureSelectedKuaishouEticketShop() {
const selected = activeKuaishouEticketShop.value
if (!selected) {
kuaishouEticketResultError.value = '请先新增并选择一个店铺配置'
return null
}
if (!selected.cookie.trim()) {
kuaishouEticketResultError.value = '当前店铺缺少 Cookie,请先补全后再操作'
return null
}
return selected
}
function ensureKuaishouEticketQueryFields() {
const selected = ensureSelectedKuaishouEticketShop()
if (!selected) return false
if (!kuaishouEticketForm.value.queryTicketCode.trim()) {
kuaishouEticketResultError.value = '请先填写查询券码'
return false
}
return true
}
function ensureKuaishouEticketConsumeFields() {
const selected = ensureSelectedKuaishouEticketShop()
if (!selected) return false
if (!kuaishouEticketDetailResult.value?.detail) {
kuaishouEticketResultError.value = '请先查询核销信息,系统会自动带出 eTicketId、oid 和 formToken'
return false
}
return true
}
function applyKuaishouEticketDetailToForm(result: AdminKuaishouEticketDetailResult | null) {
if (!result?.detail) {
return
}
kuaishouEticketForm.value.eTicketId = result.detail.eTicketId || kuaishouEticketForm.value.eTicketId
kuaishouEticketForm.value.oid = result.detail.oid || kuaishouEticketForm.value.oid
kuaishouEticketForm.value.formToken = result.detail.formToken || kuaishouEticketForm.value.formToken
const leftCount = Number(result.detail.leftCount || 0)
if (leftCount > 0) {
kuaishouEticketForm.value.num = leftCount
}
}
async function handleKuaishouEticketResolveShopInfo(shop: EditableKuaishouEticketShop) {
if (!shop.cookie.trim()) {
kuaishouEticketResultError.value = '请先为该店铺粘贴 Cookie'
showError(kuaishouEticketResultError.value)
return
}
kuaishouEticketResolvingShopId.value = shop.id
kuaishouEticketResultError.value = ''
try {
const response = await queryAdminKuaishouEticketShopInfo({
baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com',
shopId: shop.shopId.trim(),
cookie: shop.cookie.trim(),
})
kuaishouEticketShopInfoResult.value = response.data
shop.shopId = response.data.shop.shopId || shop.shopId
shop.kshopName = response.data.shop.kshopName || shop.kshopName
shop.userAvatar = response.data.shop.userAvatar || shop.userAvatar
if (!kuaishouEticketForm.value.selectedShopId) {
setKuaishouEticketDebugShop(shop.id)
}
showSuccess(`已识别店铺:${response.data.shop.kshopName || response.data.shop.shopId || '未知店铺'}`)
} catch (error) {
kuaishouEticketResultError.value = error instanceof Error ? error.message : '读取快手小店店铺信息失败'
showError(kuaishouEticketResultError.value)
} finally {
kuaishouEticketResolvingShopId.value = ''
}
}
async function handleKuaishouEticketSaveSource() {
kuaishouEticketSaving.value = true
kuaishouEticketResultError.value = ''
try {
const response = await saveAdminKuaishouEticketSourceConfig(buildKuaishouEticketConfigPayload())
kuaishouEticketFilePath.value = response.data.filePath
kuaishouEticketForm.value.baseUrl = response.data.source.baseUrl || 'https://s.kwaixiaodian.com'
const previousSelectedShopId = activeKuaishouEticketShop.value?.shopId || ''
kuaishouEticketShops.value = response.data.source.shops.map(mapEditableKuaishouEticketShop)
kuaishouEticketForm.value.selectedShopId = (
kuaishouEticketShops.value.find((item) => item.shopId && item.shopId === previousSelectedShopId)?.id
|| kuaishouEticketShops.value[0]?.id
|| ''
)
expandedKuaishouEticketShopIds.value = kuaishouEticketForm.value.selectedShopId
? [kuaishouEticketForm.value.selectedShopId]
: []
showSuccess('快手小店核销配置已保存')
} catch (error) {
kuaishouEticketResultError.value = error instanceof Error ? error.message : '快手小店核销配置保存失败'
showError(kuaishouEticketResultError.value)
} finally {
kuaishouEticketSaving.value = false
}
}
async function handleKuaishouEticketQueryDetail() {
if (!ensureKuaishouEticketQueryFields()) {
return
}
const selected = activeKuaishouEticketShop.value
kuaishouEticketDetailing.value = true
kuaishouEticketResultError.value = ''
kuaishouEticketConsumeResult.value = null
try {
const response = await queryAdminKuaishouEticketDetail({
baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com',
shopId: selected?.shopId.trim(),
cookie: selected?.cookie.trim(),
eTicketId: kuaishouEticketForm.value.queryTicketCode.trim(),
})
kuaishouEticketDetailResult.value = response.data
applyKuaishouEticketDetailToForm(response.data)
showSuccess(response.data.ok ? '核销信息查询成功' : (response.data.errorMessage || '核销信息已返回'))
} catch (error) {
kuaishouEticketResultError.value = error instanceof Error ? error.message : '查询快手小店核销信息失败'
showError(kuaishouEticketResultError.value)
} finally {
kuaishouEticketDetailing.value = false
}
}
async function handleKuaishouEticketConsume() {
if (!ensureKuaishouEticketConsumeFields()) {
return
}
const selected = activeKuaishouEticketShop.value
kuaishouEticketConsuming.value = true
kuaishouEticketResultError.value = ''
try {
const detail = kuaishouEticketDetailResult.value?.detail
const response = await consumeAdminKuaishouEticket({
baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com',
shopId: selected?.shopId.trim(),
cookie: selected?.cookie.trim(),
eTicketId: String(detail?.eTicketId || kuaishouEticketForm.value.eTicketId || '').trim(),
oid: String(detail?.oid || kuaishouEticketForm.value.oid || '').trim(),
formToken: String(detail?.formToken || kuaishouEticketForm.value.formToken || '').trim(),
num: Number(kuaishouEticketForm.value.num || 1),
storeId: kuaishouEticketForm.value.storeId.trim() || '0',
})
kuaishouEticketConsumeResult.value = response.data
showSuccess(response.data.ok ? '快手小店核销成功' : (response.data.errorMessage || '核销请求已执行'))
} catch (error) {
kuaishouEticketResultError.value = error instanceof Error ? error.message : '执行快手小店核销失败'
showError(kuaishouEticketResultError.value)
} finally {
kuaishouEticketConsuming.value = false
}
}
function buildCloudtentaclesConfigPayload(): AdminCloudtentaclesSourceConfig {
return {
enabled: true,
@@ -776,7 +1116,7 @@ onMounted(loadConfigs)
<header class="hero-card">
<div class="hero-copy">
<h1>平台配置</h1>
<p>按平台分别管理来源接入凭据与店铺能力Agiso 负责 webhook / 店铺消息配置khhao 负责来源拉单cloudtentacles 负责外部履约平台登录</p>
<p>按平台分别管理来源接入凭据与店铺能力Agiso 负责 webhook / 店铺消息配置khhao 负责来源拉单快手小店核销负责独立券核销调试cloudtentacles 负责外部履约平台登录</p>
</div>
<div class="hero-actions">
<el-button round @click="loadConfigs">刷新全部配置</el-button>
@@ -826,6 +1166,23 @@ onMounted(loadConfigs)
</div>
</button>
<button
type="button"
:class="['platform-overview-card', { 'is-active': activePlatform === 'kuaishouEticket' }]"
@click="switchPlatform('kuaishouEticket')"
>
<div class="platform-head">
<span class="platform-badge">快手小店核销</span>
<span class="platform-tag">Cookie / 券核销</span>
</div>
<strong>{{ kuaishouEticketStats.configuredShopCount }}</strong>
<span>已配置店铺</span>
<div class="platform-metrics">
<span>Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} </span>
<span>当前店铺 {{ kuaishouEticketStats.selectedShopReady ? '可调试' : '待补全' }}</span>
</div>
</button>
<button
type="button"
:class="['platform-overview-card', { 'is-active': activePlatform === 'cloudtentacles' }]"
@@ -860,6 +1217,13 @@ onMounted(loadConfigs)
>
khhao 来源与同步
</button>
<button
type="button"
:class="['switch-chip', { 'is-active': activePlatform === 'kuaishouEticket' }]"
@click="switchPlatform('kuaishouEticket')"
>
快手小店核销
</button>
<button
type="button"
:class="['switch-chip', { 'is-active': activePlatform === 'cloudtentacles' }]"
@@ -1248,6 +1612,285 @@ onMounted(loadConfigs)
</section>
</template>
<template v-else-if="activePlatform === 'kuaishouEticket'">
<section class="meta-card">
<div class="meta-line">
<span class="meta-label">配置文件</span>
<code>{{ kuaishouEticketFilePath || '-' }}</code>
</div>
<div class="meta-line">
<span class="meta-label">职责</span>
<span>这是独立的新流程只负责快手小店券核销调试与配置不接入当前已完成的订单来源和履约主链路</span>
</div>
<div class="meta-line">
<span class="meta-label">建议流程</span>
<span>先为每个店铺录入 `kshopName + Cookie`可用 info 接口自动识别店铺名与店铺 ID保存后选择目标店铺再查询券码并执行核销</span>
</div>
</section>
<section class="table-card">
<div class="section-title-row">
<div>
<h3>快手小店店铺配置</h3>
<p>支持多店铺配置建议先粘贴 Cookie再用 info 接口自动识别 `kshopName / shopId`最后统一保存到文件</p>
</div>
<div class="section-actions">
<el-button :disabled="kuaishouEticketShops.length === 0" text @click="expandAllKuaishouEticketShops">全部展开</el-button>
<el-button :disabled="kuaishouEticketShops.length === 0" text @click="collapseAllKuaishouEticketShops">全部折叠</el-button>
<el-button round @click="addKuaishouEticketShop">新增店铺</el-button>
<el-button :loading="kuaishouEticketSaving" round type="primary" @click="handleKuaishouEticketSaveSource">保存配置</el-button>
</div>
</div>
<div class="kuaishou-workspace">
<div class="kuaishou-shop-stack">
<div class="form-grid kuaishou-base-grid">
<label class="field-block field-wide">
<span>Base URL</span>
<input v-model="kuaishouEticketForm.baseUrl" class="text-input" placeholder="https://s.kwaixiaodian.com" />
</label>
</div>
<div v-if="kuaishouEticketShops.length === 0" class="empty-inline">当前还没有快手小店店铺配置先新增一条</div>
<article
v-for="shop in kuaishouEticketShops"
:key="shop.id"
:class="['shop-card', 'kuaishou-shop-card', {
'is-active': kuaishouEticketForm.selectedShopId === shop.id,
'is-expanded': isKuaishouEticketShopExpanded(shop.id),
}]"
>
<button type="button" class="shop-summary kuaishou-shop-summary" @click="toggleKuaishouEticketShop(shop.id)">
<div class="kuaishou-shop-head">
<div class="shop-avatar">
<img v-if="shop.userAvatar" :src="shop.userAvatar" :alt="shop.kshopName || shop.shopId || '店铺头像'" />
<span v-else>{{ getKuaishouEticketShopInitial(shop) }}</span>
</div>
<div class="shop-summary-main">
<div class="shop-title-row">
<strong>{{ shop.kshopName || shop.shopId || '未命名店铺' }}</strong>
<span class="shop-status" :class="{ 'is-disabled': !shop.enabled }">
{{ shop.enabled ? '已启用' : '已停用' }}
</span>
</div>
<span class="cell-subtle">店铺 ID{{ shop.shopId || '等待 info 自动识别' }}</span>
<div class="shop-tag-row">
<span class="shop-mini-tag" :class="{ 'is-ready': Boolean(shop.cookie.trim()) }">
{{ shop.cookie.trim() ? 'Cookie 已粘贴' : '待补 Cookie' }}
</span>
<span class="shop-mini-tag" :class="{ 'is-ready': Boolean(shop.userAvatar || shop.shopId.trim()) }">
{{ shop.userAvatar || shop.shopId.trim() ? '店铺信息已识别' : '未读取店铺信息' }}
</span>
<span v-if="kuaishouEticketForm.selectedShopId === shop.id" class="shop-mini-tag is-highlight">当前调试店铺</span>
</div>
</div>
</div>
<div class="shop-summary-side">
<span class="shop-summary-copy">{{ describeKuaishouEticketShop(shop) }}</span>
<span class="shop-toggle">{{ isKuaishouEticketShopExpanded(shop.id) ? '收起详情' : '展开配置' }}</span>
</div>
</button>
<div v-if="isKuaishouEticketShopExpanded(shop.id)" class="shop-body kuaishou-shop-body">
<div class="form-grid">
<label class="field-block">
<span>kshopName</span>
<input v-model="shop.kshopName" class="text-input" placeholder="例如 稚嫩游戏交易店" />
</label>
<label class="field-block">
<span>店铺 ID</span>
<input v-model="shop.shopId" class="text-input" readonly placeholder="通过 info 接口自动识别" />
</label>
<label class="field-block field-wide">
<span>Cookie</span>
<textarea
v-model="shop.cookie"
class="text-area kuaishou-cookie-area"
placeholder="直接粘贴该店铺请求头中的完整 Cookie"
/>
</label>
</div>
<div class="shop-actions kuaishou-shop-actions">
<label class="checkbox-line">
<input v-model="shop.enabled" type="checkbox" />
<span>启用该店铺</span>
</label>
<span class="cell-subtle">{{ shop.userAvatar ? '已识别头像,可直接作为调试店铺' : '建议先读取店铺信息确认 Cookie 归属' }}</span>
<el-button
:loading="kuaishouEticketResolvingShopId === shop.id"
link
type="primary"
@click="handleKuaishouEticketResolveShopInfo(shop)"
>
读取店铺信息
</el-button>
<el-button
link
:type="kuaishouEticketForm.selectedShopId === shop.id ? 'success' : 'primary'"
@click="setKuaishouEticketDebugShop(shop.id)"
>
{{ kuaishouEticketForm.selectedShopId === shop.id ? '当前调试店铺' : '设为调试店铺' }}
</el-button>
<el-button link type="danger" @click="removeKuaishouEticketShop(shop.id)">删除</el-button>
</div>
</div>
</article>
</div>
<aside class="kuaishou-side-panel">
<article class="result-card kuaishou-side-card">
<span class="result-label">店铺总数</span>
<strong>{{ kuaishouEticketStats.configuredShopCount }}</strong>
<p>其中 Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} </p>
</article>
<article class="result-card kuaishou-side-card">
<span class="result-label">当前调试店铺</span>
<strong>{{ activeKuaishouEticketShop?.kshopName || activeKuaishouEticketShop?.shopId || '未选择' }}</strong>
<p>{{ kuaishouEticketStats.selectedShopReady ? '已具备调试条件,可直接查询核销信息。' : '请选择店铺并补全 Cookie。' }}</p>
</article>
<article class="result-card kuaishou-side-card">
<span class="result-label">最近识别结果</span>
<strong>{{ kuaishouEticketShopInfoResult?.shop.kshopName || kuaishouEticketShopInfoResult?.shop.shopId || '未执行' }}</strong>
<p v-if="kuaishouEticketShopInfoResult">结算状态 {{ kuaishouEticketShopInfoResult.shop.settleStatus }}</p>
<p v-else>可用读取店铺信息校验 Cookie 是否属于预期店铺</p>
</article>
<article class="result-card kuaishou-side-card kuaishou-guide-card">
<span class="result-label">建议顺序</span>
<strong>先配置再调试</strong>
<p>1. 新增店铺并粘贴 Cookie</p>
<p>2. 读取店铺信息确认 kshopName / shopId</p>
<p>3. 设为调试店铺后查询券码并执行核销</p>
</article>
</aside>
</div>
<p v-if="kuaishouEticketResultError" class="error-copy">{{ kuaishouEticketResultError }}</p>
</section>
<section class="table-card">
<div class="section-title-row">
<div>
<h3>快手小店核销调试</h3>
<p>这里使用当前调试店铺 Cookie 执行查询与核销`eTicketId / oid / formToken` 都来自查询结果</p>
</div>
<div class="section-actions">
<el-button :loading="kuaishouEticketDetailing" round @click="handleKuaishouEticketQueryDetail">查询核销信息</el-button>
<el-button :loading="kuaishouEticketConsuming" round type="primary" @click="handleKuaishouEticketConsume">执行核销</el-button>
</div>
</div>
<div class="kuaishou-debug-layout">
<div class="kuaishou-debug-panel">
<div class="form-grid">
<label class="field-block">
<span>调试店铺</span>
<select v-model="kuaishouEticketForm.selectedShopId" class="text-input">
<option value="">请选择店铺</option>
<option v-for="shop in kuaishouEticketShops" :key="shop.id" :value="shop.id">
{{ shop.kshopName || shop.shopId || '未命名店铺' }}
</option>
</select>
</label>
<label class="field-block">
<span>查询券码</span>
<input v-model="kuaishouEticketForm.queryTicketCode" class="text-input" placeholder="用于查询核销信息" />
</label>
<label class="field-block">
<span>核销数量</span>
<input v-model.number="kuaishouEticketForm.num" class="text-input" type="number" min="1" />
</label>
<label class="field-block">
<span>storeId</span>
<input v-model="kuaishouEticketForm.storeId" class="text-input" placeholder="默认 0" />
</label>
</div>
</div>
<div class="kuaishou-debug-panel kuaishou-debug-panel-soft">
<div class="kuaishou-debug-head">
<div>
<span class="result-label">自动回填参数</span>
<strong>查询后生成</strong>
</div>
<span class="cell-subtle">这些字段不需要手动填写</span>
</div>
<div class="form-grid">
<label class="field-block">
<span>eTicketId</span>
<input :value="kuaishouEticketForm.eTicketId" class="text-input" readonly placeholder="查询后自动回填" />
</label>
<label class="field-block">
<span>oid</span>
<input :value="kuaishouEticketForm.oid" class="text-input" readonly placeholder="查询后自动回填" />
</label>
<label class="field-block field-wide">
<span>formToken</span>
<input :value="kuaishouEticketForm.formToken" class="text-input" readonly placeholder="查询后自动回填" />
</label>
</div>
</div>
</div>
<div class="result-grid">
<article class="result-card">
<span class="result-label">详情查询</span>
<strong>{{ kuaishouEticketDetailResult ? (kuaishouEticketDetailResult.ok ? '成功' : '已返回') : '未执行' }}</strong>
<p v-if="kuaishouEticketDetailResult?.detail">
{{ kuaishouEticketDetailResult.goods?.itemTitle || kuaishouEticketDetailResult.goods?.skuDesc || '-' }}
剩余 {{ kuaishouEticketDetailResult.detail.leftCount }} / {{ kuaishouEticketDetailResult.detail.totalCount }}
</p>
<p v-else>先通过查询券码获取核销详情并自动带出核销参数</p>
</article>
<article class="result-card">
<span class="result-label">核销执行</span>
<strong>{{ kuaishouEticketConsumeResult ? (kuaishouEticketConsumeResult.consumed ? '已处理' : '已返回') : '未执行' }}</strong>
<p v-if="kuaishouEticketConsumeResult">
{{ kuaishouEticketConsumeResult.errorMessage || (kuaishouEticketConsumeResult.ok ? '核销成功' : '接口已返回结果') }}
</p>
<p v-else>查询详情后再执行核销会更稳妥</p>
</article>
</div>
</section>
<section v-if="kuaishouEticketDetailResult" class="table-card">
<div class="section-title-row">
<div>
<h3>核销详情返回</h3>
<p>成功查询后会自动回填 `eTicketId``oid``formToken` 和建议核销数量</p>
</div>
</div>
<pre class="debug-result">{{ JSON.stringify(kuaishouEticketDetailResult.raw, null, 2) }}</pre>
</section>
<section v-if="kuaishouEticketConsumeResult" class="table-card">
<div class="section-title-row">
<div>
<h3>核销执行返回</h3>
<p>这里保留接口原始响应方便核对 `result``error_msg` `requestId`</p>
</div>
</div>
<pre class="debug-result">{{ JSON.stringify(kuaishouEticketConsumeResult.raw, null, 2) }}</pre>
</section>
</template>
<template v-else-if="isDevMode">
<section class="meta-card">
<div class="meta-line">
@@ -1497,7 +2140,7 @@ onMounted(loadConfigs)
.overview-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
@@ -1618,6 +2261,29 @@ onMounted(loadConfigs)
grid-column: 1 / -1;
}
.kuaishou-workspace,
.kuaishou-debug-layout {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.9fr);
gap: 16px;
}
.kuaishou-shop-stack,
.kuaishou-side-panel,
.kuaishou-debug-panel {
display: grid;
gap: 14px;
}
.kuaishou-base-grid {
padding: 14px;
border-radius: 18px;
background:
radial-gradient(circle at top left, rgba(59, 130, 246, 0.08), transparent 38%),
linear-gradient(180deg, rgba(239, 246, 255, 0.86), rgba(248, 250, 252, 0.96));
border: 1px solid rgba(148, 163, 184, 0.18);
}
.text-input,
.text-area {
width: 100%;
@@ -1642,6 +2308,10 @@ onMounted(loadConfigs)
overflow: hidden;
}
.shop-card:first-of-type {
margin-top: 0;
}
.shop-summary {
width: 100%;
border: 0;
@@ -1672,6 +2342,152 @@ onMounted(loadConfigs)
font-size: 12px;
}
.kuaishou-shop-card {
margin-top: 0;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.98));
border-color: rgba(96, 165, 250, 0.18);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.kuaishou-shop-card.is-active {
border-color: rgba(37, 99, 235, 0.34);
box-shadow: 0 18px 44px rgba(37, 99, 235, 0.12);
}
.kuaishou-shop-card.is-expanded {
transform: translateY(-1px);
}
.kuaishou-shop-summary {
align-items: center;
gap: 16px;
}
.kuaishou-shop-head,
.shop-title-row,
.shop-tag-row,
.kuaishou-debug-head {
display: flex;
gap: 10px;
}
.kuaishou-shop-head {
align-items: center;
}
.shop-title-row,
.shop-tag-row,
.kuaishou-debug-head {
flex-wrap: wrap;
}
.shop-title-row {
align-items: center;
}
.shop-avatar {
width: 54px;
height: 54px;
border-radius: 18px;
overflow: hidden;
background: linear-gradient(135deg, #dbeafe, #eff6ff);
color: #1d4ed8;
font-size: 20px;
font-weight: 800;
display: grid;
place-items: center;
flex-shrink: 0;
border: 1px solid rgba(37, 99, 235, 0.14);
}
.shop-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.shop-mini-tag {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 0 10px;
border-radius: 999px;
background: rgba(15, 23, 42, 0.05);
color: #475467;
font-size: 12px;
font-weight: 700;
}
.shop-mini-tag.is-ready {
background: rgba(22, 163, 74, 0.12);
color: #166534;
}
.shop-mini-tag.is-highlight {
background: rgba(37, 99, 235, 0.12);
color: #1d4ed8;
}
.kuaishou-shop-body {
padding-top: 16px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(248, 250, 252, 0.96));
}
.kuaishou-shop-actions {
margin-top: 14px;
padding-top: 14px;
}
.kuaishou-cookie-area {
min-height: 128px;
line-height: 1.6;
}
.kuaishou-side-panel {
align-content: start;
}
.kuaishou-side-card {
min-height: 132px;
}
.kuaishou-guide-card {
background:
radial-gradient(circle at top right, rgba(37, 99, 235, 0.08), transparent 35%),
linear-gradient(180deg, rgba(239, 246, 255, 0.9), rgba(248, 250, 252, 0.98));
}
.kuaishou-guide-card p {
font-size: 13px;
}
.kuaishou-debug-panel {
padding: 16px;
border-radius: 20px;
border: 1px solid rgba(148, 163, 184, 0.16);
background: rgba(248, 250, 252, 0.76);
}
.kuaishou-debug-panel-soft {
background:
radial-gradient(circle at top left, rgba(59, 130, 246, 0.08), transparent 34%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.98));
}
.kuaishou-debug-head {
justify-content: space-between;
align-items: flex-start;
}
.kuaishou-debug-head strong {
display: block;
margin-top: 4px;
color: #1d3555;
font-size: 22px;
}
.shop-status {
display: inline-flex;
align-items: center;
@@ -1791,7 +2607,9 @@ onMounted(loadConfigs)
@media (max-width: 1100px) {
.overview-grid,
.result-grid,
.form-grid {
.form-grid,
.kuaishou-workspace,
.kuaishou-debug-layout {
grid-template-columns: 1fr;
}
}
@@ -1805,6 +2623,14 @@ onMounted(loadConfigs)
align-items: stretch;
}
.kuaishou-shop-head,
.shop-title-row,
.shop-tag-row,
.kuaishou-debug-head {
flex-direction: column;
align-items: flex-start;
}
.shop-summary-side {
justify-items: start;
}