优化 cloudtentacles 多账号管理与任务展示

This commit is contained in:
yml
2026-05-25 16:48:23 +08:00
parent e3d318d7f3
commit 28c8575a11
17 changed files with 495 additions and 167 deletions
@@ -90,6 +90,10 @@ export function mapKuaishouCloudFulfillmentContext(value: unknown): JsonRecord |
binding: { binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending', prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKey: String(binding.cloudSourceKey || '').trim(), cloudSourceKey: String(binding.cloudSourceKey || '').trim(),
cloudSourceKeyFallbacks: Array.isArray(binding.cloudSourceKeyFallbacks)
? binding.cloudSourceKeyFallbacks.map((value) => String(value || '').trim()).filter(Boolean)
: [],
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
skuId: Number(binding.skuId || 0) || 0, skuId: Number(binding.skuId || 0) || 0,
skuName: String(binding.skuName || '').trim(), skuName: String(binding.skuName || '').trim(),
vnKey: String(binding.vnKey || '').trim(), vnKey: String(binding.vnKey || '').trim(),
@@ -8,10 +8,12 @@ import {
} from "../../platforms/cloudtentacles/source-config-service.js"; } from "../../platforms/cloudtentacles/source-config-service.js";
import { import {
clearCloudtentaclesSessionStateByKey, clearCloudtentaclesSessionStateByKey,
deleteCloudtentaclesSessionStateByKey,
getCloudtentaclesSessionFilePath, getCloudtentaclesSessionFilePath,
getCloudtentaclesSessionStateByKey, getCloudtentaclesSessionStateByKey,
saveCloudtentaclesSessionStateByKey, saveCloudtentaclesSessionStateByKey,
getAllCloudtentaclesSessionStates, getAllCloudtentaclesSessionStates,
pruneCloudtentaclesSessionStates,
} from "../../platforms/cloudtentacles/session-state-service.js"; } from "../../platforms/cloudtentacles/session-state-service.js";
import { import {
loginCloudtentaclesSession, loginCloudtentaclesSession,
@@ -102,7 +104,9 @@ export function updateAdminCloudtentaclesSourceConfig(
enabled: payload.enabled !== false, enabled: payload.enabled !== false,
sources: normalizedList, sources: normalizedList,
}); });
const sessions = getAllCloudtentaclesSessionStates(); const sessions = pruneCloudtentaclesSessionStates(
normalizedList.map((item) => item.key)
);
const sources = Array.isArray(savedList.sources) const sources = Array.isArray(savedList.sources)
? savedList.sources.map((s) => mapAdminCloudtentaclesSourceConfig(s)) ? savedList.sources.map((s) => mapAdminCloudtentaclesSourceConfig(s))
@@ -151,7 +155,7 @@ export function deleteAdminCloudtentaclesSource(sourceKey: string) {
if (!key) { if (!key) {
throw new Error("sourceKey is required for deletion"); throw new Error("sourceKey is required for deletion");
} }
clearCloudtentaclesSessionStateByKey(key); deleteCloudtentaclesSessionStateByKey(key);
deleteCloudtentaclesSourceByKey(key); deleteCloudtentaclesSourceByKey(key);
} }
@@ -190,3 +190,23 @@ test('buildAdminCloudtentaclesValidateResult assembles validate response payload
}, },
) )
}) })
test('buildAdminCloudtentaclesSessionSummary preserves sourceKey', () => {
assert.deepEqual(
buildAdminCloudtentaclesSessionSummary(
{
token: 'abcdef1234567890',
permissions: [],
},
{},
'account2',
),
{
sourceKey: 'account2',
tokenMasked: 'abcdef****567890',
permissionCount: 0,
permissions: [],
persisted: false,
},
)
})
@@ -104,6 +104,7 @@ test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', ()
externalSkuName: '', externalSkuName: '',
resolvedSkuName: '', resolvedSkuName: '',
cloudSourceKey: 'default', cloudSourceKey: 'default',
cloudSourceKeyFallbacks: [],
cloudSkuId: 0, cloudSkuId: 0,
cloudSkuName: '', cloudSkuName: '',
vnKey: '', vnKey: '',
@@ -77,6 +77,11 @@ export function mapAdminKuaishouCloudFulfillmentItem(item: JsonObject = {}) {
resolvedSkuName: String(item.resolvedSkuName || "").trim(), resolvedSkuName: String(item.resolvedSkuName || "").trim(),
cloudSourceKey: cloudSourceKey:
String(item.cloudSourceKey || "default").trim() || "default", String(item.cloudSourceKey || "default").trim() || "default",
cloudSourceKeyFallbacks: Array.isArray(item.cloudSourceKeyFallbacks)
? item.cloudSourceKeyFallbacks
.map((value) => String(value || "").trim())
.filter(Boolean)
: [],
cloudSkuId: Number(item.cloudSkuId || 0) || 0, cloudSkuId: Number(item.cloudSkuId || 0) || 0,
cloudSkuName: String(item.cloudSkuName || "").trim(), cloudSkuName: String(item.cloudSkuName || "").trim(),
vnKey: String(item.vnKey || "").trim(), vnKey: String(item.vnKey || "").trim(),
@@ -61,6 +61,13 @@ export function resolvePersistedCloudtentaclesContextWithFallback(
const session = getCloudtentaclesSessionStateByKey(sourceKey); const session = getCloudtentaclesSessionStateByKey(sourceKey);
if (!source) continue; if (!source) continue;
if (source.enabled === false) {
lastError = createHttpError(
`cloudtentacles 账号 ${sourceKey} 已停用`,
{ statusCode: 409, errorCode: "kuaishou_cloud_source_disabled" }
);
continue;
}
const token = String(session?.token || "").trim(); const token = String(session?.token || "").trim();
if (!token) { if (!token) {
@@ -65,6 +65,23 @@ export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue
return states.sessions[key] return states.sessions[key]
} }
/**
* Delete a session by sourceKey.
*/
export function deleteCloudtentaclesSessionStateByKey(sourceKey: unknown) {
const key = String(sourceKey || '').trim()
if (!key) {
throw new Error('deleteCloudtentaclesSessionStateByKey: sourceKey is required')
}
const states = loadCloudtentaclesSessionStatesFromFile()
delete states.sessions[key]
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(states, null, 2)}\n`, 'utf8')
return states
}
/** /**
* Backward-compatible clear: clears the 'default' session only. * Backward-compatible clear: clears the 'default' session only.
*/ */
@@ -86,6 +103,26 @@ export function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) {
return cleared return cleared
} }
/**
* Keep only sessions whose sourceKey still exists in source config.
*/
export function pruneCloudtentaclesSessionStates(sourceKeys: unknown[] = []) {
const allowedKeys = new Set(
Array.isArray(sourceKeys)
? sourceKeys.map((value) => String(value || '').trim()).filter(Boolean)
: []
)
const states = loadCloudtentaclesSessionStatesFromFile()
states.sessions = Object.fromEntries(
Object.entries(states.sessions || {}).filter(([key]) => allowedKeys.has(String(key || '').trim()))
)
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(states, null, 2)}\n`, 'utf8')
return states
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Internal helpers // Internal helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -133,6 +133,7 @@ function normalizeCloudtentaclesSourceItem(rawValue: unknown) {
return { return {
key: String(source.key || 'default').trim() || 'default', key: String(source.key || 'default').trim() || 'default',
label: String(source.label || '').trim(), label: String(source.label || '').trim(),
enabled: source.enabled !== false,
baseUrl: String(source.baseUrl || 'https://123.207.217.176').trim() || 'https://123.207.217.176', baseUrl: String(source.baseUrl || 'https://123.207.217.176').trim() || 'https://123.207.217.176',
username: String(source.username || '').trim(), username: String(source.username || '').trim(),
password: String(source.password || '').trim(), password: String(source.password || '').trim(),
@@ -1,6 +1,7 @@
export interface AdminCloudtentaclesSourceItem { export interface AdminCloudtentaclesSourceItem {
key: string key: string
label: string label: string
enabled: boolean
baseUrl: string baseUrl: string
username: string username: string
password: string password: string
+2
View File
@@ -174,6 +174,8 @@ export interface AdminTaskDetail {
binding: { binding: {
prepareStatus: string prepareStatus: string
cloudSourceKey: string cloudSourceKey: string
cloudSourceKeyFallbacks: string[]
resolvedSourceKey: string
skuId: number skuId: number
skuName: string skuName: string
vnKey: string vnKey: string
@@ -81,8 +81,8 @@ export function useKuaishouCloudConfig() {
vnKey: '1', vnKey: '1',
autoBuyEnabled: true, autoBuyEnabled: true,
minAssetReserve: 0, minAssetReserve: 0,
autoReturnNumberAfterDispatch: false, autoReturnNumberAfterDispatch: true,
autoConsumeAfterDispatch: false, autoConsumeAfterDispatch: true,
kuaishouConsumeShopId: defaultShop?.shopId || '', kuaishouConsumeShopId: defaultShop?.shopId || '',
kuaishouConsumeShopName: defaultShop?.kshopName || '', kuaishouConsumeShopName: defaultShop?.kshopName || '',
notes: '', notes: '',
@@ -135,7 +135,9 @@ const {
sources: cloudtentaclesSources, sources: cloudtentaclesSources,
sessions: cloudtentaclesSessions, sessions: cloudtentaclesSessions,
selectedSourceKey: selectedCloudtentaclesSourceKey, selectedSourceKey: selectedCloudtentaclesSourceKey,
sourceOptions: cloudtentaclesSourceOptions, currentSourceEnabled: currentCloudtentaclesSourceEnabled,
currentSource: currentCloudtentaclesSource,
currentSession: currentCloudtentaclesSession,
hydrateCloudtentaclesConfig, hydrateCloudtentaclesConfig,
switchSource: switchCloudtentaclesSource, switchSource: switchCloudtentaclesSource,
addSource: addCloudtentaclesSource, addSource: addCloudtentaclesSource,
@@ -426,7 +428,9 @@ onMounted(loadConfigs)
:sources="cloudtentaclesSources" :sources="cloudtentaclesSources"
:sessions="cloudtentaclesSessions" :sessions="cloudtentaclesSessions"
:selected-source-key="selectedCloudtentaclesSourceKey" :selected-source-key="selectedCloudtentaclesSourceKey"
:source-options="cloudtentaclesSourceOptions" :current-source-enabled="currentCloudtentaclesSourceEnabled"
:current-source="currentCloudtentaclesSource"
:current-session="currentCloudtentaclesSession"
:switch-source="switchCloudtentaclesSource" :switch-source="switchCloudtentaclesSource"
:add-source="addCloudtentaclesSource" :add-source="addCloudtentaclesSource"
:remove-source="removeCloudtentaclesSource" :remove-source="removeCloudtentaclesSource"
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { import type {
AdminCloudtentaclesLoginTestResult, AdminCloudtentaclesLoginTestResult,
AdminCloudtentaclesPersistedSession,
AdminCloudtentaclesSendSmsResult, AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSourceItem, AdminCloudtentaclesSourceItem,
AdminCloudtentaclesValidateSessionResult, AdminCloudtentaclesValidateSessionResult,
@@ -23,11 +24,6 @@ type CloudtentaclesForm = {
deviceType: number deviceType: number
} }
type SourceOption = {
key: string
label: string
}
type Props = { type Props = {
cloudtentaclesForm: CloudtentaclesForm cloudtentaclesForm: CloudtentaclesForm
cloudtentaclesSaving: boolean cloudtentaclesSaving: boolean
@@ -39,8 +35,10 @@ type Props = {
cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null
cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null
selectedSourceKey: string selectedSourceKey: string
sourceOptions: SourceOption[] currentSourceEnabled: boolean
sources: AdminCloudtentaclesSourceItem[] sources: AdminCloudtentaclesSourceItem[]
currentSource: AdminCloudtentaclesSourceItem | null
currentSession: AdminCloudtentaclesPersistedSession | null
handleCloudtentaclesSaveSource: () => void | Promise<void> handleCloudtentaclesSaveSource: () => void | Promise<void>
handleCloudtentaclesSendSmsCode: () => void | Promise<void> handleCloudtentaclesSendSmsCode: () => void | Promise<void>
handleCloudtentaclesTestLogin: () => void | Promise<void> handleCloudtentaclesTestLogin: () => void | Promise<void>
@@ -60,72 +58,134 @@ defineProps<Props>()
<div> <div>
<span class="card-title">cloudtentacles 履约平台配置</span> <span class="card-title">cloudtentacles 履约平台配置</span>
<p class="card-desc"> <p class="card-desc">
保存账号密码手机号和设备头再发送短信验证码最后执行登录测试或手动校验 token 维护账号清单再编辑当前账号的核心信息短信登录token 校验和调试参数已经拆开避免常用 CRUD 被大表单干扰
</p> </p>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<el-button :loading="cloudtentaclesSaving" @click="handleCloudtentaclesSaveSource" <el-button :loading="cloudtentaclesSaving" @click="handleCloudtentaclesSaveSource"
>保存履约配置</el-button >保存履约配置</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>
</div> </div>
</template> </template>
<!-- 账号选择器 --> <div class="crud-layout">
<div class="source-selector"> <div class="account-column">
<el-select <div class="account-column-head">
:model-value="selectedSourceKey" <div>
placeholder="选择账号" <strong>账号列表</strong>
@change="switchSource" <p>先选账号再编辑右侧信息</p>
class="source-select" </div>
>
<el-option
v-for="opt in sourceOptions"
:key="opt.key"
:label="opt.label"
:value="opt.key"
/>
</el-select>
<el-button type="primary" plain size="small" @click="addSource">新增账号</el-button> <el-button type="primary" plain size="small" @click="addSource">新增账号</el-button>
</div>
<div class="account-list">
<button
v-for="source in sources"
:key="source.key"
type="button"
class="account-item"
:class="{ 'account-item--active': source.key === selectedSourceKey }"
@click="switchSource(source.key)"
>
<div class="account-item-head">
<strong>{{ source.label || source.key }}</strong>
<el-tag :type="source.enabled !== false ? 'success' : 'info'" size="small">
{{ source.enabled !== false ? '启用' : '停用' }}
</el-tag>
</div>
<div class="account-item-meta">
<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>
</div>
</button>
</div>
</div>
<div class="editor-column">
<div class="editor-head">
<div>
<strong>{{ currentSource?.label || currentSource?.key || '未选择账号' }}</strong>
<p>
{{
currentSession?.hasToken
? `最近登录:${formatAdminDateTime(currentSession.loggedInAt)}`
: '当前还没有可用 token'
}}
</p>
</div>
<div class="editor-head-actions">
<el-tag :type="currentSourceEnabled ? 'success' : 'info'" effect="plain">
{{ currentSourceEnabled ? '已启用' : '已停用' }}
</el-tag>
<el-switch
:model-value="currentSourceEnabled"
inline-prompt
active-text="启用"
inactive-text="停用"
@change="
(value) => {
if (currentSource) currentSource.enabled = Boolean(value)
}
"
/>
<el-button <el-button
v-if="selectedSourceKey !== 'default'" v-if="selectedSourceKey !== 'default'"
type="danger" type="danger"
plain plain
size="small" size="small"
@click="removeSource(selectedSourceKey)" @click="removeSource(selectedSourceKey)"
>删除账号</el-button
> >
删除账号
</el-button>
</div>
</div> </div>
<el-form label-width="auto" label-position="top"> <el-form label-width="auto" label-position="top" class="editor-form">
<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-row :gutter="16">
<el-col :span="8"> <el-col :span="12">
<el-form-item label="账号"> <el-form-item label="账号名称">
<el-input
:model-value="currentSource?.label || ''"
placeholder="例如:主号 / 备用号 / 夜班号"
@update:model-value="
(value) => {
if (currentSource) currentSource.label = String(value)
}
"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="账号 Key">
<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 <el-input
v-model="cloudtentaclesForm.username" v-model="cloudtentaclesForm.username"
placeholder="cloudtentacles 登录账号" placeholder="cloudtentacles 登录账号"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="12">
<el-form-item label="密码"> <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 <el-input
v-model="cloudtentaclesForm.password" v-model="cloudtentaclesForm.password"
type="password" type="password"
@@ -134,62 +194,72 @@ defineProps<Props>()
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="12">
<el-form-item label="手机号"> <el-form-item label="Base URL">
<el-input v-model="cloudtentaclesForm.phone" placeholder="接收短信验证码的手机号" /> <el-input
v-model="cloudtentaclesForm.baseUrl"
placeholder="https://123.207.217.176"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</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>
</div>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="8"> <el-col :span="12">
<el-form-item label="短信验证码"> <el-form-item label="短信验证码">
<el-input v-model="cloudtentaclesForm.code" placeholder="收到验证码后填写" /> <el-input v-model="cloudtentaclesForm.code" placeholder="收到验证码后填写" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="12">
<el-form-item label="VN Key"> <el-form-item label="手动 token">
<el-input v-model="cloudtentaclesForm.vnKey" placeholder="例如 1" /> <el-input
</el-form-item> v-model="cloudtentaclesForm.token"
</el-col> placeholder="仅在需要手动校验时填写"
<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-form-item>
</el-col> </el-col>
</el-row> </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-row :gutter="16">
<el-col :span="8"> <el-col :span="12">
<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-form-item label="deviceId">
<el-input v-model="cloudtentaclesForm.deviceId" placeholder="-" /> <el-input v-model="cloudtentaclesForm.deviceId" placeholder="默认 -" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="deviceType"> <el-form-item label="deviceType">
<el-input-number <el-input-number
@@ -200,16 +270,50 @@ defineProps<Props>()
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> </el-row>
<el-form-item label="token(可手动粘贴)">
<el-input <el-row :gutter="16">
v-model="cloudtentaclesForm.token" <el-col :span="6">
placeholder="登录成功后可自动回填,也可手动粘贴" <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"
:min="0"
controls-position="right"
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"
:min="0"
controls-position="right"
class="w-full"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="购买数量">
<el-input-number
v-model="cloudtentaclesForm.skuCount"
:min="1"
controls-position="right"
class="w-full"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</el-form> </el-form>
</el-collapse-item>
</el-collapse>
</div>
</div>
<el-alert <el-alert
v-if="cloudtentaclesResultError" v-if="cloudtentaclesResultError"
@@ -260,15 +364,95 @@ defineProps<Props>()
flex-wrap: wrap; flex-wrap: wrap;
} }
.source-selector { .crud-layout {
display: flex; display: grid;
align-items: center; grid-template-columns: minmax(240px, 280px) minmax(0, 1fr);
gap: var(--space-2); gap: var(--space-4);
margin-bottom: var(--space-4);
} }
.source-select { .account-column,
width: 180px; .editor-column {
display: grid;
gap: var(--space-3);
}
.account-column-head,
.editor-head,
.session-card-head {
display: flex;
justify-content: space-between;
gap: var(--space-2);
align-items: flex-start;
}
.account-column-head p,
.editor-head p,
.session-card-head p {
margin: 6px 0 0;
color: var(--text-secondary);
font-size: var(--text-sm);
}
.account-list {
display: grid;
gap: var(--space-2);
}
.account-item {
width: 100%;
text-align: left;
border: 1px solid var(--el-border-color);
border-radius: var(--radius-lg);
background: var(--el-bg-color);
padding: var(--space-3);
display: grid;
gap: 10px;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
}
.account-item:hover {
border-color: var(--el-color-primary-light-5);
transform: translateY(-1px);
}
.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);
}
.account-item-head,
.account-item-meta,
.account-item-foot,
.editor-head-actions,
.session-card-actions {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
align-items: center;
}
.account-item-head {
justify-content: space-between;
}
.account-item-meta,
.account-item-foot {
color: var(--text-secondary);
font-size: var(--text-sm);
}
.account-item-foot {
font-size: var(--text-xs);
}
.editor-form,
.session-card,
.advanced-collapse {
margin-top: 0;
} }
.result-grid { .result-grid {
@@ -276,4 +460,10 @@ defineProps<Props>()
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-3); gap: var(--space-3);
} }
@media (max-width: 960px) {
.crud-layout {
grid-template-columns: 1fr;
}
}
</style> </style>
@@ -16,6 +16,13 @@ type Props = {
} }
defineProps<Props>() defineProps<Props>()
function resolveOrphanSessionEntries(
sources: AdminCloudtentaclesSourceItem[],
sessions: AdminCloudtentaclesSessionsMap,
) {
return Object.entries(sessions).filter(([key]) => !sources.some((source) => source.key === key))
}
</script> </script>
<template> <template>
@@ -47,10 +54,31 @@ defineProps<Props>()
: '未保存' : '未保存'
}} }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item v-if="Object.keys(sessions).length > 0" label="各账号会话状态"> <el-descriptions-item
v-if="sources.length > 0 || Object.keys(sessions).length > 0"
label="各账号会话状态"
>
<div class="session-list"> <div class="session-list">
<div v-for="(session, key) in sessions" :key="key" class="session-item"> <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> <el-tag :type="session.hasToken ? 'success' : 'info'" size="small">{{ key }}</el-tag>
<span>孤立会话</span>
<span>{{ session.hasToken ? session.tokenMasked : '未登录' }}</span> <span>{{ session.hasToken ? session.tokenMasked : '未登录' }}</span>
</div> </div>
</div> </div>
@@ -27,11 +27,6 @@ type CloudtentaclesForm = {
deviceType: number deviceType: number
} }
type SourceOption = {
key: string
label: string
}
type CloudtentaclesStats = { type CloudtentaclesStats = {
hasCredential: boolean hasCredential: boolean
smsSent: boolean smsSent: boolean
@@ -58,7 +53,9 @@ type Props = {
sources: AdminCloudtentaclesSourceItem[] sources: AdminCloudtentaclesSourceItem[]
sessions: AdminCloudtentaclesSessionsMap sessions: AdminCloudtentaclesSessionsMap
selectedSourceKey: string selectedSourceKey: string
sourceOptions: SourceOption[] currentSourceEnabled: boolean
currentSource: AdminCloudtentaclesSourceItem | null
currentSession: AdminCloudtentaclesPersistedSession | null
handleCloudtentaclesSaveSource: () => void | Promise<void> handleCloudtentaclesSaveSource: () => void | Promise<void>
handleCloudtentaclesSendSmsCode: () => void | Promise<void> handleCloudtentaclesSendSmsCode: () => void | Promise<void>
handleCloudtentaclesTestLogin: () => void | Promise<void> handleCloudtentaclesTestLogin: () => void | Promise<void>
@@ -107,8 +104,10 @@ defineProps<Props>()
:cloudtentacles-login-result="cloudtentaclesLoginResult" :cloudtentacles-login-result="cloudtentaclesLoginResult"
:cloudtentacles-validate-result="cloudtentaclesValidateResult" :cloudtentacles-validate-result="cloudtentaclesValidateResult"
:selected-source-key="selectedSourceKey" :selected-source-key="selectedSourceKey"
:source-options="sourceOptions" :current-source-enabled="currentSourceEnabled"
:sources="sources" :sources="sources"
:current-source="currentSource"
:current-session="currentSession"
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource" :handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
:handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode" :handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode"
:handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin" :handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin"
@@ -67,6 +67,15 @@ export function useAdminCloudtentaclesPlatform() {
) )
const currentSession = computed(() => sessions.value[selectedSourceKey.value] || null) const currentSession = computed(() => sessions.value[selectedSourceKey.value] || null)
const currentSourceEnabled = computed({
get: () => currentSource.value?.enabled !== false,
set: (value: boolean) => {
const source = sources.value.find((item) => item.key === selectedSourceKey.value)
if (source) {
source.enabled = value
}
},
})
const cloudtentaclesStats = computed(() => ({ const cloudtentaclesStats = computed(() => ({
hasCredential: Boolean( hasCredential: Boolean(
@@ -90,6 +99,7 @@ export function useAdminCloudtentaclesPlatform() {
sources.value.push({ sources.value.push({
key: 'default', key: 'default',
label: '默认账号', label: '默认账号',
enabled: true,
baseUrl: 'https://123.207.217.176', baseUrl: 'https://123.207.217.176',
username: '', username: '',
password: '', password: '',
@@ -165,6 +175,7 @@ export function useAdminCloudtentaclesPlatform() {
const newSource: AdminCloudtentaclesSourceItem = { const newSource: AdminCloudtentaclesSourceItem = {
key: newKey, key: newKey,
label: trimmed, label: trimmed,
enabled: true,
baseUrl: 'https://123.207.217.176', baseUrl: 'https://123.207.217.176',
username: '', username: '',
password: '', password: '',
@@ -220,6 +231,7 @@ export function useAdminCloudtentaclesPlatform() {
sources: sources.value.map((s) => ({ sources: sources.value.map((s) => ({
key: s.key, key: s.key,
label: s.label, label: s.label,
enabled: s.enabled !== false,
baseUrl: s.baseUrl, baseUrl: s.baseUrl,
username: s.username, username: s.username,
password: s.password, password: s.password,
@@ -421,6 +433,7 @@ export function useAdminCloudtentaclesPlatform() {
sourceOptions, sourceOptions,
currentSource, currentSource,
currentSession, currentSession,
currentSourceEnabled,
hydrateCloudtentaclesConfig, hydrateCloudtentaclesConfig,
switchSource, switchSource,
addSource, addSource,
@@ -221,7 +221,19 @@ const emit = defineEmits<{
<el-collapse-item name="cloud" title="Cloud 资源与发货"> <el-collapse-item name="cloud" title="Cloud 资源与发货">
<div class="flow-meta-list flow-meta-list--two-column"> <div class="flow-meta-list flow-meta-list--two-column">
<div> <div>
<span>cloudSourceKey</span><strong>{{ flow.binding.cloudSourceKey || '-' }}</strong> <span>规则主账号</span><strong>{{ flow.binding.cloudSourceKey || '-' }}</strong>
</div>
<div>
<span>实际使用账号</span>
<strong>{{ flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || '-' }}</strong>
</div>
<div>
<span>备选账号</span>
<strong>{{
flow.binding.cloudSourceKeyFallbacks?.length
? flow.binding.cloudSourceKeyFallbacks.join(' / ')
: '-'
}}</strong>
</div> </div>
<div> <div>
<span>cloud SKU</span> <span>cloud SKU</span>