diff --git a/apps/frontend/src/components.d.ts b/apps/frontend/src/components.d.ts index 106924a6..c11c6779 100644 --- a/apps/frontend/src/components.d.ts +++ b/apps/frontend/src/components.d.ts @@ -23,9 +23,6 @@ declare module 'vue' { ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] - ElOption: typeof import('element-plus/es')['ElOption'] - ElSelect: typeof import('element-plus/es')['ElSelect'] - ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabs: typeof import('element-plus/es')['ElTabs'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/apps/frontend/src/composables/admin/platform-shops/types.ts b/apps/frontend/src/composables/admin/platform-shops/types.ts new file mode 100644 index 00000000..223b62ce --- /dev/null +++ b/apps/frontend/src/composables/admin/platform-shops/types.ts @@ -0,0 +1,25 @@ +export type PlatformTab = 'agiso' | 'khhao' | 'kuaishouEticket' | 'cloudtentacles' + +export type EditableDefaults = { + messageTemplate: string + autoDeliveryMessageTemplate: string +} + +export type EditableShop = { + id: string + shopId: string + shopName: string + accessToken: string + enabled: boolean + messageTemplate: string + autoDeliveryMessageTemplate: string +} + +export type EditableKuaishouEticketShop = { + id: string + shopId: string + kshopName: string + cookie: string + userAvatar: string + enabled: boolean +} diff --git a/apps/frontend/src/composables/admin/platform-shops/useAdminAgisoPlatform.ts b/apps/frontend/src/composables/admin/platform-shops/useAdminAgisoPlatform.ts new file mode 100644 index 00000000..36399a1c --- /dev/null +++ b/apps/frontend/src/composables/admin/platform-shops/useAdminAgisoPlatform.ts @@ -0,0 +1,199 @@ +import { computed, ref } from 'vue' + +import { showError, showSuccess } from '@/lib/feedback' +import { saveAdminAgisoShopConfigs } from '@/services/admin' +import type { AdminAgisoMessagingDefaults, AdminAgisoObservedShopItem, AdminAgisoShopConfigItem } from '@/types/admin' + +import type { EditableDefaults, EditableShop } from './types' + +export function useAdminAgisoPlatform() { + const saving = ref(false) + const agisoFilePath = ref('') + const defaults = ref(createEmptyDefaults()) + const shops = ref([]) + const observedShops = ref([]) + const expandedShopIds = ref([]) + + const agisoStats = computed(() => ({ + configuredShopCount: shops.value.length, + observedShopCount: observedShops.value.length, + pendingObservedCount: observedShops.value.filter((item) => !item.configured).length, + })) + + function hydrateAgisoConfig(data: { + filePath: string + defaults: AdminAgisoMessagingDefaults + shops: AdminAgisoShopConfigItem[] + observedShops: AdminAgisoObservedShopItem[] + }) { + agisoFilePath.value = data.filePath + defaults.value = mapEditableDefaults(data.defaults) + shops.value = data.shops.map(mapEditableShop) + observedShops.value = data.observedShops + expandedShopIds.value = [] + } + + function addShop() { + const shop = createEmptyShop() + shops.value.unshift(shop) + expandShop(shop.id) + } + + function removeShop(id: string) { + shops.value = shops.value.filter((item) => item.id !== id) + collapseShop(id) + } + + function importObservedShop(item: AdminAgisoObservedShopItem) { + const shop: EditableShop = { + id: crypto.randomUUID(), + shopId: item.shopId, + shopName: item.detectedShopName || item.displayShopName || '', + accessToken: '', + enabled: true, + messageTemplate: '', + autoDeliveryMessageTemplate: '', + } + shops.value.unshift(shop) + expandShop(shop.id) + } + + function isShopExpanded(id: string) { + return expandedShopIds.value.includes(id) + } + + function expandShop(id: string) { + if (expandedShopIds.value.includes(id)) { + return + } + expandedShopIds.value = [id, ...expandedShopIds.value] + } + + function collapseShop(id: string) { + expandedShopIds.value = expandedShopIds.value.filter((item) => item !== id) + } + + function toggleShop(id: string) { + if (isShopExpanded(id)) { + collapseShop(id) + return + } + expandShop(id) + } + + function expandAllShops() { + expandedShopIds.value = shops.value.map((item) => item.id) + } + + function collapseAllShops() { + expandedShopIds.value = [] + } + + function describeShop(shop: EditableShop) { + const overrides: string[] = [] + + if (shop.messageTemplate.trim()) { + overrides.push('领取模板') + } + if (shop.autoDeliveryMessageTemplate.trim()) { + overrides.push('发货模板') + } + + return overrides.length === 0 ? '使用默认模板' : `已覆盖 ${overrides.join('、')}` + } + + async function saveAgisoConfigs(reloadConfigs: () => Promise) { + const payloadDefaults = { + messageTemplate: defaults.value.messageTemplate.trim(), + autoDeliveryMessageTemplate: defaults.value.autoDeliveryMessageTemplate.trim(), + } + const payload = shops.value + .map((item) => ({ + shopId: item.shopId.trim(), + shopName: item.shopName.trim(), + accessToken: item.accessToken.trim(), + enabled: item.enabled, + messageTemplate: item.messageTemplate.trim(), + autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate.trim(), + })) + .filter((item) => item.shopId && item.accessToken) + + saving.value = true + + try { + const response = await saveAdminAgisoShopConfigs({ + defaults: payloadDefaults, + shops: payload, + }) + agisoFilePath.value = response.data.filePath + defaults.value = mapEditableDefaults(response.data.defaults) + shops.value = response.data.shops.map(mapEditableShop) + showSuccess('Agiso 配置已保存') + await reloadConfigs() + } catch (error) { + showError(error instanceof Error ? error.message : '保存 Agiso 配置失败') + } finally { + saving.value = false + } + } + + return { + saving, + agisoFilePath, + defaults, + shops, + observedShops, + expandedShopIds, + agisoStats, + hydrateAgisoConfig, + addShop, + removeShop, + importObservedShop, + isShopExpanded, + expandShop, + collapseShop, + toggleShop, + expandAllShops, + collapseAllShops, + describeShop, + saveAgisoConfigs, + } +} + +function createEmptyDefaults(): EditableDefaults { + return { + messageTemplate: '', + autoDeliveryMessageTemplate: '', + } +} + +function createEmptyShop(): EditableShop { + return { + id: crypto.randomUUID(), + shopId: '', + shopName: '', + accessToken: '', + enabled: true, + messageTemplate: '', + autoDeliveryMessageTemplate: '', + } +} + +function mapEditableDefaults(item?: AdminAgisoMessagingDefaults): EditableDefaults { + return { + messageTemplate: item?.messageTemplate ?? '', + autoDeliveryMessageTemplate: item?.autoDeliveryMessageTemplate ?? '', + } +} + +function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop { + return { + id: crypto.randomUUID(), + shopId: item.shopId, + shopName: item.shopName, + accessToken: item.accessToken, + enabled: item.enabled !== false, + messageTemplate: item.messageTemplate, + autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate, + } +} diff --git a/apps/frontend/src/composables/admin/platform-shops/useAdminCloudtentaclesPlatform.ts b/apps/frontend/src/composables/admin/platform-shops/useAdminCloudtentaclesPlatform.ts new file mode 100644 index 00000000..28884300 --- /dev/null +++ b/apps/frontend/src/composables/admin/platform-shops/useAdminCloudtentaclesPlatform.ts @@ -0,0 +1,458 @@ +import { computed, ref } from 'vue' + +import { showError, showSuccess } from '@/lib/feedback' +import { + appointAdminCloudtentaclesVn, + backAdminCloudtentaclesVn, + buyAdminCloudtentaclesSku, + fetchAdminCloudtentaclesAsset, + fetchAdminCloudtentaclesBindUrl, + fetchAdminCloudtentaclesCategories, + fetchAdminCloudtentaclesKnapsack, + fetchAdminCloudtentaclesSkuList, + fetchAdminCloudtentaclesVnCode, + fetchAdminCloudtentaclesVnList, + generateAdminCloudtentaclesVnLoginCode, + runAdminCloudtentaclesFullFlow, + saveAdminCloudtentaclesSourceConfig, + sendAdminCloudtentaclesSmsCode, + testAdminCloudtentaclesLogin, + useAdminCloudtentaclesSku, + validateAdminCloudtentaclesSession, + verifyAdminCloudtentaclesVnCode, +} from '@/services/admin' +import type { + AdminCloudtentaclesLoginTestResult, + AdminCloudtentaclesPersistedSession, + AdminCloudtentaclesSendSmsResult, + AdminCloudtentaclesSourceConfig, + AdminCloudtentaclesValidateSessionResult, +} from '@/types/admin' + +export function useAdminCloudtentaclesPlatform() { + const cloudtentaclesFilePath = ref('') + const cloudtentaclesSessionFilePath = ref('') + const cloudtentaclesForm = ref({ + baseUrl: 'https://123.207.217.176', + username: '', + password: '', + phone: '', + code: '', + token: '', + vnKey: '1', + vnId: 0, + skuId: 5, + skuCount: 1, + deviceId: '-', + deviceType: 0, + }) + const cloudtentaclesSaving = ref(false) + const cloudtentaclesSendingSms = ref(false) + const cloudtentaclesTesting = ref(false) + const cloudtentaclesValidating = ref(false) + const cloudtentaclesResultError = ref('') + const cloudtentaclesSmsResult = ref(null) + const cloudtentaclesLoginResult = ref(null) + const cloudtentaclesValidateResult = ref(null) + const cloudtentaclesDebugLoading = ref(false) + const cloudtentaclesDebugResult = ref('') + const cloudtentaclesPersistedSession = ref(null) + + const cloudtentaclesStats = computed(() => ({ + hasCredential: Boolean( + cloudtentaclesForm.value.username.trim() + && cloudtentaclesForm.value.password.trim() + && cloudtentaclesForm.value.phone.trim(), + ), + smsSent: Boolean(cloudtentaclesSmsResult.value), + validated: Boolean(cloudtentaclesValidateResult.value || cloudtentaclesLoginResult.value), + })) + + function hydrateCloudtentaclesConfig(data: { + filePath: string + sessionFilePath: string + session: AdminCloudtentaclesPersistedSession + source: { + baseUrl?: string + username?: string + password?: string + phone?: string + deviceId?: string + deviceType?: number + } + }) { + cloudtentaclesFilePath.value = data.filePath + cloudtentaclesSessionFilePath.value = data.sessionFilePath + cloudtentaclesPersistedSession.value = data.session + cloudtentaclesForm.value = { + baseUrl: data.source.baseUrl || 'https://123.207.217.176', + username: data.source.username || '', + password: data.source.password || '', + phone: data.source.phone || '', + code: '', + token: data.session?.token || '', + vnKey: '1', + vnId: 0, + skuId: 5, + skuCount: 1, + deviceId: data.source.deviceId || '-', + deviceType: data.source.deviceType || 0, + } + } + + function buildCloudtentaclesConfigPayload(): AdminCloudtentaclesSourceConfig { + return { + enabled: true, + baseUrl: cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176', + username: cloudtentaclesForm.value.username.trim(), + password: cloudtentaclesForm.value.password.trim(), + phone: cloudtentaclesForm.value.phone.trim(), + deviceId: cloudtentaclesForm.value.deviceId.trim() || '-', + deviceType: Number(cloudtentaclesForm.value.deviceType || 0), + } + } + + function buildCloudtentaclesLoginPayload() { + return { + ...buildCloudtentaclesConfigPayload(), + code: cloudtentaclesForm.value.code.trim(), + } + } + + function ensureCloudtentaclesBaseFields() { + if (!cloudtentaclesForm.value.username.trim() || !cloudtentaclesForm.value.phone.trim()) { + cloudtentaclesResultError.value = '请先填写 cloudtentacles 账号和手机号' + return false + } + return true + } + + function ensureCloudtentaclesLoginFields() { + if ( + !cloudtentaclesForm.value.username.trim() + || !cloudtentaclesForm.value.password.trim() + || !cloudtentaclesForm.value.phone.trim() + ) { + cloudtentaclesResultError.value = '请先填写 cloudtentacles 账号、密码和手机号' + return false + } + + if (!cloudtentaclesForm.value.code.trim()) { + cloudtentaclesResultError.value = '请先填写短信验证码' + return false + } + + return true + } + + async function handleCloudtentaclesSaveSource() { + cloudtentaclesSaving.value = true + cloudtentaclesResultError.value = '' + + try { + const response = await saveAdminCloudtentaclesSourceConfig(buildCloudtentaclesConfigPayload()) + cloudtentaclesFilePath.value = response.data.filePath + cloudtentaclesSessionFilePath.value = response.data.sessionFilePath + cloudtentaclesPersistedSession.value = response.data.session + cloudtentaclesForm.value.token = response.data.session?.token || '' + showSuccess('cloudtentacles 履约平台配置已保存') + } catch (error) { + cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 配置保存失败' + showError(cloudtentaclesResultError.value) + } finally { + cloudtentaclesSaving.value = false + } + } + + async function handleCloudtentaclesSendSmsCode() { + if (!ensureCloudtentaclesBaseFields()) { + return + } + + cloudtentaclesSendingSms.value = true + cloudtentaclesResultError.value = '' + + try { + const response = await sendAdminCloudtentaclesSmsCode(buildCloudtentaclesConfigPayload()) + cloudtentaclesSmsResult.value = response.data + showSuccess('cloudtentacles 短信验证码已发送') + } catch (error) { + cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 发送短信验证码失败' + showError(cloudtentaclesResultError.value) + } finally { + cloudtentaclesSendingSms.value = false + } + } + + async function handleCloudtentaclesTestLogin() { + if (!ensureCloudtentaclesLoginFields()) { + return + } + + cloudtentaclesTesting.value = true + cloudtentaclesResultError.value = '' + + try { + const response = await testAdminCloudtentaclesLogin(buildCloudtentaclesLoginPayload()) + cloudtentaclesLoginResult.value = response.data + cloudtentaclesForm.value.token = response.data.token || cloudtentaclesForm.value.token + cloudtentaclesPersistedSession.value = { + token: response.data.token || '', + tokenMasked: response.data.session.tokenMasked, + baseUrl: response.data.baseUrl, + username: response.data.username, + phoneMasked: response.data.phoneMasked, + loggedInAt: response.data.loggedInAt, + deviceId: cloudtentaclesForm.value.deviceId.trim() || '-', + deviceType: Number(cloudtentaclesForm.value.deviceType || 0), + hasToken: Boolean(response.data.token), + } + showSuccess('cloudtentacles 登录测试成功') + } catch (error) { + cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 登录测试失败' + showError(cloudtentaclesResultError.value) + } finally { + cloudtentaclesTesting.value = false + } + } + + async function handleCloudtentaclesValidateSession() { + const token = cloudtentaclesForm.value.token.trim() + if (!token) { + cloudtentaclesResultError.value = '请先填写 token' + return + } + + cloudtentaclesValidating.value = true + cloudtentaclesResultError.value = '' + + try { + const response = await validateAdminCloudtentaclesSession({ + baseUrl: cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176', + token, + deviceId: cloudtentaclesForm.value.deviceId.trim() || '-', + deviceType: Number(cloudtentaclesForm.value.deviceType || 0), + }) + cloudtentaclesValidateResult.value = response.data + cloudtentaclesPersistedSession.value = { + token, + tokenMasked: response.data.session.tokenMasked, + baseUrl: response.data.baseUrl, + username: cloudtentaclesPersistedSession.value?.username || cloudtentaclesForm.value.username.trim(), + phoneMasked: cloudtentaclesPersistedSession.value?.phoneMasked || '', + loggedInAt: response.data.loggedInAt, + deviceId: cloudtentaclesForm.value.deviceId.trim() || '-', + deviceType: Number(cloudtentaclesForm.value.deviceType || 0), + hasToken: Boolean(token), + } + showSuccess('cloudtentacles 会话校验成功') + } catch (error) { + cloudtentaclesResultError.value = error instanceof Error ? error.message : 'cloudtentacles 会话校验失败' + showError(cloudtentaclesResultError.value) + } finally { + cloudtentaclesValidating.value = false + } + } + + function buildCloudtentaclesTokenPayload() { + return { + baseUrl: cloudtentaclesForm.value.baseUrl.trim() || 'https://123.207.217.176', + token: cloudtentaclesForm.value.token.trim(), + deviceId: cloudtentaclesForm.value.deviceId.trim() || '-', + deviceType: Number(cloudtentaclesForm.value.deviceType || 0), + } + } + + function ensureCloudtentaclesToken() { + if (!cloudtentaclesForm.value.token.trim()) { + cloudtentaclesResultError.value = '请先登录或手动填写 token' + return false + } + return true + } + + function renderCloudtentaclesDebugResult(label: string, data: unknown) { + cloudtentaclesDebugResult.value = `${label}\n${JSON.stringify(data, null, 2)}` + } + + async function runCloudtentaclesDebugAction(label: string, runner: () => Promise<{ data: unknown }>) { + cloudtentaclesDebugLoading.value = true + cloudtentaclesResultError.value = '' + + try { + const response = await runner() + renderCloudtentaclesDebugResult(label, response.data) + showSuccess(`${label}成功`) + } catch (error) { + cloudtentaclesResultError.value = error instanceof Error ? error.message : `${label}失败` + showError(cloudtentaclesResultError.value) + } finally { + cloudtentaclesDebugLoading.value = false + } + } + + async function handleCloudtentaclesFetchAsset() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询余额', () => fetchAdminCloudtentaclesAsset(buildCloudtentaclesTokenPayload())) + } + + async function handleCloudtentaclesFetchCategories() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询分类', () => fetchAdminCloudtentaclesCategories(buildCloudtentaclesTokenPayload())) + } + + async function handleCloudtentaclesFetchSkuList() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询SKU列表', () => fetchAdminCloudtentaclesSkuList(buildCloudtentaclesTokenPayload())) + } + + async function handleCloudtentaclesBuySku() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('购买SKU', () => buyAdminCloudtentaclesSku({ + ...buildCloudtentaclesTokenPayload(), + id: Number(cloudtentaclesForm.value.skuId || 0), + count: Number(cloudtentaclesForm.value.skuCount || 1), + })) + } + + async function handleCloudtentaclesUseSku() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('执行发货', () => useAdminCloudtentaclesSku({ + ...buildCloudtentaclesTokenPayload(), + id: Number(cloudtentaclesForm.value.skuId || 0), + virtualNumberId: Number(cloudtentaclesForm.value.vnId || 0), + phone: cloudtentaclesForm.value.phone.trim(), + })) + } + + async function handleCloudtentaclesFetchKnapsack() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询背包', () => fetchAdminCloudtentaclesKnapsack(buildCloudtentaclesTokenPayload())) + } + + async function handleCloudtentaclesFetchVnList() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('查询虚拟号列表', () => fetchAdminCloudtentaclesVnList({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + })) + } + + async function handleCloudtentaclesAppointVn() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('申请虚拟号', async () => { + const response = await appointAdminCloudtentaclesVn({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + }) + const item = (response.data as { item?: { id?: number, phone?: string } }).item + if (item?.id) { + cloudtentaclesForm.value.vnId = Number(item.id) + } + if (item?.phone) { + cloudtentaclesForm.value.phone = String(item.phone) + } + return response + }) + } + + async function handleCloudtentaclesGenerateVnLoginCode() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('生成登录码', () => generateAdminCloudtentaclesVnLoginCode({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + id: Number(cloudtentaclesForm.value.vnId || 0), + })) + } + + async function handleCloudtentaclesFetchVnCode() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('获取验证码', async () => { + const response = await fetchAdminCloudtentaclesVnCode({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + phone: cloudtentaclesForm.value.phone.trim(), + }) + const code = (response.data as { code?: string }).code + if (code) { + cloudtentaclesForm.value.code = String(code) + } + return response + }) + } + + async function handleCloudtentaclesVerifyVnCode() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('校验验证码', () => verifyAdminCloudtentaclesVnCode({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + id: Number(cloudtentaclesForm.value.vnId || 0), + code: cloudtentaclesForm.value.code.trim(), + })) + } + + async function handleCloudtentaclesFetchBindUrl() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('获取兑换链接', () => fetchAdminCloudtentaclesBindUrl({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + id: Number(cloudtentaclesForm.value.vnId || 0), + })) + } + + async function handleCloudtentaclesBackVn() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('退还号码', () => backAdminCloudtentaclesVn({ + ...buildCloudtentaclesTokenPayload(), + key: cloudtentaclesForm.value.vnKey.trim(), + id: Number(cloudtentaclesForm.value.vnId || 0), + })) + } + + async function handleCloudtentaclesRunFullFlow() { + if (!ensureCloudtentaclesToken()) return + await runCloudtentaclesDebugAction('完整测试流程', () => runAdminCloudtentaclesFullFlow({ + ...buildCloudtentaclesTokenPayload(), + skuId: Number(cloudtentaclesForm.value.skuId || 0), + skuCount: Number(cloudtentaclesForm.value.skuCount || 1), + vnKey: cloudtentaclesForm.value.vnKey.trim(), + })) + } + + return { + cloudtentaclesFilePath, + cloudtentaclesSessionFilePath, + cloudtentaclesForm, + cloudtentaclesSaving, + cloudtentaclesSendingSms, + cloudtentaclesTesting, + cloudtentaclesValidating, + cloudtentaclesResultError, + cloudtentaclesSmsResult, + cloudtentaclesLoginResult, + cloudtentaclesValidateResult, + cloudtentaclesDebugLoading, + cloudtentaclesDebugResult, + cloudtentaclesPersistedSession, + cloudtentaclesStats, + hydrateCloudtentaclesConfig, + handleCloudtentaclesSaveSource, + handleCloudtentaclesSendSmsCode, + handleCloudtentaclesTestLogin, + handleCloudtentaclesValidateSession, + handleCloudtentaclesFetchAsset, + handleCloudtentaclesFetchCategories, + handleCloudtentaclesFetchSkuList, + handleCloudtentaclesBuySku, + handleCloudtentaclesUseSku, + handleCloudtentaclesFetchKnapsack, + handleCloudtentaclesFetchVnList, + handleCloudtentaclesAppointVn, + handleCloudtentaclesGenerateVnLoginCode, + handleCloudtentaclesFetchVnCode, + handleCloudtentaclesVerifyVnCode, + handleCloudtentaclesFetchBindUrl, + handleCloudtentaclesBackVn, + handleCloudtentaclesRunFullFlow, + } +} diff --git a/apps/frontend/src/composables/admin/platform-shops/useAdminKhhaoPlatform.ts b/apps/frontend/src/composables/admin/platform-shops/useAdminKhhaoPlatform.ts new file mode 100644 index 00000000..1fa32a5e --- /dev/null +++ b/apps/frontend/src/composables/admin/platform-shops/useAdminKhhaoPlatform.ts @@ -0,0 +1,215 @@ +import { computed, ref } from 'vue' + +import { showError, showSuccess } from '@/lib/feedback' +import { + queryAdminKhhaoOrders, + saveAdminKhhaoSourceConfig, + syncAdminKhhaoOrders, + testAdminKhhaoLogin, +} from '@/services/admin' +import type { + AdminKhhaoLoginTestResult, + AdminKhhaoOrderQueryResult, + AdminKhhaoOrderSyncResult, + AdminKhhaoSyncState, +} from '@/types/admin' + +export function useAdminKhhaoPlatform() { + const khhaoFilePath = ref('') + const khhaoStateFilePath = ref('') + const khhaoForm = ref({ + baseUrl: 'https://admin.khhao.com', + username: '', + password: '', + page: 1, + limit: 10, + maxCaptchaAttempts: 3, + autoSyncEnabled: false, + autoSyncIntervalMinutes: 3, + autoSyncPages: 2, + autoSyncPageSize: 20, + }) + const khhaoSaving = ref(false) + const khhaoTesting = ref(false) + const khhaoQuerying = ref(false) + const khhaoSyncing = ref(false) + const khhaoResultError = ref('') + const khhaoLoginResult = ref(null) + const khhaoQueryResult = ref(null) + const khhaoSyncResult = ref(null) + const khhaoAutoSyncState = ref(null) + + const khhaoStats = computed(() => ({ + hasCredential: Boolean(khhaoForm.value.username.trim() && khhaoForm.value.password.trim()), + queryCount: khhaoQueryResult.value?.itemCount || 0, + syncedCount: khhaoAutoSyncState.value?.syncedCount || khhaoSyncResult.value?.syncedCount || 0, + ignoredCount: khhaoAutoSyncState.value?.ignoredCount || khhaoSyncResult.value?.ignoredCount || 0, + })) + + function hydrateKhhaoConfig(data: { + filePath: string + stateFilePath: string + syncState: AdminKhhaoSyncState + source: { + baseUrl?: string + username?: string + password?: string + maxCaptchaAttempts?: number + autoSync?: { + enabled?: boolean + intervalMinutes?: number + pages?: number + pageSize?: number + } + } + }) { + khhaoFilePath.value = data.filePath + khhaoStateFilePath.value = data.stateFilePath + khhaoAutoSyncState.value = data.syncState + khhaoForm.value = { + baseUrl: data.source.baseUrl || 'https://admin.khhao.com', + username: data.source.username || '', + password: data.source.password || '', + page: 1, + limit: 10, + maxCaptchaAttempts: data.source.maxCaptchaAttempts || 3, + autoSyncEnabled: data.source.autoSync?.enabled === true, + autoSyncIntervalMinutes: data.source.autoSync?.intervalMinutes || 3, + autoSyncPages: data.source.autoSync?.pages || 2, + autoSyncPageSize: data.source.autoSync?.pageSize || 20, + } + } + + function buildKhhaoPayload() { + return { + baseUrl: khhaoForm.value.baseUrl.trim() || 'https://admin.khhao.com', + username: khhaoForm.value.username.trim(), + password: khhaoForm.value.password.trim(), + page: Number(khhaoForm.value.page || 1), + limit: Number(khhaoForm.value.limit || 10), + maxCaptchaAttempts: Number(khhaoForm.value.maxCaptchaAttempts || 3), + } + } + + function ensureKhhaoCredentials() { + if (!khhaoForm.value.username.trim() || !khhaoForm.value.password.trim()) { + khhaoResultError.value = '请先填写 khhao 账号和密码' + return false + } + return true + } + + async function handleKhhaoSaveSource() { + khhaoSaving.value = true + khhaoResultError.value = '' + + try { + const response = await saveAdminKhhaoSourceConfig({ + enabled: true, + baseUrl: khhaoForm.value.baseUrl.trim() || 'https://admin.khhao.com', + username: khhaoForm.value.username.trim(), + password: khhaoForm.value.password.trim(), + maxCaptchaAttempts: Number(khhaoForm.value.maxCaptchaAttempts || 3), + autoSync: { + enabled: Boolean(khhaoForm.value.autoSyncEnabled), + intervalMinutes: Number(khhaoForm.value.autoSyncIntervalMinutes || 3), + pages: Number(khhaoForm.value.autoSyncPages || 2), + pageSize: Number(khhaoForm.value.autoSyncPageSize || 20), + }, + }) + khhaoFilePath.value = response.data.filePath + khhaoStateFilePath.value = response.data.stateFilePath + khhaoAutoSyncState.value = response.data.syncState + showSuccess('khhao 来源配置已保存') + } catch (error) { + khhaoResultError.value = error instanceof Error ? error.message : 'khhao 来源配置保存失败' + showError(khhaoResultError.value) + } finally { + khhaoSaving.value = false + } + } + + async function handleKhhaoTestLogin() { + if (!ensureKhhaoCredentials()) { + return + } + + khhaoTesting.value = true + khhaoResultError.value = '' + + try { + const response = await testAdminKhhaoLogin({ + ...buildKhhaoPayload(), + includeImageBase64: false, + }) + khhaoLoginResult.value = response.data + showSuccess('khhao 登录测试成功') + } catch (error) { + khhaoResultError.value = error instanceof Error ? error.message : 'khhao 登录测试失败' + showError(khhaoResultError.value) + } finally { + khhaoTesting.value = false + } + } + + async function handleKhhaoQueryOrders() { + if (!ensureKhhaoCredentials()) { + return + } + + khhaoQuerying.value = true + khhaoResultError.value = '' + + try { + const response = await queryAdminKhhaoOrders(buildKhhaoPayload()) + khhaoQueryResult.value = response.data + showSuccess(`khhao 订单查询成功,共返回 ${response.data.itemCount} 条`) + } catch (error) { + khhaoResultError.value = error instanceof Error ? error.message : 'khhao 订单查询失败' + showError(khhaoResultError.value) + } finally { + khhaoQuerying.value = false + } + } + + async function handleKhhaoSyncOrders() { + if (!ensureKhhaoCredentials()) { + return + } + + khhaoSyncing.value = true + khhaoResultError.value = '' + + try { + const response = await syncAdminKhhaoOrders(buildKhhaoPayload()) + khhaoSyncResult.value = response.data + showSuccess(`khhao 同步完成:拉取 ${response.data.fetchedCount} 条,成功 ${response.data.syncedCount} 条`) + } catch (error) { + khhaoResultError.value = error instanceof Error ? error.message : 'khhao 订单同步失败' + showError(khhaoResultError.value) + } finally { + khhaoSyncing.value = false + } + } + + return { + khhaoFilePath, + khhaoStateFilePath, + khhaoForm, + khhaoSaving, + khhaoTesting, + khhaoQuerying, + khhaoSyncing, + khhaoResultError, + khhaoLoginResult, + khhaoQueryResult, + khhaoSyncResult, + khhaoAutoSyncState, + khhaoStats, + hydrateKhhaoConfig, + handleKhhaoSaveSource, + handleKhhaoTestLogin, + handleKhhaoQueryOrders, + handleKhhaoSyncOrders, + } +} diff --git a/apps/frontend/src/composables/admin/platform-shops/useAdminKuaishouEticketPlatform.ts b/apps/frontend/src/composables/admin/platform-shops/useAdminKuaishouEticketPlatform.ts new file mode 100644 index 00000000..60b712a0 --- /dev/null +++ b/apps/frontend/src/composables/admin/platform-shops/useAdminKuaishouEticketPlatform.ts @@ -0,0 +1,382 @@ +import { computed, ref } from 'vue' + +import { showError, showSuccess } from '@/lib/feedback' +import { + consumeAdminKuaishouEticket, + queryAdminKuaishouEticketDetail, + queryAdminKuaishouEticketShopInfo, + saveAdminKuaishouEticketSourceConfig, +} from '@/services/admin' +import type { + AdminKuaishouEticketConsumeResult, + AdminKuaishouEticketDetailResult, + AdminKuaishouEticketShopConfigItem, + AdminKuaishouEticketShopInfoResult, + AdminKuaishouEticketSourceConfig, +} from '@/types/admin' + +import type { EditableKuaishouEticketShop } from './types' + +export function useAdminKuaishouEticketPlatform() { + const kuaishouEticketFilePath = ref('') + const kuaishouEticketShops = ref([]) + const expandedKuaishouEticketShopIds = ref([]) + const kuaishouEticketForm = ref({ + baseUrl: 'https://s.kwaixiaodian.com', + selectedShopId: '', + queryTicketCode: '', + eTicketId: '', + oid: '', + formToken: '', + num: 1, + storeId: '0', + }) + const kuaishouEticketSaving = ref(false) + const kuaishouEticketResolvingShopId = ref('') + const kuaishouEticketDetailing = ref(false) + const kuaishouEticketConsuming = ref(false) + const kuaishouEticketResultError = ref('') + const kuaishouEticketShopInfoResult = ref(null) + const kuaishouEticketDetailResult = ref(null) + const kuaishouEticketConsumeResult = ref(null) + + const activeKuaishouEticketShop = computed(() => ( + kuaishouEticketShops.value.find((item) => item.id === kuaishouEticketForm.value.selectedShopId) || null + )) + + const kuaishouEticketStats = computed(() => ({ + configuredShopCount: kuaishouEticketShops.value.length, + cookieReadyCount: kuaishouEticketShops.value.filter((item) => item.cookie.trim()).length, + selectedShopReady: Boolean(activeKuaishouEticketShop.value?.cookie.trim()), + detailReady: Boolean(kuaishouEticketDetailResult.value?.detail), + consumed: Boolean(kuaishouEticketConsumeResult.value?.consumed), + })) + + function hydrateKuaishouEticketConfig(data: { + filePath: string + source: { + baseUrl?: string + shops: AdminKuaishouEticketShopConfigItem[] + } + }) { + kuaishouEticketFilePath.value = data.filePath + kuaishouEticketShops.value = data.source.shops.map(mapEditableKuaishouEticketShop) + kuaishouEticketForm.value = { + baseUrl: data.source.baseUrl || 'https://s.kwaixiaodian.com', + selectedShopId: '', + queryTicketCode: '', + eTicketId: '', + oid: '', + formToken: '', + num: 1, + storeId: '0', + } + kuaishouEticketForm.value.selectedShopId = kuaishouEticketShops.value[0]?.id || '' + expandedKuaishouEticketShopIds.value = kuaishouEticketShops.value[0]?.id ? [kuaishouEticketShops.value[0].id] : [] + } + + function buildKuaishouEticketConfigPayload(): AdminKuaishouEticketSourceConfig { + return { + enabled: true, + baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com', + shops: kuaishouEticketShops.value.map((item) => ({ + shopId: item.shopId.trim(), + kshopName: item.kshopName.trim(), + cookie: item.cookie.trim(), + cookieMasked: '', + hasCookie: Boolean(item.cookie.trim()), + userAvatar: item.userAvatar.trim(), + enabled: item.enabled, + })), + } + } + + function addKuaishouEticketShop() { + const shop = createEmptyKuaishouEticketShop() + kuaishouEticketShops.value.unshift(shop) + setKuaishouEticketDebugShop(shop.id) + } + + function removeKuaishouEticketShop(id: string) { + kuaishouEticketShops.value = kuaishouEticketShops.value.filter((item) => item.id !== id) + collapseKuaishouEticketShop(id) + if (kuaishouEticketForm.value.selectedShopId === id) { + kuaishouEticketForm.value.selectedShopId = kuaishouEticketShops.value[0]?.id || '' + } + } + + function isKuaishouEticketShopExpanded(id: string) { + return expandedKuaishouEticketShopIds.value.includes(id) + } + + function expandKuaishouEticketShop(id: string) { + if (expandedKuaishouEticketShopIds.value.includes(id)) { + return + } + expandedKuaishouEticketShopIds.value = [id, ...expandedKuaishouEticketShopIds.value] + } + + function collapseKuaishouEticketShop(id: string) { + expandedKuaishouEticketShopIds.value = expandedKuaishouEticketShopIds.value.filter((item) => item !== id) + } + + function toggleKuaishouEticketShop(id: string) { + if (isKuaishouEticketShopExpanded(id)) { + collapseKuaishouEticketShop(id) + return + } + expandKuaishouEticketShop(id) + } + + function expandAllKuaishouEticketShops() { + expandedKuaishouEticketShopIds.value = kuaishouEticketShops.value.map((item) => item.id) + } + + function collapseAllKuaishouEticketShops() { + expandedKuaishouEticketShopIds.value = [] + } + + function setKuaishouEticketDebugShop(id: string) { + kuaishouEticketForm.value.selectedShopId = id + expandKuaishouEticketShop(id) + } + + function describeKuaishouEticketShop(shop: EditableKuaishouEticketShop) { + const status = shop.cookie.trim() ? 'Cookie 已就绪' : '待补 Cookie' + const identity = shop.shopId.trim() ? '已识别店铺' : '待读取信息' + return `${status} · ${identity}` + } + + function getKuaishouEticketShopInitial(shop: EditableKuaishouEticketShop) { + const label = shop.kshopName.trim() || shop.shopId.trim() || '店' + return label.slice(0, 1).toUpperCase() + } + + function ensureSelectedKuaishouEticketShop() { + const selected = activeKuaishouEticketShop.value + + if (!selected) { + kuaishouEticketResultError.value = '请先新增并选择一个店铺配置' + return null + } + + if (!selected.cookie.trim()) { + kuaishouEticketResultError.value = '当前店铺缺少 Cookie,请先补全后再操作' + return null + } + + return selected + } + + function ensureKuaishouEticketQueryFields() { + const selected = ensureSelectedKuaishouEticketShop() + if (!selected) return false + + if (!kuaishouEticketForm.value.queryTicketCode.trim()) { + kuaishouEticketResultError.value = '请先填写查询券码' + return false + } + + return true + } + + function ensureKuaishouEticketConsumeFields() { + const selected = ensureSelectedKuaishouEticketShop() + if (!selected) return false + + if (!kuaishouEticketDetailResult.value?.detail) { + kuaishouEticketResultError.value = '请先查询核销信息,系统会自动带出 eTicketId、oid 和 formToken' + return false + } + + return true + } + + function applyKuaishouEticketDetailToForm(result: AdminKuaishouEticketDetailResult | null) { + if (!result?.detail) { + return + } + + kuaishouEticketForm.value.eTicketId = result.detail.eTicketId || kuaishouEticketForm.value.eTicketId + kuaishouEticketForm.value.oid = result.detail.oid || kuaishouEticketForm.value.oid + kuaishouEticketForm.value.formToken = result.detail.formToken || kuaishouEticketForm.value.formToken + + const leftCount = Number(result.detail.leftCount || 0) + if (leftCount > 0) { + kuaishouEticketForm.value.num = leftCount + } + } + + async function handleKuaishouEticketResolveShopInfo(shop: EditableKuaishouEticketShop) { + if (!shop.cookie.trim()) { + kuaishouEticketResultError.value = '请先为该店铺粘贴 Cookie' + showError(kuaishouEticketResultError.value) + return + } + + kuaishouEticketResolvingShopId.value = shop.id + kuaishouEticketResultError.value = '' + + try { + const response = await queryAdminKuaishouEticketShopInfo({ + baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com', + shopId: shop.shopId.trim(), + cookie: shop.cookie.trim(), + }) + kuaishouEticketShopInfoResult.value = response.data + shop.shopId = response.data.shop.shopId || shop.shopId + shop.kshopName = response.data.shop.kshopName || shop.kshopName + shop.userAvatar = response.data.shop.userAvatar || shop.userAvatar + if (!kuaishouEticketForm.value.selectedShopId) { + setKuaishouEticketDebugShop(shop.id) + } + showSuccess(`已识别店铺:${response.data.shop.kshopName || response.data.shop.shopId || '未知店铺'}`) + } catch (error) { + kuaishouEticketResultError.value = error instanceof Error ? error.message : '读取快手小店店铺信息失败' + showError(kuaishouEticketResultError.value) + } finally { + kuaishouEticketResolvingShopId.value = '' + } + } + + async function handleKuaishouEticketSaveSource() { + kuaishouEticketSaving.value = true + kuaishouEticketResultError.value = '' + + try { + const response = await saveAdminKuaishouEticketSourceConfig(buildKuaishouEticketConfigPayload()) + kuaishouEticketFilePath.value = response.data.filePath + kuaishouEticketForm.value.baseUrl = response.data.source.baseUrl || 'https://s.kwaixiaodian.com' + const previousSelectedShopId = activeKuaishouEticketShop.value?.shopId || '' + kuaishouEticketShops.value = response.data.source.shops.map(mapEditableKuaishouEticketShop) + kuaishouEticketForm.value.selectedShopId = ( + kuaishouEticketShops.value.find((item) => item.shopId && item.shopId === previousSelectedShopId)?.id + || kuaishouEticketShops.value[0]?.id + || '' + ) + expandedKuaishouEticketShopIds.value = kuaishouEticketForm.value.selectedShopId + ? [kuaishouEticketForm.value.selectedShopId] + : [] + showSuccess('快手小店核销配置已保存') + } catch (error) { + kuaishouEticketResultError.value = error instanceof Error ? error.message : '快手小店核销配置保存失败' + showError(kuaishouEticketResultError.value) + } finally { + kuaishouEticketSaving.value = false + } + } + + async function handleKuaishouEticketQueryDetail() { + if (!ensureKuaishouEticketQueryFields()) { + return + } + + const selected = activeKuaishouEticketShop.value + kuaishouEticketDetailing.value = true + kuaishouEticketResultError.value = '' + kuaishouEticketConsumeResult.value = null + + try { + const response = await queryAdminKuaishouEticketDetail({ + baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com', + shopId: selected?.shopId.trim(), + cookie: selected?.cookie.trim(), + eTicketId: kuaishouEticketForm.value.queryTicketCode.trim(), + }) + kuaishouEticketDetailResult.value = response.data + applyKuaishouEticketDetailToForm(response.data) + showSuccess(response.data.ok ? '核销信息查询成功' : (response.data.errorMessage || '核销信息已返回')) + } catch (error) { + kuaishouEticketResultError.value = error instanceof Error ? error.message : '查询快手小店核销信息失败' + showError(kuaishouEticketResultError.value) + } finally { + kuaishouEticketDetailing.value = false + } + } + + async function handleKuaishouEticketConsume() { + if (!ensureKuaishouEticketConsumeFields()) { + return + } + + const selected = activeKuaishouEticketShop.value + kuaishouEticketConsuming.value = true + kuaishouEticketResultError.value = '' + + try { + const detail = kuaishouEticketDetailResult.value?.detail + const response = await consumeAdminKuaishouEticket({ + baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com', + shopId: selected?.shopId.trim(), + cookie: selected?.cookie.trim(), + eTicketId: String(detail?.eTicketId || kuaishouEticketForm.value.eTicketId || '').trim(), + oid: String(detail?.oid || kuaishouEticketForm.value.oid || '').trim(), + formToken: String(detail?.formToken || kuaishouEticketForm.value.formToken || '').trim(), + num: Number(kuaishouEticketForm.value.num || 1), + storeId: kuaishouEticketForm.value.storeId.trim() || '0', + }) + kuaishouEticketConsumeResult.value = response.data + showSuccess(response.data.ok ? '快手小店核销成功' : (response.data.errorMessage || '核销请求已执行')) + } catch (error) { + kuaishouEticketResultError.value = error instanceof Error ? error.message : '执行快手小店核销失败' + showError(kuaishouEticketResultError.value) + } finally { + kuaishouEticketConsuming.value = false + } + } + + return { + kuaishouEticketFilePath, + kuaishouEticketShops, + expandedKuaishouEticketShopIds, + kuaishouEticketForm, + kuaishouEticketSaving, + kuaishouEticketResolvingShopId, + kuaishouEticketDetailing, + kuaishouEticketConsuming, + kuaishouEticketResultError, + kuaishouEticketShopInfoResult, + kuaishouEticketDetailResult, + kuaishouEticketConsumeResult, + activeKuaishouEticketShop, + kuaishouEticketStats, + hydrateKuaishouEticketConfig, + addKuaishouEticketShop, + removeKuaishouEticketShop, + isKuaishouEticketShopExpanded, + expandKuaishouEticketShop, + collapseKuaishouEticketShop, + toggleKuaishouEticketShop, + expandAllKuaishouEticketShops, + collapseAllKuaishouEticketShops, + setKuaishouEticketDebugShop, + describeKuaishouEticketShop, + getKuaishouEticketShopInitial, + handleKuaishouEticketResolveShopInfo, + handleKuaishouEticketSaveSource, + handleKuaishouEticketQueryDetail, + handleKuaishouEticketConsume, + } +} + +function createEmptyKuaishouEticketShop(): EditableKuaishouEticketShop { + return { + id: crypto.randomUUID(), + shopId: '', + kshopName: '', + cookie: '', + userAvatar: '', + enabled: true, + } +} + +function mapEditableKuaishouEticketShop(item: AdminKuaishouEticketShopConfigItem): EditableKuaishouEticketShop { + return { + id: crypto.randomUUID(), + shopId: item.shopId, + kshopName: item.kshopName, + cookie: item.cookie, + userAvatar: item.userAvatar, + enabled: item.enabled !== false, + } +} diff --git a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue index d83aee77..cf9505d8 100644 --- a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue +++ b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue @@ -1,266 +1,131 @@