[Snow Team] Merge platform-sections-refactor's work
This commit is contained in:
@@ -5,6 +5,9 @@ import TencentAuthCard from '@/components/tencent/TencentAuthCard.vue'
|
||||
import TencentRedeemPanel from '@/components/tencent/TencentRedeemPanel.vue'
|
||||
import TencentResultPanel from '@/components/tencent/TencentResultPanel.vue'
|
||||
import { useAdminManualRedeemPage } from '@/composables/useAdminManualRedeemPage'
|
||||
import AdminManualRedeemCreateCard from './components/AdminManualRedeemCreateCard.vue'
|
||||
import AdminManualRedeemSummaryCard from './components/AdminManualRedeemSummaryCard.vue'
|
||||
import AdminManualRedeemReviewCard from './components/AdminManualRedeemReviewCard.vue'
|
||||
|
||||
const {
|
||||
form,
|
||||
@@ -104,82 +107,21 @@ const taskDetailLink = computed(() =>
|
||||
</el-button>
|
||||
</AdminPageHeader>
|
||||
|
||||
<section class="create-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>1. 创建人工兑换任务</h2>
|
||||
<p>先录入唯一凭据并锁定库存,后续二维码、角色确认和兑换都基于这条任务继续。</p>
|
||||
</div>
|
||||
</div>
|
||||
<AdminManualRedeemCreateCard
|
||||
:form="form"
|
||||
:suggestions="suggestions"
|
||||
:can-create-task="canCreateTask"
|
||||
:create-loading="createLoading"
|
||||
:create-task-now="createTaskNow"
|
||||
/>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<span>唯一凭据</span>
|
||||
<el-input
|
||||
v-model="form.proofValue"
|
||||
placeholder="输入订单号、客户名或其他唯一可追踪信息"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span>内部履约 SKU</span>
|
||||
<el-input
|
||||
v-model="form.skuCode"
|
||||
list="manual-redeem-sku-suggestions"
|
||||
placeholder="选择或输入内部履约 SKU"
|
||||
/>
|
||||
<datalist id="manual-redeem-sku-suggestions">
|
||||
<option
|
||||
v-for="item in suggestions"
|
||||
:key="`${item.credentialType}-${item.inventoryGroupCode}-${item.skuCode}`"
|
||||
:value="item.skuCode"
|
||||
:label="`${item.skuCode} · ${item.inventoryGroupCode || '未分组'} · 可用 ${item.availableCount} / 共 ${item.totalCount}`"
|
||||
/>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="field field-remark">
|
||||
<span>备注</span>
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="可选:记录客户补充信息、兑换要求或客服备注"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<el-button
|
||||
:disabled="!canCreateTask"
|
||||
:loading="createLoading"
|
||||
round
|
||||
type="primary"
|
||||
@click="createTaskNow"
|
||||
>
|
||||
创建任务并预占库存
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detail" class="summary-card">
|
||||
<article>
|
||||
<span>唯一凭据</span>
|
||||
<strong>{{ manualRequest?.proofValue || order?.platformOrderId || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>任务号</span>
|
||||
<strong>{{ task?.taskNo || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>商品</span>
|
||||
<strong>{{ orderItem?.skuName || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>当前状态</span>
|
||||
<strong>{{ statusLabel }}</strong>
|
||||
</article>
|
||||
</section>
|
||||
<AdminManualRedeemSummaryCard
|
||||
:task="task"
|
||||
:order="order"
|
||||
:order-item="orderItem"
|
||||
:manual-request="manualRequest"
|
||||
:status-label="statusLabel"
|
||||
/>
|
||||
|
||||
<div v-if="detailLoading && !detail" class="empty-block">人工兑换详情加载中</div>
|
||||
|
||||
@@ -237,43 +179,18 @@ const taskDetailLink = computed(() =>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="detail" class="review-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>2. {{ reviewCardTitle }}</h2>
|
||||
<p v-if="reviewReady">
|
||||
客户登录后,系统会把当前角色截图回传到这里。客服复制后可直接发给客户做二次确认。
|
||||
</p>
|
||||
<p v-else-if="resultReady">最终兑换完成后,客服可以把结果截图直接发给客户确认。</p>
|
||||
<p v-else>
|
||||
{{ screenshotEmptyMessage }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :disabled="!screenshotUrl || screenshotLoading" plain @click="copyScreenshot">
|
||||
复制图片
|
||||
</el-button>
|
||||
<el-button
|
||||
:disabled="!screenshotUrl || screenshotLoading"
|
||||
plain
|
||||
@click="downloadScreenshot"
|
||||
>
|
||||
下载图片
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="screenshotUrl" class="review-stage">
|
||||
<button class="screenshot-button" type="button" @click="screenshotPreviewVisible = true">
|
||||
<img :src="screenshotUrl" alt="manual redeem screenshot" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-block">
|
||||
<strong>{{ screenshotEmptyTitle }}</strong>
|
||||
<p>{{ screenshotEmptyMessage }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<AdminManualRedeemReviewCard
|
||||
v-if="detail"
|
||||
:review-card-title="reviewCardTitle"
|
||||
:review-ready="reviewReady"
|
||||
:result-ready="resultReady"
|
||||
:screenshot-empty-message="screenshotEmptyMessage"
|
||||
:screenshot-url="screenshotUrl"
|
||||
:screenshot-loading="screenshotLoading"
|
||||
:copy-screenshot="copyScreenshot"
|
||||
:download-screenshot="downloadScreenshot"
|
||||
:open-preview="() => (screenshotPreviewVisible = true)"
|
||||
/>
|
||||
|
||||
<TencentResultPanel
|
||||
v-if="detail && task?.status === 'redeemed'"
|
||||
@@ -322,99 +239,12 @@ const taskDetailLink = computed(() =>
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.header-actions,
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.task-link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.create-card,
|
||||
.summary-card,
|
||||
.review-card {
|
||||
padding: 22px var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-default);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-head h2 {
|
||||
margin: 0;
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-head p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
margin-top: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.field span {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field-remark {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.summary-card article {
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.summary-card span {
|
||||
display: block;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.summary-card strong {
|
||||
display: block;
|
||||
margin-top: var(--space-2);
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.layout-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(340px, 392px) minmax(0, 1fr);
|
||||
@@ -426,30 +256,6 @@ const taskDetailLink = computed(() =>
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.review-stage {
|
||||
margin-top: 18px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.screenshot-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.screenshot-button img,
|
||||
.preview-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
min-height: 180px;
|
||||
padding: var(--space-6);
|
||||
@@ -472,10 +278,15 @@ const taskDetailLink = computed(() =>
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.summary-card,
|
||||
.layout-grid,
|
||||
.form-grid {
|
||||
.layout-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
form: {
|
||||
proofValue: string
|
||||
skuCode: string
|
||||
remark: string
|
||||
}
|
||||
suggestions: { credentialType: string; inventoryGroupCode: string; skuCode: string; availableCount: number; totalCount: number }[]
|
||||
canCreateTask: boolean
|
||||
createLoading: boolean
|
||||
createTaskNow: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="create-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>1. 创建人工兑换任务</h2>
|
||||
<p>先录入唯一凭据并锁定库存,后续二维码、角色确认和兑换都基于这条任务继续。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<span>唯一凭据</span>
|
||||
<el-input
|
||||
v-model="form.proofValue"
|
||||
placeholder="输入订单号、客户名或其他唯一可追踪信息"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span>内部履约 SKU</span>
|
||||
<el-input
|
||||
v-model="form.skuCode"
|
||||
list="manual-redeem-sku-suggestions"
|
||||
placeholder="选择或输入内部履约 SKU"
|
||||
/>
|
||||
<datalist id="manual-redeem-sku-suggestions">
|
||||
<option
|
||||
v-for="item in suggestions"
|
||||
:key="`${item.credentialType}-${item.inventoryGroupCode}-${item.skuCode}`"
|
||||
:value="item.skuCode"
|
||||
:label="`${item.skuCode} · ${item.inventoryGroupCode || '未分组'} · 可用 ${item.availableCount} / 共 ${item.totalCount}`"
|
||||
/>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="field field-remark">
|
||||
<span>备注</span>
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="可选:记录客户补充信息、兑换要求或客服备注"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<el-button
|
||||
:disabled="!canCreateTask"
|
||||
:loading="createLoading"
|
||||
round
|
||||
type="primary"
|
||||
@click="createTaskNow"
|
||||
>
|
||||
创建任务并预占库存
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.create-card {
|
||||
padding: 22px var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-default);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.card-head h2 {
|
||||
margin: 0;
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-head p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
margin-top: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.field span {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field-remark {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
reviewCardTitle: string
|
||||
reviewReady: boolean
|
||||
resultReady: boolean
|
||||
screenshotEmptyMessage: string
|
||||
screenshotUrl: string
|
||||
screenshotLoading: boolean
|
||||
copyScreenshot: () => void
|
||||
downloadScreenshot: () => void
|
||||
openPreview: () => void
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="review-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>2. {{ reviewCardTitle }}</h2>
|
||||
<p v-if="reviewReady">
|
||||
客户登录后,系统会把当前角色截图回传到这里。客服复制后可直接发给客户做二次确认。
|
||||
</p>
|
||||
<p v-else-if="resultReady">最终兑换完成后,客服可以把结果截图直接发给客户确认。</p>
|
||||
<p v-else>
|
||||
{{ screenshotEmptyMessage }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :disabled="!screenshotUrl || screenshotLoading" plain @click="copyScreenshot">
|
||||
复制图片
|
||||
</el-button>
|
||||
<el-button
|
||||
:disabled="!screenshotUrl || screenshotLoading"
|
||||
plain
|
||||
@click="downloadScreenshot"
|
||||
>
|
||||
下载图片
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="screenshotUrl" class="review-stage">
|
||||
<button class="screenshot-button" type="button" @click="openPreview">
|
||||
<img :src="screenshotUrl" alt="manual redeem screenshot" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-block">
|
||||
<strong>{{ screenshotEmptyTitle || '' }}</strong>
|
||||
<p>{{ screenshotEmptyMessage }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.review-card {
|
||||
padding: 22px var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-default);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.card-head h2 {
|
||||
margin: 0;
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-head p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.review-stage {
|
||||
margin-top: 18px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.screenshot-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.screenshot-button img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
min-height: 180px;
|
||||
padding: var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
background: var(--bg-surface);
|
||||
border: 1px dashed var(--border-default);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-block strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.empty-block p {
|
||||
margin: var(--space-2) 0 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
type TaskDetail = {
|
||||
taskNo?: string
|
||||
taskId?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
type Order = {
|
||||
platformOrderId?: string
|
||||
}
|
||||
|
||||
type OrderItem = {
|
||||
skuName?: string
|
||||
}
|
||||
|
||||
type ManualRequest = {
|
||||
proofValue?: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
task: TaskDetail | null
|
||||
order: Order | null
|
||||
orderItem: OrderItem | null
|
||||
manualRequest: ManualRequest | null
|
||||
statusLabel: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="task" class="summary-card">
|
||||
<article>
|
||||
<span>唯一凭据</span>
|
||||
<strong>{{ manualRequest?.proofValue || order?.platformOrderId || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>任务号</span>
|
||||
<strong>{{ task.taskNo || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>商品</span>
|
||||
<strong>{{ orderItem?.skuName || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>当前状态</span>
|
||||
<strong>{{ statusLabel }}</strong>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.summary-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
padding: 22px var(--space-6);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-default);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.summary-card article {
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.summary-card span {
|
||||
display: block;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.summary-card strong {
|
||||
display: block;
|
||||
margin-top: var(--space-2);
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.summary-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
} from '@/services/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import AdminPlatformAgisoSection from './components/AdminPlatformAgisoSection.vue'
|
||||
import AdminPlatformCloudtentaclesSection from './components/AdminPlatformCloudtentaclesSection.vue'
|
||||
import AdminPlatformCloudtentaclesSection from './components/cloudtentacles/AdminPlatformCloudtentaclesSection.vue'
|
||||
import AdminPlatformNinetyoneSection from './components/AdminPlatformNinetyoneSection.vue'
|
||||
import AdminPlatformNotificationSection from './components/AdminPlatformNotificationSection.vue'
|
||||
import AdminPlatformKuaishouEticketSection from './components/AdminPlatformKuaishouEticketSection.vue'
|
||||
import AdminPlatformNotificationSection from './components/notification/AdminPlatformNotificationSection.vue'
|
||||
import AdminPlatformKuaishouEticketSection from './components/kuaishou-eticket/AdminPlatformKuaishouEticketSection.vue'
|
||||
import { useAdminAgisoPlatform } from './composables/useAdminAgisoPlatform'
|
||||
import { useAdminCloudtentaclesPlatform } from './composables/useAdminCloudtentaclesPlatform'
|
||||
import { useAdminNinetyonePlatform } from './composables/useAdminNinetyonePlatform'
|
||||
@@ -273,7 +273,9 @@ onMounted(loadConfigs)
|
||||
tag: 'Cookie / 券核销',
|
||||
value: kuaishouEticketStats.configuredShopCount,
|
||||
unit: '已配置店铺',
|
||||
meta: `Cookie 就绪 ${kuaishouEticketStats.cookieReadyCount} 家 · 当前店铺 ${kuaishouEticketStats.selectedShopReady ? '可调试' : '待补全'}`,
|
||||
meta: `Cookie 就绪 ${kuaishouEticketStats.cookieReadyCount} 家 · 当前店铺 ${
|
||||
kuaishouEticketStats.selectedShopReady ? '可调试' : '待补全'
|
||||
}`,
|
||||
},
|
||||
...(isDevMode
|
||||
? [
|
||||
@@ -283,7 +285,9 @@ onMounted(loadConfigs)
|
||||
tag: '履约 / 登录',
|
||||
value: cloudtentaclesStats.hasCredential ? '已配置' : '未配置',
|
||||
unit: '履约凭据',
|
||||
meta: `短信 ${cloudtentaclesStats.smsSent ? '已发送' : '未发送'} · 会话 ${cloudtentaclesStats.validated ? '已验证' : '未验证'}`,
|
||||
meta: `短信 ${cloudtentaclesStats.smsSent ? '已发送' : '未发送'} · 会话 ${
|
||||
cloudtentaclesStats.validated ? '已验证' : '未验证'
|
||||
}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -455,9 +459,7 @@ onMounted(loadConfigs)
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.overview-card.is-active {
|
||||
|
||||
-455
@@ -1,455 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type CloudtentaclesForm = {
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
code: string
|
||||
token: string
|
||||
vnKey: string
|
||||
vnId: number
|
||||
skuId: number
|
||||
skuCount: number
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
type CloudtentaclesStats = {
|
||||
hasCredential: boolean
|
||||
smsSent: boolean
|
||||
validated: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesFilePath: string
|
||||
cloudtentaclesSessionFilePath: string
|
||||
cloudtentaclesForm: CloudtentaclesForm
|
||||
cloudtentaclesSaving: boolean
|
||||
cloudtentaclesSendingSms: boolean
|
||||
cloudtentaclesTesting: boolean
|
||||
cloudtentaclesValidating: boolean
|
||||
cloudtentaclesResultError: string
|
||||
cloudtentaclesSmsResult: AdminCloudtentaclesSendSmsResult | null
|
||||
cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null
|
||||
cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null
|
||||
cloudtentaclesDebugLoading: boolean
|
||||
cloudtentaclesDebugResult: string
|
||||
cloudtentaclesPersistedSession: AdminCloudtentaclesPersistedSession | null
|
||||
cloudtentaclesStats: CloudtentaclesStats
|
||||
handleCloudtentaclesSaveSource: () => void | Promise<void>
|
||||
handleCloudtentaclesSendSmsCode: () => void | Promise<void>
|
||||
handleCloudtentaclesTestLogin: () => void | Promise<void>
|
||||
handleCloudtentaclesValidateSession: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchAsset: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchCategories: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchSkuList: () => void | Promise<void>
|
||||
handleCloudtentaclesBuySku: () => void | Promise<void>
|
||||
handleCloudtentaclesUseSku: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchKnapsack: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnList: () => void | Promise<void>
|
||||
handleCloudtentaclesAppointVn: () => void | Promise<void>
|
||||
handleCloudtentaclesGenerateVnLoginCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesVerifyVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchBindUrl: () => void | Promise<void>
|
||||
handleCloudtentaclesBackVn: () => void | Promise<void>
|
||||
handleCloudtentaclesRunFullFlow: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-section">
|
||||
<!-- 配置信息 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">配置信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="配置文件">
|
||||
<code>{{ cloudtentaclesFilePath || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会话文件">
|
||||
<code>{{ cloudtentaclesSessionFilePath || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="职责">
|
||||
管理 cloudtentacles 外部履约平台的账号、短信验证码登录与 token
|
||||
探活。该区域不处理订单来源同步。
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已保存 token">
|
||||
{{
|
||||
cloudtentaclesPersistedSession?.hasToken
|
||||
? `${cloudtentaclesPersistedSession.tokenMasked} / ${formatAdminDateTime(cloudtentaclesPersistedSession.loggedInAt)}`
|
||||
: '未保存'
|
||||
}}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 履约平台配置 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 履约平台配置</span>
|
||||
<p class="card-desc">
|
||||
先保存账号、密码、手机号和设备头,再发送短信验证码,最后执行登录测试或手动校验 token。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button :loading="cloudtentaclesSaving" @click="handleCloudtentaclesSaveSource"
|
||||
>保存履约配置</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesSendingSms" @click="handleCloudtentaclesSendSmsCode"
|
||||
>发送短信验证码</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesTesting"
|
||||
type="primary"
|
||||
@click="handleCloudtentaclesTestLogin"
|
||||
>测试登录</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesValidating"
|
||||
@click="handleCloudtentaclesValidateSession"
|
||||
>校验 token</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="Base URL">
|
||||
<el-input v-model="cloudtentaclesForm.baseUrl" placeholder="https://123.207.217.176" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="账号">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.username"
|
||||
placeholder="cloudtentacles 登录账号"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="密码">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="cloudtentacles 登录密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="cloudtentaclesForm.phone" placeholder="接收短信验证码的手机号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="短信验证码">
|
||||
<el-input v-model="cloudtentaclesForm.code" placeholder="收到验证码后填写" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="VN Key">
|
||||
<el-input v-model="cloudtentaclesForm.vnKey" placeholder="例如 1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="VN ID">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.vnId"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="SKU ID">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.skuId"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="购买数量">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.skuCount"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="deviceId">
|
||||
<el-input v-model="cloudtentaclesForm.deviceId" placeholder="-" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="deviceType">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.deviceType"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="token(可手动粘贴)">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.token"
|
||||
placeholder="登录成功后可自动回填,也可手动粘贴"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<el-alert
|
||||
v-if="cloudtentaclesResultError"
|
||||
:title="cloudtentaclesResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<div class="result-grid mt-4">
|
||||
<AdminResultCard label="短信发送" :value="cloudtentaclesSmsResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesSmsResult">
|
||||
{{ cloudtentaclesSmsResult.phoneMasked }},{{
|
||||
formatAdminDateTime(cloudtentaclesSmsResult.sentAt)
|
||||
}}
|
||||
</p>
|
||||
<p v-else>先发送短信验证码,确认手机号可用。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="登录结果" :value="cloudtentaclesLoginResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesLoginResult">
|
||||
{{ cloudtentaclesLoginResult.phoneMasked }},{{
|
||||
formatAdminDateTime(cloudtentaclesLoginResult.loggedInAt)
|
||||
}}
|
||||
</p>
|
||||
<p v-else>填写短信验证码后可执行测试登录。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="会话校验" :value="cloudtentaclesValidateResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesValidateResult">
|
||||
权限 {{ cloudtentaclesValidateResult.session.permissionCount }} 项,余额
|
||||
{{ cloudtentaclesValidateResult.asset }}
|
||||
</p>
|
||||
<p v-else>登录成功后可继续校验 token 会话。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 开发调试工具 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 开发调试工具</span>
|
||||
<p class="card-desc">
|
||||
仅开发模式展示。用于调试分类、SKU、购买、发货、虚拟号、兑换链接和结束退还链路。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="debug-buttons">
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchAsset"
|
||||
>余额</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesFetchCategories"
|
||||
>分类</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchSkuList"
|
||||
>SKU</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesBuySku"
|
||||
>购买</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="primary"
|
||||
@click="handleCloudtentaclesUseSku"
|
||||
>发货</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchKnapsack"
|
||||
>背包</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchVnList"
|
||||
>虚拟号列表</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesAppointVn"
|
||||
>申请虚拟号</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesGenerateVnLoginCode"
|
||||
>生成登录码</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchVnCode"
|
||||
>获取验证码</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesVerifyVnCode"
|
||||
>校验验证码</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchBindUrl"
|
||||
>兑换链接</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="warning"
|
||||
@click="handleCloudtentaclesBackVn"
|
||||
>退还号码</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="success"
|
||||
@click="handleCloudtentaclesRunFullFlow"
|
||||
>完整流程</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<pre v-if="cloudtentaclesDebugResult" class="debug-pre mt-4">{{
|
||||
cloudtentaclesDebugResult
|
||||
}}</pre>
|
||||
<el-empty
|
||||
v-else
|
||||
description="执行一次调试动作后,这里会显示原始返回数据。"
|
||||
:image-size="60"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 调试结果 -->
|
||||
<el-card v-if="cloudtentaclesLoginResult || cloudtentaclesValidateResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 调试结果</span>
|
||||
<p class="card-desc">
|
||||
用于确认当前账号会话、权限和用户信息,后续发货执行器会复用这里的登录能力。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="result-grid">
|
||||
<AdminResultCard
|
||||
label="当前 token"
|
||||
:value="
|
||||
cloudtentaclesValidateResult?.session.tokenMasked ||
|
||||
cloudtentaclesLoginResult?.session.tokenMasked ||
|
||||
'-'
|
||||
"
|
||||
>
|
||||
<p>原始 token 已写入上方输入框,可继续用于校验或后续调试。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="权限"
|
||||
:value="`${cloudtentaclesValidateResult?.session.permissionCount || cloudtentaclesLoginResult?.session.permissionCount || 0} 项`"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
(
|
||||
cloudtentaclesValidateResult?.session.permissions ||
|
||||
cloudtentaclesLoginResult?.session.permissions ||
|
||||
[]
|
||||
).join('、') || '暂无'
|
||||
}}
|
||||
</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="余额"
|
||||
:value="cloudtentaclesValidateResult?.asset || cloudtentaclesLoginResult?.asset || 0"
|
||||
>
|
||||
<p>可用于确认当前履约账号状态。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.debug-pre {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
</style>
|
||||
-654
@@ -1,654 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { EditableKuaishouEticketShop } from '../composables/types'
|
||||
import type {
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
} from '@/types/admin'
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
|
||||
type KuaishouEticketForm = {
|
||||
baseUrl: string
|
||||
selectedShopId: string
|
||||
queryTicketCode: string
|
||||
eTicketId: string
|
||||
oid: string
|
||||
formToken: string
|
||||
num: number
|
||||
storeId: string
|
||||
}
|
||||
|
||||
type KuaishouEticketStats = {
|
||||
configuredShopCount: number
|
||||
cookieReadyCount: number
|
||||
selectedShopReady: boolean
|
||||
detailReady: boolean
|
||||
consumed: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
kuaishouEticketFilePath: string
|
||||
kuaishouEticketShops: EditableKuaishouEticketShop[]
|
||||
kuaishouEticketForm: KuaishouEticketForm
|
||||
kuaishouEticketSaving: boolean
|
||||
kuaishouEticketResolvingShopId: string
|
||||
kuaishouEticketDetailing: boolean
|
||||
kuaishouEticketConsuming: boolean
|
||||
kuaishouEticketResultError: string
|
||||
kuaishouEticketShopInfoResult: AdminKuaishouEticketShopInfoResult | null
|
||||
kuaishouEticketDetailResult: AdminKuaishouEticketDetailResult | null
|
||||
kuaishouEticketConsumeResult: AdminKuaishouEticketConsumeResult | null
|
||||
activeKuaishouEticketShop: EditableKuaishouEticketShop | null
|
||||
kuaishouEticketStats: KuaishouEticketStats
|
||||
addKuaishouEticketShop: () => void
|
||||
removeKuaishouEticketShop: (id: string) => void
|
||||
isKuaishouEticketShopExpanded: (id: string) => boolean
|
||||
toggleKuaishouEticketShop: (id: string) => void
|
||||
expandAllKuaishouEticketShops: () => void
|
||||
collapseAllKuaishouEticketShops: () => void
|
||||
setKuaishouEticketDebugShop: (id: string) => void
|
||||
describeKuaishouEticketShop: (shop: EditableKuaishouEticketShop) => string
|
||||
getKuaishouEticketShopInitial: (shop: EditableKuaishouEticketShop) => string
|
||||
handleKuaishouEticketResolveShopInfo: (shop: EditableKuaishouEticketShop) => void | Promise<void>
|
||||
handleKuaishouEticketSaveSource: () => void | Promise<void>
|
||||
handleKuaishouEticketQueryDetail: () => void | Promise<void>
|
||||
handleKuaishouEticketConsume: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-section">
|
||||
<!-- 说明信息 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">说明信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="配置文件">{{
|
||||
kuaishouEticketFilePath || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="职责">
|
||||
这是独立的新流程,只负责快手小店券核销调试与配置,不接入当前已完成的订单来源和履约主链路。
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="建议流程">
|
||||
先为每个店铺录入 kshopName + Cookie,可用 info 接口自动识别店铺名与店铺
|
||||
ID;保存后选择目标店铺,再查询券码并执行核销。
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 店铺配置 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">快手小店店铺配置</span>
|
||||
<p class="card-desc">
|
||||
支持多店铺配置,建议先粘贴 Cookie,再用 info 接口自动识别 kshopName /
|
||||
shopId,最后统一保存到文件。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:disabled="kuaishouEticketShops.length === 0"
|
||||
@click="expandAllKuaishouEticketShops"
|
||||
>全部展开</el-button
|
||||
>
|
||||
<el-button
|
||||
:disabled="kuaishouEticketShops.length === 0"
|
||||
@click="collapseAllKuaishouEticketShops"
|
||||
>全部折叠</el-button
|
||||
>
|
||||
<el-button @click="addKuaishouEticketShop">新增店铺</el-button>
|
||||
<el-button
|
||||
:loading="kuaishouEticketSaving"
|
||||
type="primary"
|
||||
@click="handleKuaishouEticketSaveSource"
|
||||
>保存配置</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="Base URL">
|
||||
<el-input
|
||||
v-model="kuaishouEticketForm.baseUrl"
|
||||
placeholder="https://s.kwaixiaodian.com"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-empty
|
||||
v-if="kuaishouEticketShops.length === 0"
|
||||
description="当前还没有快手小店店铺配置,先新增一条。"
|
||||
/>
|
||||
|
||||
<div v-else class="shop-panels">
|
||||
<div class="shop-list">
|
||||
<el-collapse
|
||||
:model-value="
|
||||
kuaishouEticketShops
|
||||
.filter((s) => isKuaishouEticketShopExpanded(s.id))
|
||||
.map((s) => s.id)
|
||||
"
|
||||
>
|
||||
<el-collapse-item
|
||||
v-for="shop in kuaishouEticketShops"
|
||||
:key="shop.id"
|
||||
:name="shop.id"
|
||||
:class="[
|
||||
'shop-panel',
|
||||
{
|
||||
'is-debug': kuaishouEticketForm.selectedShopId === shop.id,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #title>
|
||||
<div class="shop-title-bar">
|
||||
<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-info">
|
||||
<div class="shop-name-row">
|
||||
<strong>{{ shop.kshopName || shop.shopId || '未命名店铺' }}</strong>
|
||||
<el-tag :type="shop.enabled ? 'success' : 'info'" size="small">{{
|
||||
shop.enabled ? '已启用' : '已停用'
|
||||
}}</el-tag>
|
||||
<el-tag
|
||||
v-if="kuaishouEticketForm.selectedShopId === shop.id"
|
||||
type="primary"
|
||||
size="small"
|
||||
>当前调试</el-tag
|
||||
>
|
||||
</div>
|
||||
<div class="shop-meta">
|
||||
<span>店铺 ID:{{ shop.shopId || '等待 info 自动识别' }}</span>
|
||||
<el-tag
|
||||
:type="Boolean(shop.cookie.trim()) ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ shop.cookie.trim() ? 'Cookie 已粘贴' : '待补 Cookie' }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="Boolean(shop.userAvatar || shop.shopId.trim()) ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ shop.userAvatar || shop.shopId.trim() ? '信息已识别' : '未读取信息' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="kshopName">
|
||||
<el-input v-model="shop.kshopName" placeholder="例如 稚嫩游戏交易店" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="店铺 ID">
|
||||
<el-input
|
||||
v-model="shop.shopId"
|
||||
readonly
|
||||
placeholder="通过 info 接口自动识别"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="Cookie">
|
||||
<el-input
|
||||
v-model="shop.cookie"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="直接粘贴该店铺请求头中的完整 Cookie"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="shop-footer">
|
||||
<div class="footer-left">
|
||||
<el-switch v-model="shop.enabled" active-text="启用该店铺" />
|
||||
<span v-if="shop.userAvatar" class="text-muted"
|
||||
>已识别头像,可直接作为调试店铺</span
|
||||
>
|
||||
<span v-else class="text-muted">建议先读取店铺信息确认 Cookie 归属</span>
|
||||
</div>
|
||||
<div class="footer-actions">
|
||||
<el-button
|
||||
:loading="kuaishouEticketResolvingShopId === shop.id"
|
||||
size="small"
|
||||
@click="handleKuaishouEticketResolveShopInfo(shop)"
|
||||
>
|
||||
读取店铺信息
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="kuaishouEticketForm.selectedShopId === shop.id ? 'success' : 'primary'"
|
||||
@click="setKuaishouEticketDebugShop(shop.id)"
|
||||
>
|
||||
{{
|
||||
kuaishouEticketForm.selectedShopId === shop.id
|
||||
? '当前调试店铺'
|
||||
: '设为调试店铺'
|
||||
}}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="removeKuaishouEticketShop(shop.id)"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
|
||||
<div class="side-panel">
|
||||
<AdminResultCard label="店铺总数" :value="kuaishouEticketStats.configuredShopCount">
|
||||
<p>其中 Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} 家。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="当前调试店铺"
|
||||
:value="
|
||||
activeKuaishouEticketShop?.kshopName || activeKuaishouEticketShop?.shopId || '未选择'
|
||||
"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
kuaishouEticketStats.selectedShopReady
|
||||
? '已具备调试条件,可直接查询核销信息。'
|
||||
: '请选择店铺并补全 Cookie。'
|
||||
}}
|
||||
</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="最近识别结果"
|
||||
:value="
|
||||
kuaishouEticketShopInfoResult?.shop.kshopName ||
|
||||
kuaishouEticketShopInfoResult?.shop.shopId ||
|
||||
'未执行'
|
||||
"
|
||||
>
|
||||
<p v-if="kuaishouEticketShopInfoResult">
|
||||
结算状态 {{ kuaishouEticketShopInfoResult.shop.settleStatus }}
|
||||
</p>
|
||||
<p v-else>可用"读取店铺信息"校验 Cookie 是否属于预期店铺。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="建议顺序" value="先配置,再调试">
|
||||
<p>1. 新增店铺并粘贴 Cookie</p>
|
||||
<p>2. 读取店铺信息确认 kshopName / shopId</p>
|
||||
<p>3. 设为调试店铺后查询券码并执行核销</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="kuaishouEticketResultError"
|
||||
:title="kuaishouEticketResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 核销调试 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">快手小店核销调试</span>
|
||||
<p class="card-desc">
|
||||
这里使用"当前调试店铺"的 Cookie 执行查询与核销;eTicketId / oid / formToken
|
||||
都来自查询结果。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button :loading="kuaishouEticketDetailing" @click="handleKuaishouEticketQueryDetail"
|
||||
>查询核销信息</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="kuaishouEticketConsuming"
|
||||
type="primary"
|
||||
@click="handleKuaishouEticketConsume"
|
||||
>执行核销</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="debug-layout">
|
||||
<div class="debug-main">
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="调试店铺">
|
||||
<el-select
|
||||
v-model="kuaishouEticketForm.selectedShopId"
|
||||
placeholder="请选择店铺"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="shop in kuaishouEticketShops"
|
||||
:key="shop.id"
|
||||
:label="shop.kshopName || shop.shopId || '未命名店铺'"
|
||||
:value="shop.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="查询券码">
|
||||
<el-input
|
||||
v-model="kuaishouEticketForm.queryTicketCode"
|
||||
placeholder="用于查询核销信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="核销数量">
|
||||
<el-input-number
|
||||
v-model="kuaishouEticketForm.num"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="storeId">
|
||||
<el-input v-model="kuaishouEticketForm.storeId" placeholder="默认 0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="auto-fill-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">自动回填参数</span>
|
||||
<span class="text-muted ml-2">查询后生成</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="eTicketId">
|
||||
<el-input
|
||||
:model-value="kuaishouEticketForm.eTicketId"
|
||||
readonly
|
||||
placeholder="查询后自动回填"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="oid">
|
||||
<el-input
|
||||
:model-value="kuaishouEticketForm.oid"
|
||||
readonly
|
||||
placeholder="查询后自动回填"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="formToken">
|
||||
<el-input
|
||||
:model-value="kuaishouEticketForm.formToken"
|
||||
readonly
|
||||
placeholder="查询后自动回填"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div class="result-grid mt-4">
|
||||
<AdminResultCard
|
||||
label="详情查询"
|
||||
:value="
|
||||
kuaishouEticketDetailResult
|
||||
? kuaishouEticketDetailResult.ok
|
||||
? '成功'
|
||||
: '已返回'
|
||||
: '未执行'
|
||||
"
|
||||
>
|
||||
<p v-if="kuaishouEticketDetailResult?.detail">
|
||||
{{
|
||||
kuaishouEticketDetailResult.goods?.itemTitle ||
|
||||
kuaishouEticketDetailResult.goods?.skuDesc ||
|
||||
'-'
|
||||
}}, 剩余 {{ kuaishouEticketDetailResult.detail.leftCount }} /
|
||||
{{ kuaishouEticketDetailResult.detail.totalCount }}
|
||||
</p>
|
||||
<p v-else>先通过查询券码获取核销详情并自动带出核销参数。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="核销执行"
|
||||
:value="
|
||||
kuaishouEticketConsumeResult
|
||||
? kuaishouEticketConsumeResult.consumed
|
||||
? '已处理'
|
||||
: '已返回'
|
||||
: '未执行'
|
||||
"
|
||||
>
|
||||
<p v-if="kuaishouEticketConsumeResult">
|
||||
{{
|
||||
kuaishouEticketConsumeResult.errorMessage ||
|
||||
(kuaishouEticketConsumeResult.ok ? '核销成功' : '接口已返回结果')
|
||||
}}
|
||||
</p>
|
||||
<p v-else>查询详情后再执行核销会更稳妥。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情结果 -->
|
||||
<el-card v-if="kuaishouEticketDetailResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">核销详情返回</span>
|
||||
<p class="card-desc">成功查询后会自动回填 eTicketId、oid、formToken 和建议核销数量。</p>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="debug-pre">{{ JSON.stringify(kuaishouEticketDetailResult.raw, null, 2) }}</pre>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="kuaishouEticketConsumeResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">核销执行返回</span>
|
||||
<p class="card-desc">这里保留接口原始响应,方便核对 result、error_msg 和 requestId。</p>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="debug-pre">{{ JSON.stringify(kuaishouEticketConsumeResult.raw, null, 2) }}</pre>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shop-panels {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(260px, 0.9fr);
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.shop-list {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.shop-panel.is-debug {
|
||||
border: 1px solid var(--border-hover);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.shop-title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: var(--space-4);
|
||||
}
|
||||
|
||||
.shop-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #dbeafe, #eff6ff);
|
||||
color: var(--color-primary-dark);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--border-hover);
|
||||
}
|
||||
|
||||
.shop-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.shop-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shop-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shop-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.shop-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-3);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(260px, 0.9fr);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.auto-fill-card {
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.debug-pre {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.shop-panels,
|
||||
.debug-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-548
@@ -1,548 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { AdminNotificationTestResult, AdminScheduledJobRuntimeState } from '@/types/admin'
|
||||
import type { EditableNotificationRecipient, EditableScheduledJob } from '../composables/types'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type Props = {
|
||||
notificationFilePath: string
|
||||
notificationForm: {
|
||||
enabled: boolean
|
||||
barkEnabled: boolean
|
||||
barkServerUrl: string
|
||||
testTitle: string
|
||||
testBody: string
|
||||
testUrl: string
|
||||
}
|
||||
notificationRecipients: EditableNotificationRecipient[]
|
||||
notificationSaving: boolean
|
||||
notificationTesting: boolean
|
||||
notificationResultError: string
|
||||
notificationTestResult: AdminNotificationTestResult | null
|
||||
scheduledJobsFilePath: string
|
||||
scheduledJobsForm: {
|
||||
enabled: boolean
|
||||
}
|
||||
scheduledJobs: EditableScheduledJob[]
|
||||
scheduledJobRuntime: AdminScheduledJobRuntimeState[]
|
||||
scheduledJobsSaving: boolean
|
||||
scheduledJobRunningId: string
|
||||
notificationStats: {
|
||||
configuredRecipientCount: number
|
||||
enabledRecipientCount: number
|
||||
barkReady: boolean
|
||||
lastSuccessCount: number
|
||||
lastFailedCount: number
|
||||
scheduledJobEnabledCount: number
|
||||
}
|
||||
addNotificationRecipient: () => void
|
||||
removeNotificationRecipient: (id: string) => void
|
||||
handleNotificationSaveConfig: () => void | Promise<void>
|
||||
handleNotificationTest: () => void | Promise<void>
|
||||
handleScheduledJobsSaveConfig: () => void | Promise<void>
|
||||
handleScheduledJobRunNow: (jobId: string) => void | Promise<void>
|
||||
getScheduledJobRuntime: (jobId: string) => AdminScheduledJobRuntimeState | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const timeUnitOptions = [
|
||||
{ label: '秒', value: 1 },
|
||||
{ label: '分钟', value: 60 },
|
||||
{ label: '小时', value: 3600 },
|
||||
]
|
||||
|
||||
function formatJobType(type: string) {
|
||||
if (type === 'cloudtentacles_health') return 'cloudtentacles 健康检查'
|
||||
return type || '-'
|
||||
}
|
||||
|
||||
function resolveTimeValue(seconds: number) {
|
||||
const normalized = Math.max(0, Number(seconds || 0))
|
||||
if (normalized >= 3600 && normalized % 3600 === 0) {
|
||||
return { amount: normalized / 3600, unit: 3600 }
|
||||
}
|
||||
if (normalized >= 60 && normalized % 60 === 0) {
|
||||
return { amount: normalized / 60, unit: 60 }
|
||||
}
|
||||
return { amount: normalized || 1, unit: 1 }
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number) {
|
||||
const value = resolveTimeValue(seconds)
|
||||
const unit = timeUnitOptions.find((item) => item.value === value.unit)?.label || '秒'
|
||||
return `${value.amount} ${unit}`
|
||||
}
|
||||
|
||||
function bindDuration(job: EditableScheduledJob, key: 'intervalSeconds' | 'cooldownSeconds') {
|
||||
const amountKey = `${key}Amount` as keyof EditableScheduledJob
|
||||
const unitKey = `${key}Unit` as keyof EditableScheduledJob
|
||||
|
||||
if (!Number(job[amountKey])) {
|
||||
const resolved = resolveTimeValue(Number(job[key] || 0))
|
||||
;(job[amountKey] as number) = resolved.amount
|
||||
;(job[unitKey] as number) = resolved.unit
|
||||
}
|
||||
|
||||
return computed<number>({
|
||||
get: () => Number(job[amountKey] || 1),
|
||||
set: (value: number) => {
|
||||
;(job[amountKey] as number) = Math.max(1, Number(value || 1))
|
||||
job[key] = Number(job[amountKey] || 1) * Number(job[unitKey] || 1)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function bindDurationUnit(job: EditableScheduledJob, key: 'intervalSeconds' | 'cooldownSeconds') {
|
||||
const amountKey = `${key}Amount` as keyof EditableScheduledJob
|
||||
const unitKey = `${key}Unit` as keyof EditableScheduledJob
|
||||
|
||||
if (!Number(job[unitKey])) {
|
||||
const resolved = resolveTimeValue(Number(job[key] || 0))
|
||||
;(job[amountKey] as number) = resolved.amount
|
||||
;(job[unitKey] as number) = resolved.unit
|
||||
}
|
||||
|
||||
return computed<number>({
|
||||
get: () => Number(job[unitKey] || 1),
|
||||
set: (value: number) => {
|
||||
;(job[unitKey] as number) = Math.max(1, Number(value || 1))
|
||||
job[key] = Number(job[amountKey] || 1) * Number(job[unitKey] || 1)
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-section">
|
||||
<!-- 配置信息 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">配置信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="职责"
|
||||
>内部运营通知,仅发送给后台管理员、客服或值班人员。</el-descriptions-item
|
||||
>
|
||||
<el-descriptions-item label="配置文件">{{
|
||||
notificationFilePath || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="监控任务">{{
|
||||
scheduledJobsFilePath || '-'
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- Bark 通知配置 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">Bark 通知</span>
|
||||
<p class="card-desc">
|
||||
已配置 {{ notificationStats.configuredRecipientCount }} 人,启用
|
||||
{{ notificationStats.enabledRecipientCount }} 人。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button @click="addNotificationRecipient">新增接收人</el-button>
|
||||
<el-button
|
||||
:loading="notificationSaving"
|
||||
type="primary"
|
||||
@click="handleNotificationSaveConfig"
|
||||
>保存配置</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
v-if="notificationResultError"
|
||||
:title="notificationResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<el-form label-width="auto" label-position="top" inline>
|
||||
<el-form-item
|
||||
><el-switch v-model="notificationForm.enabled" active-text="启用内部通知"
|
||||
/></el-form-item>
|
||||
<el-form-item
|
||||
><el-switch v-model="notificationForm.barkEnabled" active-text="启用 Bark 通道"
|
||||
/></el-form-item>
|
||||
<el-form-item label="Bark 服务地址" class="w-full">
|
||||
<el-input v-model="notificationForm.barkServerUrl" placeholder="https://api.day.app" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="notificationRecipients" stripe size="small" class="mt-4">
|
||||
<el-table-column label="接收人" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.name" placeholder="例如:客服A" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Device Key" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.deviceKey" placeholder="Bark device key" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="danger" text @click="removeNotificationRecipient(row.id)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="还没有 Bark 接收人。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 监控任务 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">监控任务</span>
|
||||
<p class="card-desc">
|
||||
间隔表示多久检查一次;冷却表示同一个异常在这段时间内最多通知一次。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:loading="scheduledJobsSaving"
|
||||
type="primary"
|
||||
@click="handleScheduledJobsSaveConfig"
|
||||
>保存任务</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item>
|
||||
<el-switch v-model="scheduledJobsForm.enabled" active-text="启用监控任务系统" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="scheduledJobs" stripe size="small" style="margin-top: 8px">
|
||||
<el-table-column label="任务" min-width="200">
|
||||
<template #default="{ row: job }">
|
||||
<div>
|
||||
<div class="fw-600">{{ formatJobType(job.type) }}</div>
|
||||
<el-switch
|
||||
v-model="job.enabled"
|
||||
size="small"
|
||||
active-text="启用"
|
||||
style="margin-top: 4px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数" min-width="320">
|
||||
<template #default="{ row: job }">
|
||||
<div class="job-params">
|
||||
<div class="param-row">
|
||||
<span class="param-label">检查间隔</span>
|
||||
<input
|
||||
v-model.number="bindDuration(job, 'intervalSeconds').value"
|
||||
type="number"
|
||||
min="1"
|
||||
class="mini-input"
|
||||
/>
|
||||
<select
|
||||
v-model.number="bindDurationUnit(job, 'intervalSeconds').value"
|
||||
class="mini-select"
|
||||
>
|
||||
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">
|
||||
{{ unit.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="param-hint"
|
||||
>每 {{ formatDuration(job.intervalSeconds) }} 检查一次</span
|
||||
>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">通知冷却</span>
|
||||
<input
|
||||
v-model.number="bindDuration(job, 'cooldownSeconds').value"
|
||||
type="number"
|
||||
min="1"
|
||||
class="mini-input"
|
||||
/>
|
||||
<select
|
||||
v-model.number="bindDurationUnit(job, 'cooldownSeconds').value"
|
||||
class="mini-select"
|
||||
>
|
||||
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">
|
||||
{{ unit.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="param-hint"
|
||||
>同类异常 {{ formatDuration(job.cooldownSeconds) }} 内只通知一次</span
|
||||
>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">余额阈值</span>
|
||||
<input
|
||||
v-model.number="job.assetThreshold"
|
||||
type="number"
|
||||
min="0"
|
||||
class="mini-input"
|
||||
/>
|
||||
<span class="param-hint">低于该值时通知</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近状态" min-width="160">
|
||||
<template #default="{ row: job }">
|
||||
<div class="runtime-info">
|
||||
<el-tag
|
||||
:type="
|
||||
getScheduledJobRuntime(job.id)?.lastStatus === 'success' ? 'success' : 'info'
|
||||
"
|
||||
size="small"
|
||||
>
|
||||
{{ getScheduledJobRuntime(job.id)?.lastStatus || 'pending' }}
|
||||
</el-tag>
|
||||
<span class="param-hint">{{
|
||||
getScheduledJobRuntime(job.id)?.lastMessage || '-'
|
||||
}}</span>
|
||||
<span class="param-hint"
|
||||
>余额:{{ getScheduledJobRuntime(job.id)?.lastAsset ?? '-' }}</span
|
||||
>
|
||||
<span class="param-hint"
|
||||
>上次:{{
|
||||
formatAdminDateTime(getScheduledJobRuntime(job.id)?.lastRunAt || '')
|
||||
}}</span
|
||||
>
|
||||
<span class="param-hint"
|
||||
>下次:{{
|
||||
formatAdminDateTime(getScheduledJobRuntime(job.id)?.nextRunAt || '')
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" align="center">
|
||||
<template #default="{ row: job }">
|
||||
<el-button
|
||||
:loading="scheduledJobRunningId === job.id"
|
||||
size="small"
|
||||
@click="handleScheduledJobRunNow(job.id)"
|
||||
>
|
||||
立即检查
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无可配置的监控任务。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 测试发送 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">测试发送</span>
|
||||
<p class="card-desc">测试会发送给所有已启用且填写了 Device Key 的接收人。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:disabled="!notificationStats.barkReady"
|
||||
:loading="notificationTesting"
|
||||
type="primary"
|
||||
@click="handleNotificationTest"
|
||||
>
|
||||
发送测试
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="notificationForm.testTitle" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="内容">
|
||||
<el-input v-model="notificationForm.testBody" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="跳转链接">
|
||||
<el-input v-model="notificationForm.testUrl" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<div v-if="notificationTestResult" class="result-grid">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">成功</div>
|
||||
<div class="stat-value">{{ notificationTestResult.successCount }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">失败</div>
|
||||
<div class="stat-value">{{ notificationTestResult.failedCount }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">通道</div>
|
||||
<div class="stat-value">{{ notificationTestResult.channel || '-' }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-if="notificationTestResult"
|
||||
:data="notificationTestResult.results"
|
||||
stripe
|
||||
size="small"
|
||||
class="mt-4"
|
||||
>
|
||||
<el-table-column label="接收人">
|
||||
<template #default="{ row }">{{
|
||||
row.recipientName || row.recipientKeyMasked || '-'
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.ok ? 'success' : 'danger'" size="small">{{
|
||||
row.ok ? '成功' : '失败'
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态码" width="80" align="center" />
|
||||
<el-table-column label="说明">
|
||||
<template #default="{ row }">{{ row.errorMessage || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.job-params {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.param-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.mini-input {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-surface);
|
||||
font-size: var(--text-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mini-select {
|
||||
width: 80px;
|
||||
height: 32px;
|
||||
padding: 0 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-surface);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.param-hint {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.runtime-info {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
</style>
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type CloudtentaclesForm = {
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
code: string
|
||||
token: string
|
||||
vnKey: string
|
||||
vnId: number
|
||||
skuId: number
|
||||
skuCount: number
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesForm: CloudtentaclesForm
|
||||
cloudtentaclesSaving: boolean
|
||||
cloudtentaclesSendingSms: boolean
|
||||
cloudtentaclesTesting: boolean
|
||||
cloudtentaclesValidating: boolean
|
||||
cloudtentaclesResultError: string
|
||||
cloudtentaclesSmsResult: AdminCloudtentaclesSendSmsResult | null
|
||||
cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null
|
||||
cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null
|
||||
handleCloudtentaclesSaveSource: () => void | Promise<void>
|
||||
handleCloudtentaclesSendSmsCode: () => void | Promise<void>
|
||||
handleCloudtentaclesTestLogin: () => void | Promise<void>
|
||||
handleCloudtentaclesValidateSession: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 履约平台配置</span>
|
||||
<p class="card-desc">
|
||||
先保存账号、密码、手机号和设备头,再发送短信验证码,最后执行登录测试或手动校验 token。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button :loading="cloudtentaclesSaving" @click="handleCloudtentaclesSaveSource"
|
||||
>保存履约配置</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesSendingSms" @click="handleCloudtentaclesSendSmsCode"
|
||||
>发送短信验证码</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesTesting"
|
||||
type="primary"
|
||||
@click="handleCloudtentaclesTestLogin"
|
||||
>测试登录</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesValidating"
|
||||
@click="handleCloudtentaclesValidateSession"
|
||||
>校验 token</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="Base URL">
|
||||
<el-input v-model="cloudtentaclesForm.baseUrl" placeholder="https://123.207.217.176" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="账号">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.username"
|
||||
placeholder="cloudtentacles 登录账号"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="密码">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="cloudtentacles 登录密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="cloudtentaclesForm.phone" placeholder="接收短信验证码的手机号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="短信验证码">
|
||||
<el-input v-model="cloudtentaclesForm.code" placeholder="收到验证码后填写" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="VN Key">
|
||||
<el-input v-model="cloudtentaclesForm.vnKey" placeholder="例如 1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="VN ID">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.vnId"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="SKU ID">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.skuId"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="购买数量">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.skuCount"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="deviceId">
|
||||
<el-input v-model="cloudtentaclesForm.deviceId" placeholder="-" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="deviceType">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.deviceType"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="token(可手动粘贴)">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.token"
|
||||
placeholder="登录成功后可自动回填,也可手动粘贴"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<el-alert
|
||||
v-if="cloudtentaclesResultError"
|
||||
:title="cloudtentaclesResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<div class="result-grid mt-4">
|
||||
<AdminResultCard label="短信发送" :value="cloudtentaclesSmsResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesSmsResult">
|
||||
{{ cloudtentaclesSmsResult.phoneMasked }},{{
|
||||
formatAdminDateTime(cloudtentaclesSmsResult.sentAt)
|
||||
}}
|
||||
</p>
|
||||
<p v-else>先发送短信验证码,确认手机号可用。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="登录结果" :value="cloudtentaclesLoginResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesLoginResult">
|
||||
{{ cloudtentaclesLoginResult.phoneMasked }},{{
|
||||
formatAdminDateTime(cloudtentaclesLoginResult.loggedInAt)
|
||||
}}
|
||||
</p>
|
||||
<p v-else>填写短信验证码后可执行测试登录。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="会话校验" :value="cloudtentaclesValidateResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesValidateResult">
|
||||
权限 {{ cloudtentaclesValidateResult.session.permissionCount }} 项,余额
|
||||
{{ cloudtentaclesValidateResult.asset }}
|
||||
</p>
|
||||
<p v-else>登录成功后可继续校验 token 会话。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
</style>
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
cloudtentaclesDebugLoading: boolean
|
||||
handleCloudtentaclesFetchAsset: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchCategories: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchSkuList: () => void | Promise<void>
|
||||
handleCloudtentaclesBuySku: () => void | Promise<void>
|
||||
handleCloudtentaclesUseSku: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchKnapsack: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnList: () => void | Promise<void>
|
||||
handleCloudtentaclesAppointVn: () => void | Promise<void>
|
||||
handleCloudtentaclesGenerateVnLoginCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesVerifyVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchBindUrl: () => void | Promise<void>
|
||||
handleCloudtentaclesBackVn: () => void | Promise<void>
|
||||
handleCloudtentaclesRunFullFlow: () => void | Promise<void>
|
||||
cloudtentaclesDebugResult: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 开发调试工具</span>
|
||||
<p class="card-desc">
|
||||
仅开发模式展示。用于调试分类、SKU、购买、发货、虚拟号、兑换链接和结束退还链路。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="debug-buttons">
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchAsset"
|
||||
>余额</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesFetchCategories"
|
||||
>分类</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchSkuList"
|
||||
>SKU</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesBuySku"
|
||||
>购买</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="primary"
|
||||
@click="handleCloudtentaclesUseSku"
|
||||
>发货</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchKnapsack"
|
||||
>背包</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchVnList"
|
||||
>虚拟号列表</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesAppointVn"
|
||||
>申请虚拟号</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesGenerateVnLoginCode"
|
||||
>生成登录码</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchVnCode"
|
||||
>获取验证码</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesVerifyVnCode"
|
||||
>校验验证码</el-button
|
||||
>
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesFetchBindUrl"
|
||||
>兑换链接</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="warning"
|
||||
@click="handleCloudtentaclesBackVn"
|
||||
>退还号码</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="success"
|
||||
@click="handleCloudtentaclesRunFullFlow"
|
||||
>完整流程</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<pre v-if="cloudtentaclesDebugResult" class="debug-pre mt-4">{{
|
||||
cloudtentaclesDebugResult
|
||||
}}</pre>
|
||||
<el-empty
|
||||
v-else
|
||||
description="执行一次调试动作后,这里会显示原始返回数据。"
|
||||
:image-size="60"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.debug-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-pre {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
</style>
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
} from '@/types/admin'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesFilePath: string
|
||||
cloudtentaclesSessionFilePath: string
|
||||
cloudtentaclesPersistedSession: AdminCloudtentaclesPersistedSession | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">配置信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="配置文件">
|
||||
<code>{{ cloudtentaclesFilePath || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会话文件">
|
||||
<code>{{ cloudtentaclesSessionFilePath || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="职责">
|
||||
管理 cloudtentacles 外部履约平台的账号、短信验证码登录与 token
|
||||
探活。该区域不处理订单来源同步。
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已保存 token">
|
||||
{{
|
||||
cloudtentaclesPersistedSession?.hasToken
|
||||
? `${cloudtentaclesPersistedSession.tokenMasked} / ${formatAdminDateTime(cloudtentaclesPersistedSession.loggedInAt)}`
|
||||
: '未保存'
|
||||
}}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null
|
||||
cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card v-if="cloudtentaclesLoginResult || cloudtentaclesValidateResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 调试结果</span>
|
||||
<p class="card-desc">
|
||||
用于确认当前账号会话、权限和用户信息,后续发货执行器会复用这里的登录能力。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="result-grid">
|
||||
<AdminResultCard
|
||||
label="当前 token"
|
||||
:value="
|
||||
cloudtentaclesValidateResult?.session.tokenMasked ||
|
||||
cloudtentaclesLoginResult?.session.tokenMasked ||
|
||||
'-'
|
||||
"
|
||||
>
|
||||
<p>原始 token 已写入上方输入框,可继续用于校验或后续调试。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="权限"
|
||||
:value="`${cloudtentaclesValidateResult?.session.permissionCount || cloudtentaclesLoginResult?.session.permissionCount || 0} 项`"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
(
|
||||
cloudtentaclesValidateResult?.session.permissions ||
|
||||
cloudtentaclesLoginResult?.session.permissions ||
|
||||
[]
|
||||
).join('、') || '暂无'
|
||||
}}
|
||||
</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="余额"
|
||||
:value="cloudtentaclesValidateResult?.asset || cloudtentaclesLoginResult?.asset || 0"
|
||||
>
|
||||
<p>可用于确认当前履约账号状态。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
</style>
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
import AdminCloudtentaclesInfoCard from './AdminCloudtentaclesInfoCard.vue'
|
||||
import AdminCloudtentaclesConfigCard from './AdminCloudtentaclesConfigCard.vue'
|
||||
import AdminCloudtentaclesDebugCard from './AdminCloudtentaclesDebugCard.vue'
|
||||
import AdminCloudtentaclesResultCard from './AdminCloudtentaclesResultCard.vue'
|
||||
|
||||
type CloudtentaclesForm = {
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
code: string
|
||||
token: string
|
||||
vnKey: string
|
||||
vnId: number
|
||||
skuId: number
|
||||
skuCount: number
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
type CloudtentaclesStats = {
|
||||
hasCredential: boolean
|
||||
smsSent: boolean
|
||||
validated: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesFilePath: string
|
||||
cloudtentaclesSessionFilePath: string
|
||||
cloudtentaclesForm: CloudtentaclesForm
|
||||
cloudtentaclesSaving: boolean
|
||||
cloudtentaclesSendingSms: boolean
|
||||
cloudtentaclesTesting: boolean
|
||||
cloudtentaclesValidating: boolean
|
||||
cloudtentaclesResultError: string
|
||||
cloudtentaclesSmsResult: AdminCloudtentaclesSendSmsResult | null
|
||||
cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null
|
||||
cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null
|
||||
cloudtentaclesDebugLoading: boolean
|
||||
cloudtentaclesDebugResult: string
|
||||
cloudtentaclesPersistedSession: AdminCloudtentaclesPersistedSession | null
|
||||
cloudtentaclesStats: CloudtentaclesStats
|
||||
handleCloudtentaclesSaveSource: () => void | Promise<void>
|
||||
handleCloudtentaclesSendSmsCode: () => void | Promise<void>
|
||||
handleCloudtentaclesTestLogin: () => void | Promise<void>
|
||||
handleCloudtentaclesValidateSession: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchAsset: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchCategories: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchSkuList: () => void | Promise<void>
|
||||
handleCloudtentaclesBuySku: () => void | Promise<void>
|
||||
handleCloudtentaclesUseSku: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchKnapsack: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnList: () => void | Promise<void>
|
||||
handleCloudtentaclesAppointVn: () => void | Promise<void>
|
||||
handleCloudtentaclesGenerateVnLoginCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesVerifyVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchBindUrl: () => void | Promise<void>
|
||||
handleCloudtentaclesBackVn: () => void | Promise<void>
|
||||
handleCloudtentaclesRunFullFlow: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-section">
|
||||
<AdminCloudtentaclesInfoCard
|
||||
:cloudtentacles-file-path="cloudtentaclesFilePath"
|
||||
:cloudtentacles-session-file-path="cloudtentaclesSessionFilePath"
|
||||
:cloudtentacles-persisted-session="cloudtentaclesPersistedSession"
|
||||
/>
|
||||
|
||||
<AdminCloudtentaclesConfigCard
|
||||
:cloudtentacles-form="cloudtentaclesForm"
|
||||
:cloudtentacles-saving="cloudtentaclesSaving"
|
||||
:cloudtentacles-sending-sms="cloudtentaclesSendingSms"
|
||||
:cloudtentacles-testing="cloudtentaclesTesting"
|
||||
:cloudtentacles-validating="cloudtentaclesValidating"
|
||||
:cloudtentacles-result-error="cloudtentaclesResultError"
|
||||
:cloudtentacles-sms-result="cloudtentaclesSmsResult"
|
||||
:cloudtentacles-login-result="cloudtentaclesLoginResult"
|
||||
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
|
||||
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
|
||||
:handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode"
|
||||
:handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin"
|
||||
:handle-cloudtentacles-validate-session="handleCloudtentaclesValidateSession"
|
||||
/>
|
||||
|
||||
<AdminCloudtentaclesDebugCard
|
||||
:cloudtentacles-debug-loading="cloudtentaclesDebugLoading"
|
||||
:cloudtentacles-debug-result="cloudtentaclesDebugResult"
|
||||
:handle-cloudtentacles-fetch-asset="handleCloudtentaclesFetchAsset"
|
||||
:handle-cloudtentacles-fetch-categories="handleCloudtentaclesFetchCategories"
|
||||
:handle-cloudtentacles-fetch-sku-list="handleCloudtentaclesFetchSkuList"
|
||||
:handle-cloudtentacles-buy-sku="handleCloudtentaclesBuySku"
|
||||
:handle-cloudtentacles-use-sku="handleCloudtentaclesUseSku"
|
||||
:handle-cloudtentacles-fetch-knapsack="handleCloudtentaclesFetchKnapsack"
|
||||
:handle-cloudtentacles-fetch-vn-list="handleCloudtentaclesFetchVnList"
|
||||
:handle-cloudtentacles-appoint-vn="handleCloudtentaclesAppointVn"
|
||||
:handle-cloudtentacles-generate-vn-login-code="handleCloudtentaclesGenerateVnLoginCode"
|
||||
:handle-cloudtentacles-fetch-vn-code="handleCloudtentaclesFetchVnCode"
|
||||
:handle-cloudtentacles-verify-vn-code="handleCloudtentaclesVerifyVnCode"
|
||||
:handle-cloudtentacles-fetch-bind-url="handleCloudtentaclesFetchBindUrl"
|
||||
:handle-cloudtentacles-back-vn="handleCloudtentaclesBackVn"
|
||||
:handle-cloudtentacles-run-full-flow="handleCloudtentaclesRunFullFlow"
|
||||
/>
|
||||
|
||||
<AdminCloudtentaclesResultCard
|
||||
:cloudtentacles-login-result="cloudtentaclesLoginResult"
|
||||
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
</style>
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
} from '@/types/admin'
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
|
||||
type KuaishouEticketDebugForm = {
|
||||
selectedShopId: string
|
||||
queryTicketCode: string
|
||||
eTicketId: string
|
||||
oid: string
|
||||
formToken: string
|
||||
num: number
|
||||
storeId: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
kuaishouEticketShops: { id: string; kshopName: string; shopId: string }[]
|
||||
kuaishouEticketForm: KuaishouEticketDebugForm
|
||||
kuaishouEticketDetailing: boolean
|
||||
kuaishouEticketConsuming: boolean
|
||||
kuaishouEticketDetailResult: AdminKuaishouEticketDetailResult | null
|
||||
kuaishouEticketConsumeResult: AdminKuaishouEticketConsumeResult | null
|
||||
handleKuaishouEticketQueryDetail: () => void | Promise<void>
|
||||
handleKuaishouEticketConsume: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">快手小店核销调试</span>
|
||||
<p class="card-desc">
|
||||
这里使用"当前调试店铺"的 Cookie 执行查询与核销;eTicketId / oid / formToken
|
||||
都来自查询结果。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button :loading="kuaishouEticketDetailing" @click="handleKuaishouEticketQueryDetail"
|
||||
>查询核销信息</el-button
|
||||
>
|
||||
<el-button
|
||||
:loading="kuaishouEticketConsuming"
|
||||
type="primary"
|
||||
@click="handleKuaishouEticketConsume"
|
||||
>执行核销</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="debug-layout">
|
||||
<div class="debug-main">
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="调试店铺">
|
||||
<el-select
|
||||
v-model="kuaishouEticketForm.selectedShopId"
|
||||
placeholder="请选择店铺"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="shop in kuaishouEticketShops"
|
||||
:key="shop.id"
|
||||
:label="shop.kshopName || shop.shopId || '未命名店铺'"
|
||||
:value="shop.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="查询券码">
|
||||
<el-input
|
||||
v-model="kuaishouEticketForm.queryTicketCode"
|
||||
placeholder="用于查询核销信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="核销数量">
|
||||
<el-input-number
|
||||
v-model="kuaishouEticketForm.num"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="storeId">
|
||||
<el-input v-model="kuaishouEticketForm.storeId" placeholder="默认 0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="auto-fill-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">自动回填参数</span>
|
||||
<span class="text-muted ml-2">查询后生成</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="eTicketId">
|
||||
<el-input
|
||||
:model-value="kuaishouEticketForm.eTicketId"
|
||||
readonly
|
||||
placeholder="查询后自动回填"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="oid">
|
||||
<el-input
|
||||
:model-value="kuaishouEticketForm.oid"
|
||||
readonly
|
||||
placeholder="查询后自动回填"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="formToken">
|
||||
<el-input
|
||||
:model-value="kuaishouEticketForm.formToken"
|
||||
readonly
|
||||
placeholder="查询后自动回填"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div class="result-grid mt-4">
|
||||
<AdminResultCard
|
||||
label="详情查询"
|
||||
:value="
|
||||
kuaishouEticketDetailResult
|
||||
? kuaishouEticketDetailResult.ok
|
||||
? '成功'
|
||||
: '已返回'
|
||||
: '未执行'
|
||||
"
|
||||
>
|
||||
<p v-if="kuaishouEticketDetailResult?.detail">
|
||||
{{
|
||||
kuaishouEticketDetailResult.goods?.itemTitle ||
|
||||
kuaishouEticketDetailResult.goods?.skuDesc ||
|
||||
'-'
|
||||
}}, 剩余 {{ kuaishouEticketDetailResult.detail.leftCount }} /
|
||||
{{ kuaishouEticketDetailResult.detail.totalCount }}
|
||||
</p>
|
||||
<p v-else>先通过查询券码获取核销详情并自动带出核销参数。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="核销执行"
|
||||
:value="
|
||||
kuaishouEticketConsumeResult
|
||||
? kuaishouEticketConsumeResult.consumed
|
||||
? '已处理'
|
||||
: '已返回'
|
||||
: '未执行'
|
||||
"
|
||||
>
|
||||
<p v-if="kuaishouEticketConsumeResult">
|
||||
{{
|
||||
kuaishouEticketConsumeResult.errorMessage ||
|
||||
(kuaishouEticketConsumeResult.ok ? '核销成功' : '接口已返回结果')
|
||||
}}
|
||||
</p>
|
||||
<p v-else>查询详情后再执行核销会更稳妥。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(260px, 0.9fr);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.auto-fill-card {
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.debug-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
kuaishouEticketFilePath: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">说明信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="配置文件">{{
|
||||
kuaishouEticketFilePath || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="职责">
|
||||
这是独立的新流程,只负责快手小店券核销调试与配置,不接入当前已完成的订单来源和履约主链路。
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="建议流程">
|
||||
先为每个店铺录入 kshopName + Cookie,可用 info 接口自动识别店铺名与店铺
|
||||
ID;保存后选择目标店铺,再查询券码并执行核销。
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
type Props = {
|
||||
kuaishouEticketDetailResult: AdminKuaishouEticketDetailResult | null
|
||||
kuaishouEticketConsumeResult: AdminKuaishouEticketConsumeResult | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 详情结果 -->
|
||||
<el-card v-if="kuaishouEticketDetailResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">核销详情返回</span>
|
||||
<p class="card-desc">成功查询后会自动回填 eTicketId、oid、formToken 和建议核销数量。</p>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="debug-pre">{{ JSON.stringify(kuaishouEticketDetailResult.raw, null, 2) }}</pre>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="kuaishouEticketConsumeResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">核销执行返回</span>
|
||||
<p class="card-desc">这里保留接口原始响应,方便核对 result、error_msg 和 requestId。</p>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="debug-pre">{{ JSON.stringify(kuaishouEticketConsumeResult.raw, null, 2) }}</pre>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.debug-pre {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
</style>
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
<script setup lang="ts">
|
||||
import type { EditableKuaishouEticketShop } from '../../composables/types'
|
||||
import type {
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
} from '@/types/admin'
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
|
||||
type KuaishouEticketStats = {
|
||||
configuredShopCount: number
|
||||
cookieReadyCount: number
|
||||
selectedShopReady: boolean
|
||||
detailReady: boolean
|
||||
consumed: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
kuaishouEticketShops: EditableKuaishouEticketShop[]
|
||||
kuaishouEticketForm: {
|
||||
baseUrl: string
|
||||
selectedShopId: string
|
||||
}
|
||||
kuaishouEticketSaving: boolean
|
||||
kuaishouEticketResolvingShopId: string
|
||||
kuaishouEticketResultError: string
|
||||
kuaishouEticketShopInfoResult: AdminKuaishouEticketShopInfoResult | null
|
||||
activeKuaishouEticketShop: EditableKuaishouEticketShop | null
|
||||
kuaishouEticketStats: KuaishouEticketStats
|
||||
addKuaishouEticketShop: () => void
|
||||
removeKuaishouEticketShop: (id: string) => void
|
||||
isKuaishouEticketShopExpanded: (id: string) => boolean
|
||||
toggleKuaishouEticketShop: (id: string) => void
|
||||
expandAllKuaishouEticketShops: () => void
|
||||
collapseAllKuaishouEticketShops: () => void
|
||||
setKuaishouEticketDebugShop: (id: string) => void
|
||||
describeKuaishouEticketShop: (shop: EditableKuaishouEticketShop) => string
|
||||
getKuaishouEticketShopInitial: (shop: EditableKuaishouEticketShop) => string
|
||||
handleKuaishouEticketResolveShopInfo: (shop: EditableKuaishouEticketShop) => void | Promise<void>
|
||||
handleKuaishouEticketSaveSource: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">快手小店店铺配置</span>
|
||||
<p class="card-desc">
|
||||
支持多店铺配置,建议先粘贴 Cookie,再用 info 接口自动识别 kshopName /
|
||||
shopId,最后统一保存到文件。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:disabled="kuaishouEticketShops.length === 0"
|
||||
@click="expandAllKuaishouEticketShops"
|
||||
>全部展开</el-button
|
||||
>
|
||||
<el-button
|
||||
:disabled="kuaishouEticketShops.length === 0"
|
||||
@click="collapseAllKuaishouEticketShops"
|
||||
>全部折叠</el-button
|
||||
>
|
||||
<el-button @click="addKuaishouEticketShop">新增店铺</el-button>
|
||||
<el-button
|
||||
:loading="kuaishouEticketSaving"
|
||||
type="primary"
|
||||
@click="handleKuaishouEticketSaveSource"
|
||||
>保存配置</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item label="Base URL">
|
||||
<el-input
|
||||
v-model="kuaishouEticketForm.baseUrl"
|
||||
placeholder="https://s.kwaixiaodian.com"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-empty
|
||||
v-if="kuaishouEticketShops.length === 0"
|
||||
description="当前还没有快手小店店铺配置,先新增一条。"
|
||||
/>
|
||||
|
||||
<div v-else class="shop-panels">
|
||||
<div class="shop-list">
|
||||
<el-collapse
|
||||
:model-value="
|
||||
kuaishouEticketShops
|
||||
.filter((s) => isKuaishouEticketShopExpanded(s.id))
|
||||
.map((s) => s.id)
|
||||
"
|
||||
>
|
||||
<el-collapse-item
|
||||
v-for="shop in kuaishouEticketShops"
|
||||
:key="shop.id"
|
||||
:name="shop.id"
|
||||
:class="[
|
||||
'shop-panel',
|
||||
{
|
||||
'is-debug': kuaishouEticketForm.selectedShopId === shop.id,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #title>
|
||||
<div class="shop-title-bar">
|
||||
<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-info">
|
||||
<div class="shop-name-row">
|
||||
<strong>{{ shop.kshopName || shop.shopId || '未命名店铺' }}</strong>
|
||||
<el-tag :type="shop.enabled ? 'success' : 'info'" size="small">{{
|
||||
shop.enabled ? '已启用' : '已停用'
|
||||
}}</el-tag>
|
||||
<el-tag
|
||||
v-if="kuaishouEticketForm.selectedShopId === shop.id"
|
||||
type="primary"
|
||||
size="small"
|
||||
>当前调试</el-tag
|
||||
>
|
||||
</div>
|
||||
<div class="shop-meta">
|
||||
<span>店铺 ID:{{ shop.shopId || '等待 info 自动识别' }}</span>
|
||||
<el-tag
|
||||
:type="Boolean(shop.cookie.trim()) ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ shop.cookie.trim() ? 'Cookie 已粘贴' : '待补 Cookie' }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="Boolean(shop.userAvatar || shop.shopId.trim()) ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ shop.userAvatar || shop.shopId.trim() ? '信息已识别' : '未读取信息' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="kshopName">
|
||||
<el-input v-model="shop.kshopName" placeholder="例如 稚嫩游戏交易店" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="店铺 ID">
|
||||
<el-input
|
||||
v-model="shop.shopId"
|
||||
readonly
|
||||
placeholder="通过 info 接口自动识别"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="Cookie">
|
||||
<el-input
|
||||
v-model="shop.cookie"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="直接粘贴该店铺请求头中的完整 Cookie"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="shop-footer">
|
||||
<div class="footer-left">
|
||||
<el-switch v-model="shop.enabled" active-text="启用该店铺" />
|
||||
<span v-if="shop.userAvatar" class="text-muted"
|
||||
>已识别头像,可直接作为调试店铺</span
|
||||
>
|
||||
<span v-else class="text-muted">建议先读取店铺信息确认 Cookie 归属</span>
|
||||
</div>
|
||||
<div class="footer-actions">
|
||||
<el-button
|
||||
:loading="kuaishouEticketResolvingShopId === shop.id"
|
||||
size="small"
|
||||
@click="handleKuaishouEticketResolveShopInfo(shop)"
|
||||
>
|
||||
读取店铺信息
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="kuaishouEticketForm.selectedShopId === shop.id ? 'success' : 'primary'"
|
||||
@click="setKuaishouEticketDebugShop(shop.id)"
|
||||
>
|
||||
{{
|
||||
kuaishouEticketForm.selectedShopId === shop.id
|
||||
? '当前调试店铺'
|
||||
: '设为调试店铺'
|
||||
}}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="removeKuaishouEticketShop(shop.id)"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
|
||||
<div class="side-panel">
|
||||
<AdminResultCard label="店铺总数" :value="kuaishouEticketStats.configuredShopCount">
|
||||
<p>其中 Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} 家。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="当前调试店铺"
|
||||
:value="
|
||||
activeKuaishouEticketShop?.kshopName || activeKuaishouEticketShop?.shopId || '未选择'
|
||||
"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
kuaishouEticketStats.selectedShopReady
|
||||
? '已具备调试条件,可直接查询核销信息。'
|
||||
: '请选择店铺并补全 Cookie。'
|
||||
}}
|
||||
</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="最近识别结果"
|
||||
:value="
|
||||
kuaishouEticketShopInfoResult?.shop.kshopName ||
|
||||
kuaishouEticketShopInfoResult?.shop.shopId ||
|
||||
'未执行'
|
||||
"
|
||||
>
|
||||
<p v-if="kuaishouEticketShopInfoResult">
|
||||
结算状态 {{ kuaishouEticketShopInfoResult.shop.settleStatus }}
|
||||
</p>
|
||||
<p v-else>可用"读取店铺信息"校验 Cookie 是否属于预期店铺。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard label="建议顺序" value="先配置,再调试">
|
||||
<p>1. 新增店铺并粘贴 Cookie</p>
|
||||
<p>2. 读取店铺信息确认 kshopName / shopId</p>
|
||||
<p>3. 设为调试店铺后查询券码并执行核销</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="kuaishouEticketResultError"
|
||||
:title="kuaishouEticketResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shop-panels {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(260px, 0.9fr);
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.shop-list {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.shop-panel.is-debug {
|
||||
border: 1px solid var(--border-hover);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.shop-title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: var(--space-4);
|
||||
}
|
||||
|
||||
.shop-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #dbeafe, #eff6ff);
|
||||
color: var(--color-primary-dark);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--border-hover);
|
||||
}
|
||||
|
||||
.shop-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.shop-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shop-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shop-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.shop-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-3);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.shop-panels {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import type { EditableKuaishouEticketShop } from '../composables/types'
|
||||
import type {
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
} from '@/types/admin'
|
||||
import AdminKuaishouEticketInfoCard from './AdminKuaishouEticketInfoCard.vue'
|
||||
import AdminKuaishouEticketShopConfig from './AdminKuaishouEticketShopConfig.vue'
|
||||
import AdminKuaishouEticketDebug from './AdminKuaishouEticketDebug.vue'
|
||||
import AdminKuaishouEticketResults from './AdminKuaishouEticketResults.vue'
|
||||
|
||||
type KuaishouEticketStats = {
|
||||
configuredShopCount: number
|
||||
cookieReadyCount: number
|
||||
selectedShopReady: boolean
|
||||
detailReady: boolean
|
||||
consumed: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
kuaishouEticketFilePath: string
|
||||
kuaishouEticketShops: EditableKuaishouEticketShop[]
|
||||
kuaishouEticketForm: {
|
||||
baseUrl: string
|
||||
selectedShopId: string
|
||||
queryTicketCode: string
|
||||
eTicketId: string
|
||||
oid: string
|
||||
formToken: string
|
||||
num: number
|
||||
storeId: string
|
||||
}
|
||||
kuaishouEticketSaving: boolean
|
||||
kuaishouEticketResolvingShopId: string
|
||||
kuaishouEticketDetailing: boolean
|
||||
kuaishouEticketConsuming: boolean
|
||||
kuaishouEticketResultError: string
|
||||
kuaishouEticketShopInfoResult: AdminKuaishouEticketShopInfoResult | null
|
||||
kuaishouEticketDetailResult: AdminKuaishouEticketDetailResult | null
|
||||
kuaishouEticketConsumeResult: AdminKuaishouEticketConsumeResult | null
|
||||
activeKuaishouEticketShop: EditableKuaishouEticketShop | null
|
||||
kuaishouEticketStats: KuaishouEticketStats
|
||||
addKuaishouEticketShop: () => void
|
||||
removeKuaishouEticketShop: (id: string) => void
|
||||
isKuaishouEticketShopExpanded: (id: string) => boolean
|
||||
toggleKuaishouEticketShop: (id: string) => void
|
||||
expandAllKuaishouEticketShops: () => void
|
||||
collapseAllKuaishouEticketShops: () => void
|
||||
setKuaishouEticketDebugShop: (id: string) => void
|
||||
describeKuaishouEticketShop: (shop: EditableKuaishouEticketShop) => string
|
||||
getKuaishouEticketShopInitial: (shop: EditableKuaishouEticketShop) => string
|
||||
handleKuaishouEticketResolveShopInfo: (shop: EditableKuaishouEticketShop) => void | Promise<void>
|
||||
handleKuaishouEticketSaveSource: () => void | Promise<void>
|
||||
handleKuaishouEticketQueryDetail: () => void | Promise<void>
|
||||
handleKuaishouEticketConsume: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-section">
|
||||
<AdminKuaishouEticketInfoCard :kuaishou-eticket-file-path="kuaishouEticketFilePath" />
|
||||
|
||||
<AdminKuaishouEticketShopConfig
|
||||
:kuaishou-eticket-shops="kuaishouEticketShops"
|
||||
:kuaishou-eticket-form="kuaishouEticketForm"
|
||||
:kuaishou-eticket-saving="kuaishouEticketSaving"
|
||||
:kuaishou-eticket-resolving-shop-id="kuaishouEticketResolvingShopId"
|
||||
:kuaishou-eticket-result-error="kuaishouEticketResultError"
|
||||
:kuaishou-eticket-shop-info-result="kuaishouEticketShopInfoResult"
|
||||
:active-kuaishou-eticket-shop="activeKuaishouEticketShop"
|
||||
:kuaishou-eticket-stats="kuaishouEticketStats"
|
||||
:add-kuaishou-eticket-shop="addKuaishouEticketShop"
|
||||
:remove-kuaishou-eticket-shop="removeKuaishouEticketShop"
|
||||
:is-kuaishou-eticket-shop-expanded="isKuaishouEticketShopExpanded"
|
||||
:toggle-kuaishou-eticket-shop="toggleKuaishouEticketShop"
|
||||
:expand-all-kuaishou-eticket-shops="expandAllKuaishouEticketShops"
|
||||
:collapse-all-kuaishou-eticket-shops="collapseAllKuaishouEticketShops"
|
||||
:set-kuaishou-eticket-debug-shop="setKuaishouEticketDebugShop"
|
||||
:describe-kuaishou-eticket-shop="describeKuaishouEticketShop"
|
||||
:get-kuaishou-eticket-shop-initial="getKuaishouEticketShopInitial"
|
||||
:handle-kuaishou-eticket-resolve-shop-info="handleKuaishouEticketResolveShopInfo"
|
||||
:handle-kuaishou-eticket-save-source="handleKuaishouEticketSaveSource"
|
||||
/>
|
||||
|
||||
<AdminKuaishouEticketDebug
|
||||
:kuaishou-eticket-shops="kuaishouEticketShops"
|
||||
:kuaishou-eticket-form="kuaishouEticketForm"
|
||||
:kuaishou-eticket-detailing="kuaishouEticketDetailing"
|
||||
:kuaishou-eticket-consuming="kuaishouEticketConsuming"
|
||||
:kuaishou-eticket-detail-result="kuaishouEticketDetailResult"
|
||||
:kuaishou-eticket-consume-result="kuaishouEticketConsumeResult"
|
||||
:handle-kuaishou-eticket-query-detail="handleKuaishouEticketQueryDetail"
|
||||
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
|
||||
/>
|
||||
|
||||
<AdminKuaishouEticketResults
|
||||
:kuaishou-eticket-detail-result="kuaishouEticketDetailResult"
|
||||
:kuaishou-eticket-consume-result="kuaishouEticketConsumeResult"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
</style>
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import type { EditableNotificationRecipient } from '../../composables/types'
|
||||
|
||||
type NotificationForm = {
|
||||
enabled: boolean
|
||||
barkEnabled: boolean
|
||||
barkServerUrl: string
|
||||
testTitle: string
|
||||
testBody: string
|
||||
testUrl: string
|
||||
}
|
||||
|
||||
type NotificationStats = {
|
||||
configuredRecipientCount: number
|
||||
enabledRecipientCount: number
|
||||
barkReady: boolean
|
||||
lastSuccessCount: number
|
||||
lastFailedCount: number
|
||||
scheduledJobEnabledCount: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
notificationForm: NotificationForm
|
||||
notificationRecipients: EditableNotificationRecipient[]
|
||||
notificationSaving: boolean
|
||||
notificationResultError: string
|
||||
notificationStats: NotificationStats
|
||||
addNotificationRecipient: () => void
|
||||
removeNotificationRecipient: (id: string) => void
|
||||
handleNotificationSaveConfig: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">Bark 通知</span>
|
||||
<p class="card-desc">
|
||||
已配置 {{ notificationStats.configuredRecipientCount }} 人,启用
|
||||
{{ notificationStats.enabledRecipientCount }} 人。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button @click="addNotificationRecipient">新增接收人</el-button>
|
||||
<el-button
|
||||
:loading="notificationSaving"
|
||||
type="primary"
|
||||
@click="handleNotificationSaveConfig"
|
||||
>保存配置</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
v-if="notificationResultError"
|
||||
:title="notificationResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<el-form label-width="auto" label-position="top" inline>
|
||||
<el-form-item
|
||||
><el-switch v-model="notificationForm.enabled" active-text="启用内部通知"
|
||||
/></el-form-item>
|
||||
<el-form-item
|
||||
><el-switch v-model="notificationForm.barkEnabled" active-text="启用 Bark 通道"
|
||||
/></el-form-item>
|
||||
<el-form-item label="Bark 服务地址" class="w-full">
|
||||
<el-input v-model="notificationForm.barkServerUrl" placeholder="https://api.day.app" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="notificationRecipients" stripe size="small" class="mt-4">
|
||||
<el-table-column label="接收人" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.name" placeholder="例如:客服A" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Device Key" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.deviceKey" placeholder="Bark device key" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="danger" text @click="removeNotificationRecipient(row.id)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="还没有 Bark 接收人。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
notificationFilePath: string
|
||||
scheduledJobsFilePath: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">配置信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="职责"
|
||||
>内部运营通知,仅发送给后台管理员、客服或值班人员。</el-descriptions-item
|
||||
>
|
||||
<el-descriptions-item label="配置文件">{{
|
||||
notificationFilePath || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="监控任务">{{
|
||||
scheduledJobsFilePath || '-'
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminScheduledJobRuntimeState } from '@/types/admin'
|
||||
import type { EditableScheduledJob } from '../../composables/types'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
const timeUnitOptions = [
|
||||
{ label: '秒', value: 1 },
|
||||
{ label: '分钟', value: 60 },
|
||||
{ label: '小时', value: 3600 },
|
||||
]
|
||||
|
||||
function formatJobType(type: string) {
|
||||
if (type === 'cloudtentacles_health') return 'cloudtentacles 健康检查'
|
||||
return type || '-'
|
||||
}
|
||||
|
||||
function resolveTimeValue(seconds: number) {
|
||||
const normalized = Math.max(0, Number(seconds || 0))
|
||||
if (normalized >= 3600 && normalized % 3600 === 0) {
|
||||
return { amount: normalized / 3600, unit: 3600 }
|
||||
}
|
||||
if (normalized >= 60 && normalized % 60 === 0) {
|
||||
return { amount: normalized / 60, unit: 60 }
|
||||
}
|
||||
return { amount: normalized || 1, unit: 1 }
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number) {
|
||||
const value = resolveTimeValue(seconds)
|
||||
const unit = timeUnitOptions.find((item) => item.value === value.unit)?.label || '秒'
|
||||
return `${value.amount} ${unit}`
|
||||
}
|
||||
|
||||
function bindDuration(job: EditableScheduledJob, key: 'intervalSeconds' | 'cooldownSeconds') {
|
||||
const amountKey = `${key}Amount` as keyof EditableScheduledJob
|
||||
const unitKey = `${key}Unit` as keyof EditableScheduledJob
|
||||
|
||||
if (!Number(job[amountKey])) {
|
||||
const resolved = resolveTimeValue(Number(job[key] || 0))
|
||||
;(job[amountKey] as number) = resolved.amount
|
||||
;(job[unitKey] as number) = resolved.unit
|
||||
}
|
||||
|
||||
return computed<number>({
|
||||
get: () => Number(job[amountKey] || 1),
|
||||
set: (value: number) => {
|
||||
;(job[amountKey] as number) = Math.max(1, Number(value || 1))
|
||||
job[key] = Number(job[amountKey] || 1) * Number(job[unitKey] || 1)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function bindDurationUnit(job: EditableScheduledJob, key: 'intervalSeconds' | 'cooldownSeconds') {
|
||||
const amountKey = `${key}Amount` as keyof EditableScheduledJob
|
||||
const unitKey = `${key}Unit` as keyof EditableScheduledJob
|
||||
|
||||
if (!Number(job[unitKey])) {
|
||||
const resolved = resolveTimeValue(Number(job[key] || 0))
|
||||
;(job[amountKey] as number) = resolved.amount
|
||||
;(job[unitKey] as number) = resolved.unit
|
||||
}
|
||||
|
||||
return computed<number>({
|
||||
get: () => Number(job[unitKey] || 1),
|
||||
set: (value: number) => {
|
||||
;(job[unitKey] as number) = Math.max(1, Number(value || 1))
|
||||
job[key] = Number(job[amountKey] || 1) * Number(job[unitKey] || 1)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
type Props = {
|
||||
scheduledJobsForm: {
|
||||
enabled: boolean
|
||||
}
|
||||
scheduledJobs: EditableScheduledJob[]
|
||||
scheduledJobRuntime: AdminScheduledJobRuntimeState[]
|
||||
scheduledJobsSaving: boolean
|
||||
scheduledJobRunningId: string
|
||||
handleScheduledJobsSaveConfig: () => void | Promise<void>
|
||||
handleScheduledJobRunNow: (jobId: string) => void | Promise<void>
|
||||
getScheduledJobRuntime: (jobId: string) => AdminScheduledJobRuntimeState | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">监控任务</span>
|
||||
<p class="card-desc">
|
||||
间隔表示多久检查一次;冷却表示同一个异常在这段时间内最多通知一次。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:loading="scheduledJobsSaving"
|
||||
type="primary"
|
||||
@click="handleScheduledJobsSaveConfig"
|
||||
>保存任务</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-form-item>
|
||||
<el-switch v-model="scheduledJobsForm.enabled" active-text="启用监控任务系统" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="scheduledJobs" stripe size="small" style="margin-top: 8px">
|
||||
<el-table-column label="任务" min-width="200">
|
||||
<template #default="{ row: job }">
|
||||
<div>
|
||||
<div class="fw-600">{{ formatJobType(job.type) }}</div>
|
||||
<el-switch
|
||||
v-model="job.enabled"
|
||||
size="small"
|
||||
active-text="启用"
|
||||
style="margin-top: 4px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数" min-width="320">
|
||||
<template #default="{ row: job }">
|
||||
<div class="job-params">
|
||||
<div class="param-row">
|
||||
<span class="param-label">检查间隔</span>
|
||||
<input
|
||||
v-model.number="bindDuration(job, 'intervalSeconds').value"
|
||||
type="number"
|
||||
min="1"
|
||||
class="mini-input"
|
||||
/>
|
||||
<select
|
||||
v-model.number="bindDurationUnit(job, 'intervalSeconds').value"
|
||||
class="mini-select"
|
||||
>
|
||||
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">
|
||||
{{ unit.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="param-hint"
|
||||
>每 {{ formatDuration(job.intervalSeconds) }} 检查一次</span
|
||||
>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">通知冷却</span>
|
||||
<input
|
||||
v-model.number="bindDuration(job, 'cooldownSeconds').value"
|
||||
type="number"
|
||||
min="1"
|
||||
class="mini-input"
|
||||
/>
|
||||
<select
|
||||
v-model.number="bindDurationUnit(job, 'cooldownSeconds').value"
|
||||
class="mini-select"
|
||||
>
|
||||
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">
|
||||
{{ unit.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="param-hint"
|
||||
>同类异常 {{ formatDuration(job.cooldownSeconds) }} 内只通知一次</span
|
||||
>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">余额阈值</span>
|
||||
<input
|
||||
v-model.number="job.assetThreshold"
|
||||
type="number"
|
||||
min="0"
|
||||
class="mini-input"
|
||||
/>
|
||||
<span class="param-hint">低于该值时通知</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近状态" min-width="160">
|
||||
<template #default="{ row: job }">
|
||||
<div class="runtime-info">
|
||||
<el-tag
|
||||
:type="
|
||||
getScheduledJobRuntime(job.id)?.lastStatus === 'success' ? 'success' : 'info'
|
||||
"
|
||||
size="small"
|
||||
>
|
||||
{{ getScheduledJobRuntime(job.id)?.lastStatus || 'pending' }}
|
||||
</el-tag>
|
||||
<span class="param-hint">{{
|
||||
getScheduledJobRuntime(job.id)?.lastMessage || '-'
|
||||
}}</span>
|
||||
<span class="param-hint"
|
||||
>余额:{{ getScheduledJobRuntime(job.id)?.lastAsset ?? '-' }}</span
|
||||
>
|
||||
<span class="param-hint"
|
||||
>上次:{{
|
||||
formatAdminDateTime(getScheduledJobRuntime(job.id)?.lastRunAt || '')
|
||||
}}</span
|
||||
>
|
||||
<span class="param-hint"
|
||||
>下次:{{
|
||||
formatAdminDateTime(getScheduledJobRuntime(job.id)?.nextRunAt || '')
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" align="center">
|
||||
<template #default="{ row: job }">
|
||||
<el-button
|
||||
:loading="scheduledJobRunningId === job.id"
|
||||
size="small"
|
||||
@click="handleScheduledJobRunNow(job.id)"
|
||||
>
|
||||
立即检查
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无可配置的监控任务。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.job-params {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.param-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.mini-input {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-surface);
|
||||
font-size: var(--text-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mini-select {
|
||||
width: 80px;
|
||||
height: 32px;
|
||||
padding: 0 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-surface);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.param-hint {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.runtime-info {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
</style>
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminNotificationTestResult } from '@/types/admin'
|
||||
|
||||
type NotificationForm = {
|
||||
enabled: boolean
|
||||
barkEnabled: boolean
|
||||
barkServerUrl: string
|
||||
testTitle: string
|
||||
testBody: string
|
||||
testUrl: string
|
||||
}
|
||||
|
||||
type NotificationStats = {
|
||||
configuredRecipientCount: number
|
||||
enabledRecipientCount: number
|
||||
barkReady: boolean
|
||||
lastSuccessCount: number
|
||||
lastFailedCount: number
|
||||
scheduledJobEnabledCount: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
notificationForm: NotificationForm
|
||||
notificationTesting: boolean
|
||||
notificationTestResult: AdminNotificationTestResult | null
|
||||
notificationStats: NotificationStats
|
||||
handleNotificationTest: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">测试发送</span>
|
||||
<p class="card-desc">测试会发送给所有已启用且填写了 Device Key 的接收人。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:disabled="!notificationStats.barkReady"
|
||||
:loading="notificationTesting"
|
||||
type="primary"
|
||||
@click="handleNotificationTest"
|
||||
>
|
||||
发送测试
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="notificationForm.testTitle" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="内容">
|
||||
<el-input v-model="notificationForm.testBody" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="跳转链接">
|
||||
<el-input v-model="notificationForm.testUrl" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<div v-if="notificationTestResult" class="result-grid">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">成功</div>
|
||||
<div class="stat-value">{{ notificationTestResult.successCount }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">失败</div>
|
||||
<div class="stat-value">{{ notificationTestResult.failedCount }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">通道</div>
|
||||
<div class="stat-value">{{ notificationTestResult.channel || '-' }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-if="notificationTestResult"
|
||||
:data="notificationTestResult.results"
|
||||
stripe
|
||||
size="small"
|
||||
class="mt-4"
|
||||
>
|
||||
<el-table-column label="接收人">
|
||||
<template #default="{ row }">{{
|
||||
row.recipientName || row.recipientKeyMasked || '-'
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.ok ? 'success' : 'danger'" size="small">{{
|
||||
row.ok ? '成功' : '失败'
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态码" width="80" align="center" />
|
||||
<el-table-column label="说明">
|
||||
<template #default="{ row }">{{ row.errorMessage || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: var(--space-1) 0 0;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
</style>
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminNotificationTestResult, AdminScheduledJobRuntimeState } from '@/types/admin'
|
||||
import type { EditableNotificationRecipient, EditableScheduledJob } from '../../composables/types'
|
||||
import AdminNotificationInfoCard from './AdminNotificationInfoCard.vue'
|
||||
import AdminNotificationBarkCard from './AdminNotificationBarkCard.vue'
|
||||
import AdminNotificationScheduledJobsCard from './AdminNotificationScheduledJobsCard.vue'
|
||||
import AdminNotificationTestCard from './AdminNotificationTestCard.vue'
|
||||
|
||||
type NotificationForm = {
|
||||
enabled: boolean
|
||||
barkEnabled: boolean
|
||||
barkServerUrl: string
|
||||
testTitle: string
|
||||
testBody: string
|
||||
testUrl: string
|
||||
}
|
||||
|
||||
type ScheduledJobsForm = {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
type NotificationStats = {
|
||||
configuredRecipientCount: number
|
||||
enabledRecipientCount: number
|
||||
barkReady: boolean
|
||||
lastSuccessCount: number
|
||||
lastFailedCount: number
|
||||
scheduledJobEnabledCount: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
notificationFilePath: string
|
||||
notificationForm: NotificationForm
|
||||
notificationRecipients: EditableNotificationRecipient[]
|
||||
notificationSaving: boolean
|
||||
notificationTesting: boolean
|
||||
notificationResultError: string
|
||||
notificationTestResult: AdminNotificationTestResult | null
|
||||
scheduledJobsFilePath: string
|
||||
scheduledJobsForm: ScheduledJobsForm
|
||||
scheduledJobs: EditableScheduledJob[]
|
||||
scheduledJobRuntime: AdminScheduledJobRuntimeState[]
|
||||
scheduledJobsSaving: boolean
|
||||
scheduledJobRunningId: string
|
||||
notificationStats: NotificationStats
|
||||
addNotificationRecipient: () => void
|
||||
removeNotificationRecipient: (id: string) => void
|
||||
handleNotificationSaveConfig: () => void | Promise<void>
|
||||
handleNotificationTest: () => void | Promise<void>
|
||||
handleScheduledJobsSaveConfig: () => void | Promise<void>
|
||||
handleScheduledJobRunNow: (jobId: string) => void | Promise<void>
|
||||
getScheduledJobRuntime: (jobId: string) => AdminScheduledJobRuntimeState | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-section">
|
||||
<AdminNotificationInfoCard
|
||||
:notification-file-path="notificationFilePath"
|
||||
:scheduled-jobs-file-path="scheduledJobsFilePath"
|
||||
/>
|
||||
|
||||
<AdminNotificationBarkCard
|
||||
:notification-form="notificationForm"
|
||||
:notification-recipients="notificationRecipients"
|
||||
:notification-saving="notificationSaving"
|
||||
:notification-result-error="notificationResultError"
|
||||
:notification-stats="notificationStats"
|
||||
:add-notification-recipient="addNotificationRecipient"
|
||||
:remove-notification-recipient="removeNotificationRecipient"
|
||||
:handle-notification-save-config="handleNotificationSaveConfig"
|
||||
/>
|
||||
|
||||
<AdminNotificationScheduledJobsCard
|
||||
:scheduled-jobs-form="scheduledJobsForm"
|
||||
:scheduled-jobs="scheduledJobs"
|
||||
:scheduled-job-runtime="scheduledJobRuntime"
|
||||
:scheduled-jobs-saving="scheduledJobsSaving"
|
||||
:scheduled-job-running-id="scheduledJobRunningId"
|
||||
:handle-scheduled-jobs-save-config="handleScheduledJobsSaveConfig"
|
||||
:handle-scheduled-job-run-now="handleScheduledJobRunNow"
|
||||
:get-scheduled-job-runtime="getScheduledJobRuntime"
|
||||
/>
|
||||
|
||||
<AdminNotificationTestCard
|
||||
:notification-form="notificationForm"
|
||||
:notification-testing="notificationTesting"
|
||||
:notification-test-result="notificationTestResult"
|
||||
:notification-stats="notificationStats"
|
||||
:handle-notification-test="handleNotificationTest"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
</style>
|
||||
+22
-225
@@ -2,24 +2,10 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
appointAdminCloudtentaclesVn,
|
||||
backAdminCloudtentaclesVn,
|
||||
buyAdminCloudtentaclesSku,
|
||||
fetchAdminCloudtentaclesAsset,
|
||||
fetchAdminCloudtentaclesBindUrl,
|
||||
fetchAdminCloudtentaclesCategories,
|
||||
fetchAdminCloudtentaclesKnapsack,
|
||||
fetchAdminCloudtentaclesSkuList,
|
||||
fetchAdminCloudtentaclesVnCode,
|
||||
fetchAdminCloudtentaclesVnList,
|
||||
generateAdminCloudtentaclesVnLoginCode,
|
||||
runAdminCloudtentaclesFullFlow,
|
||||
saveAdminCloudtentaclesSourceConfig,
|
||||
sendAdminCloudtentaclesSmsCode,
|
||||
testAdminCloudtentaclesLogin,
|
||||
useAdminCloudtentaclesSku,
|
||||
validateAdminCloudtentaclesSession,
|
||||
verifyAdminCloudtentaclesVnCode,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
@@ -29,10 +15,13 @@ import type {
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
import type { CloudtentaclesForm } from './useCloudtentaclesDebugActions'
|
||||
import { useCloudtentaclesDebugActions } from './useCloudtentaclesDebugActions'
|
||||
|
||||
export function useAdminCloudtentaclesPlatform() {
|
||||
const cloudtentaclesFilePath = ref('')
|
||||
const cloudtentaclesSessionFilePath = ref('')
|
||||
const cloudtentaclesForm = ref({
|
||||
const cloudtentaclesForm = ref<CloudtentaclesForm>({
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
username: '',
|
||||
password: '',
|
||||
@@ -54,10 +43,10 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
const cloudtentaclesSmsResult = ref<AdminCloudtentaclesSendSmsResult | null>(null)
|
||||
const cloudtentaclesLoginResult = ref<AdminCloudtentaclesLoginTestResult | null>(null)
|
||||
const cloudtentaclesValidateResult = ref<AdminCloudtentaclesValidateSessionResult | null>(null)
|
||||
const cloudtentaclesDebugLoading = ref(false)
|
||||
const cloudtentaclesDebugResult = ref('')
|
||||
const cloudtentaclesPersistedSession = ref<AdminCloudtentaclesPersistedSession | null>(null)
|
||||
|
||||
const debugActions = useCloudtentaclesDebugActions(cloudtentaclesForm, cloudtentaclesResultError)
|
||||
|
||||
const cloudtentaclesStats = computed(() => ({
|
||||
hasCredential: Boolean(
|
||||
cloudtentaclesForm.value.username.trim() &&
|
||||
@@ -260,198 +249,6 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildCloudtentaclesTokenPayload() {
|
||||
return {
|
||||
baseUrl: cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176',
|
||||
token: cloudtentaclesForm.value.token.trim(),
|
||||
deviceId: cloudtentaclesForm.value.deviceId.trim() || '-',
|
||||
deviceType: Number(cloudtentaclesForm.value.deviceType || 0),
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCloudtentaclesToken() {
|
||||
if (!cloudtentaclesForm.value.token.trim()) {
|
||||
cloudtentaclesResultError.value = '请先登录或手动填写 token'
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function renderCloudtentaclesDebugResult(label: string, data: unknown) {
|
||||
cloudtentaclesDebugResult.value = `${label}\n${JSON.stringify(data, null, 2)}`
|
||||
}
|
||||
|
||||
async function runCloudtentaclesDebugAction(
|
||||
label: string,
|
||||
runner: () => Promise<{ data: unknown }>,
|
||||
) {
|
||||
cloudtentaclesDebugLoading.value = true
|
||||
cloudtentaclesResultError.value = ''
|
||||
|
||||
try {
|
||||
const response = await runner()
|
||||
renderCloudtentaclesDebugResult(label, response.data)
|
||||
showSuccess(`${label}成功`)
|
||||
} catch (error) {
|
||||
cloudtentaclesResultError.value = error instanceof Error ? error.message : `${label}失败`
|
||||
showError(cloudtentaclesResultError.value)
|
||||
} finally {
|
||||
cloudtentaclesDebugLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchAsset() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询余额', () =>
|
||||
fetchAdminCloudtentaclesAsset(buildCloudtentaclesTokenPayload()),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchCategories() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询分类', () =>
|
||||
fetchAdminCloudtentaclesCategories(buildCloudtentaclesTokenPayload()),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchSkuList() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询SKU列表', () =>
|
||||
fetchAdminCloudtentaclesSkuList(buildCloudtentaclesTokenPayload()),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesBuySku() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('购买SKU', () =>
|
||||
buyAdminCloudtentaclesSku({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
id: Number(cloudtentaclesForm.value.skuId || 0),
|
||||
count: Number(cloudtentaclesForm.value.skuCount || 1),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesUseSku() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('执行发货', () =>
|
||||
useAdminCloudtentaclesSku({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
id: Number(cloudtentaclesForm.value.skuId || 0),
|
||||
virtualNumberId: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
phone: cloudtentaclesForm.value.phone.trim(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchKnapsack() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询背包', () =>
|
||||
fetchAdminCloudtentaclesKnapsack(buildCloudtentaclesTokenPayload()),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchVnList() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询虚拟号列表', () =>
|
||||
fetchAdminCloudtentaclesVnList({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesAppointVn() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('申请虚拟号', async () => {
|
||||
const response = await appointAdminCloudtentaclesVn({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
})
|
||||
const item = (response.data as { item?: { id?: number; phone?: string } }).item
|
||||
if (item?.id) {
|
||||
cloudtentaclesForm.value.vnId = Number(item.id)
|
||||
}
|
||||
if (item?.phone) {
|
||||
cloudtentaclesForm.value.phone = String(item.phone)
|
||||
}
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesGenerateVnLoginCode() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('生成登录码', () =>
|
||||
generateAdminCloudtentaclesVnLoginCode({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
id: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchVnCode() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('获取验证码', async () => {
|
||||
const response = await fetchAdminCloudtentaclesVnCode({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
phone: cloudtentaclesForm.value.phone.trim(),
|
||||
})
|
||||
const code = (response.data as { code?: string }).code
|
||||
if (code) {
|
||||
cloudtentaclesForm.value.code = String(code)
|
||||
}
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesVerifyVnCode() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('校验验证码', () =>
|
||||
verifyAdminCloudtentaclesVnCode({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
id: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
code: cloudtentaclesForm.value.code.trim(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchBindUrl() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('获取兑换链接', () =>
|
||||
fetchAdminCloudtentaclesBindUrl({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
id: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesBackVn() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('退还号码', () =>
|
||||
backAdminCloudtentaclesVn({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
id: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesRunFullFlow() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('完整测试流程', () =>
|
||||
runAdminCloudtentaclesFullFlow({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
skuId: Number(cloudtentaclesForm.value.skuId || 0),
|
||||
skuCount: Number(cloudtentaclesForm.value.skuCount || 1),
|
||||
vnKey: cloudtentaclesForm.value.vnKey.trim(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
cloudtentaclesFilePath,
|
||||
cloudtentaclesSessionFilePath,
|
||||
@@ -464,8 +261,8 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
cloudtentaclesSmsResult,
|
||||
cloudtentaclesLoginResult,
|
||||
cloudtentaclesValidateResult,
|
||||
cloudtentaclesDebugLoading,
|
||||
cloudtentaclesDebugResult,
|
||||
cloudtentaclesDebugLoading: debugActions.cloudtentaclesDebugLoading,
|
||||
cloudtentaclesDebugResult: debugActions.cloudtentaclesDebugResult,
|
||||
cloudtentaclesPersistedSession,
|
||||
cloudtentaclesStats,
|
||||
hydrateCloudtentaclesConfig,
|
||||
@@ -473,19 +270,19 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
handleCloudtentaclesSendSmsCode,
|
||||
handleCloudtentaclesTestLogin,
|
||||
handleCloudtentaclesValidateSession,
|
||||
handleCloudtentaclesFetchAsset,
|
||||
handleCloudtentaclesFetchCategories,
|
||||
handleCloudtentaclesFetchSkuList,
|
||||
handleCloudtentaclesBuySku,
|
||||
handleCloudtentaclesUseSku,
|
||||
handleCloudtentaclesFetchKnapsack,
|
||||
handleCloudtentaclesFetchVnList,
|
||||
handleCloudtentaclesAppointVn,
|
||||
handleCloudtentaclesGenerateVnLoginCode,
|
||||
handleCloudtentaclesFetchVnCode,
|
||||
handleCloudtentaclesVerifyVnCode,
|
||||
handleCloudtentaclesFetchBindUrl,
|
||||
handleCloudtentaclesBackVn,
|
||||
handleCloudtentaclesRunFullFlow,
|
||||
handleCloudtentaclesFetchAsset: debugActions.handleCloudtentaclesFetchAsset,
|
||||
handleCloudtentaclesFetchCategories: debugActions.handleCloudtentaclesFetchCategories,
|
||||
handleCloudtentaclesFetchSkuList: debugActions.handleCloudtentaclesFetchSkuList,
|
||||
handleCloudtentaclesBuySku: debugActions.handleCloudtentaclesBuySku,
|
||||
handleCloudtentaclesUseSku: debugActions.handleCloudtentaclesUseSku,
|
||||
handleCloudtentaclesFetchKnapsack: debugActions.handleCloudtentaclesFetchKnapsack,
|
||||
handleCloudtentaclesFetchVnList: debugActions.handleCloudtentaclesFetchVnList,
|
||||
handleCloudtentaclesAppointVn: debugActions.handleCloudtentaclesAppointVn,
|
||||
handleCloudtentaclesGenerateVnLoginCode: debugActions.handleCloudtentaclesGenerateVnLoginCode,
|
||||
handleCloudtentaclesFetchVnCode: debugActions.handleCloudtentaclesFetchVnCode,
|
||||
handleCloudtentaclesVerifyVnCode: debugActions.handleCloudtentaclesVerifyVnCode,
|
||||
handleCloudtentaclesFetchBindUrl: debugActions.handleCloudtentaclesFetchBindUrl,
|
||||
handleCloudtentaclesBackVn: debugActions.handleCloudtentaclesBackVn,
|
||||
handleCloudtentaclesRunFullFlow: debugActions.handleCloudtentaclesRunFullFlow,
|
||||
}
|
||||
}
|
||||
|
||||
+45
-108
@@ -1,5 +1,4 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { ref, computed, type Ref } from 'vue'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
consumeAdminKuaishouEticket,
|
||||
@@ -10,18 +9,28 @@ import {
|
||||
import type {
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketShopConfigItem,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketSourceConfig,
|
||||
} from '@/types/admin'
|
||||
|
||||
import type { EditableKuaishouEticketShop } from './types'
|
||||
import { useKuaishouEticketShopManager } from './useKuaishouEticketShopManager'
|
||||
|
||||
type KuaishouEticketForm = {
|
||||
baseUrl: string
|
||||
selectedShopId: string
|
||||
queryTicketCode: string
|
||||
eTicketId: string
|
||||
oid: string
|
||||
formToken: string
|
||||
num: number
|
||||
storeId: string
|
||||
}
|
||||
|
||||
export function useAdminKuaishouEticketPlatform() {
|
||||
const shopManager = useKuaishouEticketShopManager()
|
||||
|
||||
const kuaishouEticketFilePath = ref('')
|
||||
const kuaishouEticketShops = ref<EditableKuaishouEticketShop[]>([])
|
||||
const expandedKuaishouEticketShopIds = ref<string[]>([])
|
||||
const kuaishouEticketForm = ref({
|
||||
const kuaishouEticketForm = ref<KuaishouEticketForm>({
|
||||
baseUrl: 'https://s.kwaixiaodian.com',
|
||||
selectedShopId: '',
|
||||
queryTicketCode: '',
|
||||
@@ -42,14 +51,14 @@ export function useAdminKuaishouEticketPlatform() {
|
||||
|
||||
const activeKuaishouEticketShop = computed(
|
||||
() =>
|
||||
kuaishouEticketShops.value.find(
|
||||
shopManager.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,
|
||||
configuredShopCount: shopManager.kuaishouEticketShops.value.length,
|
||||
cookieReadyCount: shopManager.kuaishouEticketShops.value.filter((item) => item.cookie.trim()).length,
|
||||
selectedShopReady: Boolean(activeKuaishouEticketShop.value?.cookie.trim()),
|
||||
detailReady: Boolean(kuaishouEticketDetailResult.value?.detail),
|
||||
consumed: Boolean(kuaishouEticketConsumeResult.value?.consumed),
|
||||
@@ -59,11 +68,11 @@ export function useAdminKuaishouEticketPlatform() {
|
||||
filePath: string
|
||||
source: {
|
||||
baseUrl?: string
|
||||
shops: AdminKuaishouEticketShopConfigItem[]
|
||||
shops: AdminKuaishouEticketShopInfoResult['shop'][]
|
||||
}
|
||||
}) {
|
||||
kuaishouEticketFilePath.value = data.filePath
|
||||
kuaishouEticketShops.value = data.source.shops.map(mapEditableKuaishouEticketShop)
|
||||
shopManager.setShopsFromConfig(data.source.shops as any)
|
||||
kuaishouEticketForm.value = {
|
||||
baseUrl: data.source.baseUrl || 'https://s.kwaixiaodian.com',
|
||||
selectedShopId: '',
|
||||
@@ -74,9 +83,9 @@ export function useAdminKuaishouEticketPlatform() {
|
||||
num: 1,
|
||||
storeId: '0',
|
||||
}
|
||||
kuaishouEticketForm.value.selectedShopId = kuaishouEticketShops.value[0]?.id || ''
|
||||
expandedKuaishouEticketShopIds.value = kuaishouEticketShops.value[0]?.id
|
||||
? [kuaishouEticketShops.value[0].id]
|
||||
kuaishouEticketForm.value.selectedShopId = shopManager.kuaishouEticketShops.value[0]?.id || ''
|
||||
shopManager.expandedKuaishouEticketShopIds.value = shopManager.kuaishouEticketShops.value[0]?.id
|
||||
? [shopManager.kuaishouEticketShops.value[0].id]
|
||||
: []
|
||||
}
|
||||
|
||||
@@ -84,7 +93,7 @@ export function useAdminKuaishouEticketPlatform() {
|
||||
return {
|
||||
enabled: true,
|
||||
baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com',
|
||||
shops: kuaishouEticketShops.value.map((item) => ({
|
||||
shops: shopManager.kuaishouEticketShops.value.map((item) => ({
|
||||
shopId: item.shopId.trim(),
|
||||
kshopName: item.kshopName.trim(),
|
||||
cookie: item.cookie.trim(),
|
||||
@@ -97,66 +106,20 @@ export function useAdminKuaishouEticketPlatform() {
|
||||
}
|
||||
|
||||
function addKuaishouEticketShop() {
|
||||
const shop = createEmptyKuaishouEticketShop()
|
||||
kuaishouEticketShops.value.unshift(shop)
|
||||
setKuaishouEticketDebugShop(shop.id)
|
||||
const { newSelectedId } = shopManager.addKuaishouEticketShop(kuaishouEticketForm.value.selectedShopId)
|
||||
setKuaishouEticketDebugShop(newSelectedId)
|
||||
}
|
||||
|
||||
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,
|
||||
kuaishouEticketForm.value.selectedShopId = shopManager.removeKuaishouEticketShop(
|
||||
id,
|
||||
kuaishouEticketForm.value.selectedShopId,
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
shopManager.expandKuaishouEticketShop(id)
|
||||
}
|
||||
|
||||
function ensureSelectedKuaishouEticketShop() {
|
||||
@@ -264,14 +227,12 @@ export function useAdminKuaishouEticketPlatform() {
|
||||
kuaishouEticketForm.value.baseUrl =
|
||||
response.data.source.baseUrl || 'https://s.kwaixiaodian.com'
|
||||
const previousSelectedShopId = activeKuaishouEticketShop.value?.shopId || ''
|
||||
kuaishouEticketShops.value = response.data.source.shops.map(mapEditableKuaishouEticketShop)
|
||||
shopManager.setShopsFromConfig(response.data.source.shops)
|
||||
kuaishouEticketForm.value.selectedShopId =
|
||||
kuaishouEticketShops.value.find(
|
||||
(item) => item.shopId && item.shopId === previousSelectedShopId,
|
||||
)?.id ||
|
||||
kuaishouEticketShops.value[0]?.id ||
|
||||
shopManager.findShopByShopId(previousSelectedShopId)?.id ||
|
||||
shopManager.kuaishouEticketShops.value[0]?.id ||
|
||||
''
|
||||
expandedKuaishouEticketShopIds.value = kuaishouEticketForm.value.selectedShopId
|
||||
shopManager.expandedKuaishouEticketShopIds.value = kuaishouEticketForm.value.selectedShopId
|
||||
? [kuaishouEticketForm.value.selectedShopId]
|
||||
: []
|
||||
showSuccess('快手小店核销配置已保存')
|
||||
@@ -351,8 +312,8 @@ export function useAdminKuaishouEticketPlatform() {
|
||||
|
||||
return {
|
||||
kuaishouEticketFilePath,
|
||||
kuaishouEticketShops,
|
||||
expandedKuaishouEticketShopIds,
|
||||
kuaishouEticketShops: shopManager.kuaishouEticketShops,
|
||||
expandedKuaishouEticketShopIds: shopManager.expandedKuaishouEticketShopIds,
|
||||
kuaishouEticketForm,
|
||||
kuaishouEticketSaving,
|
||||
kuaishouEticketResolvingShopId,
|
||||
@@ -367,42 +328,18 @@ export function useAdminKuaishouEticketPlatform() {
|
||||
hydrateKuaishouEticketConfig,
|
||||
addKuaishouEticketShop,
|
||||
removeKuaishouEticketShop,
|
||||
isKuaishouEticketShopExpanded,
|
||||
expandKuaishouEticketShop,
|
||||
collapseKuaishouEticketShop,
|
||||
toggleKuaishouEticketShop,
|
||||
expandAllKuaishouEticketShops,
|
||||
collapseAllKuaishouEticketShops,
|
||||
isKuaishouEticketShopExpanded: shopManager.isKuaishouEticketShopExpanded,
|
||||
expandKuaishouEticketShop: shopManager.expandKuaishouEticketShop,
|
||||
collapseKuaishouEticketShop: shopManager.collapseKuaishouEticketShop,
|
||||
toggleKuaishouEticketShop: shopManager.toggleKuaishouEticketShop,
|
||||
expandAllKuaishouEticketShops: shopManager.expandAllKuaishouEticketShops,
|
||||
collapseAllKuaishouEticketShops: shopManager.collapseAllKuaishouEticketShops,
|
||||
setKuaishouEticketDebugShop,
|
||||
describeKuaishouEticketShop,
|
||||
getKuaishouEticketShopInitial,
|
||||
describeKuaishouEticketShop: shopManager.describeKuaishouEticketShop,
|
||||
getKuaishouEticketShopInitial: shopManager.getKuaishouEticketShopInitial,
|
||||
handleKuaishouEticketResolveShopInfo,
|
||||
handleKuaishouEticketSaveSource,
|
||||
handleKuaishouEticketQueryDetail,
|
||||
handleKuaishouEticketConsume,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import { ref } from 'vue'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
appointAdminCloudtentaclesVn,
|
||||
backAdminCloudtentaclesVn,
|
||||
buyAdminCloudtentaclesSku,
|
||||
fetchAdminCloudtentaclesAsset,
|
||||
fetchAdminCloudtentaclesBindUrl,
|
||||
fetchAdminCloudtentaclesCategories,
|
||||
fetchAdminCloudtentaclesKnapsack,
|
||||
fetchAdminCloudtentaclesSkuList,
|
||||
fetchAdminCloudtentaclesVnCode,
|
||||
fetchAdminCloudtentaclesVnList,
|
||||
generateAdminCloudtentaclesVnLoginCode,
|
||||
runAdminCloudtentaclesFullFlow,
|
||||
useAdminCloudtentaclesSku,
|
||||
verifyAdminCloudtentaclesVnCode,
|
||||
} from '@/services/admin'
|
||||
|
||||
export type CloudtentaclesForm = {
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
code: string
|
||||
token: string
|
||||
vnKey: string
|
||||
vnId: number
|
||||
skuId: number
|
||||
skuCount: number
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
export function useCloudtentaclesDebugActions(
|
||||
cloudtentaclesForm: { value: CloudtentaclesForm },
|
||||
cloudtentaclesResultError: { value: string },
|
||||
) {
|
||||
const cloudtentaclesDebugLoading = ref(false)
|
||||
const cloudtentaclesDebugResult = ref('')
|
||||
|
||||
function buildCloudtentaclesTokenPayload() {
|
||||
return {
|
||||
baseUrl: cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176',
|
||||
token: cloudtentaclesForm.value.token.trim(),
|
||||
deviceId: cloudtentaclesForm.value.deviceId.trim() || '-',
|
||||
deviceType: Number(cloudtentaclesForm.value.deviceType || 0),
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCloudtentaclesToken() {
|
||||
if (!cloudtentaclesForm.value.token.trim()) {
|
||||
cloudtentaclesResultError.value = '请先登录或手动填写 token'
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function renderCloudtentaclesDebugResult(label: string, data: unknown) {
|
||||
cloudtentaclesDebugResult.value = `${label}\n${JSON.stringify(data, null, 2)}`
|
||||
}
|
||||
|
||||
async function runCloudtentaclesDebugAction(
|
||||
label: string,
|
||||
runner: () => Promise<{ data: unknown }>,
|
||||
) {
|
||||
cloudtentaclesDebugLoading.value = true
|
||||
cloudtentaclesResultError.value = ''
|
||||
|
||||
try {
|
||||
const response = await runner()
|
||||
renderCloudtentaclesDebugResult(label, response.data)
|
||||
showSuccess(`${label}成功`)
|
||||
} catch (error) {
|
||||
cloudtentaclesResultError.value = error instanceof Error ? error.message : `${label}失败`
|
||||
showError(cloudtentaclesResultError.value)
|
||||
} finally {
|
||||
cloudtentaclesDebugLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchAsset() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询余额', () =>
|
||||
fetchAdminCloudtentaclesAsset(buildCloudtentaclesTokenPayload()),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchCategories() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询分类', () =>
|
||||
fetchAdminCloudtentaclesCategories(buildCloudtentaclesTokenPayload()),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchSkuList() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询SKU列表', () =>
|
||||
fetchAdminCloudtentaclesSkuList(buildCloudtentaclesTokenPayload()),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesBuySku() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('购买SKU', () =>
|
||||
buyAdminCloudtentaclesSku({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
id: Number(cloudtentaclesForm.value.skuId || 0),
|
||||
count: Number(cloudtentaclesForm.value.skuCount || 1),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesUseSku() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('执行发货', () =>
|
||||
useAdminCloudtentaclesSku({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
id: Number(cloudtentaclesForm.value.skuId || 0),
|
||||
virtualNumberId: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
phone: cloudtentaclesForm.value.phone.trim(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchKnapsack() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询背包', () =>
|
||||
fetchAdminCloudtentaclesKnapsack(buildCloudtentaclesTokenPayload()),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchVnList() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('查询虚拟号列表', () =>
|
||||
fetchAdminCloudtentaclesVnList({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesAppointVn() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('申请虚拟号', async () => {
|
||||
const response = await appointAdminCloudtentaclesVn({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
})
|
||||
const item = (response.data as { item?: { id?: number; phone?: string } }).item
|
||||
if (item?.id) {
|
||||
cloudtentaclesForm.value.vnId = Number(item.id)
|
||||
}
|
||||
if (item?.phone) {
|
||||
cloudtentaclesForm.value.phone = String(item.phone)
|
||||
}
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesGenerateVnLoginCode() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('生成登录码', () =>
|
||||
generateAdminCloudtentaclesVnLoginCode({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
id: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchVnCode() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('获取验证码', async () => {
|
||||
const response = await fetchAdminCloudtentaclesVnCode({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
phone: cloudtentaclesForm.value.phone.trim(),
|
||||
})
|
||||
const code = (response.data as { code?: string }).code
|
||||
if (code) {
|
||||
cloudtentaclesForm.value.code = String(code)
|
||||
}
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesVerifyVnCode() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('校验验证码', () =>
|
||||
verifyAdminCloudtentaclesVnCode({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
id: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
code: cloudtentaclesForm.value.code.trim(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesFetchBindUrl() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('获取兑换链接', () =>
|
||||
fetchAdminCloudtentaclesBindUrl({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
id: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesBackVn() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('退还号码', () =>
|
||||
backAdminCloudtentaclesVn({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
key: cloudtentaclesForm.value.vnKey.trim(),
|
||||
id: Number(cloudtentaclesForm.value.vnId || 0),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCloudtentaclesRunFullFlow() {
|
||||
if (!ensureCloudtentaclesToken()) return
|
||||
await runCloudtentaclesDebugAction('完整测试流程', () =>
|
||||
runAdminCloudtentaclesFullFlow({
|
||||
...buildCloudtentaclesTokenPayload(),
|
||||
skuId: Number(cloudtentaclesForm.value.skuId || 0),
|
||||
skuCount: Number(cloudtentaclesForm.value.skuCount || 1),
|
||||
vnKey: cloudtentaclesForm.value.vnKey.trim(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
cloudtentaclesDebugLoading,
|
||||
cloudtentaclesDebugResult,
|
||||
buildCloudtentaclesTokenPayload,
|
||||
ensureCloudtentaclesToken,
|
||||
handleCloudtentaclesFetchAsset,
|
||||
handleCloudtentaclesFetchCategories,
|
||||
handleCloudtentaclesFetchSkuList,
|
||||
handleCloudtentaclesBuySku,
|
||||
handleCloudtentaclesUseSku,
|
||||
handleCloudtentaclesFetchKnapsack,
|
||||
handleCloudtentaclesFetchVnList,
|
||||
handleCloudtentaclesAppointVn,
|
||||
handleCloudtentaclesGenerateVnLoginCode,
|
||||
handleCloudtentaclesFetchVnCode,
|
||||
handleCloudtentaclesVerifyVnCode,
|
||||
handleCloudtentaclesFetchBindUrl,
|
||||
handleCloudtentaclesBackVn,
|
||||
handleCloudtentaclesRunFullFlow,
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { ref } from 'vue'
|
||||
import type { EditableKuaishouEticketShop } from '../types'
|
||||
import type { AdminKuaishouEticketShopConfigItem } from '@/types/admin'
|
||||
|
||||
export function useKuaishouEticketShopManager() {
|
||||
const kuaishouEticketShops = ref<EditableKuaishouEticketShop[]>([])
|
||||
const expandedKuaishouEticketShopIds = ref<string[]>([])
|
||||
|
||||
function addKuaishouEticketShop(selectedShopId: string) {
|
||||
const shop = createEmptyKuaishouEticketShop()
|
||||
kuaishouEticketShops.value.unshift(shop)
|
||||
return { shop, newSelectedId: shop.id }
|
||||
}
|
||||
|
||||
function removeKuaishouEticketShop(id: string, currentSelectedId: string) {
|
||||
kuaishouEticketShops.value = kuaishouEticketShops.value.filter((item) => item.id !== id)
|
||||
collapseKuaishouEticketShop(id)
|
||||
if (currentSelectedId === id) {
|
||||
return kuaishouEticketShops.value[0]?.id || ''
|
||||
}
|
||||
return currentSelectedId
|
||||
}
|
||||
|
||||
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 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 setShopsFromConfig(shops: AdminKuaishouEticketShopConfigItem[]) {
|
||||
kuaishouEticketShops.value = shops.map(mapEditableKuaishouEticketShop)
|
||||
return kuaishouEticketShops.value
|
||||
}
|
||||
|
||||
function findShopByShopId(shopId: string) {
|
||||
return kuaishouEticketShops.value.find((item) => item.shopId && item.shopId === shopId)
|
||||
}
|
||||
|
||||
return {
|
||||
kuaishouEticketShops,
|
||||
expandedKuaishouEticketShopIds,
|
||||
addKuaishouEticketShop,
|
||||
removeKuaishouEticketShop,
|
||||
isKuaishouEticketShopExpanded,
|
||||
expandKuaishouEticketShop,
|
||||
collapseKuaishouEticketShop,
|
||||
toggleKuaishouEticketShop,
|
||||
expandAllKuaishouEticketShops,
|
||||
collapseAllKuaishouEticketShops,
|
||||
describeKuaishouEticketShop,
|
||||
getKuaishouEticketShopInitial,
|
||||
setShopsFromConfig,
|
||||
findShopByShopId,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user