拆分平台配置页脚本逻辑

This commit is contained in:
yml
2026-05-04 10:29:48 +08:00
parent 8e1a4be895
commit dff7da73d7
7 changed files with 1394 additions and 1084 deletions
@@ -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
}
@@ -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<EditableDefaults>(createEmptyDefaults())
const shops = ref<EditableShop[]>([])
const observedShops = ref<AdminAgisoObservedShopItem[]>([])
const expandedShopIds = ref<string[]>([])
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<void>) {
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,
}
}
@@ -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<AdminCloudtentaclesSendSmsResult | null>(null)
const cloudtentaclesLoginResult = ref<AdminCloudtentaclesLoginTestResult | null>(null)
const cloudtentaclesValidateResult = ref<AdminCloudtentaclesValidateSessionResult | null>(null)
const cloudtentaclesDebugLoading = ref(false)
const cloudtentaclesDebugResult = ref('')
const cloudtentaclesPersistedSession = ref<AdminCloudtentaclesPersistedSession | null>(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,
}
}
@@ -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<AdminKhhaoLoginTestResult | null>(null)
const khhaoQueryResult = ref<AdminKhhaoOrderQueryResult | null>(null)
const khhaoSyncResult = ref<AdminKhhaoOrderSyncResult | null>(null)
const khhaoAutoSyncState = ref<AdminKhhaoSyncState | null>(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,
}
}
@@ -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<EditableKuaishouEticketShop[]>([])
const expandedKuaishouEticketShopIds = ref<string[]>([])
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<AdminKuaishouEticketShopInfoResult | null>(null)
const kuaishouEticketDetailResult = ref<AdminKuaishouEticketDetailResult | null>(null)
const kuaishouEticketConsumeResult = ref<AdminKuaishouEticketConsumeResult | null>(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,
}
}