优化 cloudtentacles 多账号管理与任务展示
This commit is contained in:
@@ -90,6 +90,10 @@ export function mapKuaishouCloudFulfillmentContext(value: unknown): JsonRecord |
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
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,
|
||||
skuName: String(binding.skuName || '').trim(),
|
||||
vnKey: String(binding.vnKey || '').trim(),
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
} from "../../platforms/cloudtentacles/source-config-service.js";
|
||||
import {
|
||||
clearCloudtentaclesSessionStateByKey,
|
||||
deleteCloudtentaclesSessionStateByKey,
|
||||
getCloudtentaclesSessionFilePath,
|
||||
getCloudtentaclesSessionStateByKey,
|
||||
saveCloudtentaclesSessionStateByKey,
|
||||
getAllCloudtentaclesSessionStates,
|
||||
pruneCloudtentaclesSessionStates,
|
||||
} from "../../platforms/cloudtentacles/session-state-service.js";
|
||||
import {
|
||||
loginCloudtentaclesSession,
|
||||
@@ -102,7 +104,9 @@ export function updateAdminCloudtentaclesSourceConfig(
|
||||
enabled: payload.enabled !== false,
|
||||
sources: normalizedList,
|
||||
});
|
||||
const sessions = getAllCloudtentaclesSessionStates();
|
||||
const sessions = pruneCloudtentaclesSessionStates(
|
||||
normalizedList.map((item) => item.key)
|
||||
);
|
||||
|
||||
const sources = Array.isArray(savedList.sources)
|
||||
? savedList.sources.map((s) => mapAdminCloudtentaclesSourceConfig(s))
|
||||
@@ -151,7 +155,7 @@ export function deleteAdminCloudtentaclesSource(sourceKey: string) {
|
||||
if (!key) {
|
||||
throw new Error("sourceKey is required for deletion");
|
||||
}
|
||||
clearCloudtentaclesSessionStateByKey(key);
|
||||
deleteCloudtentaclesSessionStateByKey(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: '',
|
||||
resolvedSkuName: '',
|
||||
cloudSourceKey: 'default',
|
||||
cloudSourceKeyFallbacks: [],
|
||||
cloudSkuId: 0,
|
||||
cloudSkuName: '',
|
||||
vnKey: '',
|
||||
|
||||
@@ -77,6 +77,11 @@ export function mapAdminKuaishouCloudFulfillmentItem(item: JsonObject = {}) {
|
||||
resolvedSkuName: String(item.resolvedSkuName || "").trim(),
|
||||
cloudSourceKey:
|
||||
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,
|
||||
cloudSkuName: String(item.cloudSkuName || "").trim(),
|
||||
vnKey: String(item.vnKey || "").trim(),
|
||||
|
||||
@@ -61,6 +61,13 @@ export function resolvePersistedCloudtentaclesContextWithFallback(
|
||||
const session = getCloudtentaclesSessionStateByKey(sourceKey);
|
||||
|
||||
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();
|
||||
if (!token) {
|
||||
|
||||
@@ -65,6 +65,23 @@ export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue
|
||||
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.
|
||||
*/
|
||||
@@ -86,6 +103,26 @@ export function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) {
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -133,6 +133,7 @@ function normalizeCloudtentaclesSourceItem(rawValue: unknown) {
|
||||
return {
|
||||
key: String(source.key || 'default').trim() || 'default',
|
||||
label: String(source.label || '').trim(),
|
||||
enabled: source.enabled !== false,
|
||||
baseUrl: String(source.baseUrl || 'https://123.207.217.176').trim() || 'https://123.207.217.176',
|
||||
username: String(source.username || '').trim(),
|
||||
password: String(source.password || '').trim(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface AdminCloudtentaclesSourceItem {
|
||||
key: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
|
||||
@@ -174,6 +174,8 @@ export interface AdminTaskDetail {
|
||||
binding: {
|
||||
prepareStatus: string
|
||||
cloudSourceKey: string
|
||||
cloudSourceKeyFallbacks: string[]
|
||||
resolvedSourceKey: string
|
||||
skuId: number
|
||||
skuName: string
|
||||
vnKey: string
|
||||
|
||||
+2
-2
@@ -81,8 +81,8 @@ export function useKuaishouCloudConfig() {
|
||||
vnKey: '1',
|
||||
autoBuyEnabled: true,
|
||||
minAssetReserve: 0,
|
||||
autoReturnNumberAfterDispatch: false,
|
||||
autoConsumeAfterDispatch: false,
|
||||
autoReturnNumberAfterDispatch: true,
|
||||
autoConsumeAfterDispatch: true,
|
||||
kuaishouConsumeShopId: defaultShop?.shopId || '',
|
||||
kuaishouConsumeShopName: defaultShop?.kshopName || '',
|
||||
notes: '',
|
||||
|
||||
@@ -135,7 +135,9 @@ const {
|
||||
sources: cloudtentaclesSources,
|
||||
sessions: cloudtentaclesSessions,
|
||||
selectedSourceKey: selectedCloudtentaclesSourceKey,
|
||||
sourceOptions: cloudtentaclesSourceOptions,
|
||||
currentSourceEnabled: currentCloudtentaclesSourceEnabled,
|
||||
currentSource: currentCloudtentaclesSource,
|
||||
currentSession: currentCloudtentaclesSession,
|
||||
hydrateCloudtentaclesConfig,
|
||||
switchSource: switchCloudtentaclesSource,
|
||||
addSource: addCloudtentaclesSource,
|
||||
@@ -426,7 +428,9 @@ onMounted(loadConfigs)
|
||||
:sources="cloudtentaclesSources"
|
||||
:sessions="cloudtentaclesSessions"
|
||||
:selected-source-key="selectedCloudtentaclesSourceKey"
|
||||
:source-options="cloudtentaclesSourceOptions"
|
||||
:current-source-enabled="currentCloudtentaclesSourceEnabled"
|
||||
:current-source="currentCloudtentaclesSource"
|
||||
:current-session="currentCloudtentaclesSession"
|
||||
:switch-source="switchCloudtentaclesSource"
|
||||
:add-source="addCloudtentaclesSource"
|
||||
:remove-source="removeCloudtentaclesSource"
|
||||
|
||||
+341
-151
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
@@ -23,11 +24,6 @@ type CloudtentaclesForm = {
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
type SourceOption = {
|
||||
key: string
|
||||
label: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesForm: CloudtentaclesForm
|
||||
cloudtentaclesSaving: boolean
|
||||
@@ -39,8 +35,10 @@ type Props = {
|
||||
cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null
|
||||
cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null
|
||||
selectedSourceKey: string
|
||||
sourceOptions: SourceOption[]
|
||||
currentSourceEnabled: boolean
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
currentSource: AdminCloudtentaclesSourceItem | null
|
||||
currentSession: AdminCloudtentaclesPersistedSession | null
|
||||
handleCloudtentaclesSaveSource: () => void | Promise<void>
|
||||
handleCloudtentaclesSendSmsCode: () => void | Promise<void>
|
||||
handleCloudtentaclesTestLogin: () => void | Promise<void>
|
||||
@@ -60,156 +58,262 @@ defineProps<Props>()
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 履约平台配置</span>
|
||||
<p class="card-desc">
|
||||
先保存账号、密码、手机号和设备头,再发送短信验证码,最后执行登录测试或手动校验 token。
|
||||
先维护账号清单,再编辑当前账号的核心信息。短信登录、token 校验和调试参数已经拆开,避免常用 CRUD 被大表单干扰。
|
||||
</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>
|
||||
|
||||
<!-- 账号选择器 -->
|
||||
<div class="source-selector">
|
||||
<el-select
|
||||
:model-value="selectedSourceKey"
|
||||
placeholder="选择账号"
|
||||
@change="switchSource"
|
||||
class="source-select"
|
||||
>
|
||||
<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
|
||||
v-if="selectedSourceKey !== 'default'"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="removeSource(selectedSourceKey)"
|
||||
>删除账号</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="crud-layout">
|
||||
<div class="account-column">
|
||||
<div class="account-column-head">
|
||||
<div>
|
||||
<strong>账号列表</strong>
|
||||
<p>先选账号,再编辑右侧信息。</p>
|
||||
</div>
|
||||
<el-button type="primary" plain size="small" @click="addSource">新增账号</el-button>
|
||||
</div>
|
||||
|
||||
<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 登录账号"
|
||||
<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-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-button
|
||||
v-if="selectedSourceKey !== 'default'"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="removeSource(selectedSourceKey)"
|
||||
>
|
||||
删除账号
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form label-width="auto" label-position="top" class="editor-form">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<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
|
||||
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"
|
||||
type="password"
|
||||
show-password
|
||||
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>
|
||||
|
||||
<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-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-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"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<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-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="cloudtentaclesResultError"
|
||||
@@ -260,15 +364,95 @@ defineProps<Props>()
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.source-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
.crud-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 280px) minmax(0, 1fr);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.source-select {
|
||||
width: 180px;
|
||||
.account-column,
|
||||
.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 {
|
||||
@@ -276,4 +460,10 @@ defineProps<Props>()
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.crud-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+30
-2
@@ -16,6 +16,13 @@ type Props = {
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
function resolveOrphanSessionEntries(
|
||||
sources: AdminCloudtentaclesSourceItem[],
|
||||
sessions: AdminCloudtentaclesSessionsMap,
|
||||
) {
|
||||
return Object.entries(sessions).filter(([key]) => !sources.some((source) => source.key === key))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -47,10 +54,31 @@ defineProps<Props>()
|
||||
: '未保存'
|
||||
}}
|
||||
</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 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>
|
||||
<span>孤立会话</span>
|
||||
<span>{{ session.hasToken ? session.tokenMasked : '未登录' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+6
-7
@@ -27,11 +27,6 @@ type CloudtentaclesForm = {
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
type SourceOption = {
|
||||
key: string
|
||||
label: string
|
||||
}
|
||||
|
||||
type CloudtentaclesStats = {
|
||||
hasCredential: boolean
|
||||
smsSent: boolean
|
||||
@@ -58,7 +53,9 @@ type Props = {
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
sessions: AdminCloudtentaclesSessionsMap
|
||||
selectedSourceKey: string
|
||||
sourceOptions: SourceOption[]
|
||||
currentSourceEnabled: boolean
|
||||
currentSource: AdminCloudtentaclesSourceItem | null
|
||||
currentSession: AdminCloudtentaclesPersistedSession | null
|
||||
handleCloudtentaclesSaveSource: () => void | Promise<void>
|
||||
handleCloudtentaclesSendSmsCode: () => void | Promise<void>
|
||||
handleCloudtentaclesTestLogin: () => void | Promise<void>
|
||||
@@ -107,8 +104,10 @@ defineProps<Props>()
|
||||
:cloudtentacles-login-result="cloudtentaclesLoginResult"
|
||||
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
|
||||
:selected-source-key="selectedSourceKey"
|
||||
:source-options="sourceOptions"
|
||||
:current-source-enabled="currentSourceEnabled"
|
||||
:sources="sources"
|
||||
:current-source="currentSource"
|
||||
:current-session="currentSession"
|
||||
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
|
||||
:handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode"
|
||||
:handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin"
|
||||
|
||||
+13
@@ -67,6 +67,15 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
)
|
||||
|
||||
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(() => ({
|
||||
hasCredential: Boolean(
|
||||
@@ -90,6 +99,7 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
sources.value.push({
|
||||
key: 'default',
|
||||
label: '默认账号',
|
||||
enabled: true,
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
username: '',
|
||||
password: '',
|
||||
@@ -165,6 +175,7 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
const newSource: AdminCloudtentaclesSourceItem = {
|
||||
key: newKey,
|
||||
label: trimmed,
|
||||
enabled: true,
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
username: '',
|
||||
password: '',
|
||||
@@ -220,6 +231,7 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
sources: sources.value.map((s) => ({
|
||||
key: s.key,
|
||||
label: s.label,
|
||||
enabled: s.enabled !== false,
|
||||
baseUrl: s.baseUrl,
|
||||
username: s.username,
|
||||
password: s.password,
|
||||
@@ -421,6 +433,7 @@ export function useAdminCloudtentaclesPlatform() {
|
||||
sourceOptions,
|
||||
currentSource,
|
||||
currentSession,
|
||||
currentSourceEnabled,
|
||||
hydrateCloudtentaclesConfig,
|
||||
switchSource,
|
||||
addSource,
|
||||
|
||||
@@ -221,7 +221,19 @@ const emit = defineEmits<{
|
||||
<el-collapse-item name="cloud" title="Cloud 资源与发货">
|
||||
<div class="flow-meta-list flow-meta-list--two-column">
|
||||
<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>
|
||||
<span>cloud SKU</span>
|
||||
|
||||
Reference in New Issue
Block a user