优化多账号
This commit is contained in:
@@ -86,7 +86,7 @@ export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObje
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single source by key. Throws if key is 'default' (cannot delete default source).
|
||||
* Delete a single source by key.
|
||||
*/
|
||||
export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
||||
const key = String(sourceKey || '').trim()
|
||||
@@ -96,12 +96,6 @@ export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
|
||||
errorCode: 'cloudtentacles_source_key_required',
|
||||
})
|
||||
}
|
||||
if (key === 'default') {
|
||||
throw createHttpError('不能删除默认 cloudtentacles 账号', {
|
||||
statusCode: 409,
|
||||
errorCode: 'cloudtentacles_default_source_forbidden',
|
||||
})
|
||||
}
|
||||
|
||||
const config = loadCloudtentaclesSourcesConfigFromFile()
|
||||
const existingIndex = config.sources.findIndex(s => s.key === key)
|
||||
|
||||
Vendored
-3
@@ -15,7 +15,6 @@ declare module 'vue' {
|
||||
AdminResultCard: typeof import('./components/admin/AdminResultCard.vue')['default']
|
||||
AdminStatusTag: typeof import('./components/admin/AdminStatusTag.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
@@ -49,8 +48,6 @@ declare module 'vue' {
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
+282
-142
@@ -3,10 +3,10 @@ import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type CloudtentaclesForm = {
|
||||
@@ -37,6 +37,7 @@ type Props = {
|
||||
selectedSourceKey: string
|
||||
currentSourceEnabled: boolean
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
sessions: AdminCloudtentaclesSessionsMap
|
||||
currentSource: AdminCloudtentaclesSourceItem | null
|
||||
currentSession: AdminCloudtentaclesPersistedSession | null
|
||||
handleCloudtentaclesSaveSource: () => void | Promise<void>
|
||||
@@ -52,29 +53,31 @@ defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<el-card class="cloud-config-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="config-header">
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 履约平台配置</span>
|
||||
<p class="card-desc">
|
||||
先维护账号清单,再编辑当前账号的核心信息。短信登录、token 校验和调试参数已经拆开,避免常用 CRUD 被大表单干扰。
|
||||
</p>
|
||||
<p class="card-desc">维护账号、凭据与登录会话;调试字段默认折叠。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button :loading="cloudtentaclesSaving" @click="handleCloudtentaclesSaveSource"
|
||||
>保存履约配置</el-button
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="cloudtentaclesSaving"
|
||||
@click="handleCloudtentaclesSaveSource"
|
||||
>
|
||||
保存配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="crud-layout">
|
||||
<div class="account-column">
|
||||
<aside class="account-column">
|
||||
<div class="account-column-head">
|
||||
<div>
|
||||
<strong>账号列表</strong>
|
||||
<p>先选账号,再编辑右侧信息。</p>
|
||||
<p>{{ sources.length }} 个账号</p>
|
||||
</div>
|
||||
<el-button type="primary" plain size="small" @click="addSource">新增账号</el-button>
|
||||
</div>
|
||||
@@ -95,18 +98,24 @@ defineProps<Props>()
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="account-item-meta">
|
||||
<span>{{ source.username || '未填写账号' }}</span>
|
||||
<span>{{ source.phone || '未填写手机号' }}</span>
|
||||
<span>{{ source.username || '账号未填' }}</span>
|
||||
<span>{{ source.phone || '手机号未填' }}</span>
|
||||
</div>
|
||||
<div class="account-item-foot">
|
||||
<span>{{ source.key }}</span>
|
||||
<span>{{ currentSession && source.key === selectedSourceKey ? (currentSession.hasToken ? '已登录' : '未登录') : '-' }}</span>
|
||||
<span>{{
|
||||
sessions[source.key]
|
||||
? sessions[source.key].hasToken
|
||||
? '已登录'
|
||||
: '未登录'
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="editor-column">
|
||||
<section class="editor-column">
|
||||
<div class="editor-head">
|
||||
<div>
|
||||
<strong>{{ currentSource?.label || currentSource?.key || '未选择账号' }}</strong>
|
||||
@@ -134,7 +143,6 @@ defineProps<Props>()
|
||||
"
|
||||
/>
|
||||
<el-button
|
||||
v-if="selectedSourceKey !== 'default'"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@@ -145,13 +153,13 @@ defineProps<Props>()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form label-width="auto" label-position="top" class="editor-form">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<div class="editor-panel">
|
||||
<el-form label-width="auto" label-position="top" class="editor-form">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="账号名称">
|
||||
<el-input
|
||||
:model-value="currentSource?.label || ''"
|
||||
placeholder="例如:主号 / 备用号 / 夜班号"
|
||||
placeholder="例如:生产履约号 / 夜班履约号 / 华东履约号"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
if (currentSource) currentSource.label = String(value)
|
||||
@@ -159,32 +167,22 @@ defineProps<Props>()
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="账号 Key">
|
||||
|
||||
<el-form-item label="系统 ID">
|
||||
<el-input :model-value="currentSource?.key || ''" readonly />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="登录账号">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.username"
|
||||
placeholder="cloudtentacles 登录账号"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
|
||||
<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="12">
|
||||
<el-form-item label="登录密码">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.password"
|
||||
@@ -193,74 +191,67 @@ defineProps<Props>()
|
||||
placeholder="cloudtentacles 登录密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
|
||||
<el-form-item label="Base URL">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.baseUrl"
|
||||
placeholder="https://123.207.217.176"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<el-card shadow="never" class="session-card">
|
||||
<div class="session-card-head">
|
||||
<div>
|
||||
<strong>登录与会话</strong>
|
||||
<p>短信验证码、登录测试、token 校验都放在这里,和账号 CRUD 分开。</p>
|
||||
</div>
|
||||
<div class="session-card-actions">
|
||||
<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 class="session-panel">
|
||||
<div class="session-card-head">
|
||||
<div>
|
||||
<strong>登录与会话</strong>
|
||||
<p>发送验证码后测试登录;已有 token 可直接校验。</p>
|
||||
</div>
|
||||
<div class="session-card-actions">
|
||||
<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>
|
||||
|
||||
<el-form label-position="top" class="session-form">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="短信验证码">
|
||||
<el-input v-model="cloudtentaclesForm.code" placeholder="收到验证码后填写" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手动 token">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.token"
|
||||
placeholder="仅在需要手动校验时填写"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="短信验证码">
|
||||
<el-input v-model="cloudtentaclesForm.code" placeholder="收到验证码后填写" />
|
||||
</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-card>
|
||||
|
||||
<el-collapse class="advanced-collapse">
|
||||
<el-collapse-item name="advanced" title="高级参数与调试字段">
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-collapse class="advanced-collapse">
|
||||
<el-collapse-item name="advanced" title="高级参数与调试字段">
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="deviceId">
|
||||
<el-input v-model="cloudtentaclesForm.deviceId" placeholder="默认 -" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="deviceType">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.deviceType"
|
||||
@@ -269,16 +260,12 @@ defineProps<Props>()
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<div class="debug-field-grid">
|
||||
<el-form-item label="VN Key">
|
||||
<el-input v-model="cloudtentaclesForm.vnKey" placeholder="例如 1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="VN ID">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.vnId"
|
||||
@@ -287,8 +274,6 @@ defineProps<Props>()
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="SKU ID">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.skuId"
|
||||
@@ -297,8 +282,6 @@ defineProps<Props>()
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="购买数量">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.skuCount"
|
||||
@@ -307,12 +290,12 @@ defineProps<Props>()
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
@@ -324,39 +307,56 @@ defineProps<Props>()
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<div class="result-grid mt-4">
|
||||
<AdminResultCard label="短信发送" :value="cloudtentaclesSmsResult ? '成功' : '未执行'">
|
||||
<p v-if="cloudtentaclesSmsResult">
|
||||
{{ cloudtentaclesSmsResult.phoneMasked }},{{
|
||||
formatAdminDateTime(cloudtentaclesSmsResult.sentAt)
|
||||
<div class="status-strip mt-4">
|
||||
<div class="status-step" :class="{ 'status-step--done': cloudtentaclesSmsResult }">
|
||||
<span>短信</span>
|
||||
<strong>{{ cloudtentaclesSmsResult ? '已发送' : '未执行' }}</strong>
|
||||
<em>
|
||||
{{
|
||||
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)
|
||||
</em>
|
||||
</div>
|
||||
<div class="status-step" :class="{ 'status-step--done': cloudtentaclesLoginResult }">
|
||||
<span>登录</span>
|
||||
<strong>{{ cloudtentaclesLoginResult ? '成功' : '未执行' }}</strong>
|
||||
<em>
|
||||
{{
|
||||
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>
|
||||
</em>
|
||||
</div>
|
||||
<div class="status-step" :class="{ 'status-step--done': cloudtentaclesValidateResult }">
|
||||
<span>会话</span>
|
||||
<strong>{{ cloudtentaclesValidateResult ? '已校验' : '未执行' }}</strong>
|
||||
<em>
|
||||
{{
|
||||
cloudtentaclesValidateResult
|
||||
? `权限 ${cloudtentaclesValidateResult.session.permissionCount} 项 · 余额 ${cloudtentaclesValidateResult.asset}`
|
||||
: '登录成功后校验 token'
|
||||
}}
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
.cloud-config-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
@@ -366,13 +366,14 @@ defineProps<Props>()
|
||||
|
||||
.crud-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 280px) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.account-column,
|
||||
.editor-column {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
@@ -388,7 +389,7 @@ defineProps<Props>()
|
||||
.account-column-head p,
|
||||
.editor-head p,
|
||||
.session-card-head p {
|
||||
margin: 6px 0 0;
|
||||
margin: 4px 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
@@ -401,27 +402,28 @@ defineProps<Props>()
|
||||
.account-item {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #f8fafc;
|
||||
padding: var(--space-3);
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
background 0.2s ease;
|
||||
}
|
||||
|
||||
.account-item:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
transform: translateY(-1px);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.account-item--active {
|
||||
border-color: var(--el-color-primary);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--el-color-primary) 35%, transparent);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
.account-item-head,
|
||||
@@ -437,6 +439,16 @@ defineProps<Props>()
|
||||
|
||||
.account-item-head {
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.account-item-head strong,
|
||||
.account-item-meta span,
|
||||
.account-item-foot span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.account-item-meta,
|
||||
@@ -445,25 +457,153 @@ defineProps<Props>()
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.account-item-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.account-item-foot {
|
||||
justify-content: space-between;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.editor-form,
|
||||
.session-card,
|
||||
.advanced-collapse {
|
||||
margin-top: 0;
|
||||
.editor-head {
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
.editor-head > div:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.editor-head > div:first-child strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 2px var(--space-4);
|
||||
}
|
||||
|
||||
.session-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid rgba(59, 130, 246, 0.14);
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(180deg, rgba(239, 246, 255, 0.72), rgba(255, 255, 255, 0.92));
|
||||
}
|
||||
|
||||
.session-card-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.session-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.advanced-collapse {
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.advanced-collapse :deep(.el-collapse-item__header) {
|
||||
height: 38px;
|
||||
background: transparent;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.advanced-collapse :deep(.el-collapse-item__wrap) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.debug-field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 2px var(--space-3);
|
||||
}
|
||||
|
||||
.status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.status-step {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.status-step span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-step strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.status-step em {
|
||||
overflow: hidden;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-style: normal;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-step--done {
|
||||
border-color: rgba(34, 197, 94, 0.2);
|
||||
background: var(--color-success-light);
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.crud-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.account-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.config-header,
|
||||
.editor-head,
|
||||
.session-card-head {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-grid,
|
||||
.debug-field-grid,
|
||||
.status-strip,
|
||||
.account-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+203
-75
@@ -22,96 +22,202 @@ 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>
|
||||
<el-card class="debug-card">
|
||||
<el-collapse>
|
||||
<el-collapse-item name="debug">
|
||||
<template #title>
|
||||
<div class="debug-title">
|
||||
<strong>开发调试工具</strong>
|
||||
<span>分类、SKU、虚拟号和完整流程调试</span>
|
||||
</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>
|
||||
<div class="debug-panel">
|
||||
<div class="debug-group">
|
||||
<span>基础</span>
|
||||
<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="handleCloudtentaclesFetchKnapsack"
|
||||
>
|
||||
背包
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre v-if="cloudtentaclesDebugResult" class="debug-pre mt-4">{{
|
||||
cloudtentaclesDebugResult
|
||||
}}</pre>
|
||||
<el-empty
|
||||
v-else
|
||||
description="执行一次调试动作后,这里会显示原始返回数据。"
|
||||
:image-size="60"
|
||||
/>
|
||||
<div class="debug-group">
|
||||
<span>履约</span>
|
||||
<div class="debug-buttons">
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesBuySku">
|
||||
购买
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="primary"
|
||||
@click="handleCloudtentaclesUseSku"
|
||||
>
|
||||
发货
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="success"
|
||||
@click="handleCloudtentaclesRunFullFlow"
|
||||
>
|
||||
完整流程
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debug-group">
|
||||
<span>虚拟号</span>
|
||||
<div class="debug-buttons">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre v-if="cloudtentaclesDebugResult" class="debug-pre mt-4">{{
|
||||
cloudtentaclesDebugResult
|
||||
}}</pre>
|
||||
<div v-else class="debug-empty">执行调试动作后显示原始返回数据。</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
.debug-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.debug-card :deep(.el-card__body) {
|
||||
padding: 0 var(--space-4);
|
||||
}
|
||||
|
||||
.debug-card :deep(.el-collapse) {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.debug-card :deep(.el-collapse-item__header) {
|
||||
height: 54px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.debug-card :deep(.el-collapse-item__wrap) {
|
||||
border-bottom: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.debug-title {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.debug-title strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.debug-title span {
|
||||
overflow: hidden;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.debug-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.debug-group {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.debug-group > span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 800;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.debug-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-buttons :deep(.el-button) {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.debug-pre {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
@@ -125,4 +231,26 @@ defineProps<Props>()
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.debug-empty {
|
||||
margin-bottom: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
border: 1px dashed rgba(148, 163, 184, 0.35);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.debug-title {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.debug-group {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+197
-66
@@ -23,84 +23,215 @@ function resolveOrphanSessionEntries(
|
||||
) {
|
||||
return Object.entries(sessions).filter(([key]) => !sources.some((source) => source.key === key))
|
||||
}
|
||||
|
||||
function countSavedTokens(sessions: AdminCloudtentaclesSessionsMap) {
|
||||
return Object.values(sessions).filter((session) => session?.hasToken).length
|
||||
}
|
||||
</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="账号数量">
|
||||
{{ sources.length }} 个
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前选中账号">
|
||||
{{ selectedSourceKey }}
|
||||
</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-item
|
||||
v-if="sources.length > 0 || Object.keys(sessions).length > 0"
|
||||
label="各账号会话状态"
|
||||
<section class="cloudtentacles-overview">
|
||||
<div class="overview-copy">
|
||||
<span class="eyebrow">履约账号</span>
|
||||
<strong>cloudtentacles</strong>
|
||||
<p>管理外部履约账号、短信登录和 token 会话。订单来源同步不在这里处理。</p>
|
||||
</div>
|
||||
|
||||
<div class="overview-metrics">
|
||||
<div class="metric-item">
|
||||
<span>账号</span>
|
||||
<strong>{{ sources.length }}</strong>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>已保存 token</span>
|
||||
<strong>{{ countSavedTokens(sessions) }}</strong>
|
||||
</div>
|
||||
<div class="metric-item metric-item--wide">
|
||||
<span>当前账号</span>
|
||||
<strong>{{ selectedSourceKey || '-' }}</strong>
|
||||
</div>
|
||||
<div class="metric-item metric-item--wide">
|
||||
<span>当前 token</span>
|
||||
<strong>
|
||||
{{
|
||||
cloudtentaclesPersistedSession?.hasToken
|
||||
? cloudtentaclesPersistedSession.tokenMasked
|
||||
: '未保存'
|
||||
}}
|
||||
</strong>
|
||||
<small v-if="cloudtentaclesPersistedSession?.hasToken">
|
||||
{{ formatAdminDateTime(cloudtentaclesPersistedSession.loggedInAt) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-strip">
|
||||
<span>配置 {{ cloudtentaclesFilePath || '-' }}</span>
|
||||
<span>会话 {{ cloudtentaclesSessionFilePath || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="sources.length > 0 || Object.keys(sessions).length > 0" class="session-strip">
|
||||
<span v-for="source in sources" :key="source.key" class="session-chip">
|
||||
<strong>{{ source.label || source.key }}</strong>
|
||||
<em>{{ sessions[source.key]?.hasToken ? sessions[source.key]?.tokenMasked : '未登录' }}</em>
|
||||
</span>
|
||||
<span
|
||||
v-for="[key, session] in resolveOrphanSessionEntries(sources, sessions)"
|
||||
:key="`orphan-${key}`"
|
||||
class="session-chip session-chip--orphan"
|
||||
>
|
||||
<div class="session-list">
|
||||
<div v-for="source in sources" :key="source.key" class="session-item">
|
||||
<el-tag :type="source.enabled !== false ? 'success' : 'info'" size="small">
|
||||
{{ source.key }}
|
||||
</el-tag>
|
||||
<span>{{ source.label || source.key }}</span>
|
||||
<span>
|
||||
{{
|
||||
sessions[source.key]?.hasToken
|
||||
? sessions[source.key]?.tokenMasked
|
||||
: '未登录'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="[key, session] in resolveOrphanSessionEntries(sources, sessions)"
|
||||
:key="`orphan-${key}`"
|
||||
class="session-item"
|
||||
>
|
||||
<el-tag :type="session.hasToken ? 'success' : 'info'" size="small">{{ key }}</el-tag>
|
||||
<span>孤立会话</span>
|
||||
<span>{{ session.hasToken ? session.tokenMasked : '未登录' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
<strong>{{ key }}</strong>
|
||||
<em>{{ session.hasToken ? session.tokenMasked : '孤立会话' }}</em>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
.cloudtentacles-overview {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(420px, 1.5fr);
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.session-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
.overview-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.eyebrow {
|
||||
display: block;
|
||||
color: var(--color-primary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.overview-copy strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-xl);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.overview-copy p {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.overview-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: 92px 128px minmax(120px, 1fr) minmax(180px, 1.2fr);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
align-content: center;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.metric-item span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-item strong {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.15;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metric-item small {
|
||||
overflow: hidden;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-strip,
|
||||
.session-strip {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.file-strip span {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: 5px 9px;
|
||||
border-radius: var(--radius-full);
|
||||
background: #eef3f8;
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.session-strip {
|
||||
padding-top: var(--space-1);
|
||||
}
|
||||
|
||||
.session-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 260px;
|
||||
padding: 4px 9px;
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success-dark);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.session-chip strong,
|
||||
.session-chip em {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-chip em {
|
||||
color: inherit;
|
||||
font-style: normal;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.session-chip--orphan {
|
||||
border-color: rgba(245, 158, 11, 0.24);
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning-dark);
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.cloudtentacles-overview,
|
||||
.overview-metrics {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.cloudtentacles-overview,
|
||||
.overview-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+1
@@ -106,6 +106,7 @@ defineProps<Props>()
|
||||
:selected-source-key="selectedSourceKey"
|
||||
:current-source-enabled="currentSourceEnabled"
|
||||
:sources="sources"
|
||||
:sessions="sessions"
|
||||
:current-source="currentSource"
|
||||
:current-session="currentSession"
|
||||
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
|
||||
|
||||
+92
-60
@@ -25,7 +25,7 @@ import { useCloudtentaclesDebugActions } from './useCloudtentaclesDebugActions'
|
||||
export function useAdminCloudtentaclesPlatform() {
|
||||
const cloudtentaclesFilePath = ref('')
|
||||
const cloudtentaclesSessionFilePath = ref('')
|
||||
const selectedSourceKey = ref('default')
|
||||
const selectedSourceKey = ref('')
|
||||
|
||||
const sources = ref<AdminCloudtentaclesSourceItem[]>([])
|
||||
const sessions = ref<AdminCloudtentaclesSessionsMap>({})
|
||||
@@ -88,33 +88,70 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
sourceCount: sources.value.length,
|
||||
}))
|
||||
|
||||
function createSourceLabel(index: number) {
|
||||
return `履约账号 ${index}`
|
||||
}
|
||||
|
||||
function generateSourceKey() {
|
||||
let key = ''
|
||||
do {
|
||||
key = `account_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
|
||||
} while (sources.value.some((source) => source.key === key))
|
||||
return key
|
||||
}
|
||||
|
||||
function createEmptySource(label = createSourceLabel(sources.value.length + 1)) {
|
||||
return {
|
||||
key: generateSourceKey(),
|
||||
label,
|
||||
enabled: true,
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
deviceId: '-',
|
||||
deviceType: 0,
|
||||
} satisfies AdminCloudtentaclesSourceItem
|
||||
}
|
||||
|
||||
function resetCloudtentaclesTransientState() {
|
||||
cloudtentaclesSmsResult.value = null
|
||||
cloudtentaclesLoginResult.value = null
|
||||
cloudtentaclesValidateResult.value = null
|
||||
cloudtentaclesResultError.value = ''
|
||||
}
|
||||
|
||||
function persistCurrentFormToSelectedSource() {
|
||||
const source = sources.value.find((item) => item.key === selectedSourceKey.value)
|
||||
if (!source) return
|
||||
|
||||
source.baseUrl = cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176'
|
||||
source.username = cloudtentaclesForm.value.username.trim()
|
||||
source.password = cloudtentaclesForm.value.password.trim()
|
||||
source.phone = cloudtentaclesForm.value.phone.trim()
|
||||
source.deviceId = cloudtentaclesForm.value.deviceId.trim() || '-'
|
||||
source.deviceType = Number(cloudtentaclesForm.value.deviceType || 0)
|
||||
}
|
||||
|
||||
function selectFallbackSource(preferredKey = '') {
|
||||
const preferred = sources.value.find((source) => source.key === preferredKey)
|
||||
selectedSourceKey.value = preferred?.key || sources.value[0]?.key || ''
|
||||
}
|
||||
|
||||
function hydrateCloudtentaclesConfig(data: AdminCloudtentaclesSourceConfigResponse) {
|
||||
cloudtentaclesFilePath.value = data.filePath
|
||||
cloudtentaclesSessionFilePath.value = data.sessionFilePath
|
||||
sources.value = data.sources || []
|
||||
sessions.value = data.sessions || {}
|
||||
|
||||
// Ensure 'default' always exists
|
||||
if (!sources.value.length) {
|
||||
sources.value.push({
|
||||
key: 'default',
|
||||
label: '默认账号',
|
||||
enabled: true,
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
deviceId: '-',
|
||||
deviceType: 0,
|
||||
})
|
||||
sources.value.push(createEmptySource())
|
||||
}
|
||||
|
||||
// Default selectedSourceKey to 'default' or first source
|
||||
if (!sources.value.find((s) => s.key === selectedSourceKey.value)) {
|
||||
selectedSourceKey.value = sources.value[0].key
|
||||
selectFallbackSource()
|
||||
}
|
||||
|
||||
// Hydrate form from the currently selected source
|
||||
hydrateFormFromSource(selectedSourceKey.value)
|
||||
}
|
||||
|
||||
@@ -141,19 +178,19 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
}
|
||||
|
||||
function switchSource(sourceKey: string) {
|
||||
if (sourceKey === selectedSourceKey.value) return
|
||||
if (!sources.value.some((source) => source.key === sourceKey)) return
|
||||
|
||||
persistCurrentFormToSelectedSource()
|
||||
selectedSourceKey.value = sourceKey
|
||||
// Clear transient results when switching
|
||||
cloudtentaclesSmsResult.value = null
|
||||
cloudtentaclesLoginResult.value = null
|
||||
cloudtentaclesValidateResult.value = null
|
||||
cloudtentaclesResultError.value = ''
|
||||
resetCloudtentaclesTransientState()
|
||||
hydrateFormFromSource(sourceKey)
|
||||
}
|
||||
|
||||
async function addSource() {
|
||||
try {
|
||||
const result = await showPrompt(
|
||||
'请输入新账号名称(例如:运营号、客服号)',
|
||||
'请输入账号显示名称(例如:生产履约号、夜班履约号)',
|
||||
'新增 cloudtentacles 账号',
|
||||
{
|
||||
inputPattern: /\S/,
|
||||
@@ -165,27 +202,12 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
showError('账号名称不能为空')
|
||||
return
|
||||
}
|
||||
// Generate a URL-safe key from the label, ensure uniqueness
|
||||
const baseKey = trimmed.replace(/[^a-zA-Z0-9\u4e00-\u9fff_]/g, '_').toLowerCase()
|
||||
let newKey = baseKey
|
||||
let counter = 2
|
||||
while (sources.value.some((s) => s.key === newKey)) {
|
||||
newKey = `${baseKey}_${counter++}`
|
||||
}
|
||||
const newSource: AdminCloudtentaclesSourceItem = {
|
||||
key: newKey,
|
||||
label: trimmed,
|
||||
enabled: true,
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
deviceId: '-',
|
||||
deviceType: 0,
|
||||
}
|
||||
persistCurrentFormToSelectedSource()
|
||||
const newSource = createEmptySource(trimmed)
|
||||
sources.value.push(newSource)
|
||||
selectedSourceKey.value = newKey
|
||||
hydrateFormFromSource(newKey)
|
||||
selectedSourceKey.value = newSource.key
|
||||
resetCloudtentaclesTransientState()
|
||||
hydrateFormFromSource(newSource.key)
|
||||
} catch (error) {
|
||||
if (isFeedbackDismissed(error)) return // user cancelled / closed
|
||||
showError(error instanceof Error ? error.message : '新增账号失败')
|
||||
@@ -193,20 +215,39 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
}
|
||||
|
||||
async function removeSource(sourceKey: string) {
|
||||
if (sourceKey === 'default') {
|
||||
showError('默认账号不可删除')
|
||||
const sourceIndex = sources.value.findIndex((source) => source.key === sourceKey)
|
||||
if (sourceIndex < 0) {
|
||||
showError('账号不存在')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
cloudtentaclesSaving.value = true
|
||||
await deleteAdminCloudtentaclesSource(sourceKey)
|
||||
sources.value = sources.value.filter((s) => s.key !== sourceKey)
|
||||
delete sessions.value[sourceKey]
|
||||
if (selectedSourceKey.value === sourceKey) {
|
||||
selectedSourceKey.value = 'default'
|
||||
hydrateFormFromSource('default')
|
||||
persistCurrentFormToSelectedSource()
|
||||
|
||||
if (sources.value.length === 1) {
|
||||
const replacement = createEmptySource()
|
||||
const response = await saveAdminCloudtentaclesSourceConfig({
|
||||
enabled: true,
|
||||
sources: [replacement],
|
||||
})
|
||||
cloudtentaclesFilePath.value = response.data.filePath
|
||||
cloudtentaclesSessionFilePath.value = response.data.sessionFilePath
|
||||
sources.value = response.data.sources
|
||||
sessions.value = response.data.sessions
|
||||
selectFallbackSource(replacement.key)
|
||||
} else {
|
||||
await deleteAdminCloudtentaclesSource(sourceKey)
|
||||
sources.value = sources.value.filter((s) => s.key !== sourceKey)
|
||||
delete sessions.value[sourceKey]
|
||||
|
||||
if (selectedSourceKey.value === sourceKey) {
|
||||
const nextSource = sources.value[sourceIndex] || sources.value[sourceIndex - 1] || sources.value[0]
|
||||
selectFallbackSource(nextSource?.key)
|
||||
}
|
||||
}
|
||||
resetCloudtentaclesTransientState()
|
||||
hydrateFormFromSource(selectedSourceKey.value)
|
||||
showSuccess('账号已删除')
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '删除账号失败')
|
||||
@@ -216,16 +257,7 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
}
|
||||
|
||||
function buildCloudtentaclesConfigPayload(): AdminCloudtentaclesSourcesConfig {
|
||||
// Update current source from form
|
||||
const current = sources.value.find((s) => s.key === selectedSourceKey.value)
|
||||
if (current) {
|
||||
current.baseUrl = cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176'
|
||||
current.username = cloudtentaclesForm.value.username.trim()
|
||||
current.password = cloudtentaclesForm.value.password.trim()
|
||||
current.phone = cloudtentaclesForm.value.phone.trim()
|
||||
current.deviceId = cloudtentaclesForm.value.deviceId.trim() || '-'
|
||||
current.deviceType = Number(cloudtentaclesForm.value.deviceType || 0)
|
||||
}
|
||||
persistCurrentFormToSelectedSource()
|
||||
return {
|
||||
enabled: true,
|
||||
sources: sources.value.map((s) => ({
|
||||
|
||||
Reference in New Issue
Block a user