优化 cloudtentacles 多账号管理与任务展示
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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"
|
||||||
|
|||||||
+287
-97
@@ -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>
|
||||||
|
|||||||
+30
-2
@@ -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>
|
||||||
|
|||||||
+6
-7
@@ -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"
|
||||||
|
|||||||
+13
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user