删除 vue
This commit is contained in:
@@ -1 +0,0 @@
|
||||
VITE_API_TARGET=http://127.0.0.1:3000
|
||||
@@ -1 +0,0 @@
|
||||
VITE_API_TARGET=http://127.0.0.1:3000
|
||||
@@ -1,25 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.ace-tool/
|
||||
@@ -1,4 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
src/auto-imports.d.ts
|
||||
src/components.d.ts
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
# 快手轻量前端
|
||||
|
||||
这是快手自动发货拆分后的 Vue 3 前端,只保留快手领取和运营后台相关入口。
|
||||
|
||||
## 功能范围
|
||||
|
||||
- `/claim/:token` 快手 Cloud 领取页
|
||||
- `/admin` 运营后台
|
||||
- 91 卡券订单补全与任务生成
|
||||
- 快手小店核销配置
|
||||
- Cloudtentacles 履约配置与调试
|
||||
- 内部通知和定时任务配置
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
开发环境默认通过 `VITE_API_TARGET` 代理后端:
|
||||
|
||||
```bash
|
||||
VITE_API_TARGET=http://localhost:3000 npm run dev
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run build
|
||||
```
|
||||
@@ -1,34 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import prettier from 'eslint-config-prettier'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**', 'src/auto-imports.d.ts', 'src/components.d.ts'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
prettier,
|
||||
{
|
||||
files: ['**/*.{ts,vue}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-mutating-props': ['error', { shallowOnly: true }],
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>自动兑换前端</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
-4211
File diff suppressed because it is too large
Load Diff
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "order-site-rewrite",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"axios": "^1.9.0",
|
||||
"element-plus": "^2.10.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.5.30",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^24.12.0",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vue/tsconfig": "^0.9.0",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-vue": "^10.9.1",
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"sass": "^1.99.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.59.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^32.0.0",
|
||||
"vite": "^8.0.7",
|
||||
"vue-tsc": "^3.2.5"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 245 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 137 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 329 KiB |
@@ -1,10 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider :locale="zhCn">
|
||||
<RouterView />
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const computed: typeof import('vue').computed
|
||||
const createApp: typeof import('vue').createApp
|
||||
const customRef: typeof import('vue').customRef
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
const h: typeof import('vue').h
|
||||
const inject: typeof import('vue').inject
|
||||
const isProxy: typeof import('vue').isProxy
|
||||
const isReactive: typeof import('vue').isReactive
|
||||
const isReadonly: typeof import('vue').isReadonly
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
|
||||
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||
const onMounted: typeof import('vue').onMounted
|
||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||
const onUnmounted: typeof import('vue').onUnmounted
|
||||
const onUpdated: typeof import('vue').onUpdated
|
||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||
const provide: typeof import('vue').provide
|
||||
const reactive: typeof import('vue').reactive
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
const toValue: typeof import('vue').toValue
|
||||
const triggerRef: typeof import('vue').triggerRef
|
||||
const unref: typeof import('vue').unref
|
||||
const useAttrs: typeof import('vue').useAttrs
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useId: typeof import('vue').useId
|
||||
const useLink: typeof import('vue-router').useLink
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useRoute: typeof import('vue-router').useRoute
|
||||
const useRouter: typeof import('vue-router').useRouter
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const watch: typeof import('vue').watch
|
||||
const watchEffect: typeof import('vue').watchEffect
|
||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AdminPageHeader: typeof import('./components/admin/AdminPageHeader.vue')['default']
|
||||
AdminResultCard: typeof import('./components/admin/AdminResultCard.vue')['default']
|
||||
AdminStatusTag: typeof import('./components/admin/AdminStatusTag.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElResult: typeof import('element-plus/es')['ElResult']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>{{ title }}</h1>
|
||||
<p v-if="description">{{ description }}</p>
|
||||
</div>
|
||||
<div v-if="$slots.extra" class="header-extra">
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: var(--text-3xl);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.header-extra {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
value: string | number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never" class="result-card">
|
||||
<span class="result-label">{{ label }}</span>
|
||||
<strong>{{ value }}</strong>
|
||||
<slot />
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.result-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.result-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.result-card strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.result-card :deep(p) {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
</style>
|
||||
@@ -1,167 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { formatStatusWithRaw } from '@/utils/admin-display'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
status: string
|
||||
showRaw?: boolean
|
||||
}>(),
|
||||
{
|
||||
showRaw: true,
|
||||
},
|
||||
)
|
||||
|
||||
type ElTagType = 'success' | 'primary' | 'warning' | 'danger' | 'info'
|
||||
|
||||
function resolveType(status: string): ElTagType {
|
||||
const normalized = String(status || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
if (
|
||||
[
|
||||
'paid',
|
||||
'link_generated',
|
||||
'redeemed',
|
||||
'available',
|
||||
'delivered',
|
||||
'success',
|
||||
'active',
|
||||
'system_bound',
|
||||
'binding_completed',
|
||||
'completed',
|
||||
].includes(normalized)
|
||||
) {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
'claimed',
|
||||
'role_confirmed',
|
||||
'redeeming',
|
||||
'reserved',
|
||||
'processing',
|
||||
'admin',
|
||||
'support',
|
||||
'waiting_user_claim',
|
||||
'link_opened',
|
||||
'binding_confirmed',
|
||||
'binding_in_progress',
|
||||
'user_binding',
|
||||
'pending',
|
||||
'waiting',
|
||||
'waiting_binding',
|
||||
].includes(normalized)
|
||||
) {
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
'retry_pending',
|
||||
'manual_review',
|
||||
'invalid',
|
||||
'expired',
|
||||
'operator',
|
||||
'pending_binding',
|
||||
'pending_config',
|
||||
'binding_exception',
|
||||
'not_started',
|
||||
'consumed',
|
||||
'skipped',
|
||||
'pending_binding_prepare',
|
||||
'dispatched_pending_return',
|
||||
].includes(normalized)
|
||||
) {
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
if (['closed', 'failed', 'unpaid', 'revoked', 'disabled'].includes(normalized)) {
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function resolveLabel(status: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
|
||||
if (!normalized) return '-'
|
||||
|
||||
const labelMap: Record<string, string> = {
|
||||
created: '已创建',
|
||||
paid: '已支付',
|
||||
unpaid: '未支付',
|
||||
failed: '失败',
|
||||
refunded: '已退款',
|
||||
refunding: '退款中',
|
||||
closed: '已关闭',
|
||||
pending_payment: '待支付',
|
||||
cdk_reserved: '资源已准备',
|
||||
link_generated: '已生成链接',
|
||||
claimed: '已创建会话',
|
||||
role_confirmed: '已确认角色',
|
||||
redeeming: '兑换中',
|
||||
redeemed: '已兑换',
|
||||
retry_pending: '待重试',
|
||||
manual_review: '人工处理',
|
||||
expired: '已过期',
|
||||
available: '可用',
|
||||
reserved: '资源已准备',
|
||||
delivered: '已发放',
|
||||
consumed: '已使用',
|
||||
invalid: '已作废',
|
||||
active: '生效中',
|
||||
admin: '管理员',
|
||||
operator: '普通运营',
|
||||
support: '客服',
|
||||
revoked: '已撤销',
|
||||
disabled: '已停用',
|
||||
success: '成功',
|
||||
skipped: '已跳过',
|
||||
pending: '待触发',
|
||||
waiting: '等待中',
|
||||
processing: '处理中',
|
||||
system_bound: '资源已准备',
|
||||
pending_binding: '待准备资源',
|
||||
resource_ready: '资源已准备',
|
||||
pending_prepare: '待准备资源',
|
||||
pending_config: '待配置',
|
||||
pending_binding_prepare: '待准备资源',
|
||||
not_started: '未开始',
|
||||
waiting_binding: '待客户绑定',
|
||||
dispatched_pending_return: '待退还号码',
|
||||
completed: '已完成',
|
||||
waiting_user_claim: '待客户打开链接',
|
||||
waiting_customer: '待客户绑定',
|
||||
link_opened: '已打开链接',
|
||||
binding_confirmed: '已提交',
|
||||
binding_in_progress: '处理中',
|
||||
binding_completed: '履约完成',
|
||||
binding_exception: '履约异常',
|
||||
customer_confirmed: '已提交',
|
||||
customer_processing: '处理中',
|
||||
customer_completed: '履约完成',
|
||||
customer_exception: '履约异常',
|
||||
resource_exception: '资源异常',
|
||||
user_binding: '客户处理中',
|
||||
}
|
||||
|
||||
if (!props.showRaw) {
|
||||
return labelMap[normalized.toLowerCase()] || normalized
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(normalized, labelMap)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-tag
|
||||
:type="resolveType(props.status)"
|
||||
size="small"
|
||||
:title="props.showRaw ? undefined : props.status"
|
||||
>
|
||||
{{ resolveLabel(props.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { isFeedbackDismissed, showConfirm, showError, showSuccess } from '@/lib/feedback'
|
||||
|
||||
export interface RunActionOptions {
|
||||
id: number | string
|
||||
action: () => Promise<unknown>
|
||||
successMessage: string
|
||||
confirmText: string
|
||||
onAfterAction?: () => Promise<unknown> | void
|
||||
}
|
||||
|
||||
export function useAdminAction() {
|
||||
const actionLoadingId = ref<number | string | null>(null)
|
||||
|
||||
async function runAction(options: RunActionOptions) {
|
||||
try {
|
||||
await showConfirm(options.confirmText, '确认操作', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '继续执行',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
} catch (error) {
|
||||
if (isFeedbackDismissed(error)) return
|
||||
throw error
|
||||
}
|
||||
|
||||
actionLoadingId.value = options.id
|
||||
|
||||
try {
|
||||
await options.action()
|
||||
showSuccess(options.successMessage)
|
||||
if (options.onAfterAction) await options.onAfterAction()
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '操作失败')
|
||||
} finally {
|
||||
actionLoadingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return { actionLoadingId, runAction }
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { ApiEnvelope } from '@/lib/http'
|
||||
import type { AdminPagination } from '@/types/admin'
|
||||
|
||||
export interface AdminListPageData<TItem> {
|
||||
items: TItem[]
|
||||
pagination: AdminPagination
|
||||
}
|
||||
|
||||
export interface UseAdminListPageOptions<TItem> {
|
||||
defaultErrorMessage: string
|
||||
pageSize?: number
|
||||
fetchPage: (page: number, pageSize: number) => Promise<ApiEnvelope<AdminListPageData<TItem>>>
|
||||
}
|
||||
|
||||
export function useAdminListPage<TItem>(options: UseAdminListPageOptions<TItem>) {
|
||||
const loading = ref(true)
|
||||
const errorMessage = ref('')
|
||||
const items = ref<TItem[]>([])
|
||||
const pagination = ref<AdminPagination>({
|
||||
page: 1,
|
||||
pageSize: options.pageSize ?? 20,
|
||||
total: 0,
|
||||
})
|
||||
let latestRequestId = 0
|
||||
|
||||
async function loadPage(page = pagination.value.page) {
|
||||
const requestId = ++latestRequestId
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await options.fetchPage(page, pagination.value.pageSize)
|
||||
|
||||
if (requestId !== latestRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
items.value = response.data.items
|
||||
pagination.value = response.data.pagination
|
||||
} catch (error) {
|
||||
if (requestId !== latestRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
errorMessage.value = error instanceof Error ? error.message : options.defaultErrorMessage
|
||||
} finally {
|
||||
if (requestId === latestRequestId) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
errorMessage,
|
||||
items,
|
||||
pagination,
|
||||
loadPage,
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
|
||||
|
||||
export interface ListQueryFilters {
|
||||
[key: string]: string | string[] | undefined
|
||||
}
|
||||
|
||||
export function useAdminListQuerySync(route: RouteLocationNormalizedLoaded, router: Router) {
|
||||
function readQueryString(key: string): string {
|
||||
const value = route.query[key]
|
||||
const rawValue = Array.isArray(value) ? value[0] : value
|
||||
return typeof rawValue === 'string' ? rawValue.trim() : ''
|
||||
}
|
||||
|
||||
function readQueryPage(): number {
|
||||
const page = Number(readQueryString('page'))
|
||||
return Number.isInteger(page) && page > 0 ? page : 1
|
||||
}
|
||||
|
||||
async function syncRouteQuery(page: number, filters: ListQueryFilters = {}) {
|
||||
const query: Record<string, string> = {}
|
||||
if (page > 1) { query.page = String(page) }
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
const trimmed = (Array.isArray(value) ? value.join(',') : value)?.trim()
|
||||
if (trimmed) { query[key] = trimmed }
|
||||
}
|
||||
await router.replace({ path: route.path, query })
|
||||
}
|
||||
|
||||
return { readQueryString, readQueryPage, syncRouteQuery }
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
export const TASK_STATUS = {
|
||||
PENDING_PAYMENT: "pending_payment",
|
||||
PAID: "paid",
|
||||
LINK_GENERATED: "link_generated",
|
||||
CLAIMED: "claimed",
|
||||
PENDING_BINDING_PREPARE: "pending_binding_prepare",
|
||||
WAITING_BINDING: "waiting_binding",
|
||||
ROLE_CONFIRMED: "role_confirmed",
|
||||
REDEEMING: "redeeming",
|
||||
DISPATCHED_PENDING_RETURN: "dispatched_pending_return",
|
||||
COMPLETED: "completed",
|
||||
REDEEMED: "redeemed",
|
||||
RETRY_PENDING: "retry_pending",
|
||||
MANUAL_REVIEW: "manual_review",
|
||||
FAILED: "failed",
|
||||
EXPIRED: "expired",
|
||||
CLOSED: "closed",
|
||||
} as const;
|
||||
|
||||
export type KnownTaskStatus = (typeof TASK_STATUS)[keyof typeof TASK_STATUS];
|
||||
export type TaskStatus = KnownTaskStatus | (string & {});
|
||||
|
||||
const CLAIM_INACTIVE_STATUSES = new Set<TaskStatus>([
|
||||
TASK_STATUS.COMPLETED,
|
||||
TASK_STATUS.REDEEMED,
|
||||
TASK_STATUS.MANUAL_REVIEW,
|
||||
TASK_STATUS.FAILED,
|
||||
TASK_STATUS.EXPIRED,
|
||||
TASK_STATUS.CLOSED,
|
||||
]);
|
||||
|
||||
const KUAISHOU_CLOUD_RESULT_STATUSES = new Set<TaskStatus>([
|
||||
TASK_STATUS.DISPATCHED_PENDING_RETURN,
|
||||
TASK_STATUS.COMPLETED,
|
||||
TASK_STATUS.REDEEMED,
|
||||
TASK_STATUS.MANUAL_REVIEW,
|
||||
TASK_STATUS.FAILED,
|
||||
]);
|
||||
|
||||
export function normalizeTaskStatus(value: unknown): TaskStatus {
|
||||
return String(value || "").trim() as TaskStatus;
|
||||
}
|
||||
|
||||
export function isClaimInactiveTaskStatus(status: unknown): boolean {
|
||||
return CLAIM_INACTIVE_STATUSES.has(normalizeTaskStatus(status));
|
||||
}
|
||||
|
||||
export function isKuaishouCloudRoleConfirmedStatus(status: unknown): boolean {
|
||||
return normalizeTaskStatus(status) === TASK_STATUS.ROLE_CONFIRMED;
|
||||
}
|
||||
|
||||
export function isKuaishouCloudCompletedStatus(status: unknown): boolean {
|
||||
const normalized = normalizeTaskStatus(status);
|
||||
return normalized === TASK_STATUS.COMPLETED || normalized === TASK_STATUS.REDEEMED;
|
||||
}
|
||||
|
||||
export function hasKuaishouCloudRedeemResultStatus(status: unknown): boolean {
|
||||
return KUAISHOU_CLOUD_RESULT_STATUSES.has(normalizeTaskStatus(status));
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { ElMessageBoxOptions, MessageOptions } from 'element-plus'
|
||||
|
||||
import 'element-plus/theme-chalk/el-message.css'
|
||||
import 'element-plus/theme-chalk/el-overlay.css'
|
||||
import 'element-plus/theme-chalk/el-message-box.css'
|
||||
|
||||
const baseMessageOptions: Partial<MessageOptions> = {
|
||||
grouping: true,
|
||||
}
|
||||
|
||||
const baseConfirmOptions: ElMessageBoxOptions = {
|
||||
type: 'warning',
|
||||
confirmButtonText: '继续执行',
|
||||
cancelButtonText: '取消',
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
autofocus: false,
|
||||
distinguishCancelAndClose: true,
|
||||
}
|
||||
|
||||
const basePromptOptions: ElMessageBoxOptions = {
|
||||
confirmButtonText: '提交',
|
||||
cancelButtonText: '取消',
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
autofocus: false,
|
||||
distinguishCancelAndClose: true,
|
||||
}
|
||||
|
||||
export function showSuccess(message: string, options: Partial<MessageOptions> = {}) {
|
||||
return ElMessage.success({
|
||||
...baseMessageOptions,
|
||||
...options,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
export function showError(message: string, options: Partial<MessageOptions> = {}) {
|
||||
return ElMessage.error({
|
||||
...baseMessageOptions,
|
||||
...options,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
export function showConfirm(
|
||||
message: string,
|
||||
title = '确认操作',
|
||||
options: ElMessageBoxOptions = {},
|
||||
) {
|
||||
return ElMessageBox.confirm(message, title, {
|
||||
...baseConfirmOptions,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function showPrompt(message: string, title: string, options: ElMessageBoxOptions = {}) {
|
||||
return ElMessageBox.prompt(message, title, {
|
||||
...basePromptOptions,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function isFeedbackDismissed(error: unknown) {
|
||||
return error === 'cancel' || error === 'close'
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { clearAdminSession, getAdminToken } from '@/utils/admin-auth'
|
||||
|
||||
export interface ApiEnvelope<T> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
errorCode?: string
|
||||
}
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: '/',
|
||||
timeout: 50_000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
http.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data as ApiEnvelope<unknown>
|
||||
|
||||
// Business-level error: HTTP 200 but code !== 0
|
||||
if (typeof data?.code === 'number' && data.code !== 0) {
|
||||
const error = new Error(data.msg || '操作失败') as Error & {
|
||||
errorCode?: string
|
||||
code?: number
|
||||
}
|
||||
error.errorCode = data.errorCode
|
||||
error.code = data.code
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
return response.data
|
||||
},
|
||||
(error) => {
|
||||
const responseMessage =
|
||||
typeof error?.response?.data?.msg === 'string' ? error.response.data.msg.trim() : ''
|
||||
const fallbackMessage = resolveFallbackHttpMessage(error)
|
||||
const normalizedError = new Error(responseMessage || fallbackMessage) as Error & {
|
||||
errorCode?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
if (error?.response?.data?.errorCode) {
|
||||
normalizedError.errorCode = String(error.response.data.errorCode)
|
||||
}
|
||||
|
||||
if (typeof error?.response?.status === 'number') {
|
||||
normalizedError.status = error.response.status
|
||||
}
|
||||
|
||||
if (error?.config?.url && String(error.config.url).startsWith('/api/v1/admin')) {
|
||||
const status = Number(error?.response?.status || 0)
|
||||
const errorCode = String(error?.response?.data?.errorCode || '').trim()
|
||||
|
||||
if (status === 401 && shouldClearAdminSession(errorCode)) {
|
||||
clearAdminSession()
|
||||
|
||||
if (
|
||||
window.location.hash.startsWith('#/admin') &&
|
||||
!window.location.hash.startsWith('#/admin/login')
|
||||
) {
|
||||
window.location.hash = '#/admin/login'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(normalizedError)
|
||||
},
|
||||
)
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
if (String(config.url || '').startsWith('/api/v1/admin')) {
|
||||
const token = getAdminToken()
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
function resolveFallbackHttpMessage(error: unknown) {
|
||||
const message = String((error as { message?: string })?.message ?? '')
|
||||
|
||||
if (message.includes('timeout')) {
|
||||
return '网络超时'
|
||||
}
|
||||
|
||||
if (message === 'Network Error') {
|
||||
return '网络连接错误'
|
||||
}
|
||||
|
||||
if (typeof (error as { response?: { statusText?: string } })?.response?.statusText === 'string') {
|
||||
return String((error as { response: { statusText: string } }).response.statusText).trim()
|
||||
}
|
||||
|
||||
return '接口请求失败'
|
||||
}
|
||||
|
||||
function shouldClearAdminSession(errorCode: string) {
|
||||
return [
|
||||
'admin_auth_required',
|
||||
'admin_auth_invalid',
|
||||
'admin_auth_expired',
|
||||
'admin_auth_user_invalid',
|
||||
'admin_auth_stale',
|
||||
].includes(errorCode)
|
||||
}
|
||||
|
||||
function request<T>(config: Parameters<typeof http.request<ApiEnvelope<T>>>[0]) {
|
||||
return http.request<ApiEnvelope<T>, ApiEnvelope<T>>(config)
|
||||
}
|
||||
|
||||
export function apiGet<T>(url: string, params?: Record<string, unknown>) {
|
||||
return request<T>({ method: 'GET', url, params })
|
||||
}
|
||||
|
||||
export function apiGetBlob(url: string, params?: Record<string, unknown>) {
|
||||
return http.request<Blob, Blob>({
|
||||
method: 'GET',
|
||||
url,
|
||||
params,
|
||||
responseType: 'blob',
|
||||
})
|
||||
}
|
||||
|
||||
export function apiPost<T>(url: string, data?: unknown) {
|
||||
return request<T>({ method: 'POST', url, data: data as Record<string, unknown> })
|
||||
}
|
||||
|
||||
export function apiDelete<T>(url: string) {
|
||||
return request<T>({ method: 'DELETE', url })
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/tokens.css'
|
||||
import './styles/main.css'
|
||||
import './styles/element-overrides.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -1,113 +0,0 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { getAdminRole, hasAdminSession } from '@/utils/admin-auth'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/admin/dashboard',
|
||||
},
|
||||
{
|
||||
path: '/claim/:token',
|
||||
component: () => import('@/views/claim/ClaimView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
redirect: '/admin/dashboard',
|
||||
},
|
||||
{
|
||||
path: '/admin/login',
|
||||
component: () => import('@/views/admin/auth/AdminLoginView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: () => import('@/views/admin/layout/AdminLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: '/admin/dashboard',
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: () => import('@/views/admin/dashboard/AdminDashboardView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
meta: { allowedRoles: ['admin'] },
|
||||
component: () => import('@/views/admin/users/AdminUsersView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
component: () => import('@/views/admin/orders/AdminOrdersView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'orders/:orderId',
|
||||
component: () => import('@/views/admin/orders/AdminOrderDetailView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
component: () => import('@/views/admin/tasks/AdminTasksView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'tasks/:taskId',
|
||||
component: () => import('@/views/admin/tasks/AdminTaskDetailView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'cloudtentacles-records',
|
||||
component: () =>
|
||||
import('@/views/admin/cloudtentacles-records/AdminCloudtentaclesRecordsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'platform-shops',
|
||||
meta: { allowedRoles: ['admin'] },
|
||||
component: () => import('@/views/admin/platform-shops/AdminPlatformShopsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'platform-fulfillment',
|
||||
meta: { allowedRoles: ['admin'] },
|
||||
component: () => import('@/views/admin/fulfillment/AdminFulfillmentConfigHubView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'audit-logs',
|
||||
meta: { allowedRoles: ['admin'] },
|
||||
component: () => import('@/views/admin/audit-logs/AdminAuditLogsView.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: () => import('@/views/NotFoundView.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if (!to.path.startsWith('/admin')) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (to.path === '/admin/login') {
|
||||
if (hasAdminSession()) {
|
||||
return '/admin/dashboard'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (!hasAdminSession()) {
|
||||
return '/admin/login'
|
||||
}
|
||||
|
||||
const allowedRoles = to.matched
|
||||
.map((record) => (record.meta?.allowedRoles as string[] | undefined) || [])
|
||||
.find((roles) => Array.isArray(roles) && roles.length > 0)
|
||||
|
||||
if (allowedRoles && !allowedRoles.includes(getAdminRole())) {
|
||||
return '/admin/dashboard'
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,9 +0,0 @@
|
||||
import { apiGet } from '@/lib/http'
|
||||
import type { AdminAuditLogItem, AdminPagination } from '@/types/admin'
|
||||
|
||||
export function fetchAdminAuditLogs(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminAuditLogItem[]; pagination: AdminPagination }>(
|
||||
'/api/v1/admin/audit-logs',
|
||||
params,
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type { AdminLoginResponse, AdminSessionSummary } from '@/types/admin'
|
||||
|
||||
export function loginAdmin(payload: { username: string; password: string }) {
|
||||
return apiPost<AdminLoginResponse>('/api/v1/admin/auth/login', payload)
|
||||
}
|
||||
|
||||
export function fetchAdminSession() {
|
||||
return apiGet<AdminSessionSummary>('/api/v1/admin/auth/session')
|
||||
}
|
||||
|
||||
export function logoutAdmin() {
|
||||
return apiPost<{ success: boolean }>('/api/v1/admin/auth/logout', {})
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { apiGet } from '@/lib/http'
|
||||
import type { AdminDashboardSummary } from '@/types/admin'
|
||||
|
||||
export function fetchAdminDashboardSummary() {
|
||||
return apiGet<AdminDashboardSummary>('/api/v1/admin/dashboard/summary')
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export * from './auth'
|
||||
export * from './dashboard'
|
||||
export * from './users'
|
||||
export * from './audit-logs'
|
||||
export * from './platform-config'
|
||||
export * from './orders'
|
||||
export * from './tasks'
|
||||
@@ -1,22 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type { AdminOrderDetail, AdminOrderListItem, AdminPagination } from '@/types/admin'
|
||||
|
||||
export function fetchAdminOrders(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminOrderListItem[]; pagination: AdminPagination }>(
|
||||
'/api/v1/admin/orders',
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminOrderDetail(orderId: number | string) {
|
||||
return apiGet<AdminOrderDetail>(`/api/v1/admin/orders/${orderId}`)
|
||||
}
|
||||
|
||||
export function resendAdminOrderKuaishouIndustryVoucherCode(orderId: number | string) {
|
||||
return apiPost<{
|
||||
success: boolean
|
||||
resentCount: number
|
||||
failedCount: number
|
||||
errorMessage: string
|
||||
}>(`/api/v1/admin/orders/${orderId}/kuaishou-industry/resend-code`, {})
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
import { apiDelete, apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesDeliveryRecordListResult,
|
||||
AdminCloudtentaclesOverrideRuleConfig,
|
||||
AdminCloudtentaclesSkuListResult,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminCloudtentaclesSourceConfig() {
|
||||
return apiGet<AdminCloudtentaclesSourceConfigResponse>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles-source',
|
||||
)
|
||||
}
|
||||
|
||||
export function saveAdminCloudtentaclesSourceConfig(
|
||||
payload:
|
||||
| AdminCloudtentaclesSourcesConfig
|
||||
| ({ sourceKey: string } & Partial<AdminCloudtentaclesSourceItem>),
|
||||
) {
|
||||
return apiPost<AdminCloudtentaclesSourceConfigResponse>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles-source',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function deleteAdminCloudtentaclesSource(sourceKey: string) {
|
||||
return apiDelete<{ success: boolean }>(
|
||||
`/api/v1/admin/platform-config/cloudtentacles-source/${sourceKey}`,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesOverrideRules() {
|
||||
return apiGet<AdminCloudtentaclesOverrideRuleConfig>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
|
||||
)
|
||||
}
|
||||
|
||||
export function saveAdminCloudtentaclesOverrideRules(
|
||||
payload: {
|
||||
enabled: boolean
|
||||
rules: Array<{
|
||||
id?: string
|
||||
enabled?: boolean
|
||||
productName?: string
|
||||
sourceKey?: string
|
||||
deliveryItems?: Array<{
|
||||
cloudSkuId?: number
|
||||
cloudSkuName?: string
|
||||
quantity?: number
|
||||
}>
|
||||
notes?: string
|
||||
}>
|
||||
},
|
||||
) {
|
||||
return apiPost<AdminCloudtentaclesOverrideRuleConfig>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function sendAdminCloudtentaclesSmsCode(payload: {
|
||||
baseUrl?: string
|
||||
username?: string
|
||||
phone?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesSendSmsResult>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/send-sms-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function testAdminCloudtentaclesLogin(payload: {
|
||||
baseUrl?: string
|
||||
username?: string
|
||||
password?: string
|
||||
phone?: string
|
||||
code?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesLoginTestResult>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/test-login',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function validateAdminCloudtentaclesSession(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesValidateSessionResult>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/validate-session',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesAsset(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/asset',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesCategories(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/categories',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesSkuList(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesSkuListResult>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/sku/list',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function buyAdminCloudtentaclesSku(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
id?: number
|
||||
count?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/sku/buy',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function useAdminCloudtentaclesSku(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
id?: number
|
||||
virtualNumberId?: number
|
||||
phone?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/sku/use',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesKnapsack(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/knapsack',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesDeliveryRecords(payload: {
|
||||
sourceKey?: string
|
||||
page?: number
|
||||
size?: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
platformOrderId?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesDeliveryRecordListResult>(
|
||||
'/api/v1/admin/cloudtentacles-records',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesRecordSources() {
|
||||
return apiGet<{
|
||||
sources: Array<{
|
||||
key: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
hasToken: boolean
|
||||
loggedInAt: string
|
||||
}>
|
||||
}>('/api/v1/admin/cloudtentacles-records/sources')
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesVnList(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/list',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function appointAdminCloudtentaclesVn(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/appoint',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function generateAdminCloudtentaclesVnLoginCode(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
id?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/generate-login-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesVnCode(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
phone?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/fetch-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function verifyAdminCloudtentaclesVnCode(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
id?: number
|
||||
code?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/verify-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesBindUrl(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
id?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/bind-url',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function backAdminCloudtentaclesVn(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
id?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/back',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function runAdminCloudtentaclesFullFlow(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
skuId?: number
|
||||
skuCount?: number
|
||||
vnKey?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/debug/full-flow',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './notifications'
|
||||
export * from './scheduled-jobs'
|
||||
export * from './ninetyone'
|
||||
export * from './kuaishou-eticket'
|
||||
export * from './kuaishou-feifei'
|
||||
export * from './cloudtentacles'
|
||||
@@ -1,60 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketSourceConfig,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminKuaishouEticketSourceConfig() {
|
||||
return apiGet<{
|
||||
filePath: string
|
||||
source: AdminKuaishouEticketSourceConfig
|
||||
}>('/api/v1/admin/platform-config/kuaishou-eticket-source')
|
||||
}
|
||||
|
||||
export function saveAdminKuaishouEticketSourceConfig(payload: AdminKuaishouEticketSourceConfig) {
|
||||
return apiPost<{
|
||||
filePath: string
|
||||
source: AdminKuaishouEticketSourceConfig
|
||||
}>('/api/v1/admin/platform-config/kuaishou-eticket-source', payload)
|
||||
}
|
||||
|
||||
export function queryAdminKuaishouEticketDetail(payload: {
|
||||
baseUrl?: string
|
||||
shopId?: string
|
||||
cookie?: string
|
||||
eTicketId: string
|
||||
}) {
|
||||
return apiPost<AdminKuaishouEticketDetailResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-eticket/query-detail',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function queryAdminKuaishouEticketShopInfo(payload: {
|
||||
baseUrl?: string
|
||||
shopId?: string
|
||||
cookie?: string
|
||||
}) {
|
||||
return apiPost<AdminKuaishouEticketShopInfoResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-eticket/query-shop-info',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function consumeAdminKuaishouEticket(payload: {
|
||||
baseUrl?: string
|
||||
shopId?: string
|
||||
cookie?: string
|
||||
eTicketId: string
|
||||
oid?: string
|
||||
formToken?: string
|
||||
num?: number
|
||||
storeId?: string
|
||||
}) {
|
||||
return apiPost<AdminKuaishouEticketConsumeResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-eticket/consume',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
AdminKuaishouFeifeiProductListResult,
|
||||
AdminKuaishouFeifeiProductSyncResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminKuaishouFeifeiConfig() {
|
||||
return apiGet<AdminKuaishouFeifeiConfigResponse>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei',
|
||||
)
|
||||
}
|
||||
|
||||
export function saveAdminKuaishouFeifeiConfig(payload: AdminKuaishouFeifeiConfig) {
|
||||
return apiPost<AdminKuaishouFeifeiConfigResponse>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function matchAdminKuaishouFeifeiProduct(productName: string) {
|
||||
return apiPost<AdminKuaishouFeifeiMatchResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/match',
|
||||
{ productName },
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminKuaishouFeifeiProducts(payload: {
|
||||
page?: number
|
||||
perPage?: number
|
||||
status?: string
|
||||
supplyProductName?: string
|
||||
}) {
|
||||
return apiPost<AdminKuaishouFeifeiProductListResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/products',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function syncAdminKuaishouFeifeiProducts(payload: {
|
||||
status?: string
|
||||
supplyProductName?: string
|
||||
} = {}) {
|
||||
return apiPost<AdminKuaishouFeifeiProductSyncResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/sync-products',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function createAdminKuaishouFeifeiTestOrder(payload: {
|
||||
platformOrderNo: string
|
||||
productCode: string
|
||||
platformBuyNum?: number
|
||||
platformAmount?: number | null
|
||||
playerAccount?: string
|
||||
playerGameRegion?: string
|
||||
playerGameSrv?: string
|
||||
playerGameRole?: string
|
||||
submitPlayer?: boolean
|
||||
notifyUrl?: string
|
||||
}) {
|
||||
return apiPost<AdminKuaishouFeifeiOrderResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/test-order',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function queryAdminKuaishouFeifeiOrder(payload: {
|
||||
platformOrderNo?: string
|
||||
orderNo?: string
|
||||
}) {
|
||||
return apiPost<AdminKuaishouFeifeiOrderResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/query-order',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminNinetyoneOrderActionResult,
|
||||
AdminNinetyoneOrderListResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminNinetyoneOrders(
|
||||
params: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: string
|
||||
} = {},
|
||||
) {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (params.page) {
|
||||
searchParams.set('page', String(params.page))
|
||||
}
|
||||
if (params.pageSize) {
|
||||
searchParams.set('pageSize', String(params.pageSize))
|
||||
}
|
||||
if (params.status) {
|
||||
searchParams.set('status', params.status)
|
||||
}
|
||||
const queryString = searchParams.toString()
|
||||
return apiGet<AdminNinetyoneOrderListResult>(
|
||||
`/api/v1/admin/platform-config/ninetyone/orders${queryString ? `?${queryString}` : ''}`,
|
||||
)
|
||||
}
|
||||
|
||||
export function retryAdminNinetyoneOrder(orderId: number | string) {
|
||||
return apiPost<AdminNinetyoneOrderActionResult>(
|
||||
`/api/v1/admin/platform-config/ninetyone/orders/${orderId}/retry`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function failAdminNinetyoneOrder(orderId: number | string, payload: { reason?: string }) {
|
||||
return apiPost<AdminNinetyoneOrderActionResult>(
|
||||
`/api/v1/admin/platform-config/ninetyone/orders/${orderId}/fail`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type { AdminNotificationConfig, AdminNotificationTestResult } from '@/types/admin'
|
||||
|
||||
export function fetchAdminNotificationConfig() {
|
||||
return apiGet<{
|
||||
filePath: string
|
||||
source: AdminNotificationConfig
|
||||
}>('/api/v1/admin/platform-config/notifications')
|
||||
}
|
||||
|
||||
export function saveAdminNotificationConfig(payload: AdminNotificationConfig) {
|
||||
return apiPost<{
|
||||
filePath: string
|
||||
source: AdminNotificationConfig
|
||||
}>('/api/v1/admin/platform-config/notifications', payload)
|
||||
}
|
||||
|
||||
export function testAdminNotification(payload: { title?: string; body?: string; url?: string }) {
|
||||
return apiPost<AdminNotificationTestResult>(
|
||||
'/api/v1/admin/platform-config/notifications/test',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type { AdminScheduledJobsConfig, AdminScheduledJobsResponse } from '@/types/admin'
|
||||
|
||||
export function fetchAdminScheduledJobsConfig() {
|
||||
return apiGet<AdminScheduledJobsResponse>('/api/v1/admin/platform-config/scheduled-jobs')
|
||||
}
|
||||
|
||||
export function saveAdminScheduledJobsConfig(payload: AdminScheduledJobsConfig) {
|
||||
return apiPost<AdminScheduledJobsResponse>(
|
||||
'/api/v1/admin/platform-config/scheduled-jobs',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function runAdminScheduledJob(jobId: string) {
|
||||
return apiPost<{
|
||||
result: Record<string, unknown>
|
||||
runtime: AdminScheduledJobsResponse['runtime']
|
||||
}>(`/api/v1/admin/platform-config/scheduled-jobs/${jobId}/run`, {})
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import { apiGet, apiGetBlob, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminPagination,
|
||||
AdminTaskActionResponse,
|
||||
AdminTaskDetail,
|
||||
AdminTaskListItem,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminTasks(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminTaskListItem[]; pagination: AdminPagination }>(
|
||||
'/api/v1/admin/tasks',
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminTaskDetail(taskId: number | string) {
|
||||
return apiGet<AdminTaskDetail>(`/api/v1/admin/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
export function fetchAdminTaskScreenshot(taskId: number | string) {
|
||||
return apiGetBlob(`/api/v1/admin/tasks/${taskId}/screenshot`)
|
||||
}
|
||||
|
||||
export function retryAdminTask(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/retry`, {})
|
||||
}
|
||||
|
||||
export function regenerateAdminTaskClaimLink(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/regenerate-claim-link`, {})
|
||||
}
|
||||
|
||||
export function closeAdminTask(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/close`, {})
|
||||
}
|
||||
|
||||
export function markAdminTaskManualReview(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/mark-manual-review`, {})
|
||||
}
|
||||
|
||||
export function completeAdminTaskManualDispatch(
|
||||
taskId: number | string,
|
||||
payload: {
|
||||
outcome: 'delivered' | 'failed'
|
||||
resultMessage?: string
|
||||
deliveryReference?: string
|
||||
deliveredCredential?: string
|
||||
},
|
||||
) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/complete-manual-dispatch`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function prepareAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/prepare`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function refreshAdminTaskKuaishouCloudRoleInfo(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/refresh-role-info`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function rebindAdminTaskKuaishouCloudRole(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/rebind-role`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
taskId: number | string,
|
||||
payload: {
|
||||
ticketCode?: string
|
||||
} = {},
|
||||
) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/dispatch`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function returnNumberAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/return-number`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function resendAdminTaskKuaishouIndustryVoucherCode(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-industry/resend-code`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function consumeAdminTaskKuaishouIndustryVoucher(
|
||||
taskId: number | string,
|
||||
payload: {
|
||||
consumeType?: string
|
||||
storeName?: string
|
||||
storeAddress?: string
|
||||
expressCode?: string
|
||||
expressNo?: string
|
||||
} = {},
|
||||
) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-industry/consume`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type { AdminPagination, AdminUserListItem } from '@/types/admin'
|
||||
|
||||
export function fetchAdminUsers(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminUserListItem[]; pagination: AdminPagination }>(
|
||||
'/api/v1/admin/users',
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
export function createAdminUser(payload: {
|
||||
username: string
|
||||
password: string
|
||||
role: string
|
||||
status?: string
|
||||
}) {
|
||||
return apiPost<{ user: AdminUserListItem }>('/api/v1/admin/users', payload)
|
||||
}
|
||||
|
||||
export function updateAdminUserRole(userId: number | string, payload: { role: string }) {
|
||||
return apiPost<{ user: AdminUserListItem }>(`/api/v1/admin/users/${userId}/role`, payload)
|
||||
}
|
||||
|
||||
export function updateAdminUserStatus(userId: number | string, payload: { status: string }) {
|
||||
return apiPost<{ user: AdminUserListItem }>(`/api/v1/admin/users/${userId}/status`, payload)
|
||||
}
|
||||
|
||||
export function resetAdminUserPassword(userId: number | string, payload: { password: string }) {
|
||||
return apiPost<{ user: AdminUserListItem }>(
|
||||
`/api/v1/admin/users/${userId}/reset-password`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type { ClaimDetailData } from '@/types/claim'
|
||||
|
||||
export function fetchClaimDetail(token: string) {
|
||||
return apiGet<ClaimDetailData>(`/api/v1/claim/${token}`)
|
||||
}
|
||||
|
||||
export function verifyKuaishouCloudClaimTicket(
|
||||
token: string,
|
||||
payload: {
|
||||
ticketCode: string
|
||||
},
|
||||
) {
|
||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/verify-ticket`, payload)
|
||||
}
|
||||
|
||||
export function confirmKuaishouCloudClaimRole(token: string) {
|
||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
|
||||
}
|
||||
|
||||
export function rebindKuaishouCloudClaimRole(token: string) {
|
||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/rebind-role`, {})
|
||||
}
|
||||
|
||||
export function redeemKuaishouCloudClaim(token: string) {
|
||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/redeem`, {})
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
/* ============================================================
|
||||
管理员配置页面共享结构样式
|
||||
引入: AdminKuaishouCloudFulfillment.css
|
||||
各页面自行覆盖颜色、圆角、主题属性
|
||||
============================================================ */
|
||||
|
||||
/* ---------- 分区标题 ---------- */
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.section-title-row h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-title-row p {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
/* ---------- 头部工具 ---------- */
|
||||
.section-header-tools,
|
||||
.section-mini-stats,
|
||||
.section-action-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-header-tools {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* ---------- 卡片容器 ---------- */
|
||||
.overview-card,
|
||||
.table-card,
|
||||
.empty-block,
|
||||
.error-copy {
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(86, 108, 138, 0.1);
|
||||
}
|
||||
|
||||
.table-card--dense {
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ---------- 概览文件 & 统计 ---------- */
|
||||
.overview-file {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.overview-file code {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.overview-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.overview-stat {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 12px 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.overview-stat span {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.overview-stat strong {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.overview-notes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ---------- 分区工具 ---------- */
|
||||
.section-title-row--tight {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ---------- 字段块 ---------- */
|
||||
.field-block {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.field-block span {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-block--action {
|
||||
align-content: end;
|
||||
}
|
||||
|
||||
.field-block--action :deep(.el-button) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-help {
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ---------- 文本输入 ---------- */
|
||||
.text-input {
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
/* ---------- 校验 & 错误 ---------- */
|
||||
.validation-banner,
|
||||
.binding-inline-error {
|
||||
margin: 10px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.binding-inline-error {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* ---------- 数据表 (用于 el-table,具体单元格样式见各页面 :deep() 规则) ---------- */
|
||||
.data-table--config {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.cell-stack--config {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cell-subtle {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
/* ---------- 绑定卡片 ---------- */
|
||||
.binding-card {
|
||||
margin-top: 12px;
|
||||
padding: 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.binding-card--invalid {
|
||||
border-color: rgba(220, 38, 38, 0.3);
|
||||
box-shadow: 0 0 0 4px rgba(254, 226, 226, 0.85);
|
||||
}
|
||||
|
||||
.binding-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.binding-summary {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.binding-title-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.binding-subtle {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* ---------- 状态标签 ---------- */
|
||||
.binding-status-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-full);
|
||||
padding: 2px 10px;
|
||||
background: #fff1f2;
|
||||
color: #be123c;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.binding-status-chip--done {
|
||||
background: #ecfdf3;
|
||||
color: #027a48;
|
||||
}
|
||||
|
||||
.binding-collapsed-preview {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ---------- 操作栏 ---------- */
|
||||
.binding-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ---------- 工具栏提示 ---------- */
|
||||
.toolbar-hint {
|
||||
margin: 10px 0 0;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
/* ============================================================
|
||||
管理员列表页共享样式
|
||||
引入: AdminOrdersView, AdminTasksView, AdminUsersView
|
||||
各页面自行覆盖 grid-template-columns 等布局差异
|
||||
============================================================ */
|
||||
|
||||
/* ---------- 页面包裹 ---------- */
|
||||
.list-page {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* ---------- 总量标签 ---------- */
|
||||
.total-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(37, 89, 167, 0.14);
|
||||
border-radius: var(--radius-full);
|
||||
background: rgba(37, 89, 167, 0.08);
|
||||
color: #2559a7;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ---------- 卡片区域 ---------- */
|
||||
.section-card {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
/* ---------- 筛选网格 ---------- */
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-control {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
min-width: 156px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* ---------- 摘要条 ---------- */
|
||||
.summary-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-height: 38px;
|
||||
padding: 0;
|
||||
margin-top: var(--space-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.summary-metric {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
background: #f8fafc;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.summary-metric strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-metric[data-tone='success'] {
|
||||
color: var(--color-success-dark);
|
||||
background: var(--color-success-light);
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
.summary-metric[data-tone='danger'] {
|
||||
color: var(--color-danger-dark);
|
||||
background: var(--color-danger-light);
|
||||
border-color: #fecaca;
|
||||
}
|
||||
.summary-metric[data-tone='primary'] {
|
||||
color: var(--color-primary-dark);
|
||||
background: var(--color-primary-light);
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
.summary-metric[data-tone='warning'] {
|
||||
color: var(--color-warning-dark);
|
||||
background: var(--color-warning-light);
|
||||
border-color: #fed7aa;
|
||||
}
|
||||
.summary-metric[data-tone='muted'] {
|
||||
color: var(--text-secondary);
|
||||
background: #f8fafc;
|
||||
border-color: var(--border-default);
|
||||
}
|
||||
|
||||
/* ---------- 表格工具栏 ---------- */
|
||||
.table-toolbar {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.table-toolbar strong {
|
||||
display: block;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.table-toolbar span {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
/* ---------- el-table 统一主题变量 ---------- */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
--el-table-bg-color: transparent;
|
||||
--el-table-tr-bg-color: transparent;
|
||||
--el-table-header-bg-color: rgba(239, 246, 255, 0.72);
|
||||
--el-table-row-hover-bg-color: rgba(239, 246, 255, 0.72);
|
||||
--el-table-border-color: rgba(86, 108, 138, 0.1);
|
||||
}
|
||||
|
||||
.data-table :deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.data-table :deep(th.el-table__cell) {
|
||||
height: 42px;
|
||||
background: var(--el-table-header-bg-color);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ---------- 单元格通用 ---------- */
|
||||
.cell-stack {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.cell-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.compact-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
max-width: 240px;
|
||||
padding: 0 9px;
|
||||
border-radius: var(--radius-full);
|
||||
background: #f4f7fb;
|
||||
border: 1px solid var(--border-default);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.compact-chip--muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.cell-title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cell-subline {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #475467;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
/* ---------- ID 链接 ---------- */
|
||||
.id-link {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
font-size: var(--text-base);
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.id-link:hover {
|
||||
color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
/* ---------- 详情链接按钮 ---------- */
|
||||
.detail-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid var(--border-default);
|
||||
color: var(--color-primary-dark);
|
||||
background: var(--color-primary-light);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.detail-link:hover {
|
||||
border-color: var(--border-hover);
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
/* ---------- 操作 & 状态容器 ---------- */
|
||||
.action-stack {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.action-stack--nowrap {
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.action-stack--vertical {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.action-stack--tight {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.status-stack {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-stack :deep(.el-tag) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* ---------- 响应式 ---------- */
|
||||
@media (max-width: 1200px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.filter-buttons {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.filter-buttons {
|
||||
width: 100%;
|
||||
}
|
||||
.filter-buttons :deep(.el-button) {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/* ===== Element Plus Global Overrides =====
|
||||
Single place for all Element Plus style normalization.
|
||||
Eliminates the need for scattered :deep() overrides in components.
|
||||
*/
|
||||
|
||||
/* ---- Buttons ---- */
|
||||
.el-button {
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
--el-button-bg-color: var(--color-primary);
|
||||
--el-button-border-color: var(--color-primary);
|
||||
--el-button-hover-bg-color: var(--color-primary-dark);
|
||||
--el-button-hover-border-color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
/* ---- Dialogs ---- */
|
||||
.el-dialog {
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
/* ---- Cards ---- */
|
||||
.el-card {
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-surface);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* ---- Tables ---- */
|
||||
.el-table {
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-table th.el-table__cell {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- Tags ---- */
|
||||
.el-tag {
|
||||
border-radius: var(--radius-full);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- Menus ---- */
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ---- Inputs & Selects ---- */
|
||||
.el-input .el-input__wrapper,
|
||||
.el-select .el-select__wrapper,
|
||||
.el-date-editor.el-input__wrapper {
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.el-date-editor.el-input__wrapper {
|
||||
border: 0;
|
||||
box-shadow: 0 0 0 1px var(--border-default) inset;
|
||||
}
|
||||
|
||||
.el-input .el-input__wrapper:hover,
|
||||
.el-select .el-select__wrapper:hover,
|
||||
.el-date-editor.el-input__wrapper:hover {
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.el-date-editor.el-input__wrapper:hover {
|
||||
box-shadow: 0 0 0 1px var(--border-hover) inset;
|
||||
}
|
||||
|
||||
.el-input.is-focus .el-input__wrapper,
|
||||
.el-select.is-focus .el-select__wrapper,
|
||||
.el-date-editor.el-input__wrapper.is-active {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 1px var(--color-primary) inset;
|
||||
}
|
||||
|
||||
/* ---- Pagination ---- */
|
||||
.el-pagination {
|
||||
justify-content: center;
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
/* ---- Tabs ---- */
|
||||
.el-tabs__header {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
/* ---- Switch ---- */
|
||||
.el-switch.is-checked .el-switch__core {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ---- Skeleton ---- */
|
||||
.el-skeleton {
|
||||
--el-skeleton-color: var(--bg-subtle);
|
||||
}
|
||||
|
||||
/* ---- Empty ---- */
|
||||
.el-empty__description {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ---- 通用工具类 ---- */
|
||||
.mt-4 {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
.mb-4 {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
.text-muted-xs {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.text-muted-xs.mt-1 {
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
.fw-600 {
|
||||
font-weight: 600;
|
||||
}
|
||||
.text-danger {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.ml-2 {
|
||||
margin-left: var(--space-2);
|
||||
}
|
||||
.pagination-center {
|
||||
margin-top: var(--space-4);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
align-items: flex-end;
|
||||
}
|
||||
.card-header--split {
|
||||
align-items: center;
|
||||
}
|
||||
.card-title {
|
||||
display: block;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
.card-desc {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
.filter-col-160 {
|
||||
width: 160px;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
:root {
|
||||
color: var(--text-primary);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(44, 133, 247, 0.22), transparent 32%),
|
||||
radial-gradient(circle at top right, rgba(114, 184, 255, 0.18), transparent 26%),
|
||||
linear-gradient(180deg, var(--bg-page) 0%, #f7fbff 100%);
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(20, 47, 95, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(20, 47, 95, 0.02) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
mask-image: radial-gradient(circle at center, black, transparent 75%);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/* ===== Design Tokens =====
|
||||
Single source of truth for all visual values.
|
||||
Every component/view must reference these variables instead of hardcoded values.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ---- Color Palette ---- */
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-light: #eff6ff;
|
||||
--color-primary-dark: #1d4ed8;
|
||||
|
||||
--color-success: #22c55e;
|
||||
--color-success-light: #f0fdf4;
|
||||
--color-success-dark: #15803d;
|
||||
|
||||
--color-warning: #f59e0b;
|
||||
--color-warning-light: #fffbeb;
|
||||
--color-warning-dark: #b45309;
|
||||
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-light: #fef2f2;
|
||||
--color-danger-dark: #b91c1c;
|
||||
|
||||
/* ---- Surfaces & Backgrounds ---- */
|
||||
--bg-page: #f3f7fc;
|
||||
--bg-surface: rgba(255, 255, 255, 0.94);
|
||||
--bg-sidebar: rgba(255, 255, 255, 0.92);
|
||||
--bg-subtle: rgba(15, 93, 216, 0.06);
|
||||
|
||||
/* ---- Text ---- */
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-muted: #94a3b8;
|
||||
|
||||
/* ---- Borders ---- */
|
||||
--border-default: rgba(86, 108, 138, 0.12);
|
||||
--border-hover: rgba(37, 99, 235, 0.28);
|
||||
|
||||
/* ---- Spacing (4px base) ---- */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
|
||||
/* ---- Border Radius ---- */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
--radius-full: 999px;
|
||||
|
||||
/* ---- Shadows ---- */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.07);
|
||||
--shadow-lg: 0 10px 30px rgba(30, 49, 78, 0.08);
|
||||
|
||||
/* ---- Typography ---- */
|
||||
--font-sans: 'PingFang SC', 'SF Pro Display', 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'SFMono-Regular', ui-monospace, monospace;
|
||||
|
||||
--text-xs: 12px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 14px;
|
||||
--text-lg: 16px;
|
||||
--text-xl: 20px;
|
||||
--text-2xl: 24px;
|
||||
--text-3xl: 28px;
|
||||
|
||||
/* ---- Breakpoints (for reference, not used as CSS variables directly) ---- */
|
||||
--bp-lg: 1200px;
|
||||
--bp-md: 900px;
|
||||
--bp-sm: 640px;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { AdminRole } from './auth'
|
||||
|
||||
export interface AdminAuditLogItem {
|
||||
logId: number
|
||||
actorUserId: number
|
||||
actorUsername: string
|
||||
actorRole: AdminRole
|
||||
action: string
|
||||
targetType: string
|
||||
targetId: string
|
||||
payload: Record<string, unknown>
|
||||
createdAt: string
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export type AdminRole = 'admin' | 'operator' | 'support'
|
||||
export type AdminUserStatus = 'active' | 'disabled'
|
||||
|
||||
export interface AdminLoginResponse {
|
||||
token: string
|
||||
expiresAt: string
|
||||
user: {
|
||||
userId: number
|
||||
username: string
|
||||
role: AdminRole
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminSessionSummary {
|
||||
authenticated: boolean
|
||||
expiresAt: string
|
||||
user: {
|
||||
userId: number
|
||||
username: string
|
||||
role: AdminRole
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface AdminPagination {
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export interface AdminDashboardSummary {
|
||||
todayOrders: number
|
||||
paidPendingClaim: number
|
||||
claimingTasks: number
|
||||
redeemedToday: number
|
||||
abnormalTasks: number
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// Common types
|
||||
export type { AdminPagination } from './common'
|
||||
|
||||
// Auth types
|
||||
export type { AdminRole, AdminUserStatus, AdminLoginResponse, AdminSessionSummary } from './auth'
|
||||
|
||||
// Users types
|
||||
export type { AdminUserListItem } from './users'
|
||||
|
||||
// Audit logs types
|
||||
export type { AdminAuditLogItem } from './audit-logs'
|
||||
|
||||
// Dashboard types
|
||||
export type { AdminDashboardSummary } from './dashboard'
|
||||
|
||||
// Orders types
|
||||
export type {
|
||||
AdminOrderFulfillmentProgress,
|
||||
AdminOrderListItem,
|
||||
AdminOrderDetail,
|
||||
} from './orders'
|
||||
|
||||
// Tasks types
|
||||
export type {
|
||||
AdminTaskListItem,
|
||||
AdminTaskOperations,
|
||||
AdminTaskActionResponse,
|
||||
AdminTaskDetail,
|
||||
} from './tasks'
|
||||
|
||||
// Platform config types
|
||||
export type {
|
||||
AdminNotificationBarkRecipient,
|
||||
AdminNotificationWpushRecipient,
|
||||
AdminNotificationConfig,
|
||||
AdminNotificationSendResult,
|
||||
AdminNotificationTestResult,
|
||||
AdminCloudtentaclesMonitorAccount,
|
||||
AdminScheduledJobAccountRuntime,
|
||||
AdminScheduledJobCloudtentaclesAccount,
|
||||
AdminScheduledJobItem,
|
||||
AdminScheduledJobsConfig,
|
||||
AdminScheduledJobRuntimeState,
|
||||
AdminScheduledJobsResponse,
|
||||
AdminNinetyoneOrderItem,
|
||||
AdminNinetyoneOrderListResult,
|
||||
AdminNinetyoneOrderActionResult,
|
||||
AdminKuaishouEticketSourceConfig,
|
||||
AdminKuaishouEticketShopConfigItem,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiProductSyncResult,
|
||||
AdminKuaishouFeifeiProductMatch,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductItem,
|
||||
AdminKuaishouFeifeiProductListResult,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesOverrideRuleConfig,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceConfig,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSessionItem,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSessionSummary,
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
AdminCloudtentaclesSkuItem,
|
||||
AdminCloudtentaclesSkuListResult,
|
||||
AdminCloudtentaclesDeliveryRecordItem,
|
||||
AdminCloudtentaclesDeliveryRecordListResult,
|
||||
} from './platform-config'
|
||||
@@ -1,87 +0,0 @@
|
||||
import type { AdminTaskListItem } from './tasks'
|
||||
|
||||
export interface AdminOrderFulfillmentProgress {
|
||||
totalTaskCount: number
|
||||
preparedTaskCount: number
|
||||
completedTaskCount: number
|
||||
resourceStatus: string
|
||||
customerStatus: string
|
||||
}
|
||||
|
||||
export interface AdminOrderListItem {
|
||||
orderId: number
|
||||
provider: string
|
||||
platform: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
platformOrderId: string
|
||||
orderStatus: string
|
||||
payStatus: string
|
||||
buyerId: string
|
||||
buyerName: string
|
||||
receiverContact: string
|
||||
totalAmount: string
|
||||
totalAmountFen: number
|
||||
currency: string
|
||||
paidAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
itemCount: number
|
||||
totalQuantity: number
|
||||
itemSummary: string
|
||||
taskCount: number
|
||||
resourceStatus: string
|
||||
customerStatus: string
|
||||
preparedTaskCount: number
|
||||
completedTaskCount: number
|
||||
}
|
||||
|
||||
export interface AdminOrderDetail {
|
||||
order: {
|
||||
orderId: number
|
||||
provider: string
|
||||
platform: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
platformOrderId: string
|
||||
orderStatus: string
|
||||
payStatus: string
|
||||
buyerId: string
|
||||
buyerName: string
|
||||
receiverContact: string
|
||||
totalAmount: string
|
||||
totalAmountFen: number
|
||||
currency: string
|
||||
paidAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
itemSummary: string
|
||||
rawPayload: Record<string, unknown>
|
||||
fulfillmentProgress: AdminOrderFulfillmentProgress
|
||||
}
|
||||
items: Array<{
|
||||
orderItemId: number
|
||||
skuCode: string
|
||||
skuName: string
|
||||
itemTitle: string
|
||||
quantity: number
|
||||
deliveryMode: string
|
||||
spec: Record<string, unknown>
|
||||
}>
|
||||
tasks: AdminTaskListItem[]
|
||||
kuaishouIndustryVouchers: Array<{
|
||||
oid: string
|
||||
voucherCode: string
|
||||
unitIndex: number
|
||||
status: string
|
||||
sendCallbackStatus: string
|
||||
sendCallbackAttemptCount: number
|
||||
sendCallbackLastError: string
|
||||
sendCallbackSentAt: string | null
|
||||
consumedAt: string | null
|
||||
destroyedAt: string | null
|
||||
}>
|
||||
operations: {
|
||||
canResendKuaishouIndustryVoucherCode: boolean
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
export interface AdminCloudtentaclesSourceItem {
|
||||
key: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSourcesConfig {
|
||||
enabled: boolean
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesPersistedSession {
|
||||
token: string
|
||||
tokenMasked: string
|
||||
baseUrl: string
|
||||
username: string
|
||||
phoneMasked: string
|
||||
loggedInAt: string
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
hasToken: boolean
|
||||
}
|
||||
|
||||
export type AdminCloudtentaclesSessionItem = AdminCloudtentaclesPersistedSession
|
||||
|
||||
export interface AdminCloudtentaclesSessionsMap {
|
||||
[sourceKey: string]: AdminCloudtentaclesPersistedSession
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSourceConfigResponse {
|
||||
filePath: string
|
||||
sessionFilePath: string
|
||||
enabled: boolean
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
sessions: AdminCloudtentaclesSessionsMap
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesOverrideDeliveryItem {
|
||||
cloudSkuId: number
|
||||
cloudSkuName: string
|
||||
quantity: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesOverrideRule {
|
||||
id: string
|
||||
enabled: boolean
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
sourceKey: string
|
||||
deliveryItems: AdminCloudtentaclesOverrideDeliveryItem[]
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesOverrideRuleConfig {
|
||||
filePath: string
|
||||
enabled: boolean
|
||||
rules: AdminCloudtentaclesOverrideRule[]
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSendSmsResult {
|
||||
baseUrl: string
|
||||
username: string
|
||||
phone: string
|
||||
phoneMasked: string
|
||||
sentAt: string
|
||||
responseMessage: string
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSessionSummary {
|
||||
tokenMasked: string
|
||||
permissionCount: number
|
||||
permissions: string[]
|
||||
persisted?: boolean
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesLoginTestResult {
|
||||
baseUrl: string
|
||||
username: string
|
||||
phoneMasked: string
|
||||
loggedInAt: string
|
||||
responseMessage: string
|
||||
token: string
|
||||
session: AdminCloudtentaclesSessionSummary
|
||||
userInfo: Record<string, unknown>
|
||||
asset: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesValidateSessionResult {
|
||||
baseUrl: string
|
||||
loggedInAt: string
|
||||
session: AdminCloudtentaclesSessionSummary
|
||||
userInfo: Record<string, unknown>
|
||||
asset: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSkuItem {
|
||||
id: number
|
||||
categoriesId: number
|
||||
name: string
|
||||
description: string
|
||||
image: string
|
||||
inventory: number
|
||||
price: number
|
||||
listingTime: string
|
||||
delistingTime: string
|
||||
buyLimitMin: number
|
||||
buyLimitMax: number
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSkuListResult {
|
||||
baseUrl: string
|
||||
itemCount: number
|
||||
items: AdminCloudtentaclesSkuItem[]
|
||||
rawItems: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesDeliveryRecordItem {
|
||||
createdAt: string
|
||||
recordId: string
|
||||
virtualNumberId: number
|
||||
userId: number
|
||||
count: number
|
||||
cdk: string
|
||||
exchangeUrl: string
|
||||
name: string
|
||||
skuId: number
|
||||
skuName: string
|
||||
skuImage: string
|
||||
phone: string
|
||||
status: number
|
||||
fulfillUser: string
|
||||
operatorName: string
|
||||
platformOrderId: string
|
||||
taskId: number
|
||||
taskNo: string
|
||||
orderShopId: string
|
||||
orderShopName: string
|
||||
consumeShopId: string
|
||||
consumeShopName: string
|
||||
roleName: string
|
||||
roleId: string
|
||||
localTaskStatus: string
|
||||
localDeliveryStatus: string
|
||||
localDispatchStatus: string
|
||||
localDispatchAt: string
|
||||
localResultCode: string
|
||||
localResultMessage: string
|
||||
matchConfidence: string
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesDeliveryRecordListResult {
|
||||
baseUrl: string
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
sourceKey?: string
|
||||
queryStartDate?: string
|
||||
queryEndDate?: string
|
||||
localOrderMatched?: boolean
|
||||
items: AdminCloudtentaclesDeliveryRecordItem[]
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** @deprecated Use AdminCloudtentaclesSourceItem + AdminCloudtentaclesSourcesConfig instead */
|
||||
export interface AdminCloudtentaclesSourceConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
export type {
|
||||
AdminNotificationBarkRecipient,
|
||||
AdminNotificationWpushRecipient,
|
||||
AdminNotificationConfig,
|
||||
AdminNotificationSendResult,
|
||||
AdminNotificationTestResult,
|
||||
} from './notifications'
|
||||
|
||||
export type {
|
||||
AdminCloudtentaclesMonitorAccount,
|
||||
AdminScheduledJobAccountRuntime,
|
||||
AdminScheduledJobCloudtentaclesAccount,
|
||||
AdminScheduledJobItem,
|
||||
AdminScheduledJobsConfig,
|
||||
AdminScheduledJobRuntimeState,
|
||||
AdminScheduledJobsResponse,
|
||||
} from './scheduled-jobs'
|
||||
|
||||
export type {
|
||||
AdminNinetyoneOrderItem,
|
||||
AdminNinetyoneOrderListResult,
|
||||
AdminNinetyoneOrderActionResult,
|
||||
} from './ninetyone'
|
||||
|
||||
export type {
|
||||
AdminKuaishouEticketSourceConfig,
|
||||
AdminKuaishouEticketShopConfigItem,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
} from './kuaishou-eticket'
|
||||
|
||||
export type {
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiProductSyncResult,
|
||||
AdminKuaishouFeifeiProductMatch,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductItem,
|
||||
AdminKuaishouFeifeiProductListResult,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
} from './kuaishou-feifei'
|
||||
|
||||
export type {
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesOverrideRuleConfig,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceConfig,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSessionItem,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSessionSummary,
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
AdminCloudtentaclesSkuItem,
|
||||
AdminCloudtentaclesSkuListResult,
|
||||
AdminCloudtentaclesDeliveryRecordItem,
|
||||
AdminCloudtentaclesDeliveryRecordListResult,
|
||||
} from './cloudtentacles'
|
||||
@@ -1,83 +0,0 @@
|
||||
export interface AdminKuaishouEticketShopConfigItem {
|
||||
shopId: string
|
||||
kshopName: string
|
||||
cookie: string
|
||||
cookieMasked: string
|
||||
hasCookie: boolean
|
||||
userAvatar: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface AdminKuaishouEticketSourceConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
shops: AdminKuaishouEticketShopConfigItem[]
|
||||
}
|
||||
|
||||
export interface AdminKuaishouEticketShopInfoResult {
|
||||
baseUrl: string
|
||||
ok: boolean
|
||||
result: number
|
||||
errorMessage: string
|
||||
requestId: string
|
||||
shop: {
|
||||
shopId: string
|
||||
kshopName: string
|
||||
userAvatar: string
|
||||
settleStatus: number
|
||||
hasCookie: boolean
|
||||
}
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminKuaishouEticketDetailResult {
|
||||
baseUrl: string
|
||||
eTicketId: string
|
||||
ok: boolean
|
||||
alreadyConsumed: boolean
|
||||
result: number
|
||||
errorMessage: string
|
||||
requestId: string
|
||||
serverTimestamp: string
|
||||
detail: {
|
||||
uid: string
|
||||
fulfillDetailId: string
|
||||
sellerId: string
|
||||
formToken: string
|
||||
validEndTime: string
|
||||
validStartTime: string
|
||||
leftReverseCount: number
|
||||
eTicketId: string
|
||||
oid: string
|
||||
totalCount: number
|
||||
leftCount: number
|
||||
status: string
|
||||
} | null
|
||||
goods: {
|
||||
itemId: string
|
||||
itemPicUrl: string
|
||||
itemTitle: string
|
||||
price: string
|
||||
skuDesc: string
|
||||
skuId: string
|
||||
} | null
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminKuaishouEticketConsumeResult {
|
||||
baseUrl: string
|
||||
eTicketId: string
|
||||
oid: string
|
||||
formToken: string
|
||||
num: number
|
||||
storeId: string
|
||||
ok: boolean
|
||||
consumed: boolean
|
||||
alreadyConsumed: boolean
|
||||
result: number
|
||||
errorMessage: string
|
||||
requestId: string
|
||||
serverTimestamp: string
|
||||
detail: AdminKuaishouEticketDetailResult['detail']
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
export interface AdminKuaishouFeifeiProductRule {
|
||||
id: string
|
||||
enabled: boolean
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
productCode: string
|
||||
skuName: string
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
appKey: string
|
||||
appSecret: string
|
||||
timeoutMs: number
|
||||
notifyUrl: string
|
||||
productRules: AdminKuaishouFeifeiProductRule[]
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiEffectiveConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
timeoutMs: number
|
||||
notifyUrl: string
|
||||
hasAppKey: boolean
|
||||
hasAppSecret: boolean
|
||||
productRuleCount: number
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiConfigResponse {
|
||||
filePath: string
|
||||
source: AdminKuaishouFeifeiConfig
|
||||
effective: AdminKuaishouFeifeiEffectiveConfig
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiProductSyncResult extends AdminKuaishouFeifeiConfigResponse {
|
||||
sync: {
|
||||
productCount: number
|
||||
ruleCount: number
|
||||
status: string
|
||||
supplyProductName: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiProductMatch {
|
||||
matchMode: string
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
productCode: string
|
||||
skuName: string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiMatchResult {
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
matched: boolean
|
||||
match: AdminKuaishouFeifeiProductMatch | null
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiProductItem {
|
||||
productCode: string
|
||||
name: string
|
||||
channel: string
|
||||
externalProductId: string
|
||||
imageUrl: string
|
||||
status: string
|
||||
supplyStatus: string
|
||||
supplyPricePoints: number
|
||||
feePoints: number
|
||||
unitCostPoints: number
|
||||
maxOrderQuantity: number
|
||||
salePricePoints: number | null
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiProductListResult {
|
||||
items: AdminKuaishouFeifeiProductItem[]
|
||||
total: number
|
||||
page: number
|
||||
perPage: number
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiOrderResult {
|
||||
orderNo: string
|
||||
platformOrderNo: string
|
||||
productCode: string
|
||||
productName: string
|
||||
rechargeStatus: number
|
||||
rechargeStatusLabel: string
|
||||
pointsCharged: number
|
||||
playerAccount: string
|
||||
platformBuyNum: number
|
||||
rechargeResultMessage: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
rechargeFinishAt: string
|
||||
h5: {
|
||||
entryUrl: string
|
||||
rechargeUrl: string
|
||||
}
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
export interface AdminNinetyoneOrderItem {
|
||||
orderId: number
|
||||
orderNo: string
|
||||
outTradeNo: string
|
||||
orderStatus: string
|
||||
payStatus: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
productNo: string
|
||||
productName: string
|
||||
buyNum: number
|
||||
taskCount: number
|
||||
failReason: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
items: Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export interface AdminNinetyoneOrderListResult {
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
items: AdminNinetyoneOrderItem[]
|
||||
}
|
||||
|
||||
export interface AdminNinetyoneOrderActionResult {
|
||||
orderId: number
|
||||
orderNo: string
|
||||
orderStatus: string
|
||||
orderItemCount: number
|
||||
taskCount: number
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
export interface AdminNotificationBarkRecipient {
|
||||
id: string
|
||||
name: string
|
||||
deviceKey: string
|
||||
deviceKeyMasked: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface AdminNotificationWpushRecipient {
|
||||
id: string
|
||||
name: string
|
||||
apiKey: string
|
||||
apiKeyMasked: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface AdminNotificationConfig {
|
||||
enabled: boolean
|
||||
channels: {
|
||||
bark: {
|
||||
enabled: boolean
|
||||
serverUrl: string
|
||||
recipients: AdminNotificationBarkRecipient[]
|
||||
}
|
||||
wpush: {
|
||||
enabled: boolean
|
||||
recipients: AdminNotificationWpushRecipient[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminNotificationSendResult {
|
||||
channel: string
|
||||
recipientId: string
|
||||
recipientName: string
|
||||
recipientKeyMasked: string
|
||||
ok: boolean
|
||||
status: number
|
||||
errorMessage: string
|
||||
response: unknown
|
||||
}
|
||||
|
||||
export interface AdminNotificationTestResult {
|
||||
enabled: boolean
|
||||
channel: string
|
||||
successCount: number
|
||||
failedCount: number
|
||||
skippedCount: number
|
||||
results: AdminNotificationSendResult[]
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
export interface AdminScheduledJobCloudtentaclesAccount {
|
||||
sourceKey: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
assetThreshold: number
|
||||
}
|
||||
|
||||
export interface AdminScheduledJobItem {
|
||||
id: string
|
||||
type: string
|
||||
enabled: boolean
|
||||
intervalSeconds: number
|
||||
cooldownSeconds: number
|
||||
config: {
|
||||
assetThreshold: number
|
||||
accounts: AdminScheduledJobCloudtentaclesAccount[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminScheduledJobsConfig {
|
||||
enabled: boolean
|
||||
jobs: AdminScheduledJobItem[]
|
||||
}
|
||||
|
||||
export interface AdminScheduledJobRuntimeState {
|
||||
id: string
|
||||
enabled: boolean
|
||||
running: boolean
|
||||
lastRunAt: string
|
||||
lastFinishedAt: string
|
||||
nextRunAt: string
|
||||
lastStatus: string
|
||||
lastMessage: string
|
||||
lastAsset: number | null
|
||||
lastThreshold: number | null
|
||||
lastAccounts: AdminScheduledJobAccountRuntime[]
|
||||
lastAccountCount: number
|
||||
lastCheckedCount: number
|
||||
lastOkCount: number
|
||||
lastLowAssetCount: number
|
||||
lastFailedCount: number
|
||||
lastManual: boolean
|
||||
}
|
||||
|
||||
export interface AdminScheduledJobAccountRuntime {
|
||||
sourceKey: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
hasToken: boolean
|
||||
ok: boolean
|
||||
skipped: boolean
|
||||
status: string
|
||||
message: string
|
||||
asset: number | null
|
||||
threshold: number
|
||||
checkedAt: string
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesMonitorAccount {
|
||||
sourceKey: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
username: string
|
||||
phoneMasked: string
|
||||
hasToken: boolean
|
||||
loggedInAt: string
|
||||
}
|
||||
|
||||
export interface AdminScheduledJobsResponse {
|
||||
filePath: string
|
||||
source: AdminScheduledJobsConfig
|
||||
runtime: AdminScheduledJobRuntimeState[]
|
||||
cloudtentaclesAccounts: AdminCloudtentaclesMonitorAccount[]
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
export interface AdminTaskListItem {
|
||||
taskId: number
|
||||
taskNo: string
|
||||
platformOrderId: string
|
||||
skuCode: string
|
||||
skuName: string
|
||||
status: string
|
||||
executorKey: string
|
||||
deliveryStatus: string
|
||||
resultCode: string
|
||||
resultMessage: string
|
||||
resourceStatus: string
|
||||
customerStatus: string
|
||||
loginType: string
|
||||
roleName: string
|
||||
roleId: string
|
||||
runtimeSessionId: string
|
||||
claimedAt: string | null
|
||||
roleConfirmedAt: string | null
|
||||
redeemedAt: string | null
|
||||
retryCount: number
|
||||
lastError: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
claimToken: string
|
||||
screenshotPath: string
|
||||
}
|
||||
|
||||
export interface AdminTaskOperations {
|
||||
canRetry: boolean
|
||||
canRegenerateClaimLink: boolean
|
||||
canClose: boolean
|
||||
canMarkManualReview: boolean
|
||||
canCompleteManualDispatch: boolean
|
||||
canPrepareKuaishouCloudFulfillment: boolean
|
||||
canRefreshKuaishouCloudRoleInfo: boolean
|
||||
canRebindKuaishouCloudRole: boolean
|
||||
canDispatchKuaishouCloudFulfillment: boolean
|
||||
canReturnKuaishouCloudFulfillment: boolean
|
||||
canResendKuaishouIndustryVoucherCode: boolean
|
||||
canConsumeKuaishouIndustryVoucher: boolean
|
||||
canViewSensitiveTaskData: boolean
|
||||
}
|
||||
|
||||
export interface AdminTaskActionResponse {
|
||||
task: {
|
||||
taskId: number
|
||||
taskNo: string
|
||||
status: string
|
||||
deliveryStatus: string
|
||||
resultCode: string
|
||||
resultMessage: string
|
||||
primaryClaimTokenId: number | null
|
||||
lastError: string
|
||||
updatedAt: string
|
||||
}
|
||||
claimUrl?: string
|
||||
token?: string
|
||||
}
|
||||
|
||||
export interface AdminTaskDetail {
|
||||
task: AdminTaskListItem
|
||||
order: null | {
|
||||
orderId: number
|
||||
provider: string
|
||||
platform: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
platformOrderId: string
|
||||
payStatus: string
|
||||
orderStatus: string
|
||||
}
|
||||
orderItem: null | {
|
||||
orderItemId: number
|
||||
skuCode: string
|
||||
skuName: string
|
||||
quantity: number
|
||||
}
|
||||
claimToken: null | {
|
||||
primaryClaimTokenId: number
|
||||
token: string
|
||||
status: string
|
||||
expiredAt: string
|
||||
claimUrl: string
|
||||
}
|
||||
artifacts: Record<string, unknown>
|
||||
screenshotUrl: string
|
||||
review: {
|
||||
required: boolean
|
||||
screenshotCapturedAt: string | null
|
||||
roleId: string
|
||||
roleName: string
|
||||
}
|
||||
redeemResolution: null | {
|
||||
status: string
|
||||
taskStatus: string
|
||||
replacementCount: number
|
||||
finishedAt: string | null
|
||||
attempts: Array<{
|
||||
attempt: number
|
||||
codeMasked: string
|
||||
credentialType: string
|
||||
outcome: string
|
||||
resultCode: string
|
||||
resultMessage: string
|
||||
}>
|
||||
}
|
||||
kuaishouCloudFulfillment: null | {
|
||||
flowType: string
|
||||
configId: string
|
||||
internalSkuCode: string
|
||||
internalSkuName: string
|
||||
ticket: {
|
||||
code: string
|
||||
capturedAt: string | null
|
||||
status: string
|
||||
verifiedAt: string | null
|
||||
goodsTitle: string
|
||||
leftCount: number
|
||||
}
|
||||
binding: {
|
||||
prepareStatus: string
|
||||
cloudSourceKeys: string[]
|
||||
cloudSourceLabels: string[]
|
||||
resolvedSourceKey: string
|
||||
resolvedSourceLabel: string
|
||||
skuId: number
|
||||
skuName: string
|
||||
vnKey: string
|
||||
vnId: number
|
||||
vnPhone: string
|
||||
bindUrl: string
|
||||
bindPreparedAt: string | null
|
||||
bindExpiresAt: string | null
|
||||
bindProbeAt: string | null
|
||||
bindProbeStatus: string
|
||||
bindProbeMessage: string
|
||||
}
|
||||
role: {
|
||||
status: string
|
||||
name: string
|
||||
rid: string
|
||||
refreshedAt: string | null
|
||||
errorMessage: string
|
||||
rawInfo: Record<string, unknown> | null
|
||||
}
|
||||
purchase: {
|
||||
autoBuyEnabled: boolean
|
||||
minAssetReserve: number
|
||||
usedKnapsack: boolean
|
||||
purchaseTriggered: boolean
|
||||
assetBefore: number
|
||||
assetAfter: number
|
||||
purchaseAt: string | null
|
||||
}
|
||||
dispatch: {
|
||||
status: string
|
||||
dispatchAt: string | null
|
||||
sendType: number
|
||||
note: string
|
||||
}
|
||||
returnNumber: {
|
||||
status: string
|
||||
returnedAt: string | null
|
||||
autoReturnEnabled: boolean
|
||||
}
|
||||
consume: {
|
||||
status: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
autoConsumeEnabled: boolean
|
||||
consumedAt: string | null
|
||||
errorMessage: string
|
||||
}
|
||||
rebind: {
|
||||
currentAttempt: number
|
||||
history: Array<{
|
||||
attempt: number
|
||||
source: string
|
||||
requestedAt: string | null
|
||||
requestedBy: Record<string, unknown> | null
|
||||
status: string
|
||||
errorMessage: string
|
||||
oldBinding: {
|
||||
vnKey: string
|
||||
vnId: number
|
||||
vnPhone: string
|
||||
bindUrl: string
|
||||
bindPreparedAt: string | null
|
||||
bindExpiresAt: string | null
|
||||
roleName: string
|
||||
roleId: string
|
||||
}
|
||||
newBinding: null | {
|
||||
vnKey: string
|
||||
vnId: number
|
||||
vnPhone: string
|
||||
bindUrl: string
|
||||
bindPreparedAt: string | null
|
||||
bindExpiresAt: string | null
|
||||
}
|
||||
}>
|
||||
}
|
||||
notes: string
|
||||
}
|
||||
kuaishouIndustryVoucher: null | {
|
||||
oid: string
|
||||
voucherCode: string
|
||||
unitIndex: number
|
||||
status: string
|
||||
sendCallbackStatus: string
|
||||
sendCallbackLastError: string
|
||||
sendCallbackAttemptCount: number
|
||||
sendCallbackSentAt: string | null
|
||||
consumeSerialNum: string
|
||||
consumeDetails: Array<Record<string, unknown>>
|
||||
consumedAt: string | null
|
||||
destroyedAt: string | null
|
||||
}
|
||||
manualDispatch: null | {
|
||||
outcome: string
|
||||
deliveryReference: string
|
||||
deliveredCredential: string
|
||||
resultMessage: string
|
||||
completedAt: string | null
|
||||
completedBy: null | {
|
||||
userId: number
|
||||
username: string
|
||||
role: string
|
||||
}
|
||||
}
|
||||
events: Array<{
|
||||
eventId: number
|
||||
eventType: string
|
||||
payload: Record<string, unknown>
|
||||
createdAt: string
|
||||
}>
|
||||
operations: AdminTaskOperations
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { AdminRole, AdminUserStatus } from './auth'
|
||||
|
||||
export interface AdminUserListItem {
|
||||
userId: number
|
||||
username: string
|
||||
role: AdminRole
|
||||
status: AdminUserStatus
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import type { KnownTaskStatus } from '@/domain/task-status'
|
||||
|
||||
export type ClaimTokenStatus = 'active' | 'used' | 'expired' | 'revoked' | (string & {})
|
||||
|
||||
export type ClaimTaskStatus = KnownTaskStatus | (string & {})
|
||||
|
||||
export interface ClaimTaskInfo {
|
||||
taskId: number
|
||||
taskNo: string
|
||||
status: ClaimTaskStatus
|
||||
executorKey: string
|
||||
requiresSupportReview: boolean
|
||||
expiresAt: string | null
|
||||
claimedAt: string | null
|
||||
roleConfirmedAt: string | null
|
||||
redeemedAt: string | null
|
||||
loginType: string
|
||||
lastError: string
|
||||
runtimeSessionId: string
|
||||
}
|
||||
|
||||
export interface ClaimOrderInfo {
|
||||
orderId: number
|
||||
platform: string
|
||||
platformOrderId: string
|
||||
payStatus: string
|
||||
orderStatus: string
|
||||
totalAmount: string
|
||||
totalAmountFen: number
|
||||
currency: string
|
||||
}
|
||||
|
||||
export interface ClaimOrderItemInfo {
|
||||
orderItemId: number
|
||||
skuCode: string
|
||||
skuName: string
|
||||
quantity: number
|
||||
}
|
||||
|
||||
export interface ClaimProductInfo {
|
||||
title: string
|
||||
skuCode: string
|
||||
quantity: number
|
||||
isBundle: boolean
|
||||
items: Array<{
|
||||
cloudSkuId: number
|
||||
name: string
|
||||
quantity: number
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ClaimResultInfo {
|
||||
resultCode: string
|
||||
resultMessage: string
|
||||
screenshotReady: boolean
|
||||
screenshotUrl: string
|
||||
}
|
||||
|
||||
export interface ClaimKuaishouCloudFlowInfo {
|
||||
flowType: 'kuaishou_cloud'
|
||||
shopId: string
|
||||
shopName: string
|
||||
guideImages: string[]
|
||||
ticket: {
|
||||
code: string
|
||||
status: string
|
||||
capturedAt: string | null
|
||||
verifiedAt: string | null
|
||||
goodsTitle: string
|
||||
leftCount: number
|
||||
}
|
||||
binding: {
|
||||
prepareStatus: string
|
||||
cloudSourceKeys: string[]
|
||||
resolvedSourceKey: string
|
||||
skuId: number
|
||||
skuName: string
|
||||
vnKey: string
|
||||
vnId: number
|
||||
bindUrl: string
|
||||
bindPreparedAt: string | null
|
||||
bindExpiresAt: string | null
|
||||
bindProbeAt: string | null
|
||||
bindProbeStatus: string
|
||||
bindProbeMessage: string
|
||||
vnPhone: string
|
||||
roleName: string
|
||||
roleId: string
|
||||
}
|
||||
role: {
|
||||
status: string
|
||||
name: string
|
||||
rid: string
|
||||
refreshedAt: string | null
|
||||
errorMessage: string
|
||||
defaultName: string
|
||||
defaultRid: string
|
||||
defaultCapturedAt: string | null
|
||||
defaultCaptureStatus: string
|
||||
defaultErrorMessage: string
|
||||
isDefaultRole: boolean
|
||||
}
|
||||
purchase: {
|
||||
autoBuyEnabled: boolean
|
||||
minAssetReserve: number
|
||||
usedKnapsack: boolean
|
||||
purchaseTriggered: boolean
|
||||
assetBefore: number
|
||||
assetAfter: number
|
||||
purchaseAt: string | null
|
||||
}
|
||||
dispatch: {
|
||||
status: string
|
||||
dispatchAt: string | null
|
||||
failedAt: string | null
|
||||
failedStage: string
|
||||
errorCode: string
|
||||
errorMessage: string
|
||||
note: string
|
||||
}
|
||||
returnNumber: {
|
||||
status: string
|
||||
returnedAt: string | null
|
||||
}
|
||||
consume: {
|
||||
status: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
consumedAt: string | null
|
||||
errorMessage: string
|
||||
}
|
||||
rebind?: {
|
||||
currentAttempt: number
|
||||
history: Array<Record<string, unknown>>
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClaimKuaishouFeifeiFlowInfo {
|
||||
flowType: 'kuaishou_feifei'
|
||||
productCode: string
|
||||
productName: string
|
||||
platformOrderNo: string
|
||||
orderNo: string
|
||||
rechargeStatus: number
|
||||
rechargeStatusLabel: string
|
||||
rechargeResultMessage: string
|
||||
claimUrl: string
|
||||
consumeStatus: string
|
||||
h5: {
|
||||
entryUrl: string
|
||||
rechargeUrl: string
|
||||
}
|
||||
lastSyncedAt: string | null
|
||||
}
|
||||
|
||||
export interface ClaimDetailData {
|
||||
tokenStatus: ClaimTokenStatus
|
||||
claimUrl: string
|
||||
flowType: 'kuaishou_cloud' | 'kuaishou_ct_assisted' | 'kuaishou_feifei' | (string & {})
|
||||
task: ClaimTaskInfo
|
||||
order: ClaimOrderInfo
|
||||
orderItem: ClaimOrderItemInfo
|
||||
product: ClaimProductInfo
|
||||
session: unknown | null
|
||||
kuaishouCloudFulfillment: ClaimKuaishouCloudFlowInfo | null
|
||||
kuaishouFeifei: ClaimKuaishouFeifeiFlowInfo | null
|
||||
result: ClaimResultInfo | null
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
const ADMIN_TOKEN_KEY = 'order-site-admin-token'
|
||||
const ADMIN_EXPIRES_AT_KEY = 'order-site-admin-token-expires-at'
|
||||
const ADMIN_USER_ID_KEY = 'order-site-admin-user-id'
|
||||
const ADMIN_USERNAME_KEY = 'order-site-admin-username'
|
||||
const ADMIN_ROLE_KEY = 'order-site-admin-role'
|
||||
|
||||
const ADMIN_ROLE_LEVEL: Record<'support' | 'operator' | 'admin', number> = {
|
||||
support: 1,
|
||||
operator: 2,
|
||||
admin: 3,
|
||||
}
|
||||
|
||||
export function getAdminToken() {
|
||||
return localStorage.getItem(ADMIN_TOKEN_KEY) || ''
|
||||
}
|
||||
|
||||
export function getAdminTokenExpiresAt() {
|
||||
return localStorage.getItem(ADMIN_EXPIRES_AT_KEY) || ''
|
||||
}
|
||||
|
||||
export function getAdminUserId() {
|
||||
const value = Number(localStorage.getItem(ADMIN_USER_ID_KEY) || 0)
|
||||
return Number.isFinite(value) && value > 0 ? value : 0
|
||||
}
|
||||
|
||||
export function getAdminUsername() {
|
||||
return localStorage.getItem(ADMIN_USERNAME_KEY) || ''
|
||||
}
|
||||
|
||||
export function getAdminRole() {
|
||||
const role = localStorage.getItem(ADMIN_ROLE_KEY)
|
||||
if (role === 'admin' || role === 'operator' || role === 'support') {
|
||||
return role
|
||||
}
|
||||
|
||||
return 'support'
|
||||
}
|
||||
|
||||
export function hasAdminRole(role: 'admin' | 'operator' | 'support') {
|
||||
if (!hasAdminSession()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ADMIN_ROLE_LEVEL[getAdminRole()] >= ADMIN_ROLE_LEVEL[role]
|
||||
}
|
||||
|
||||
export function setAdminSession(
|
||||
token: string,
|
||||
expiresAt: string,
|
||||
user?: { userId: number; username: string; role: 'admin' | 'operator' | 'support' },
|
||||
) {
|
||||
localStorage.setItem(ADMIN_TOKEN_KEY, token)
|
||||
localStorage.setItem(ADMIN_EXPIRES_AT_KEY, expiresAt)
|
||||
|
||||
if (user?.userId) {
|
||||
localStorage.setItem(ADMIN_USER_ID_KEY, String(user.userId))
|
||||
}
|
||||
|
||||
if (user?.username) {
|
||||
localStorage.setItem(ADMIN_USERNAME_KEY, user.username)
|
||||
}
|
||||
|
||||
if (user?.role) {
|
||||
localStorage.setItem(ADMIN_ROLE_KEY, user.role)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAdminSession() {
|
||||
localStorage.removeItem(ADMIN_TOKEN_KEY)
|
||||
localStorage.removeItem(ADMIN_EXPIRES_AT_KEY)
|
||||
localStorage.removeItem(ADMIN_USER_ID_KEY)
|
||||
localStorage.removeItem(ADMIN_USERNAME_KEY)
|
||||
localStorage.removeItem(ADMIN_ROLE_KEY)
|
||||
}
|
||||
|
||||
export function hasAdminSession() {
|
||||
const token = getAdminToken()
|
||||
const expiresAt = getAdminTokenExpiresAt()
|
||||
|
||||
if (!token) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isExpired(expiresAt)) {
|
||||
clearAdminSession()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function isExpired(expiresAt: string) {
|
||||
if (!expiresAt) {
|
||||
return false
|
||||
}
|
||||
|
||||
const expiresAtMs = Date.parse(expiresAt)
|
||||
return Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now()
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
export function formatStatusWithRaw(status: string, labelMap: Record<string, string> = {}) {
|
||||
const normalized = String(status || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
const key = normalized.toLowerCase()
|
||||
const label = labelMap[key]
|
||||
|
||||
if (!label || label === normalized) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
return `${label} (${normalized})`
|
||||
}
|
||||
|
||||
export function formatAuditAction(action: string) {
|
||||
const labelMap: Record<string, string> = {
|
||||
admin_user_created: '创建后台用户',
|
||||
admin_user_role_updated: '修改用户角色',
|
||||
admin_user_status_updated: '修改用户状态',
|
||||
admin_user_password_reset: '重置用户密码',
|
||||
task_regenerate_claim_link: '重发领取链接',
|
||||
task_kuaishou_cloud_rebind_role: '换绑角色',
|
||||
task_closed: '关闭任务',
|
||||
task_mark_manual_review: '转人工处理',
|
||||
task_complete_manual_dispatch: '完成人工履约',
|
||||
platform_shop_config_updated: '更新店铺配置',
|
||||
platform_fulfillment_config_updated: '更新履约配置',
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(action, labelMap)
|
||||
}
|
||||
|
||||
export function formatAuditTargetType(targetType: string) {
|
||||
const labelMap: Record<string, string> = {
|
||||
admin_user: '后台用户',
|
||||
task: '交付任务',
|
||||
platform_config: '平台配置',
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(targetType, labelMap)
|
||||
}
|
||||
|
||||
export function formatTaskEventType(eventType: string) {
|
||||
const labelMap: Record<string, string> = {
|
||||
manual_dispatch_completed: '人工履约已回写',
|
||||
claim_redeem_code_used: '兑换码已使用',
|
||||
claim_redeem_code_invalid: '兑换码错误',
|
||||
claim_redeem_completed: '兑换链路完成',
|
||||
kuaishou_cloud_rebind_requested: '发起角色换绑',
|
||||
kuaishou_cloud_rebind_old_number_returned: '换绑旧号已退还',
|
||||
kuaishou_cloud_rebind_prepared: '换绑资源已准备',
|
||||
kuaishou_cloud_rebind_failed: '换绑失败',
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(eventType, labelMap)
|
||||
}
|
||||
|
||||
export function formatRedeemOutcomeLabel(value: string) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
switch (normalized) {
|
||||
case 'success':
|
||||
return '兑换成功'
|
||||
case 'code_used':
|
||||
return '兑换码已使用'
|
||||
case 'code_invalid':
|
||||
return '兑换码错误'
|
||||
case 'captcha_rejected':
|
||||
return '验证码错误'
|
||||
case 'failed':
|
||||
return '兑换失败'
|
||||
default:
|
||||
return normalized || '-'
|
||||
}
|
||||
}
|
||||
|
||||
export function formatRedeemResolutionStatus(value: string) {
|
||||
return value === 'success' ? '已完成' : value === 'failed' ? '失败收口' : value || '-'
|
||||
}
|
||||
|
||||
export function formatKuaishouRoleInfoLabel(value: string) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
switch (normalized) {
|
||||
case 'name':
|
||||
return '角色名称'
|
||||
case 'rid':
|
||||
return '角色 ID'
|
||||
default:
|
||||
return normalized || '-'
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTaskEventPayload(payload: Record<string, unknown>) {
|
||||
const parts = [
|
||||
payload.outcome ? `结果 ${payload.outcome}` : '',
|
||||
payload.resultCode ? `代码 ${payload.resultCode}` : '',
|
||||
payload.resultMessage ? `说明 ${payload.resultMessage}` : '',
|
||||
payload.codeMasked ? `凭据 ${payload.codeMasked}` : '',
|
||||
payload.previousCodeMasked ? `旧码 ${payload.previousCodeMasked}` : '',
|
||||
payload.nextCodeMasked ? `新码 ${payload.nextCodeMasked}` : '',
|
||||
payload.ticketCodeMasked ? `卡券号 ${payload.ticketCodeMasked}` : '',
|
||||
payload.vnId ? `虚拟号 #${payload.vnId}` : '',
|
||||
payload.oldVnId ? `旧虚拟号 #${payload.oldVnId}` : '',
|
||||
payload.vnPhoneMasked ? `号码 ${payload.vnPhoneMasked}` : '',
|
||||
payload.oldVnPhoneMasked ? `旧号码 ${payload.oldVnPhoneMasked}` : '',
|
||||
payload.oldRoleName ? `旧角色 ${payload.oldRoleName}` : '',
|
||||
payload.oldRoleId ? `旧角色ID ${payload.oldRoleId}` : '',
|
||||
payload.attempt ? `第 ${payload.attempt} 次` : '',
|
||||
payload.bindUrl ? `绑定链接 ${payload.bindUrl}` : '',
|
||||
payload.sendType ? `发送类型 ${payload.sendType}` : '',
|
||||
payload.note ? `备注 ${payload.note}` : '',
|
||||
payload.previousOutcome
|
||||
? `切换原因 ${formatRedeemOutcomeLabel(String(payload.previousOutcome || ''))}`
|
||||
: '',
|
||||
payload.deliveryReference ? `单号 ${payload.deliveryReference}` : '',
|
||||
payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : '',
|
||||
payload.trigger ? `触发 ${payload.trigger}` : '',
|
||||
payload.reason ? `原因 ${payload.reason}` : '',
|
||||
payload.purchaseTriggered === true ? '已触发购买' : '',
|
||||
payload.usedKnapsack === true ? '使用背包库存' : '',
|
||||
payload.responseStatus ? `HTTP ${payload.responseStatus}` : '',
|
||||
payload.requestId ? `请求 ${payload.requestId}` : '',
|
||||
payload.errorMessage ? `错误 ${payload.errorMessage}` : '',
|
||||
].filter(Boolean)
|
||||
|
||||
if (parts.length > 0) {
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
return JSON.stringify(payload || {})
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
export const adminPayStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '未支付', value: 'unpaid' },
|
||||
{ label: '已支付', value: 'paid' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
{ label: '已退款', value: 'refunded' },
|
||||
]
|
||||
|
||||
export const adminTaskStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '待支付', value: 'pending_payment' },
|
||||
{ label: '已支付', value: 'paid' },
|
||||
{ label: '已生成链接', value: 'link_generated' },
|
||||
{ label: '已创建会话', value: 'claimed' },
|
||||
{ label: '已确认角色', value: 'role_confirmed' },
|
||||
{ label: '兑换中', value: 'redeeming' },
|
||||
{ label: '已兑换', value: 'redeemed' },
|
||||
{ label: '待重试', value: 'retry_pending' },
|
||||
{ label: '人工处理', value: 'manual_review' },
|
||||
{ label: '已过期', value: 'expired' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
]
|
||||
|
||||
export const adminUserRoleOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '管理员', value: 'admin' },
|
||||
{ label: '普通运营', value: 'operator' },
|
||||
{ label: '客服', value: 'support' },
|
||||
]
|
||||
|
||||
export const adminUserStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '生效中', value: 'active' },
|
||||
{ label: '已停用', value: 'disabled' },
|
||||
]
|
||||
|
||||
export const adminAuditActionOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '创建后台用户', value: 'admin_user_created' },
|
||||
{ label: '修改用户角色', value: 'admin_user_role_updated' },
|
||||
{ label: '修改用户状态', value: 'admin_user_status_updated' },
|
||||
{ label: '重置用户密码', value: 'admin_user_password_reset' },
|
||||
{ label: '换绑角色', value: 'task_kuaishou_cloud_rebind_role' },
|
||||
{ label: '关闭任务', value: 'task_closed' },
|
||||
{ label: '转人工处理', value: 'task_mark_manual_review' },
|
||||
]
|
||||
|
||||
export const adminAuditTargetTypeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '后台用户', value: 'admin_user' },
|
||||
{ label: '交付任务', value: 'task' },
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
export { formatDateTime as formatAdminDateTime } from '@/utils/date-time'
|
||||
@@ -1,54 +0,0 @@
|
||||
const ISO_DATE_TIME_PATTERN =
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/
|
||||
|
||||
export function isIsoDateTimeString(value: unknown): value is string {
|
||||
return typeof value === 'string' && ISO_DATE_TIME_PATTERN.test(value.trim())
|
||||
}
|
||||
|
||||
export function formatDateTime(value: string | null | undefined, fallback = '-') {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const date = new Date(normalized)
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return normalized.replace('T', ' ').replace('Z', '')
|
||||
}
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
export function normalizeDateTimeValue(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => normalizeDateTimeValue(item))
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, currentValue]) => [
|
||||
key,
|
||||
normalizeDateTimeValue(currentValue),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
if (isIsoDateTimeString(value)) {
|
||||
return formatDateTime(value, '')
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function stringifyDisplayJson(value: unknown, space = 2) {
|
||||
return JSON.stringify(normalizeDateTimeValue(value), null, space)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<main class="not-found">
|
||||
<div class="card">
|
||||
<p class="code">404</p>
|
||||
<h1>页面不存在</h1>
|
||||
<p>当前快手轻量前端没有对应页面。</p>
|
||||
<RouterLink to="/admin/dashboard">前往后台</RouterLink>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.not-found {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(560px, 100%);
|
||||
padding: 40px;
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 18px 64px rgba(17, 38, 78, 0.12);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.code {
|
||||
margin: 0;
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 8px 0 16px;
|
||||
font-size: clamp(30px, 5vw, 46px);
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
margin-top: 20px;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -1,214 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { useAdminListPage } from '@/composables/useAdminListPage'
|
||||
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
|
||||
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
|
||||
import { fetchAdminAuditLogs } from '@/services/admin'
|
||||
import type { AdminAuditLogItem } from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { formatAuditAction, formatAuditTargetType } from '@/utils/admin-display'
|
||||
import { adminAuditActionOptions, adminAuditTargetTypeOptions } from '@/utils/admin-options'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { stringifyDisplayJson } from '@/utils/date-time'
|
||||
|
||||
const actorUsername = ref('')
|
||||
const action = ref('')
|
||||
const targetType = ref('')
|
||||
const dateFrom = ref('')
|
||||
const dateTo = ref('')
|
||||
|
||||
const isAdmin = hasAdminRole('admin')
|
||||
|
||||
const {
|
||||
loading,
|
||||
errorMessage,
|
||||
items,
|
||||
pagination,
|
||||
loadPage: loadAuditLogs,
|
||||
} = useAdminListPage<AdminAuditLogItem>({
|
||||
defaultErrorMessage: '读取审计日志失败',
|
||||
fetchPage: isAdmin
|
||||
? (page, pageSize) =>
|
||||
fetchAdminAuditLogs({
|
||||
page,
|
||||
pageSize,
|
||||
actorUsername: actorUsername.value.trim(),
|
||||
action: action.value.trim(),
|
||||
targetType: targetType.value.trim(),
|
||||
dateFrom: dateFrom.value.trim(),
|
||||
dateTo: dateTo.value.trim(),
|
||||
})
|
||||
: () =>
|
||||
Promise.resolve({
|
||||
code: 0,
|
||||
msg: '',
|
||||
data: { items: [], pagination: { page: 1, pageSize: 20, total: 0 } },
|
||||
}),
|
||||
})
|
||||
|
||||
function formatPayload(payload: Record<string, unknown>) {
|
||||
const text = stringifyDisplayJson(payload, 0)
|
||||
return text.length > 120 ? `${text.slice(0, 120)}...` : text
|
||||
}
|
||||
|
||||
function resolveTargetLabel(targetType: string, targetId: string) {
|
||||
return `${formatAuditTargetType(targetType)} #${targetId || '-'}`
|
||||
}
|
||||
|
||||
const totalLabel = computed(() => `共 ${pagination.value.total} 条审计记录`)
|
||||
|
||||
function resetFilters() {
|
||||
actorUsername.value = ''
|
||||
action.value = ''
|
||||
targetType.value = ''
|
||||
dateFrom.value = ''
|
||||
dateTo.value = ''
|
||||
void loadAuditLogs(1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadAuditLogs(1)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="audit-page list-page">
|
||||
<AdminPageHeader
|
||||
title="操作审计"
|
||||
description="集中查看高风险后台动作,便于排查谁在什么时间改了什么。"
|
||||
>
|
||||
<template #extra>
|
||||
<span class="total-badge">{{ totalLabel }}</span>
|
||||
</template>
|
||||
</AdminPageHeader>
|
||||
|
||||
<!-- 权限检查 -->
|
||||
<el-result v-if="!isAdmin" icon="warning" title="仅管理员可以访问操作审计" />
|
||||
|
||||
<template v-else>
|
||||
<!-- 筛选栏 -->
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">筛选审计日志</span>
|
||||
<span class="card-desc">按操作人、动作类型、目标类型和日期筛选。</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="filter-grid">
|
||||
<el-input
|
||||
v-model="actorUsername"
|
||||
placeholder="操作账号"
|
||||
clearable
|
||||
class="filter-control"
|
||||
@keyup.enter="loadAuditLogs()"
|
||||
/>
|
||||
<el-select v-model="action" placeholder="动作" clearable class="filter-control">
|
||||
<el-option
|
||||
v-for="opt in adminAuditActionOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select v-model="targetType" placeholder="目标类型" clearable class="filter-control">
|
||||
<el-option
|
||||
v-for="opt in adminAuditTargetTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="dateFrom"
|
||||
type="date"
|
||||
placeholder="开始日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="filter-control"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="dateTo"
|
||||
type="date"
|
||||
placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="filter-control"
|
||||
/>
|
||||
<div class="filter-buttons">
|
||||
<el-button round @click="resetFilters">重置</el-button>
|
||||
<el-button round type="primary" @click="loadAuditLogs()">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-card shadow="never" class="section-card">
|
||||
<el-table
|
||||
:data="items"
|
||||
stripe
|
||||
size="small"
|
||||
v-loading="loading"
|
||||
element-loading-text="审计日志加载中"
|
||||
class="data-table"
|
||||
>
|
||||
<el-table-column label="时间" width="180">
|
||||
<template #default="{ row }">{{ formatAdminDateTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actorUsername" label="操作人" width="120" />
|
||||
<el-table-column label="角色" width="100">
|
||||
<template #default="{ row }"><AdminStatusTag :status="row.actorRole" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="动作" width="140">
|
||||
<template #default="{ row }">
|
||||
<strong>{{ formatAuditAction(row.action) }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<strong>{{ resolveTargetLabel(row.targetType, row.targetId) }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="摘要" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="payload-text">{{ formatPayload(row.payload) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty :image-size="60" description="暂无审计记录" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-if="pagination.total > 0"
|
||||
v-model:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, prev, pager, next"
|
||||
class="pagination-center"
|
||||
@current-change="loadAuditLogs"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import '../../../styles/admin-list-pages.css';
|
||||
|
||||
.payload-text {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
word-break: break-all;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,122 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import { loginAdmin } from '@/services/admin'
|
||||
import { setAdminSession } from '@/utils/admin-auth'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
|
||||
async function submitLogin() {
|
||||
if (!username.value.trim() || !password.value.trim()) {
|
||||
showError('请输入账号和密码')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await loginAdmin({
|
||||
username: username.value.trim(),
|
||||
password: password.value.trim(),
|
||||
})
|
||||
|
||||
setAdminSession(response.data.token, response.data.expiresAt, response.data.user)
|
||||
showSuccess('登录成功')
|
||||
await router.replace('/admin/dashboard')
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '后台登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="login-page">
|
||||
<section class="login-card">
|
||||
<header class="login-header">
|
||||
<p class="eyebrow">Order Site Admin</p>
|
||||
<h1>运营后台登录</h1>
|
||||
<p>使用账号密码进入后台,并按角色开放不同操作权限。</p>
|
||||
</header>
|
||||
|
||||
<label class="field">
|
||||
<span>账号</span>
|
||||
<el-input v-model="username" placeholder="输入账号" @keyup.enter="submitLogin" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>密码</span>
|
||||
<el-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="输入密码"
|
||||
show-password
|
||||
@keyup.enter="submitLogin"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<el-button :loading="loading" round type="primary" class="submit-btn" @click="submitLogin">
|
||||
登录后台
|
||||
</el-button>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--space-4);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.12), transparent 28%),
|
||||
radial-gradient(circle at bottom right, rgba(59, 130, 246, 0.08), transparent 26%),
|
||||
linear-gradient(160deg, var(--color-primary-light) 0%, #f7fbff 42%, #edf6f3 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: min(460px, 100%);
|
||||
padding: 28px;
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-surface);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
margin: 10px 0 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-top: var(--space-6);
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
</style>
|
||||
-649
@@ -1,649 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminCloudtentaclesDeliveryRecords,
|
||||
fetchAdminCloudtentaclesRecordSources,
|
||||
} from '@/services/admin'
|
||||
import type { AdminCloudtentaclesDeliveryRecordItem } from '@/types/admin'
|
||||
import { getAdminToken } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type RecordSourceOption = {
|
||||
key: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
hasToken: boolean
|
||||
loggedInAt: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const sourceLoading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const sourceKey = ref('')
|
||||
const platformOrderId = ref('')
|
||||
const dateRange = ref<[string, string]>(createDefaultDateRange())
|
||||
const page = ref(1)
|
||||
const pageSize = ref(100)
|
||||
const total = ref(0)
|
||||
const items = ref<AdminCloudtentaclesDeliveryRecordItem[]>([])
|
||||
const sourceOptions = ref<RecordSourceOption[]>([])
|
||||
const previewVisible = ref(false)
|
||||
const previewImageUrl = ref('')
|
||||
const previewFileName = ref('')
|
||||
|
||||
const usableSourceOptions = computed(() =>
|
||||
sourceOptions.value.filter((source) => source.enabled && source.hasToken),
|
||||
)
|
||||
|
||||
const currentSourceLabel = computed(() => {
|
||||
const matched = sourceOptions.value.find((source) => source.key === sourceKey.value)
|
||||
return matched?.label || sourceKey.value || '-'
|
||||
})
|
||||
|
||||
async function loadSources() {
|
||||
sourceLoading.value = true
|
||||
try {
|
||||
const response = await fetchAdminCloudtentaclesRecordSources()
|
||||
sourceOptions.value = response.data.sources || []
|
||||
if (!sourceKey.value) {
|
||||
sourceKey.value = usableSourceOptions.value[0]?.key || sourceOptions.value[0]?.key || ''
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取 cloudtentacles 账号失败'
|
||||
} finally {
|
||||
sourceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecords(nextPage = page.value) {
|
||||
const orderKeyword = platformOrderId.value.trim()
|
||||
if (!sourceKey.value && !orderKeyword) {
|
||||
showError('请先选择 cloudtentacles 账号')
|
||||
return
|
||||
}
|
||||
|
||||
const [startDate, endDate] = dateRange.value || []
|
||||
if (!startDate || !endDate) {
|
||||
showError('请选择查询时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminCloudtentaclesDeliveryRecords({
|
||||
sourceKey: sourceKey.value || undefined,
|
||||
page: nextPage,
|
||||
size: pageSize.value,
|
||||
startDate,
|
||||
endDate,
|
||||
platformOrderId: orderKeyword || undefined,
|
||||
})
|
||||
if (response.data.sourceKey && sourceKey.value !== response.data.sourceKey) {
|
||||
sourceKey.value = response.data.sourceKey
|
||||
}
|
||||
page.value = response.data.page
|
||||
pageSize.value = response.data.size
|
||||
total.value = response.data.total
|
||||
items.value = response.data.items || []
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '查询发货记录失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
platformOrderId.value = ''
|
||||
dateRange.value = createDefaultDateRange()
|
||||
page.value = 1
|
||||
pageSize.value = 100
|
||||
void loadRecords(1)
|
||||
}
|
||||
|
||||
function handlePageChange(nextPage: number) {
|
||||
void loadRecords(nextPage)
|
||||
}
|
||||
|
||||
function handlePageSizeChange(nextSize: number) {
|
||||
pageSize.value = nextSize
|
||||
void loadRecords(1)
|
||||
}
|
||||
|
||||
async function generateRecordImage(record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
try {
|
||||
const dataUrl = await drawRecordImage(record)
|
||||
previewImageUrl.value = dataUrl
|
||||
previewFileName.value = buildRecordImageFileName(record)
|
||||
previewVisible.value = true
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '生成发货记录图片失败')
|
||||
}
|
||||
}
|
||||
|
||||
function downloadPreviewImage() {
|
||||
if (!previewImageUrl.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = previewImageUrl.value
|
||||
link.download = previewFileName.value || 'cloudtentacles-delivery-record.png'
|
||||
link.click()
|
||||
showSuccess('发货记录图片已生成')
|
||||
}
|
||||
|
||||
function getStatusLabel(status: number) {
|
||||
if (status === 4) return '已完成'
|
||||
if (status === 1) return '处理中'
|
||||
if (status === 2) return '待处理'
|
||||
if (status === 3) return '失败'
|
||||
return `状态 ${status || '-'}`
|
||||
}
|
||||
|
||||
function getStatusType(status: number) {
|
||||
if (status === 4) return 'success'
|
||||
if (status === 3) return 'danger'
|
||||
if (status === 1) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function getLocalDispatchStatusLabel(status: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
if (normalized === 'success') return '本地已发货'
|
||||
if (normalized === 'failed') return '本地发货失败'
|
||||
if (normalized === 'pending') return '本地待发货'
|
||||
return normalized || '-'
|
||||
}
|
||||
|
||||
function getMatchConfidenceLabel(value: string) {
|
||||
if (value === 'high') return '高'
|
||||
if (value === 'medium') return '中'
|
||||
if (value === 'low') return '低'
|
||||
if (value === 'local') return '本地'
|
||||
return ''
|
||||
}
|
||||
|
||||
function createDefaultDateRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date(end)
|
||||
start.setDate(start.getDate() - 7)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
return [formatLocalDateTime(start), formatLocalDateTime(end)]
|
||||
}
|
||||
|
||||
function formatLocalDateTime(date: Date) {
|
||||
const pad = (value: number) => String(value).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
|
||||
date.getHours(),
|
||||
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
async function drawRecordImage(record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
const productImage = await loadRecordProductImage(record.skuImage)
|
||||
const canvas = document.createElement('canvas')
|
||||
const scale = window.devicePixelRatio || 1
|
||||
const width = 720
|
||||
const height = 330
|
||||
canvas.width = width * scale
|
||||
canvas.height = height * scale
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
throw new Error('当前浏览器不支持图片生成')
|
||||
}
|
||||
|
||||
ctx.scale(scale, scale)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
ctx.fillStyle = '#1f2937'
|
||||
ctx.font = '20px sans-serif'
|
||||
ctx.fillText('订单详情', 28, 38)
|
||||
ctx.fillStyle = '#9ca3af'
|
||||
ctx.font = '24px sans-serif'
|
||||
ctx.fillText('×', width - 40, 38)
|
||||
|
||||
ctx.fillStyle = '#111827'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(`购买时间:${formatAdminDateTime(record.createdAt)}`, width / 2, 88)
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
drawTicketCard(ctx, record, productImage)
|
||||
drawRecordMeta(ctx, record)
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
function drawTicketCard(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
record: AdminCloudtentaclesDeliveryRecordItem,
|
||||
productImage: HTMLImageElement | null,
|
||||
) {
|
||||
const x = 170
|
||||
const y = 108
|
||||
const width = 392
|
||||
const height = 108
|
||||
const imageWidth = 106
|
||||
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + 18, y)
|
||||
ctx.lineTo(x + width, y)
|
||||
ctx.lineTo(x + width, y + height - 22)
|
||||
ctx.lineTo(x + width - 18, y + height)
|
||||
ctx.lineTo(x, y + height)
|
||||
ctx.lineTo(x, y + 18)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = '#f8fbff'
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = '#2f80d1'
|
||||
ctx.lineWidth = 1
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
|
||||
const gradient = ctx.createLinearGradient(x, y, x + imageWidth, y + height)
|
||||
gradient.addColorStop(0, '#e7f1ff')
|
||||
gradient.addColorStop(1, '#b8d7ff')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(x, y, imageWidth, height)
|
||||
if (productImage) {
|
||||
drawCoverImage(ctx, productImage, x, y, imageWidth, height)
|
||||
} else {
|
||||
ctx.fillStyle = '#355d91'
|
||||
ctx.font = '12px sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText('套装发货记录', x + imageWidth / 2, y + 25)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(x + 34, y + 42, 38, 38)
|
||||
ctx.strokeStyle = '#8ab6ef'
|
||||
ctx.strokeRect(x + 34, y + 42, 38, 38)
|
||||
ctx.fillStyle = '#2f80d1'
|
||||
ctx.font = '20px sans-serif'
|
||||
ctx.fillText('GP', x + imageWidth / 2, y + 69)
|
||||
}
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
const contentX = x + imageWidth + 16
|
||||
const orderText = `订单号:${record.platformOrderId || record.recordId.slice(-8) || '-'}`
|
||||
ctx.fillStyle = '#1d5ea8'
|
||||
ctx.font = '16px sans-serif'
|
||||
drawEllipsisText(ctx, record.name || '未命名商品', contentX, y + 31, 170)
|
||||
ctx.fillStyle = '#3478c6'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.textAlign = 'right'
|
||||
ctx.fillText(orderText, x + width - 12, y + 30)
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
ctx.strokeStyle = '#c8d8ec'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(contentX, y + 42)
|
||||
ctx.lineTo(x + width - 12, y + 42)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.fillStyle = '#f59e0b'
|
||||
ctx.font = '14px sans-serif'
|
||||
ctx.fillText(`购买机会 × ${record.count || 1}`, contentX, y + 66)
|
||||
ctx.fillStyle = '#16a34a'
|
||||
ctx.textAlign = 'right'
|
||||
ctx.fillText(getStatusLabel(record.status), x + width - 12, y + 66)
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
ctx.fillStyle = '#3478c6'
|
||||
ctx.font = '14px sans-serif'
|
||||
drawEllipsisText(ctx, `账号:${record.fulfillUser || record.phone || '-'}`, contentX, y + 90, 250)
|
||||
}
|
||||
|
||||
function drawRecordMeta(ctx: CanvasRenderingContext2D, record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
const left = 80
|
||||
const top = 250
|
||||
const lineHeight = 22
|
||||
const values = [
|
||||
`订单店铺:${record.orderShopName || record.orderShopId || '-'}`,
|
||||
`核销店铺:${record.consumeShopName || record.consumeShopId || '-'}`,
|
||||
`绑定角色:${record.roleName || record.fulfillUser || '-'}${record.roleId ? `(${record.roleId})` : ''}`,
|
||||
]
|
||||
|
||||
ctx.fillStyle = '#374151'
|
||||
ctx.font = '13px sans-serif'
|
||||
values.forEach((value, index) => {
|
||||
drawEllipsisText(ctx, value, left, top + index * lineHeight, 560)
|
||||
})
|
||||
}
|
||||
|
||||
async function loadRecordProductImage(imageUrl: string) {
|
||||
const normalizedUrl = String(imageUrl || '').trim()
|
||||
if (!normalizedUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
const proxyUrl = `/api/v1/admin/cloudtentacles-records/image?url=${encodeURIComponent(
|
||||
normalizedUrl,
|
||||
)}`
|
||||
return loadProtectedImage(proxyUrl).catch(() => null)
|
||||
}
|
||||
|
||||
async function loadProtectedImage(src: string) {
|
||||
const token = getAdminToken()
|
||||
const response = await fetch(src, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('商品图片加载失败')
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(await response.blob())
|
||||
try {
|
||||
return await loadImage(objectUrl)
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
}
|
||||
|
||||
function loadImage(src: string) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image()
|
||||
image.onload = () => resolve(image)
|
||||
image.onerror = () => reject(new Error('商品图片加载失败'))
|
||||
image.src = src
|
||||
})
|
||||
}
|
||||
|
||||
function drawCoverImage(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
image: HTMLImageElement,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
) {
|
||||
const ratio = Math.max(width / image.naturalWidth, height / image.naturalHeight)
|
||||
const drawWidth = image.naturalWidth * ratio
|
||||
const drawHeight = image.naturalHeight * ratio
|
||||
const drawX = x + (width - drawWidth) / 2
|
||||
const drawY = y + (height - drawHeight) / 2
|
||||
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.rect(x, y, width, height)
|
||||
ctx.clip()
|
||||
ctx.drawImage(image, drawX, drawY, drawWidth, drawHeight)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
function drawEllipsisText(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
maxWidth: number,
|
||||
) {
|
||||
let output = text
|
||||
while (output.length > 0 && ctx.measureText(output).width > maxWidth) {
|
||||
output = `${output.slice(0, -2)}…`
|
||||
}
|
||||
ctx.fillText(output || '-', x, y)
|
||||
}
|
||||
|
||||
function buildRecordImageFileName(record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
const name = sanitizeFileName(record.name || '发货记录')
|
||||
const id = sanitizeFileName(
|
||||
record.platformOrderId || record.recordId.slice(0, 8) || String(record.virtualNumberId || 'record'),
|
||||
)
|
||||
return `${name}-${id}.png`
|
||||
}
|
||||
|
||||
function sanitizeFileName(value: string) {
|
||||
return value.replace(/[\\/:*?"<>|]/g, '_').slice(0, 48)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSources()
|
||||
if (sourceKey.value) {
|
||||
await loadRecords(1)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cloud-records-page list-page">
|
||||
<AdminPageHeader title="查询发货记录" description="按订单号、cloudtentacles 账号和时间范围查询平台发货记录。">
|
||||
<template #extra>
|
||||
<span class="total-badge">共 {{ total }} 条记录</span>
|
||||
</template>
|
||||
</AdminPageHeader>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">查询条件</span>
|
||||
<span class="card-desc">输入订单号时会先关联本地任务,再匹配 cloudtentacles 发货记录。</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="filter-grid cloud-records-filter">
|
||||
<el-select
|
||||
v-model="sourceKey"
|
||||
class="filter-control"
|
||||
placeholder="cloudtentacles 账号"
|
||||
:loading="sourceLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="source in sourceOptions"
|
||||
:key="source.key"
|
||||
:label="`${source.label || source.key}${source.hasToken ? '' : ' · 未登录'}`"
|
||||
:value="source.key"
|
||||
:disabled="!source.enabled || !source.hasToken"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model.trim="platformOrderId"
|
||||
class="filter-control"
|
||||
clearable
|
||||
placeholder="订单号"
|
||||
@keyup.enter="loadRecords(1)"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
class="filter-control cloud-records-date-range"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
<div class="filter-buttons">
|
||||
<el-button round @click="resetFilters">重置</el-button>
|
||||
<el-button round type="primary" :loading="loading" @click="loadRecords(1)">
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<el-card
|
||||
shadow="never"
|
||||
class="section-card"
|
||||
v-loading="loading"
|
||||
element-loading-text="发货记录加载中"
|
||||
>
|
||||
<template v-if="items.length === 0 && !loading">
|
||||
<el-empty description="当前筛选条件下暂无发货记录" :image-size="60" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="table-toolbar">
|
||||
<strong>发货记录</strong>
|
||||
<span>账号 {{ currentSourceLabel }} · 第 {{ page }} 页,当前展示 {{ items.length }} 条</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="items" stripe size="small" class="data-table cloud-records-table">
|
||||
<el-table-column label="商品 / 角色" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="cell-title" :title="row.name">{{ row.name || '-' }}</span>
|
||||
<span class="cell-subline" :title="row.fulfillUser">
|
||||
账号:{{ row.fulfillUser || '-' }}
|
||||
</span>
|
||||
<span v-if="row.roleName || row.roleId" class="cell-subline" :title="`${row.roleName} ${row.roleId}`">
|
||||
角色:{{ row.roleName || '-' }}{{ row.roleId ? `(${row.roleId})` : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单 / 店铺" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="cell-title" :title="row.platformOrderId">
|
||||
{{ row.platformOrderId || '-' }}
|
||||
</span>
|
||||
<span class="cell-subline" :title="row.orderShopName || row.orderShopId">
|
||||
订单店铺:{{ row.orderShopName || row.orderShopId || '-' }}
|
||||
</span>
|
||||
<span class="cell-subline" :title="row.consumeShopName || row.consumeShopId">
|
||||
核销店铺:{{ row.consumeShopName || row.consumeShopId || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="虚拟号" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="cell-title">{{ row.phone || '-' }}</span>
|
||||
<span class="cell-subline">VN {{ row.virtualNumberId || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<div class="status-stack">
|
||||
<el-tag :type="getStatusType(row.status)" effect="plain">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
<span v-if="row.localDispatchStatus" class="cell-subline">
|
||||
{{ getLocalDispatchStatusLabel(row.localDispatchStatus) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" min-width="120">
|
||||
<template #default="{ row }">{{ row.operatorName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发货时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatAdminDateTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="记录 ID" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="cell-subline" :title="row.recordId">{{ row.recordId || '-' }}</span>
|
||||
<span v-if="row.taskNo" class="cell-subline" :title="row.taskNo">
|
||||
任务:{{ row.taskNo }}
|
||||
<template v-if="getMatchConfidenceLabel(row.matchConfidence)">
|
||||
· 匹配{{ getMatchConfidenceLabel(row.matchConfidence) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="136" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" plain @click="generateRecordImage(row)">
|
||||
生成图片
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="cloud-records-pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="total"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[100, 200, 500, 1000]"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="previewVisible" title="发货记录图片" width="760px" destroy-on-close>
|
||||
<div class="record-preview">
|
||||
<img v-if="previewImageUrl" :src="previewImageUrl" alt="发货记录图片预览" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="previewVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="downloadPreviewImage">下载图片</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import '@/styles/admin-list-pages.css';
|
||||
|
||||
.cloud-records-filter {
|
||||
grid-template-columns: minmax(180px, 0.28fr) minmax(220px, 0.36fr) minmax(360px, 1fr) auto;
|
||||
}
|
||||
|
||||
.cloud-records-date-range {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cloud-records-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.record-preview {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 260px;
|
||||
padding: var(--space-3);
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.record-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--border-default);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.status-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.cloud-records-filter {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cloud-records-pagination {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,91 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
|
||||
import { fetchAdminDashboardSummary } from '@/services/admin'
|
||||
import type { AdminDashboardSummary } from '@/types/admin'
|
||||
|
||||
const loading = ref(true)
|
||||
const summary = ref<AdminDashboardSummary | null>(null)
|
||||
const errorMessage = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await fetchAdminDashboardSummary()
|
||||
summary.value = response.data
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取概览失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const cards = [
|
||||
{ key: 'todayOrders', label: '今日订单' },
|
||||
{ key: 'paidPendingClaim', label: '待领取' },
|
||||
{ key: 'claimingTasks', label: '领取中' },
|
||||
{ key: 'redeemedToday', label: '今日成功' },
|
||||
{ key: 'abnormalTasks', label: '异常任务' },
|
||||
] as const
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<AdminPageHeader title="系统概览" description="快速查看订单和交付链路的运行状态。" />
|
||||
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<el-skeleton v-if="loading" :rows="12" animated class="mt-4" />
|
||||
|
||||
<div v-else class="summary-grid">
|
||||
<el-card v-for="card in cards" :key="card.key" shadow="never" class="stat-card">
|
||||
<span class="stat-label">{{ card.label }}</span>
|
||||
<strong class="stat-value">{{ summary?.[card.key] ?? 0 }}</strong>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-page {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: var(--text-3xl);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import AdminKuaishouCloudFulfillmentView from './kuaishou-cloud/AdminKuaishouCloudFulfillmentView.vue'
|
||||
import AdminKuaishouFeifeiFulfillmentView from './kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue'
|
||||
|
||||
const activeFulfillmentTab = ref('kuaishou-feifei')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hub-panel">
|
||||
<AdminPageHeader
|
||||
title="履约配置中心"
|
||||
description="维护 91 卡券快手商品到 Cloud 与 kuaishou-feifei 的履约映射规则。"
|
||||
/>
|
||||
|
||||
<el-tabs v-model="activeFulfillmentTab" class="fulfillment-tabs">
|
||||
<el-tab-pane label="kuaishou-feifei" name="kuaishou-feifei">
|
||||
<AdminKuaishouFeifeiFulfillmentView />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="kuaishou-cloud" name="kuaishou-cloud">
|
||||
<AdminKuaishouCloudFulfillmentView />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hub-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.fulfillment-tabs {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
-271
@@ -1,271 +0,0 @@
|
||||
.admin-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.fulfillment-simple-page {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fulfillment-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) minmax(120px, auto);
|
||||
gap: var(--space-3);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.metric-card span {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.metric-card strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-2xl);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.overview-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fulfillment-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
gap: var(--space-4);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.source-panel,
|
||||
.catalog-panel {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.source-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.catalog-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.section-head h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-head p {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.source-list {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.source-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-surface);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.source-item:hover,
|
||||
.source-item--active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.source-item span {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.source-item strong,
|
||||
.source-item small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.source-item strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.source-item small {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.catalog-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.catalog-toolbar-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.match-checker {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 1fr) auto;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.sku-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sku-list-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
padding-top: var(--space-2);
|
||||
border-top: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.sku-name-cell {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sku-name-cell strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sku-name-cell small {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.match-name {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-primary);
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.override-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
min-width: 0;
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.override-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.override-rule-list {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.override-rule-item {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.override-rule-head {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(220px, 1fr) minmax(160px, 220px) auto;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.delivery-item-list {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.delivery-item-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1.1fr) minmax(180px, 1fr) 120px auto;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.fulfillment-overview,
|
||||
.fulfillment-layout,
|
||||
.match-checker,
|
||||
.override-rule-head,
|
||||
.delivery-item-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.overview-actions,
|
||||
.catalog-toolbar,
|
||||
.catalog-toolbar-actions,
|
||||
.override-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.catalog-toolbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
-545
@@ -1,545 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminCloudtentaclesOverrideRules,
|
||||
fetchAdminCloudtentaclesSkuList,
|
||||
fetchAdminCloudtentaclesSourceConfig,
|
||||
saveAdminCloudtentaclesOverrideRules,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesSessionItem,
|
||||
AdminCloudtentaclesSkuItem,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
} from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
|
||||
type MatchState = {
|
||||
input: string
|
||||
}
|
||||
|
||||
type EditableOverrideRule = Omit<AdminCloudtentaclesOverrideRule, 'normalizedProductName'> & {
|
||||
normalizedProductName?: string
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const skuLoading = ref(false)
|
||||
const overrideLoading = ref(false)
|
||||
const overrideSaving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const skuErrorMessage = ref('')
|
||||
const overrideErrorMessage = ref('')
|
||||
const selectedSourceKey = ref('')
|
||||
const sources = ref<AdminCloudtentaclesSourceItem[]>([])
|
||||
const sessions = ref<AdminCloudtentaclesSessionsMap>({})
|
||||
const skuItems = ref<AdminCloudtentaclesSkuItem[]>([])
|
||||
const overrideEnabled = ref(true)
|
||||
const overrideRules = ref<EditableOverrideRule[]>([])
|
||||
const matchState = reactive<MatchState>({
|
||||
input: '',
|
||||
})
|
||||
|
||||
const sourceRows = computed(() =>
|
||||
sources.value.map((source) => {
|
||||
const session = resolveSourceSession(source.key)
|
||||
return {
|
||||
...source,
|
||||
session,
|
||||
ready: source.enabled !== false && Boolean(session?.hasToken),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const readySources = computed(() => sourceRows.value.filter((source) => source.ready))
|
||||
const selectedSource = computed(
|
||||
() => sourceRows.value.find((source) => source.key === selectedSourceKey.value) || null,
|
||||
)
|
||||
|
||||
const metrics = computed(() => ({
|
||||
sourceCount: sources.value.length,
|
||||
readySourceCount: readySources.value.length,
|
||||
skuCount: skuItems.value.length,
|
||||
inventoryTotal: skuItems.value.reduce((sum, item) => sum + Math.max(0, Number(item.inventory || 0)), 0),
|
||||
}))
|
||||
|
||||
const matchedSku = computed(() => {
|
||||
const normalizedInput = normalizeMatchName(matchState.input)
|
||||
if (!normalizedInput) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
skuItems.value.find((item) => String(item.name || '').trim() === matchState.input.trim()) ||
|
||||
skuItems.value.find((item) => normalizeMatchName(item.name) === normalizedInput) ||
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
const overrideRuleCount = computed(() => overrideRules.value.length)
|
||||
|
||||
watch(selectedSourceKey, (sourceKey) => {
|
||||
if (sourceKey) {
|
||||
void loadSkuList(sourceKey)
|
||||
} else {
|
||||
skuItems.value = []
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(loadPage)
|
||||
|
||||
async function loadPage() {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const [sourceResponse, overrideResponse] = await Promise.all([
|
||||
fetchAdminCloudtentaclesSourceConfig(),
|
||||
fetchAdminCloudtentaclesOverrideRules(),
|
||||
])
|
||||
sources.value = Array.isArray(sourceResponse.data.sources) ? sourceResponse.data.sources : []
|
||||
sessions.value = sourceResponse.data.sessions || {}
|
||||
overrideEnabled.value = overrideResponse.data.enabled !== false
|
||||
overrideRules.value = Array.isArray(overrideResponse.data.rules)
|
||||
? overrideResponse.data.rules.map((rule) => normalizeEditableRule(rule))
|
||||
: []
|
||||
selectedSourceKey.value = resolveDefaultSourceKey()
|
||||
|
||||
if (!selectedSourceKey.value) {
|
||||
skuItems.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取 cloudtentacles 账号失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOverrideRules() {
|
||||
overrideSaving.value = true
|
||||
overrideErrorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await saveAdminCloudtentaclesOverrideRules({
|
||||
enabled: overrideEnabled.value,
|
||||
rules: overrideRules.value.map((rule) => ({
|
||||
id: rule.id,
|
||||
enabled: rule.enabled,
|
||||
productName: rule.productName,
|
||||
sourceKey: rule.sourceKey,
|
||||
deliveryItems: rule.deliveryItems,
|
||||
notes: rule.notes,
|
||||
})),
|
||||
})
|
||||
overrideEnabled.value = response.data.enabled !== false
|
||||
overrideRules.value = Array.isArray(response.data.rules)
|
||||
? response.data.rules.map((rule) => normalizeEditableRule(rule))
|
||||
: []
|
||||
showSuccess(`商品覆盖规则已保存,共 ${overrideRules.value.length} 条`)
|
||||
} catch (error) {
|
||||
overrideErrorMessage.value = error instanceof Error ? error.message : '保存 cloudtentacles 覆盖规则失败'
|
||||
showError(overrideErrorMessage.value)
|
||||
} finally {
|
||||
overrideSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkuList(sourceKey = selectedSourceKey.value) {
|
||||
if (!sourceKey) {
|
||||
skuItems.value = []
|
||||
return
|
||||
}
|
||||
|
||||
skuLoading.value = true
|
||||
skuErrorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminCloudtentaclesSkuList({ sourceKey })
|
||||
skuItems.value = Array.isArray(response.data.items) ? response.data.items : []
|
||||
} catch (error) {
|
||||
skuItems.value = []
|
||||
skuErrorMessage.value = error instanceof Error ? error.message : '读取 cloudtentacles 商品失败'
|
||||
} finally {
|
||||
skuLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
void loadPage()
|
||||
}
|
||||
|
||||
function addOverrideRule() {
|
||||
overrideRules.value.unshift({
|
||||
id: createRuleId(),
|
||||
enabled: true,
|
||||
productName: matchState.input.trim(),
|
||||
normalizedProductName: normalizeMatchName(matchState.input),
|
||||
sourceKey: selectedSourceKey.value,
|
||||
deliveryItems: [createDeliveryItem()],
|
||||
notes: '',
|
||||
})
|
||||
}
|
||||
|
||||
function removeOverrideRule(index: number) {
|
||||
overrideRules.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function addDeliveryItem(rule: EditableOverrideRule) {
|
||||
rule.deliveryItems.push(createDeliveryItem())
|
||||
}
|
||||
|
||||
function removeDeliveryItem(rule: EditableOverrideRule, index: number) {
|
||||
rule.deliveryItems.splice(index, 1)
|
||||
if (rule.deliveryItems.length === 0) {
|
||||
rule.deliveryItems.push(createDeliveryItem())
|
||||
}
|
||||
}
|
||||
|
||||
function handleRuleSourceChange(sourceKey: string) {
|
||||
if (sourceKey) {
|
||||
selectedSourceKey.value = sourceKey
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeliverySkuChange(item: AdminCloudtentaclesOverrideDeliveryItem, skuId: number) {
|
||||
const sku = skuItems.value.find((current) => Number(current.id || 0) === Number(skuId || 0))
|
||||
item.cloudSkuId = Number(skuId || 0) || 0
|
||||
item.cloudSkuName = String(sku?.name || item.cloudSkuName || '').trim()
|
||||
}
|
||||
|
||||
function resolveDefaultSourceKey() {
|
||||
const current = sourceRows.value.find((source) => source.key === selectedSourceKey.value)
|
||||
if (current?.ready) {
|
||||
return current.key
|
||||
}
|
||||
|
||||
return readySources.value[0]?.key || sourceRows.value[0]?.key || ''
|
||||
}
|
||||
|
||||
function resolveSourceSession(sourceKey: string): AdminCloudtentaclesSessionItem | null {
|
||||
return sessions.value[sourceKey] || null
|
||||
}
|
||||
|
||||
function normalizeMatchName(value: unknown) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[【】\[\]()()]/g, ' ')
|
||||
.replace(/(自动发货|秒发|极速发货|官方直充|官方充值)/gi, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function formatSourceLabel(source: AdminCloudtentaclesSourceItem) {
|
||||
return source.label || source.username || source.key
|
||||
}
|
||||
|
||||
function formatCloudPrice(value: unknown) {
|
||||
const price = Number(value)
|
||||
return Number.isFinite(price) ? String(price) : '-'
|
||||
}
|
||||
|
||||
function formatSkuOption(sku: AdminCloudtentaclesSkuItem) {
|
||||
return `#${sku.id} ${sku.name}`
|
||||
}
|
||||
|
||||
function getSourceTagType(source: { ready: boolean; enabled?: boolean }) {
|
||||
if (source.ready) return 'success'
|
||||
if (source.enabled === false) return 'info'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function getSourceStatusText(source: { ready: boolean; enabled?: boolean; session: AdminCloudtentaclesSessionItem | null }) {
|
||||
if (source.ready) return '可用'
|
||||
if (source.enabled === false) return '已停用'
|
||||
if (!source.session?.hasToken) return '未登录'
|
||||
return '待检查'
|
||||
}
|
||||
|
||||
function normalizeEditableRule(rule: AdminCloudtentaclesOverrideRule): EditableOverrideRule {
|
||||
return {
|
||||
id: String(rule.id || createRuleId()).trim(),
|
||||
enabled: rule.enabled !== false,
|
||||
productName: String(rule.productName || '').trim(),
|
||||
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||
sourceKey: String(rule.sourceKey || '').trim(),
|
||||
deliveryItems: Array.isArray(rule.deliveryItems) && rule.deliveryItems.length > 0
|
||||
? rule.deliveryItems.map((item) => ({
|
||||
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
|
||||
cloudSkuName: String(item.cloudSkuName || '').trim(),
|
||||
quantity: Math.max(1, Number(item.quantity || 1) || 1),
|
||||
}))
|
||||
: [createDeliveryItem()],
|
||||
notes: String(rule.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function createDeliveryItem(): AdminCloudtentaclesOverrideDeliveryItem {
|
||||
const firstSku = skuItems.value[0]
|
||||
return {
|
||||
cloudSkuId: Number(firstSku?.id || 0) || 0,
|
||||
cloudSkuName: String(firstSku?.name || '').trim(),
|
||||
quantity: 1,
|
||||
}
|
||||
}
|
||||
|
||||
function createRuleId() {
|
||||
return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-panel fulfillment-simple-page">
|
||||
<el-result v-if="!hasAdminRole('admin')" icon="warning" title="仅管理员可以查看履约配置" />
|
||||
|
||||
<template v-else>
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<el-skeleton v-if="loading" :rows="10" animated />
|
||||
|
||||
<template v-else>
|
||||
<section class="fulfillment-overview">
|
||||
<div class="metric-card">
|
||||
<span>履约账号</span>
|
||||
<strong>{{ metrics.readySourceCount }} / {{ metrics.sourceCount }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>可发商品</span>
|
||||
<strong>{{ metrics.skuCount }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>总库存</span>
|
||||
<strong>{{ metrics.inventoryTotal }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>覆盖规则</span>
|
||||
<strong>{{ overrideRuleCount }}</strong>
|
||||
</div>
|
||||
<div class="overview-actions">
|
||||
<el-button :loading="loading || skuLoading" @click="refreshAll">刷新</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="fulfillment-layout">
|
||||
<aside class="source-panel">
|
||||
<div class="section-head">
|
||||
<h3>cloudtentacles 账号</h3>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="sourceRows.length === 0" description="暂无账号,请先到平台店铺配置登录。" />
|
||||
|
||||
<div v-else class="source-list">
|
||||
<button
|
||||
v-for="source in sourceRows"
|
||||
:key="source.key"
|
||||
type="button"
|
||||
class="source-item"
|
||||
:class="{ 'source-item--active': source.key === selectedSourceKey }"
|
||||
@click="selectedSourceKey = source.key"
|
||||
>
|
||||
<span>
|
||||
<strong>{{ formatSourceLabel(source) }}</strong>
|
||||
<small>{{ source.username || source.key }}</small>
|
||||
</span>
|
||||
<el-tag :type="getSourceTagType(source)" effect="light">
|
||||
{{ getSourceStatusText(source) }}
|
||||
</el-tag>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="catalog-panel">
|
||||
<div class="catalog-toolbar">
|
||||
<div class="section-head">
|
||||
<h3>匹配状态与覆盖规则</h3>
|
||||
<p v-if="selectedSource">
|
||||
当前账号:{{ formatSourceLabel(selectedSource) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="catalog-toolbar-actions">
|
||||
<el-button :loading="skuLoading" :disabled="!selectedSourceKey" @click="loadSkuList()">
|
||||
刷新商品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="skuErrorMessage"
|
||||
:title="skuErrorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<div class="match-checker">
|
||||
<el-input
|
||||
v-model="matchState.input"
|
||||
clearable
|
||||
placeholder="输入 91 卡券 productNo / 商品名字,查看会命中哪个 cloudtentacles 商品"
|
||||
/>
|
||||
<el-tag v-if="matchedSku" type="success" effect="light">
|
||||
命中 #{{ matchedSku.id }} {{ matchedSku.name }}
|
||||
</el-tag>
|
||||
<el-tag v-else-if="matchState.input.trim()" type="warning" effect="light">
|
||||
未命中
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<section class="override-panel">
|
||||
<div class="catalog-toolbar">
|
||||
<div class="section-head">
|
||||
<h3>商品覆盖规则</h3>
|
||||
<p>套装、多数量商品会优先按这里发货;没有规则时继续同名匹配。</p>
|
||||
</div>
|
||||
<div class="override-actions">
|
||||
<el-switch v-model="overrideEnabled" active-text="启用" inactive-text="停用" />
|
||||
<el-button @click="addOverrideRule">新增规则</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="overrideLoading || overrideSaving"
|
||||
@click="saveOverrideRules"
|
||||
>
|
||||
保存规则
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="overrideErrorMessage"
|
||||
:title="overrideErrorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<el-empty v-if="overrideRules.length === 0" description="暂无覆盖规则" />
|
||||
|
||||
<div v-else class="override-rule-list">
|
||||
<article
|
||||
v-for="(rule, ruleIndex) in overrideRules"
|
||||
:key="rule.id"
|
||||
class="override-rule-item"
|
||||
>
|
||||
<div class="override-rule-head">
|
||||
<el-switch v-model="rule.enabled" active-text="启用" inactive-text="停用" />
|
||||
<el-input v-model="rule.productName" placeholder="91 商品名 / productNo" clearable />
|
||||
<el-select
|
||||
v-model="rule.sourceKey"
|
||||
clearable
|
||||
placeholder="全部账号"
|
||||
@change="handleRuleSourceChange"
|
||||
>
|
||||
<el-option label="全部账号" value="" />
|
||||
<el-option
|
||||
v-for="source in sourceRows"
|
||||
:key="source.key"
|
||||
:label="formatSourceLabel(source)"
|
||||
:value="source.key"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button type="danger" plain @click="removeOverrideRule(ruleIndex)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="delivery-item-list">
|
||||
<div
|
||||
v-for="(item, itemIndex) in rule.deliveryItems"
|
||||
:key="`${rule.id}-${itemIndex}`"
|
||||
class="delivery-item-row"
|
||||
>
|
||||
<el-select
|
||||
v-model="item.cloudSkuId"
|
||||
filterable
|
||||
placeholder="选择 cloudtentacles 商品"
|
||||
@change="(value: number) => handleDeliverySkuChange(item, value)"
|
||||
>
|
||||
<el-option
|
||||
v-for="sku in skuItems"
|
||||
:key="sku.id"
|
||||
:label="formatSkuOption(sku)"
|
||||
:value="sku.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-model="item.cloudSkuName" placeholder="cloudtentacles 商品名" clearable />
|
||||
<el-input-number v-model="item.quantity" :min="1" :max="999" controls-position="right" />
|
||||
<el-button plain @click="removeDeliveryItem(rule, itemIndex)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button plain @click="addDeliveryItem(rule)">增加发货商品</el-button>
|
||||
</div>
|
||||
|
||||
<el-input v-model="rule.notes" placeholder="备注" clearable />
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sku-list-panel">
|
||||
<div class="section-head">
|
||||
<h3>cloudtentacles 商品列表</h3>
|
||||
<p>用于核对商品名、库存和覆盖规则里的发货商品。</p>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="skuLoading"
|
||||
:data="skuItems"
|
||||
height="420"
|
||||
border
|
||||
stripe
|
||||
class="sku-table"
|
||||
empty-text="当前账号暂无可展示商品"
|
||||
>
|
||||
<el-table-column label="cloudtentacles 商品" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="sku-name-cell">
|
||||
<strong>{{ row.name || '-' }}</strong>
|
||||
<small>#{{ row.id }} · {{ row.description || row.name || '-' }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="91 自动匹配名" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<code class="match-name">{{ row.name || '-' }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" width="120">
|
||||
<template #default="{ row }">{{ formatCloudPrice(row.price) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" prop="inventory" width="120" sortable />
|
||||
<el-table-column label="发货限制" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.buyLimitMin || 1 }} - {{ row.buyLimitMax || 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="Number(row.inventory || 0) > 0 ? 'success' : 'danger'" effect="light">
|
||||
{{ Number(row.inventory || 0) > 0 ? '可履约' : '无库存' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</main>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped src="./AdminKuaishouCloudFulfillment.css"></style>
|
||||
-349
@@ -1,349 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RefreshRight, Search } from '@element-plus/icons-vue'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminKuaishouFeifeiConfig,
|
||||
matchAdminKuaishouFeifeiProduct,
|
||||
syncAdminKuaishouFeifeiProducts,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
} from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
|
||||
const loading = ref(true)
|
||||
const syncing = ref(false)
|
||||
const matching = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const filePath = ref('')
|
||||
const matchInput = ref('')
|
||||
const matchResult = ref<AdminKuaishouFeifeiMatchResult | null>(null)
|
||||
const rules = ref<AdminKuaishouFeifeiProductRule[]>([])
|
||||
const lastSyncText = ref('')
|
||||
|
||||
const enabledRuleCount = computed(() => rules.value.filter((rule) => rule.enabled !== false).length)
|
||||
const mappingCount = computed(() =>
|
||||
rules.value.filter((rule) => rule.productName.trim() && rule.productCode.trim()).length,
|
||||
)
|
||||
const metrics = computed(() => [
|
||||
{
|
||||
label: '名字映射',
|
||||
value: String(mappingCount.value),
|
||||
detail: `启用 ${enabledRuleCount.value} 条`,
|
||||
},
|
||||
{
|
||||
label: '匹配方式',
|
||||
value: '同名',
|
||||
detail: '使用 91 商品名规范化匹配',
|
||||
},
|
||||
{
|
||||
label: '配置文件',
|
||||
value: filePath.value ? 'GUI' : '默认',
|
||||
detail: filePath.value || 'data/kuaishou-feifei-config.json',
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(loadConfig)
|
||||
|
||||
async function loadConfig() {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminKuaishouFeifeiConfig()
|
||||
hydrateConfig(response.data)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 履约映射失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function syncProducts() {
|
||||
syncing.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await syncAdminKuaishouFeifeiProducts({
|
||||
status: 'on_sale',
|
||||
})
|
||||
hydrateConfig(response.data)
|
||||
lastSyncText.value = `商品 ${response.data.sync.productCount} 个,映射 ${response.data.sync.ruleCount} 条`
|
||||
showSuccess(`kuaishou-feifei 商品映射已同步,${lastSyncText.value}`)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '同步 kuaishou-feifei 商品映射失败'
|
||||
showError(errorMessage.value)
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function previewMatch() {
|
||||
const productName = matchInput.value.trim()
|
||||
if (!productName) {
|
||||
showError('请输入 91 商品名')
|
||||
return
|
||||
}
|
||||
|
||||
matching.value = true
|
||||
matchResult.value = null
|
||||
|
||||
try {
|
||||
const response = await matchAdminKuaishouFeifeiProduct(productName)
|
||||
matchResult.value = response.data
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '预览 kuaishou-feifei 映射失败')
|
||||
} finally {
|
||||
matching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
|
||||
filePath.value = data.filePath || ''
|
||||
rules.value = Array.isArray(data.source.productRules)
|
||||
? data.source.productRules.map((rule) => normalizeRule(rule))
|
||||
: []
|
||||
}
|
||||
|
||||
function normalizeRule(rule: Partial<AdminKuaishouFeifeiProductRule>): AdminKuaishouFeifeiProductRule {
|
||||
const productName = String(rule.productName || '').trim()
|
||||
const productCode = String(rule.productCode || '').trim()
|
||||
|
||||
return {
|
||||
id: String(rule.id || productName || productCode).trim(),
|
||||
enabled: rule.enabled !== false,
|
||||
productName,
|
||||
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||
productCode,
|
||||
skuName: String(rule.skuName || productName || productCode).trim(),
|
||||
notes: String(rule.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-loading="loading" class="feifei-fulfillment-page">
|
||||
<el-result v-if="!hasAdminRole('admin')" icon="warning" title="仅管理员可以查看履约配置" />
|
||||
|
||||
<template v-else>
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
:title="errorMessage"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<section class="rule-overview">
|
||||
<div v-for="metric in metrics" :key="metric.label" class="metric-card">
|
||||
<span>{{ metric.label }}</span>
|
||||
<strong>{{ metric.value }}</strong>
|
||||
<small>{{ metric.detail }}</small>
|
||||
</div>
|
||||
<div class="overview-actions">
|
||||
<el-button :icon="RefreshRight" :loading="loading" @click="loadConfig">刷新</el-button>
|
||||
<el-button type="primary" :icon="RefreshRight" :loading="syncing" @click="syncProducts">
|
||||
同步商品映射
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rule-panel">
|
||||
<div class="section-head rule-head-title">
|
||||
<div>
|
||||
<h3>商品名字映射</h3>
|
||||
<p>{{ lastSyncText || 'feifei 商品名会按规范化名字匹配 91 商品名。' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="match-checker">
|
||||
<el-input
|
||||
v-model="matchInput"
|
||||
clearable
|
||||
placeholder="91 商品名,例如 套装-Alan Walker"
|
||||
@keyup.enter="previewMatch"
|
||||
/>
|
||||
<el-button :icon="Search" :loading="matching" @click="previewMatch">预览命中</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="matchResult" class="match-result" :class="{ 'is-matched': matchResult.matched }">
|
||||
<strong>{{ matchResult.matched ? '已命中' : '未命中' }}</strong>
|
||||
<span v-if="matchResult.match">
|
||||
{{ matchResult.match.productName }} -> {{ matchResult.match.productCode }}
|
||||
</span>
|
||||
<span v-else>{{ matchResult.normalizedProductName || matchResult.productName }}</span>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="rules.length === 0" description="暂无 kuaishou-feifei 商品映射" />
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
:data="rules"
|
||||
border
|
||||
stripe
|
||||
height="520"
|
||||
class="rule-table"
|
||||
>
|
||||
<el-table-column label="feifei 商品名" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="product-cell">
|
||||
<strong>{{ row.productName || row.skuName || '-' }}</strong>
|
||||
<small>{{ row.normalizedProductName || '-' }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="product_code" prop="productCode" min-width="150" />
|
||||
<el-table-column label="展示名称" prop="skuName" min-width="220" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled !== false ? 'success' : 'info'" effect="light">
|
||||
{{ row.enabled !== false ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源" prop="notes" min-width="180" />
|
||||
</el-table>
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feifei-fulfillment-page {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rule-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(190px, auto);
|
||||
gap: var(--space-3);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.metric-card,
|
||||
.rule-panel {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.metric-card span,
|
||||
.metric-card small,
|
||||
.section-head p,
|
||||
.product-cell small {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.metric-card strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-2xl);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.overview-actions,
|
||||
.rule-head-title,
|
||||
.match-checker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.overview-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.rule-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.section-head h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-head p {
|
||||
margin: 0;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.rule-head-title {
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.match-checker {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.match-result {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.match-result.is-matched {
|
||||
color: var(--el-color-success);
|
||||
background: var(--el-color-success-light-9);
|
||||
}
|
||||
|
||||
.rule-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-cell {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-cell strong,
|
||||
.product-cell small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.rule-overview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.overview-actions,
|
||||
.rule-head-title,
|
||||
.match-checker {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,450 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
Bell,
|
||||
Connection,
|
||||
Monitor,
|
||||
DataAnalysis,
|
||||
Document,
|
||||
Expand,
|
||||
Fold,
|
||||
List,
|
||||
Memo,
|
||||
Setting,
|
||||
SwitchButton,
|
||||
Tickets,
|
||||
User,
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
import { showSuccess } from '@/lib/feedback'
|
||||
import { logoutAdmin } from '@/services/admin'
|
||||
import {
|
||||
clearAdminSession,
|
||||
getAdminRole,
|
||||
getAdminTokenExpiresAt,
|
||||
getAdminUsername,
|
||||
} from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const sidebarStorageKey = 'admin-sidebar-collapsed'
|
||||
const sidebarMenuOpenedsStorageKey = 'admin-sidebar-menu-openeds'
|
||||
const isSidebarCollapsed = ref(
|
||||
typeof window !== 'undefined' && window.localStorage.getItem(sidebarStorageKey) === '1',
|
||||
)
|
||||
const sidebarMenuOpeneds = ref(readSidebarMenuOpeneds())
|
||||
|
||||
const currentRole = computed(() => getAdminRole())
|
||||
const isAdmin = computed(() => currentRole.value === 'admin')
|
||||
|
||||
const roleLabel = computed(() => {
|
||||
if (currentRole.value === 'admin') return '管理员'
|
||||
if (currentRole.value === 'support') return '客服'
|
||||
return '普通运营'
|
||||
})
|
||||
|
||||
type AdminNavItem = {
|
||||
to: string
|
||||
label: string
|
||||
icon: typeof DataAnalysis
|
||||
children?: AdminNavItem[]
|
||||
}
|
||||
|
||||
const platformConfigNavItems = computed<AdminNavItem[]>(() => {
|
||||
return [
|
||||
{ to: '/admin/platform-shops?tab=ninetyone', label: '91卡券', icon: Connection },
|
||||
{ to: '/admin/platform-shops?tab=notifications', label: '内部通知', icon: Bell },
|
||||
{ to: '/admin/platform-shops?tab=kuaishouEticket', label: '快手小店核销', icon: Tickets },
|
||||
{ to: '/admin/platform-shops?tab=kuaishouFeifei', label: 'kuaishou-feifei', icon: Tickets },
|
||||
{
|
||||
to: '/admin/platform-shops?tab=cloudtentacles',
|
||||
label: 'cloudtentacles',
|
||||
icon: Setting,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const navItems = computed(() => {
|
||||
const items: AdminNavItem[] = [
|
||||
{ to: '/admin/dashboard', label: '概览', icon: DataAnalysis },
|
||||
]
|
||||
|
||||
if (isAdmin.value) {
|
||||
items.push({ to: '/admin/users', label: '用户', icon: User })
|
||||
}
|
||||
|
||||
items.push(
|
||||
{ to: '/admin/orders', label: '订单', icon: Document },
|
||||
{ to: '/admin/tasks', label: '任务', icon: List },
|
||||
{ to: '/admin/cloudtentacles-records', label: '查询发货记录', icon: Memo },
|
||||
)
|
||||
|
||||
if (isAdmin.value) {
|
||||
items.push(
|
||||
{
|
||||
to: '/admin/platform-shops',
|
||||
label: '平台配置',
|
||||
icon: Setting,
|
||||
children: platformConfigNavItems.value,
|
||||
},
|
||||
{ to: '/admin/platform-fulfillment', label: '履约配置', icon: Tickets },
|
||||
{ to: '/admin/audit-logs', label: '审计', icon: Monitor },
|
||||
)
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
const activeNavIndex = computed(() => {
|
||||
if (route.path === '/admin/platform-shops') {
|
||||
const tab = typeof route.query.tab === 'string' ? route.query.tab : 'ninetyone'
|
||||
return `/admin/platform-shops?tab=${tab}`
|
||||
}
|
||||
|
||||
return route.path
|
||||
})
|
||||
|
||||
function toggleSidebarCollapse() {
|
||||
isSidebarCollapsed.value = !isSidebarCollapsed.value
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(sidebarStorageKey, isSidebarCollapsed.value ? '1' : '0')
|
||||
}
|
||||
}
|
||||
|
||||
function handleSidebarMenuOpen(index: string) {
|
||||
const normalizedIndex = String(index || '').trim()
|
||||
if (!normalizedIndex) return
|
||||
|
||||
sidebarMenuOpeneds.value = Array.from(new Set([...sidebarMenuOpeneds.value, normalizedIndex]))
|
||||
writeSidebarMenuOpeneds(sidebarMenuOpeneds.value)
|
||||
}
|
||||
|
||||
function handleSidebarMenuClose(index: string) {
|
||||
const normalizedIndex = String(index || '').trim()
|
||||
sidebarMenuOpeneds.value = sidebarMenuOpeneds.value.filter((item) => item !== normalizedIndex)
|
||||
writeSidebarMenuOpeneds(sidebarMenuOpeneds.value)
|
||||
}
|
||||
|
||||
async function submitLogout() {
|
||||
try {
|
||||
await logoutAdmin()
|
||||
} catch {
|
||||
// 后台退出是无状态的,本地清理优先
|
||||
} finally {
|
||||
clearAdminSession()
|
||||
showSuccess('已退出后台')
|
||||
await router.replace('/admin/login')
|
||||
}
|
||||
}
|
||||
|
||||
function readSidebarMenuOpeneds(): string[] {
|
||||
if (typeof window === 'undefined') {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const rawValue = window.localStorage.getItem(sidebarMenuOpenedsStorageKey)
|
||||
if (!rawValue) {
|
||||
return []
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(rawValue)
|
||||
return Array.isArray(parsed)
|
||||
? Array.from(new Set(parsed.map((item) => String(item || '').trim()).filter(Boolean)))
|
||||
: []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function writeSidebarMenuOpeneds(openeds: string[]) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
window.localStorage.setItem(sidebarMenuOpenedsStorageKey, JSON.stringify(openeds))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-page" :class="{ 'is-sidebar-collapsed': isSidebarCollapsed }">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div v-if="!isSidebarCollapsed" class="brand-copy">
|
||||
<strong>运营后台</strong>
|
||||
<p>订单与交付管理</p>
|
||||
</div>
|
||||
<el-tooltip :content="isSidebarCollapsed ? '展开导航' : '折叠导航'" placement="right">
|
||||
<el-button
|
||||
class="sidebar-toggle"
|
||||
circle
|
||||
:aria-label="isSidebarCollapsed ? '展开导航' : '折叠导航'"
|
||||
@click="toggleSidebarCollapse"
|
||||
>
|
||||
<el-icon><component :is="isSidebarCollapsed ? Expand : Fold" /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="!isSidebarCollapsed" class="sidebar-user">
|
||||
<div class="user-name">{{ getAdminUsername() || '未读取' }}</div>
|
||||
<div class="user-role">{{ roleLabel }}</div>
|
||||
<div class="user-expiry">有效期至 {{ formatAdminDateTime(getAdminTokenExpiresAt()) }}</div>
|
||||
</div>
|
||||
|
||||
<el-menu
|
||||
:default-active="activeNavIndex"
|
||||
:default-openeds="sidebarMenuOpeneds"
|
||||
router
|
||||
class="sidebar-menu"
|
||||
:collapse="isSidebarCollapsed"
|
||||
:collapse-transition="false"
|
||||
:ellipsis="false"
|
||||
@open="handleSidebarMenuOpen"
|
||||
@close="handleSidebarMenuClose"
|
||||
>
|
||||
<template v-for="item in navItems" :key="item.to">
|
||||
<el-sub-menu v-if="item.children?.length" :index="item.to">
|
||||
<template #title>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</template>
|
||||
|
||||
<el-menu-item
|
||||
v-for="child in item.children"
|
||||
:key="child.to"
|
||||
:index="child.to"
|
||||
:title="child.label"
|
||||
>
|
||||
<el-icon><component :is="child.icon" /></el-icon>
|
||||
<span>{{ child.label }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item v-else :index="item.to" :title="item.label">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
|
||||
<el-tooltip :disabled="!isSidebarCollapsed" content="退出登录" placement="right">
|
||||
<el-button
|
||||
class="logout-btn"
|
||||
:circle="isSidebarCollapsed"
|
||||
:round="!isSidebarCollapsed"
|
||||
@click="submitLogout"
|
||||
>
|
||||
<el-icon v-if="isSidebarCollapsed"><SwitchButton /></el-icon>
|
||||
<span v-else>退出登录</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</aside>
|
||||
|
||||
<section class="admin-content">
|
||||
<RouterView />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-page {
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 232px minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
transition: grid-template-columns 0.18s ease;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(73, 136, 255, 0.1), transparent 30%),
|
||||
linear-gradient(180deg, var(--bg-page) 0%, #edf3fb 100%);
|
||||
}
|
||||
|
||||
.admin-page.is-sidebar-collapsed {
|
||||
grid-template-columns: 72px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--space-6) 0;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid var(--border-default);
|
||||
background: var(--bg-sidebar);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: 0 var(--space-5);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-copy strong {
|
||||
display: block;
|
||||
font-size: var(--text-xl);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.brand-copy p {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-secondary);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.sidebar-user {
|
||||
margin: 0 var(--space-4) var(--space-4);
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
background: #f5f8fc;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.user-expiry {
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-menu :deep(.el-menu-item),
|
||||
.sidebar-menu :deep(.el-sub-menu__title) {
|
||||
margin: 2px var(--space-2);
|
||||
border-radius: var(--radius-md);
|
||||
min-height: 44px;
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
.sidebar-menu :deep(.el-menu-item.is-active),
|
||||
.sidebar-menu :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.sidebar-menu :deep(.el-sub-menu .el-menu-item) {
|
||||
margin-left: var(--space-4);
|
||||
min-height: 38px;
|
||||
line-height: 38px;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.sidebar-menu :deep(.el-sub-menu .el-menu-item.is-active) {
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
margin: var(--space-3) var(--space-4) 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.is-sidebar-collapsed .admin-sidebar {
|
||||
align-items: stretch;
|
||||
padding-top: var(--space-5);
|
||||
}
|
||||
|
||||
.is-sidebar-collapsed .sidebar-brand {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item),
|
||||
.is-sidebar-collapsed .sidebar-menu :deep(.el-sub-menu__title) {
|
||||
justify-content: center;
|
||||
margin: 4px 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item .el-icon),
|
||||
.is-sidebar-collapsed .sidebar-menu :deep(.el-sub-menu__title .el-icon) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.is-sidebar-collapsed .logout-btn {
|
||||
width: 40px;
|
||||
margin: var(--space-3) auto 0;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
min-width: 0;
|
||||
height: 100vh;
|
||||
padding: var(--space-6);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.admin-page {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
grid-template-columns: 1fr;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.admin-page.is-sidebar-collapsed {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
padding-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.is-sidebar-collapsed .admin-sidebar {
|
||||
padding-top: var(--space-4);
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item),
|
||||
.is-sidebar-collapsed .sidebar-menu :deep(.el-sub-menu__title) {
|
||||
justify-content: flex-start;
|
||||
margin: 2px var(--space-2);
|
||||
padding: 0 var(--space-5);
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,487 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
|
||||
import { fetchAdminOrderDetail, resendAdminOrderKuaishouIndustryVoucherCode } from '@/services/admin'
|
||||
import type { AdminOrderDetail } from '@/types/admin'
|
||||
import { showConfirm, showError, showSuccess } from '@/lib/feedback'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
const actionLoading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detail = ref<AdminOrderDetail | null>(null)
|
||||
|
||||
type TagType = 'success' | 'primary' | 'warning' | 'danger' | 'info'
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await fetchAdminOrderDetail(String(route.params.orderId || ''))
|
||||
detail.value = response.data
|
||||
errorMessage.value = ''
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取订单详情失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadDetail)
|
||||
|
||||
async function handleResendKuaishouIndustryCode() {
|
||||
if (!detail.value) return
|
||||
|
||||
try {
|
||||
await showConfirm(
|
||||
`确认重发订单 ${detail.value.order.platformOrderId} 的电子凭证发码回调吗?系统会复用当前核销码。`,
|
||||
'确认操作',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '继续执行',
|
||||
cancelButtonText: '取消',
|
||||
},
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const response = await resendAdminOrderKuaishouIndustryVoucherCode(detail.value.order.orderId)
|
||||
showSuccess(`电子凭证发码回调已重发,共 ${response.data.resentCount} 张`)
|
||||
await loadDetail()
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '重发电子凭证发码回调失败')
|
||||
await loadDetail()
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatJsonBlock(value: Record<string, unknown>) {
|
||||
return JSON.stringify(value || {}, null, 2)
|
||||
}
|
||||
|
||||
function formatSpecSummary(spec: Record<string, unknown>) {
|
||||
const entries = Object.entries(spec || {})
|
||||
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '')
|
||||
.slice(0, 4)
|
||||
.map(([key, value]) => `${key}: ${String(value)}`)
|
||||
|
||||
return entries.length > 0 ? entries.join(' | ') : '-'
|
||||
}
|
||||
|
||||
function resolveResourceStageLabel(status: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
const labelMap: Record<string, string> = {
|
||||
resource_ready: '资源已准备',
|
||||
pending_prepare: '待准备资源',
|
||||
pending_payment: '待支付',
|
||||
pending_binding_prepare: '待准备资源',
|
||||
retry_pending: '待重试',
|
||||
manual_review: '人工处理',
|
||||
resource_exception: '资源异常',
|
||||
closed: '已关闭',
|
||||
expired: '已过期',
|
||||
completed: '已完成',
|
||||
not_started: '未开始',
|
||||
}
|
||||
|
||||
return labelMap[normalized] || normalized || '-'
|
||||
}
|
||||
|
||||
function resolveCustomerStageLabel(status: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
const labelMap: Record<string, string> = {
|
||||
not_started: '待打开链接',
|
||||
waiting_customer: '待客户绑定',
|
||||
link_opened: '已打开链接',
|
||||
customer_processing: '处理中',
|
||||
customer_confirmed: '已提交',
|
||||
customer_completed: '客户已完成',
|
||||
customer_exception: '客户步骤异常',
|
||||
closed: '已关闭',
|
||||
expired: '已过期',
|
||||
}
|
||||
|
||||
return labelMap[normalized] || normalized || '-'
|
||||
}
|
||||
|
||||
function resolveStageTone(status: string): TagType {
|
||||
const normalized = String(status || '').trim()
|
||||
if (['resource_ready', 'customer_completed', 'completed'].includes(normalized)) return 'success'
|
||||
if (
|
||||
[
|
||||
'waiting_customer',
|
||||
'link_opened',
|
||||
'customer_confirmed',
|
||||
'customer_processing',
|
||||
].includes(normalized)
|
||||
) return 'primary'
|
||||
if (['resource_exception', 'customer_exception', 'manual_review', 'failed'].includes(normalized)) return 'danger'
|
||||
if (['pending_prepare', 'pending_binding_prepare', 'retry_pending'].includes(normalized)) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function resolveOrderProgressText(summary: AdminOrderDetail['order']['fulfillmentProgress']) {
|
||||
if (!summary.totalTaskCount) return '未生成履约任务'
|
||||
if (summary.completedTaskCount >= summary.totalTaskCount) return '履约完成'
|
||||
if (summary.preparedTaskCount > 0) return '等待客户绑定'
|
||||
return '待准备领取链接'
|
||||
}
|
||||
|
||||
function resolveOrderProgressTone(summary: AdminOrderDetail['order']['fulfillmentProgress']): TagType {
|
||||
if (!summary.totalTaskCount) return 'warning'
|
||||
if (summary.completedTaskCount >= summary.totalTaskCount) return 'success'
|
||||
return 'warning'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-panel">
|
||||
<AdminPageHeader title="订单详情" description="查看 91 订单、核销店铺、商品和自动履约任务。" />
|
||||
|
||||
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" />
|
||||
|
||||
<el-skeleton v-if="loading" :rows="8" animated />
|
||||
|
||||
<template v-else-if="detail">
|
||||
<el-card shadow="never" class="section-card">
|
||||
<section class="order-hero">
|
||||
<div class="order-hero-main">
|
||||
<h2>{{ detail.order.platformOrderId }}</h2>
|
||||
<p class="subtitle">
|
||||
{{ detail.order.provider }} / {{ detail.order.platform }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="status-stack">
|
||||
<AdminStatusTag :status="detail.order.payStatus" />
|
||||
<AdminStatusTag :status="detail.order.orderStatus" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div class="summary-block summary-block--wide">
|
||||
<span>91 商品</span>
|
||||
<strong>{{ detail.order.itemSummary || '-' }}</strong>
|
||||
<small>用于匹配 cloudtentacles 商品或覆盖规则</small>
|
||||
</div>
|
||||
<div class="summary-block">
|
||||
<span>核销店铺</span>
|
||||
<strong>{{ detail.order.shopName || detail.order.shopId || '-' }}</strong>
|
||||
<small>shopId:{{ detail.order.shopId || '-' }}</small>
|
||||
</div>
|
||||
<div class="summary-block">
|
||||
<span>订单金额</span>
|
||||
<strong>{{ detail.order.totalAmount }} {{ detail.order.currency }}</strong>
|
||||
<small>支付:{{ formatAdminDateTime(detail.order.paidAt) }}</small>
|
||||
</div>
|
||||
<div class="summary-block">
|
||||
<span>履约进度</span>
|
||||
<el-tag :type="resolveOrderProgressTone(detail.order.fulfillmentProgress)" effect="light">
|
||||
{{ resolveOrderProgressText(detail.order.fulfillmentProgress) }}
|
||||
</el-tag>
|
||||
<small>
|
||||
完成 {{ detail.order.fulfillmentProgress.completedTaskCount }}/{{ detail.order.fulfillmentProgress.totalTaskCount }}
|
||||
· 资源 {{ detail.order.fulfillmentProgress.preparedTaskCount }}/{{ detail.order.fulfillmentProgress.totalTaskCount }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="order-facts">
|
||||
<div>
|
||||
<dt>内部订单</dt>
|
||||
<dd>{{ detail.order.orderId }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>创建时间</dt>
|
||||
<dd>{{ formatAdminDateTime(detail.order.createdAt) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>更新时间</dt>
|
||||
<dd>{{ formatAdminDateTime(detail.order.updatedAt) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="detail.kuaishouIndustryVouchers.length > 0" shadow="never" class="section-card">
|
||||
<div class="section-headline">
|
||||
<h3>电子凭证</h3>
|
||||
<el-button
|
||||
v-if="detail.operations.canResendKuaishouIndustryVoucherCode"
|
||||
:loading="actionLoading"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleResendKuaishouIndustryCode"
|
||||
>
|
||||
重发发码回调
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="detail.kuaishouIndustryVouchers" size="small">
|
||||
<el-table-column prop="voucherCode" label="核销码" min-width="190" />
|
||||
<el-table-column prop="status" label="券状态" width="110" />
|
||||
<el-table-column prop="sendCallbackStatus" label="发码回调" width="120" />
|
||||
<el-table-column label="回调时间" width="180">
|
||||
<template #default="{ row }">{{ formatAdminDateTime(row.sendCallbackSentAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sendCallbackAttemptCount" label="次数" width="80" />
|
||||
<el-table-column label="异常">
|
||||
<template #default="{ row }">{{ row.sendCallbackLastError || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<h3>履约任务</h3>
|
||||
<el-table :data="detail.tasks" size="small">
|
||||
<el-table-column label="任务号">
|
||||
<template #default="{ row }">
|
||||
<RouterLink :to="`/admin/tasks/${row.taskId}`">{{ row.taskNo }}</RouterLink>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<AdminStatusTag :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="资源准备">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="resolveStageTone(row.resourceStatus)" effect="light" size="small">
|
||||
{{ resolveResourceStageLabel(row.resourceStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户步骤">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="resolveStageTone(row.customerStatus)" effect="light" size="small">
|
||||
{{ resolveCustomerStageLabel(row.customerStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="错误">
|
||||
<template #default="{ row }">{{ row.lastError || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<h3>商品子项</h3>
|
||||
<el-table :data="detail.items" size="small">
|
||||
<el-table-column prop="skuName" label="91 商品" min-width="220" />
|
||||
<el-table-column prop="skuCode" label="匹配标识" min-width="180" />
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
<el-table-column label="规格摘要">
|
||||
<template #default="{ row }">{{ formatSpecSummary(row.spec) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<h3>原始载荷</h3>
|
||||
<details class="payload-box">
|
||||
<summary>展开查看原始订单 JSON</summary>
|
||||
<pre>{{ formatJsonBlock(detail.order.rawPayload) }}</pre>
|
||||
</details>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<el-empty v-else description="订单不存在或已被删除" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card h3 {
|
||||
margin: 0 0 var(--space-4);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.section-headline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.section-headline h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.order-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.order-hero h2 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 30px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-stack {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 1.4fr) repeat(3, minmax(180px, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.summary-block {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 112px;
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.summary-block--wide {
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.summary-block span,
|
||||
.summary-block small {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.summary-block span {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.summary-block strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.summary-block small {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.summary-block :deep(.el-tag) {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.order-facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--border-default);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.order-facts div {
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 10px var(--space-4);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.order-facts dt,
|
||||
.order-facts dd {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.order-facts dt {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-facts dd {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.payload-box summary {
|
||||
cursor: pointer;
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.payload-box pre {
|
||||
margin: var(--space-3) 0 0;
|
||||
padding: 14px;
|
||||
overflow-y: auto;
|
||||
border-radius: var(--radius-md);
|
||||
background: #0f172a;
|
||||
color: #dbeafe;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.summary-block--wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.order-hero {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-stack {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.summary-grid,
|
||||
.order-facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.summary-block--wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.order-facts {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,471 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import { useAdminListPage } from "@/composables/useAdminListPage";
|
||||
import { useAdminListQuerySync } from "@/composables/useAdminListQuerySync";
|
||||
import AdminPageHeader from "@/components/admin/AdminPageHeader.vue";
|
||||
import AdminStatusTag from "@/components/admin/AdminStatusTag.vue";
|
||||
import { fetchAdminOrders } from "@/services/admin";
|
||||
import type { AdminOrderListItem } from "@/types/admin";
|
||||
import { adminPayStatusOptions } from "@/utils/admin-options";
|
||||
import { formatAdminDateTime } from "@/utils/admin-time";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { readQueryString, readQueryPage, syncRouteQuery } =
|
||||
useAdminListQuerySync(route, router);
|
||||
|
||||
const platformOrderId = ref(readQueryString("platformOrderId"));
|
||||
const payStatus = ref(readQueryString("payStatus"));
|
||||
const skuCode = ref(readQueryString("skuCode"));
|
||||
const dateRange = ref<string[]>(
|
||||
[readQueryString("dateFrom"), readQueryString("dateTo")].filter(Boolean)
|
||||
);
|
||||
|
||||
const {
|
||||
loading,
|
||||
errorMessage,
|
||||
items,
|
||||
pagination,
|
||||
loadPage: loadOrders,
|
||||
} = useAdminListPage<AdminOrderListItem>({
|
||||
defaultErrorMessage: "读取订单列表失败",
|
||||
fetchPage: (page, pageSize) =>
|
||||
fetchAdminOrders({
|
||||
page,
|
||||
pageSize,
|
||||
platformOrderId: platformOrderId.value.trim(),
|
||||
payStatus: payStatus.value.trim(),
|
||||
skuCode: skuCode.value.trim(),
|
||||
dateFrom: dateRange.value[0] || "",
|
||||
dateTo: dateRange.value[1] || "",
|
||||
}),
|
||||
});
|
||||
|
||||
const summary = computed(() => ({
|
||||
paidCount: items.value.filter((item) => item.payStatus === "paid").length,
|
||||
refundedCount: items.value.filter((item) => item.payStatus === "refunded")
|
||||
.length,
|
||||
completedCount: items.value.filter(
|
||||
(item) =>
|
||||
item.taskCount > 0 &&
|
||||
item.completedTaskCount >= item.taskCount &&
|
||||
item.payStatus === "paid"
|
||||
).length,
|
||||
issueCount: items.value.filter((item) => hasOrderIssue(item)).length,
|
||||
}));
|
||||
|
||||
function resetFilters() {
|
||||
platformOrderId.value = "";
|
||||
payStatus.value = "";
|
||||
skuCode.value = "";
|
||||
dateRange.value = [];
|
||||
void applyFilters(1);
|
||||
}
|
||||
|
||||
async function applyFilters(page = 1) {
|
||||
await syncRouteQuery(page, {
|
||||
platformOrderId: platformOrderId.value,
|
||||
payStatus: payStatus.value,
|
||||
skuCode: skuCode.value,
|
||||
dateFrom: dateRange.value[0] || "",
|
||||
dateTo: dateRange.value[1] || "",
|
||||
});
|
||||
await loadOrders(page);
|
||||
}
|
||||
|
||||
function resolveShopLabel(item: AdminOrderListItem) {
|
||||
return item.shopName || item.shopId || "未识别店铺";
|
||||
}
|
||||
|
||||
function resolveItemSummary(item: AdminOrderListItem) {
|
||||
return item.itemSummary || "未识别商品";
|
||||
}
|
||||
|
||||
function hasOrderIssue(item: AdminOrderListItem) {
|
||||
return (
|
||||
["refunded", "failed", "unpaid"].includes(item.payStatus) ||
|
||||
["resource_exception", "manual_review", "retry_pending"].includes(item.resourceStatus) ||
|
||||
item.customerStatus === "customer_exception"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveFollowUpText(item: AdminOrderListItem) {
|
||||
if (item.payStatus === "unpaid") return "待支付";
|
||||
if (item.payStatus === "refunded") return "已退款";
|
||||
if (item.payStatus === "failed") return "支付失败";
|
||||
if (
|
||||
item.customerStatus === "customer_exception" ||
|
||||
["resource_exception", "manual_review", "retry_pending"].includes(item.resourceStatus)
|
||||
) {
|
||||
return "履约异常";
|
||||
}
|
||||
if (item.taskCount <= 0) return "未生成履约任务";
|
||||
if (item.completedTaskCount >= item.taskCount) return "履约完成";
|
||||
if (item.preparedTaskCount > 0) return "等待客户绑定";
|
||||
return "待准备领取链接";
|
||||
}
|
||||
|
||||
function resolveFollowUpTone(item: AdminOrderListItem) {
|
||||
if (hasOrderIssue(item)) return "danger";
|
||||
if (item.taskCount <= 0 || item.completedTaskCount < item.taskCount)
|
||||
return "warning";
|
||||
return "success";
|
||||
}
|
||||
|
||||
function resolveResourceStageLabel(status: string) {
|
||||
const normalized = String(status || "").trim();
|
||||
const labelMap: Record<string, string> = {
|
||||
resource_ready: "资源已准备",
|
||||
pending_prepare: "待准备资源",
|
||||
pending_payment: "待支付",
|
||||
pending_binding_prepare: "待准备资源",
|
||||
retry_pending: "待重试",
|
||||
manual_review: "人工处理",
|
||||
resource_exception: "资源异常",
|
||||
closed: "已关闭",
|
||||
expired: "已过期",
|
||||
completed: "已完成",
|
||||
not_started: "未开始",
|
||||
};
|
||||
|
||||
return labelMap[normalized] || normalized || "-";
|
||||
}
|
||||
|
||||
function resolveCustomerStageLabel(status: string) {
|
||||
const normalized = String(status || "").trim();
|
||||
const labelMap: Record<string, string> = {
|
||||
not_started: "待打开链接",
|
||||
waiting_customer: "待客户绑定",
|
||||
link_opened: "已打开链接",
|
||||
customer_processing: "处理中",
|
||||
customer_confirmed: "已提交",
|
||||
customer_completed: "客户已完成",
|
||||
customer_exception: "客户步骤异常",
|
||||
closed: "已关闭",
|
||||
expired: "已过期",
|
||||
};
|
||||
|
||||
return labelMap[normalized] || normalized || "-";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void applyFilters(readQueryPage());
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="orders-page list-page">
|
||||
<AdminPageHeader
|
||||
title="订单列表"
|
||||
description="查看 91 订单、核销店铺、商品和自动履约进度。"
|
||||
>
|
||||
<template #extra>
|
||||
<span class="total-badge">共 {{ pagination.total }} 条订单</span>
|
||||
</template>
|
||||
</AdminPageHeader>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">查询订单</span>
|
||||
<span class="card-desc"
|
||||
>按订单号、支付状态、商品名和日期范围筛选。</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<div class="filter-grid">
|
||||
<el-input
|
||||
v-model="platformOrderId"
|
||||
class="filter-control"
|
||||
placeholder="平台订单号(模糊匹配)"
|
||||
clearable
|
||||
@keyup.enter="applyFilters(1)"
|
||||
/>
|
||||
<el-select
|
||||
v-model="payStatus"
|
||||
class="filter-control"
|
||||
placeholder="支付状态"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in adminPayStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="skuCode"
|
||||
class="filter-control"
|
||||
placeholder="商品名 / SKU"
|
||||
clearable
|
||||
@keyup.enter="applyFilters(1)"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
class="filter-control"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
range-separator="至"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
<div class="filter-buttons">
|
||||
<el-button round @click="resetFilters">重置</el-button>
|
||||
<el-button round type="primary" @click="applyFilters(1)"
|
||||
>查询</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div v-if="!loading && items.length > 0" class="summary-strip">
|
||||
<span class="summary-title">当前页</span>
|
||||
<span class="summary-metric" data-tone="success"
|
||||
>已支付 <strong>{{ summary.paidCount }}</strong></span
|
||||
>
|
||||
<span class="summary-metric" data-tone="danger"
|
||||
>已退款 <strong>{{ summary.refundedCount }}</strong></span
|
||||
>
|
||||
<span class="summary-metric" data-tone="primary"
|
||||
>已完成 <strong>{{ summary.completedCount }}</strong></span
|
||||
>
|
||||
<span
|
||||
class="summary-metric"
|
||||
:data-tone="summary.issueCount > 0 ? 'warning' : 'muted'"
|
||||
>需关注 <strong>{{ summary.issueCount }}</strong></span
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<el-card
|
||||
shadow="never"
|
||||
class="section-card"
|
||||
v-loading="loading"
|
||||
element-loading-text="订单列表加载中"
|
||||
>
|
||||
<template v-if="items.length === 0 && !loading">
|
||||
<el-empty description="当前筛选条件下暂无订单" :image-size="60" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="orders-toolbar table-toolbar">
|
||||
<strong>订单明细</strong>
|
||||
<span
|
||||
>第 {{ pagination.page }} 页,当前展示 {{ items.length }} 条</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="items"
|
||||
stripe
|
||||
size="small"
|
||||
class="orders-table data-table"
|
||||
>
|
||||
<el-table-column label="订单信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<RouterLink
|
||||
:to="`/admin/orders/${row.orderId}`"
|
||||
class="order-no id-link"
|
||||
:title="row.platformOrderId"
|
||||
>
|
||||
{{ row.platformOrderId }}
|
||||
</RouterLink>
|
||||
<div class="order-meta-block">
|
||||
<span class="source-badge">{{ row.platform || "-" }}</span>
|
||||
<span class="order-meta-text" :title="resolveShopLabel(row)">
|
||||
核销店铺 {{ resolveShopLabel(row) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品 / 任务" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="cell-title" :title="resolveItemSummary(row)">{{
|
||||
resolveItemSummary(row)
|
||||
}}</span>
|
||||
<span class="cell-subline"
|
||||
>{{ row.itemCount }} 项 · 合计 {{ row.totalQuantity }} 件</span
|
||||
>
|
||||
<span class="cell-subline">履约任务 {{ row.taskCount }} 个</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="230">
|
||||
<template #default="{ row }">
|
||||
<div class="order-status-summary">
|
||||
<span
|
||||
class="follow-up-chip"
|
||||
:data-tone="resolveFollowUpTone(row)"
|
||||
>{{ resolveFollowUpText(row) }}</span
|
||||
>
|
||||
<div class="status-note-line">
|
||||
<span>支付</span>
|
||||
<AdminStatusTag :status="row.payStatus" :show-raw="false" />
|
||||
</div>
|
||||
<p class="status-note">
|
||||
客户 {{ resolveCustomerStageLabel(row.customerStatus) }}
|
||||
· 资源 {{ resolveResourceStageLabel(row.resourceStatus) }}
|
||||
· {{ row.completedTaskCount }}/{{ row.taskCount }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额 / 时间" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="cell-title amount-text"
|
||||
>{{ row.totalAmount }} {{ row.currency }}</span
|
||||
>
|
||||
<span class="cell-subline"
|
||||
>支付 {{ formatAdminDateTime(row.paidAt) }}</span
|
||||
>
|
||||
<span class="cell-subline"
|
||||
>更新 {{ formatAdminDateTime(row.updatedAt) }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<RouterLink
|
||||
:to="`/admin/orders/${row.orderId}`"
|
||||
class="detail-link"
|
||||
>详情</RouterLink
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-if="pagination.total > 0"
|
||||
v-model:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, prev, pager, next"
|
||||
class="pagination-center"
|
||||
@current-change="applyFilters"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "../../../styles/admin-list-pages.css";
|
||||
|
||||
.filter-grid {
|
||||
grid-template-columns:
|
||||
minmax(200px, 1fr) minmax(130px, 0.6fr) minmax(180px, 0.9fr)
|
||||
minmax(320px, 1.4fr) minmax(150px, 0.55fr);
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-meta-block {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(72px, max-content) minmax(0, 1fr);
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.source-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-meta-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-status-summary {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
align-content: start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-note-line {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.status-note-line :deep(.el-tag) {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.status-note {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.follow-up-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
min-height: 26px;
|
||||
padding: 0 9px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 800;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.follow-up-chip[data-tone="success"] {
|
||||
color: #0f766e;
|
||||
background: var(--color-success-light);
|
||||
border-color: #a6f4c5;
|
||||
}
|
||||
.follow-up-chip[data-tone="warning"] {
|
||||
color: #b54708;
|
||||
background: var(--color-warning-light);
|
||||
border-color: #fedf89;
|
||||
}
|
||||
.follow-up-chip[data-tone="danger"] {
|
||||
color: var(--color-danger-dark);
|
||||
background: var(--color-danger-light);
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,539 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { RefreshRight } from '@element-plus/icons-vue'
|
||||
|
||||
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
|
||||
import {
|
||||
fetchAdminCloudtentaclesSourceConfig,
|
||||
fetchAdminKuaishouFeifeiConfig,
|
||||
fetchAdminKuaishouEticketSourceConfig,
|
||||
fetchAdminNotificationConfig,
|
||||
fetchAdminScheduledJobsConfig,
|
||||
} from '@/services/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import AdminPlatformCloudtentaclesSection from './components/cloudtentacles/AdminPlatformCloudtentaclesSection.vue'
|
||||
import AdminPlatformNinetyoneSection from './components/AdminPlatformNinetyoneSection.vue'
|
||||
import AdminPlatformNotificationSection from './components/notification/AdminPlatformNotificationSection.vue'
|
||||
import AdminPlatformKuaishouEticketSection from './components/kuaishou-eticket/AdminPlatformKuaishouEticketSection.vue'
|
||||
import AdminPlatformKuaishouFeifeiSection from './components/kuaishou-feifei/AdminPlatformKuaishouFeifeiSection.vue'
|
||||
import { useAdminCloudtentaclesPlatform } from './composables/useAdminCloudtentaclesPlatform'
|
||||
import { useAdminNinetyonePlatform } from './composables/useAdminNinetyonePlatform'
|
||||
import { useAdminNotificationPlatform } from './composables/useAdminNotificationPlatform'
|
||||
import { useAdminKuaishouEticketPlatform } from './composables/useAdminKuaishouEticketPlatform'
|
||||
import type { PlatformTab } from './composables/types'
|
||||
import type { AdminKuaishouFeifeiConfigResponse } from '@/types/admin'
|
||||
|
||||
const loading = ref(true)
|
||||
const errorMessage = ref('')
|
||||
const route = useRoute()
|
||||
|
||||
const platformTabs: PlatformTab[] = [
|
||||
'ninetyone',
|
||||
'notifications',
|
||||
'kuaishouEticket',
|
||||
'kuaishouFeifei',
|
||||
'cloudtentacles',
|
||||
]
|
||||
|
||||
function normalizePlatformTab(value: unknown): PlatformTab {
|
||||
return typeof value === 'string' && platformTabs.includes(value as PlatformTab)
|
||||
? (value as PlatformTab)
|
||||
: 'ninetyone'
|
||||
}
|
||||
|
||||
const activePlatform = computed(() => normalizePlatformTab(route.query.tab))
|
||||
const kuaishouFeifeiConfig = ref<AdminKuaishouFeifeiConfigResponse | null>(null)
|
||||
|
||||
const {
|
||||
ninetyoneLoading,
|
||||
ninetyoneActionLoadingId,
|
||||
ninetyoneResultError,
|
||||
ninetyoneStatus,
|
||||
ninetyoneOrders,
|
||||
ninetyoneStats,
|
||||
loadNinetyoneOrders,
|
||||
handleNinetyoneStatusChange,
|
||||
handleNinetyoneRetryOrder,
|
||||
handleNinetyoneFailOrder,
|
||||
} = useAdminNinetyonePlatform()
|
||||
|
||||
const {
|
||||
notificationFilePath,
|
||||
notificationForm,
|
||||
notificationRecipients,
|
||||
wpushRecipients,
|
||||
notificationSaving,
|
||||
notificationTesting,
|
||||
notificationResultError,
|
||||
notificationTestResult,
|
||||
scheduledJobsFilePath,
|
||||
scheduledJobsForm,
|
||||
scheduledJobs,
|
||||
scheduledJobRuntime,
|
||||
cloudtentaclesMonitorAccounts,
|
||||
scheduledJobsSaving,
|
||||
scheduledJobRunningId,
|
||||
notificationStats,
|
||||
hydrateNotificationConfig,
|
||||
hydrateScheduledJobsConfig,
|
||||
addNotificationRecipient,
|
||||
removeNotificationRecipient,
|
||||
handleNotificationSaveConfig,
|
||||
handleNotificationTest,
|
||||
handleScheduledJobsSaveConfig,
|
||||
handleScheduledJobRunNow,
|
||||
getScheduledJobRuntime,
|
||||
} = useAdminNotificationPlatform()
|
||||
|
||||
const {
|
||||
kuaishouEticketFilePath,
|
||||
kuaishouEticketShops,
|
||||
kuaishouEticketForm,
|
||||
kuaishouEticketSaving,
|
||||
kuaishouEticketResolvingShopId,
|
||||
kuaishouEticketDetailing,
|
||||
kuaishouEticketConsuming,
|
||||
kuaishouEticketResultError,
|
||||
kuaishouEticketShopInfoResult,
|
||||
kuaishouEticketDetailResult,
|
||||
kuaishouEticketConsumeResult,
|
||||
activeKuaishouEticketShop,
|
||||
kuaishouEticketStats,
|
||||
hydrateKuaishouEticketConfig,
|
||||
addKuaishouEticketShop,
|
||||
removeKuaishouEticketShop,
|
||||
setKuaishouEticketDebugShop,
|
||||
describeKuaishouEticketShop,
|
||||
getKuaishouEticketShopInitial,
|
||||
handleKuaishouEticketResolveShopInfo,
|
||||
handleKuaishouEticketSaveSource,
|
||||
handleKuaishouEticketQueryDetail,
|
||||
handleKuaishouEticketConsume,
|
||||
} = useAdminKuaishouEticketPlatform()
|
||||
|
||||
const {
|
||||
cloudtentaclesFilePath,
|
||||
cloudtentaclesSessionFilePath,
|
||||
cloudtentaclesForm,
|
||||
cloudtentaclesSaving,
|
||||
cloudtentaclesSendingSms,
|
||||
cloudtentaclesTesting,
|
||||
cloudtentaclesValidating,
|
||||
cloudtentaclesResultError,
|
||||
cloudtentaclesSmsResult,
|
||||
cloudtentaclesLoginResult,
|
||||
cloudtentaclesValidateResult,
|
||||
cloudtentaclesDebugLoading,
|
||||
cloudtentaclesDebugResult,
|
||||
cloudtentaclesPersistedSession,
|
||||
cloudtentaclesStats,
|
||||
sources: cloudtentaclesSources,
|
||||
sessions: cloudtentaclesSessions,
|
||||
selectedSourceKey: selectedCloudtentaclesSourceKey,
|
||||
currentSourceEnabled: currentCloudtentaclesSourceEnabled,
|
||||
currentSource: currentCloudtentaclesSource,
|
||||
currentSession: currentCloudtentaclesSession,
|
||||
hydrateCloudtentaclesConfig,
|
||||
switchSource: switchCloudtentaclesSource,
|
||||
addSource: addCloudtentaclesSource,
|
||||
removeSource: removeCloudtentaclesSource,
|
||||
handleCloudtentaclesSaveSource,
|
||||
handleCloudtentaclesSendSmsCode,
|
||||
handleCloudtentaclesTestLogin,
|
||||
handleCloudtentaclesValidateSession,
|
||||
handleCloudtentaclesFetchAsset,
|
||||
handleCloudtentaclesFetchCategories,
|
||||
handleCloudtentaclesFetchSkuList,
|
||||
handleCloudtentaclesBuySku,
|
||||
handleCloudtentaclesUseSku,
|
||||
handleCloudtentaclesFetchKnapsack,
|
||||
handleCloudtentaclesFetchVnList,
|
||||
handleCloudtentaclesAppointVn,
|
||||
handleCloudtentaclesGenerateVnLoginCode,
|
||||
handleCloudtentaclesFetchVnCode,
|
||||
handleCloudtentaclesVerifyVnCode,
|
||||
handleCloudtentaclesFetchBindUrl,
|
||||
handleCloudtentaclesBackVn,
|
||||
handleCloudtentaclesRunFullFlow,
|
||||
} = useAdminCloudtentaclesPlatform()
|
||||
|
||||
const overviewCards = computed(() => {
|
||||
const feifeiSource = kuaishouFeifeiConfig.value?.source
|
||||
const feifeiEffective = kuaishouFeifeiConfig.value?.effective
|
||||
const feifeiEnabledRuleCount = (feifeiSource?.productRules || []).filter(
|
||||
(rule) => rule.enabled !== false,
|
||||
).length
|
||||
const feifeiReady = Boolean(
|
||||
feifeiEffective?.enabled &&
|
||||
feifeiEffective.hasAppKey &&
|
||||
feifeiEffective.hasAppSecret &&
|
||||
feifeiEnabledRuleCount > 0,
|
||||
)
|
||||
const cards = [
|
||||
{
|
||||
key: 'notifications',
|
||||
label: '内部通知',
|
||||
tag: '通知与监控',
|
||||
value:
|
||||
notificationStats.value.barkEnabledRecipientCount +
|
||||
notificationStats.value.wpushEnabledRecipientCount,
|
||||
unit: '启用接收人',
|
||||
meta: `Bark ${notificationStats.value.barkConfiguredRecipientCount} 人 · WPush ${notificationStats.value.wpushConfiguredRecipientCount} 人 · 最近成功 ${notificationStats.value.lastSuccessCount} 个`,
|
||||
status: notificationStats.value.ready ? '已就绪' : '待配置',
|
||||
},
|
||||
{
|
||||
key: 'ninetyone',
|
||||
label: '91卡券',
|
||||
tag: '来源订单',
|
||||
value: ninetyoneStats.value.pendingCount,
|
||||
unit: '待补全订单',
|
||||
meta: `当前 ${ninetyoneStats.value.currentCount} 条 · 已生成任务 ${ninetyoneStats.value.taskReadyCount} 条`,
|
||||
status: ninetyoneStats.value.pendingCount > 0 ? '待处理' : '正常',
|
||||
},
|
||||
{
|
||||
key: 'kuaishouEticket',
|
||||
label: '快手小店核销',
|
||||
tag: '店铺凭据',
|
||||
value: kuaishouEticketStats.value.configuredShopCount,
|
||||
unit: '已配置店铺',
|
||||
meta: `Cookie 就绪 ${kuaishouEticketStats.value.cookieReadyCount} 家 · 当前店铺 ${
|
||||
kuaishouEticketStats.value.selectedShopReady ? '可调试' : '待补全'
|
||||
}`,
|
||||
status: kuaishouEticketStats.value.cookieReadyCount > 0 ? '已就绪' : '待配置',
|
||||
},
|
||||
{
|
||||
key: 'kuaishouFeifei',
|
||||
label: 'kuaishou-feifei',
|
||||
tag: '补充发货平台',
|
||||
value: feifeiEnabledRuleCount,
|
||||
unit: '启用规则',
|
||||
meta: `API ${
|
||||
feifeiEffective?.hasAppKey && feifeiEffective?.hasAppSecret ? '已配置' : '待补全'
|
||||
} · 总规则 ${feifeiSource?.productRules.length || 0} 条`,
|
||||
status: feifeiReady ? '已就绪' : '待配置',
|
||||
},
|
||||
{
|
||||
key: 'cloudtentacles',
|
||||
label: 'cloudtentacles',
|
||||
tag: '履约账号',
|
||||
value: cloudtentaclesStats.value.hasCredential ? '已配置' : '未配置',
|
||||
unit: '履约凭据',
|
||||
meta: `短信 ${cloudtentaclesStats.value.smsSent ? '已发送' : '未发送'} · 会话 ${
|
||||
cloudtentaclesStats.value.validated ? '已验证' : '未验证'
|
||||
}`,
|
||||
status: cloudtentaclesStats.value.validated ? '已验证' : '待验证',
|
||||
},
|
||||
]
|
||||
|
||||
return cards
|
||||
})
|
||||
|
||||
const activeOverview = computed(
|
||||
() =>
|
||||
overviewCards.value.find((card) => card.key === activePlatform.value) || overviewCards.value[0],
|
||||
)
|
||||
|
||||
async function loadConfigs() {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const [
|
||||
notificationResponse,
|
||||
scheduledJobsResponse,
|
||||
kuaishouEticketResponse,
|
||||
kuaishouFeifeiResponse,
|
||||
cloudtentaclesResponse,
|
||||
] = await Promise.all([
|
||||
fetchAdminNotificationConfig(),
|
||||
fetchAdminScheduledJobsConfig(),
|
||||
fetchAdminKuaishouEticketSourceConfig(),
|
||||
fetchAdminKuaishouFeifeiConfig(),
|
||||
fetchAdminCloudtentaclesSourceConfig(),
|
||||
])
|
||||
|
||||
hydrateNotificationConfig(notificationResponse.data)
|
||||
hydrateScheduledJobsConfig(scheduledJobsResponse.data)
|
||||
hydrateKuaishouEticketConfig(kuaishouEticketResponse.data)
|
||||
kuaishouFeifeiConfig.value = kuaishouFeifeiResponse.data
|
||||
hydrateCloudtentaclesConfig(cloudtentaclesResponse.data)
|
||||
await loadNinetyoneOrders(1)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取平台配置失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadConfigs)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-page">
|
||||
<AdminPageHeader title="平台配置" description="按平台分别管理来源接入、凭据与店铺能力。">
|
||||
<template #extra>
|
||||
<el-button circle title="刷新全部配置" @click="loadConfigs">
|
||||
<template #icon
|
||||
><el-icon><RefreshRight /></el-icon
|
||||
></template>
|
||||
</el-button>
|
||||
</template>
|
||||
</AdminPageHeader>
|
||||
|
||||
<!-- 权限不足 -->
|
||||
<el-result
|
||||
v-if="!hasAdminRole('admin')"
|
||||
icon="warning"
|
||||
title="仅管理员可以维护平台配置"
|
||||
sub-title="请切换管理员账号登录"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<!-- 错误提示 -->
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<!-- 加载态 -->
|
||||
<el-skeleton v-if="loading" :rows="8" animated />
|
||||
|
||||
<template v-else>
|
||||
<main class="platform-detail">
|
||||
<section v-if="activeOverview" class="active-summary">
|
||||
<div class="active-summary-copy">
|
||||
<div class="summary-kicker">{{ activeOverview.tag }}</div>
|
||||
<h2>{{ activeOverview.label }}</h2>
|
||||
<p>{{ activeOverview.meta }}</p>
|
||||
</div>
|
||||
<div class="summary-metrics">
|
||||
<div class="summary-metric">
|
||||
<span>{{ activeOverview.unit }}</span>
|
||||
<strong>{{ activeOverview.value }}</strong>
|
||||
</div>
|
||||
<el-tag size="large" effect="plain">{{ activeOverview.status }}</el-tag>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AdminPlatformNinetyoneSection
|
||||
v-if="activePlatform === 'ninetyone'"
|
||||
:ninetyone-loading="ninetyoneLoading"
|
||||
:ninetyone-action-loading-id="ninetyoneActionLoadingId"
|
||||
:ninetyone-result-error="ninetyoneResultError"
|
||||
:ninetyone-status="ninetyoneStatus"
|
||||
:ninetyone-orders="ninetyoneOrders"
|
||||
:ninetyone-stats="ninetyoneStats"
|
||||
:load-ninetyone-orders="loadNinetyoneOrders"
|
||||
:handle-ninetyone-status-change="handleNinetyoneStatusChange"
|
||||
:handle-ninetyone-retry-order="handleNinetyoneRetryOrder"
|
||||
:handle-ninetyone-fail-order="handleNinetyoneFailOrder"
|
||||
/>
|
||||
|
||||
<AdminPlatformNotificationSection
|
||||
v-else-if="activePlatform === 'notifications'"
|
||||
:notification-file-path="notificationFilePath"
|
||||
:notification-form="notificationForm"
|
||||
:notification-recipients="notificationRecipients"
|
||||
:wpush-recipients="wpushRecipients"
|
||||
:notification-saving="notificationSaving"
|
||||
:notification-testing="notificationTesting"
|
||||
:notification-result-error="notificationResultError"
|
||||
:notification-test-result="notificationTestResult"
|
||||
:scheduled-jobs-file-path="scheduledJobsFilePath"
|
||||
:scheduled-jobs-form="scheduledJobsForm"
|
||||
:scheduled-jobs="scheduledJobs"
|
||||
:scheduled-job-runtime="scheduledJobRuntime"
|
||||
:cloudtentacles-monitor-accounts="cloudtentaclesMonitorAccounts"
|
||||
:scheduled-jobs-saving="scheduledJobsSaving"
|
||||
:scheduled-job-running-id="scheduledJobRunningId"
|
||||
:notification-stats="notificationStats"
|
||||
:add-notification-recipient="addNotificationRecipient"
|
||||
:remove-notification-recipient="removeNotificationRecipient"
|
||||
:handle-notification-save-config="handleNotificationSaveConfig"
|
||||
:handle-notification-test="handleNotificationTest"
|
||||
:handle-scheduled-jobs-save-config="handleScheduledJobsSaveConfig"
|
||||
:handle-scheduled-job-run-now="handleScheduledJobRunNow"
|
||||
:get-scheduled-job-runtime="getScheduledJobRuntime"
|
||||
/>
|
||||
|
||||
<AdminPlatformKuaishouEticketSection
|
||||
v-else-if="activePlatform === 'kuaishouEticket'"
|
||||
:kuaishou-eticket-file-path="kuaishouEticketFilePath"
|
||||
:kuaishou-eticket-shops="kuaishouEticketShops"
|
||||
:kuaishou-eticket-form="kuaishouEticketForm"
|
||||
:kuaishou-eticket-saving="kuaishouEticketSaving"
|
||||
:kuaishou-eticket-resolving-shop-id="kuaishouEticketResolvingShopId"
|
||||
:kuaishou-eticket-detailing="kuaishouEticketDetailing"
|
||||
:kuaishou-eticket-consuming="kuaishouEticketConsuming"
|
||||
:kuaishou-eticket-result-error="kuaishouEticketResultError"
|
||||
:kuaishou-eticket-shop-info-result="kuaishouEticketShopInfoResult"
|
||||
:kuaishou-eticket-detail-result="kuaishouEticketDetailResult"
|
||||
:kuaishou-eticket-consume-result="kuaishouEticketConsumeResult"
|
||||
:active-kuaishou-eticket-shop="activeKuaishouEticketShop"
|
||||
:kuaishou-eticket-stats="kuaishouEticketStats"
|
||||
:add-kuaishou-eticket-shop="addKuaishouEticketShop"
|
||||
:remove-kuaishou-eticket-shop="removeKuaishouEticketShop"
|
||||
:set-kuaishou-eticket-debug-shop="setKuaishouEticketDebugShop"
|
||||
:describe-kuaishou-eticket-shop="describeKuaishouEticketShop"
|
||||
:get-kuaishou-eticket-shop-initial="getKuaishouEticketShopInitial"
|
||||
:handle-kuaishou-eticket-resolve-shop-info="handleKuaishouEticketResolveShopInfo"
|
||||
:handle-kuaishou-eticket-save-source="handleKuaishouEticketSaveSource"
|
||||
:handle-kuaishou-eticket-query-detail="handleKuaishouEticketQueryDetail"
|
||||
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
|
||||
/>
|
||||
|
||||
<AdminPlatformKuaishouFeifeiSection
|
||||
v-else-if="activePlatform === 'kuaishouFeifei'"
|
||||
@loaded="kuaishouFeifeiConfig = $event"
|
||||
@saved="kuaishouFeifeiConfig = $event"
|
||||
/>
|
||||
|
||||
<AdminPlatformCloudtentaclesSection
|
||||
v-else-if="activePlatform === 'cloudtentacles'"
|
||||
:cloudtentacles-file-path="cloudtentaclesFilePath"
|
||||
:cloudtentacles-session-file-path="cloudtentaclesSessionFilePath"
|
||||
:cloudtentacles-form="cloudtentaclesForm"
|
||||
:cloudtentacles-saving="cloudtentaclesSaving"
|
||||
:cloudtentacles-sending-sms="cloudtentaclesSendingSms"
|
||||
:cloudtentacles-testing="cloudtentaclesTesting"
|
||||
:cloudtentacles-validating="cloudtentaclesValidating"
|
||||
:cloudtentacles-result-error="cloudtentaclesResultError"
|
||||
:cloudtentacles-sms-result="cloudtentaclesSmsResult"
|
||||
:cloudtentacles-login-result="cloudtentaclesLoginResult"
|
||||
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
|
||||
:cloudtentacles-debug-loading="cloudtentaclesDebugLoading"
|
||||
:cloudtentacles-debug-result="cloudtentaclesDebugResult"
|
||||
:cloudtentacles-persisted-session="cloudtentaclesPersistedSession"
|
||||
:cloudtentacles-stats="cloudtentaclesStats"
|
||||
:sources="cloudtentaclesSources"
|
||||
:sessions="cloudtentaclesSessions"
|
||||
:selected-source-key="selectedCloudtentaclesSourceKey"
|
||||
:current-source-enabled="currentCloudtentaclesSourceEnabled"
|
||||
:current-source="currentCloudtentaclesSource"
|
||||
:current-session="currentCloudtentaclesSession"
|
||||
:switch-source="switchCloudtentaclesSource"
|
||||
:add-source="addCloudtentaclesSource"
|
||||
:remove-source="removeCloudtentaclesSource"
|
||||
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
|
||||
:handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode"
|
||||
:handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin"
|
||||
:handle-cloudtentacles-validate-session="handleCloudtentaclesValidateSession"
|
||||
:handle-cloudtentacles-fetch-asset="handleCloudtentaclesFetchAsset"
|
||||
:handle-cloudtentacles-fetch-categories="handleCloudtentaclesFetchCategories"
|
||||
:handle-cloudtentacles-fetch-sku-list="handleCloudtentaclesFetchSkuList"
|
||||
:handle-cloudtentacles-buy-sku="handleCloudtentaclesBuySku"
|
||||
:handle-cloudtentacles-use-sku="handleCloudtentaclesUseSku"
|
||||
:handle-cloudtentacles-fetch-knapsack="handleCloudtentaclesFetchKnapsack"
|
||||
:handle-cloudtentacles-fetch-vn-list="handleCloudtentaclesFetchVnList"
|
||||
:handle-cloudtentacles-appoint-vn="handleCloudtentaclesAppointVn"
|
||||
:handle-cloudtentacles-generate-vn-login-code="handleCloudtentaclesGenerateVnLoginCode"
|
||||
:handle-cloudtentacles-fetch-vn-code="handleCloudtentaclesFetchVnCode"
|
||||
:handle-cloudtentacles-verify-vn-code="handleCloudtentaclesVerifyVnCode"
|
||||
:handle-cloudtentacles-fetch-bind-url="handleCloudtentaclesFetchBindUrl"
|
||||
:handle-cloudtentacles-back-vn="handleCloudtentaclesBackVn"
|
||||
:handle-cloudtentacles-run-full-flow="handleCloudtentaclesRunFullFlow"
|
||||
/>
|
||||
</main>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-page {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.platform-detail {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.active-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
align-items: center;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.active-summary-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.summary-kicker {
|
||||
color: #2563eb;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.active-summary h2 {
|
||||
margin: 4px 0 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-xl);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.active-summary p {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.summary-metrics {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summary-metric {
|
||||
min-width: 112px;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.summary-metric span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.summary-metric strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-xl);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.active-summary {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.summary-metrics {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminNinetyoneOrderItem, AdminNinetyoneOrderListResult } from '@/types/admin'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type Props = {
|
||||
ninetyoneLoading: boolean
|
||||
ninetyoneActionLoadingId: number | null
|
||||
ninetyoneResultError: string
|
||||
ninetyoneStatus: 'pending_config' | 'all' | 'manual_failed'
|
||||
ninetyoneOrders: AdminNinetyoneOrderListResult
|
||||
ninetyoneStats: {
|
||||
total: number
|
||||
currentCount: number
|
||||
pendingCount: number
|
||||
failedCount: number
|
||||
taskReadyCount: number
|
||||
}
|
||||
loadNinetyoneOrders: (page?: number) => void | Promise<void>
|
||||
handleNinetyoneStatusChange: (
|
||||
status: 'pending_config' | 'all' | 'manual_failed',
|
||||
) => void | Promise<void>
|
||||
handleNinetyoneRetryOrder: (item: AdminNinetyoneOrderItem) => void | Promise<void>
|
||||
handleNinetyoneFailOrder: (item: AdminNinetyoneOrderItem) => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-section">
|
||||
<!-- 订单队列 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">91卡券订单队列</span>
|
||||
<p class="card-desc">
|
||||
当前筛选 {{ ninetyoneStats.currentCount }} 条,总计
|
||||
{{ ninetyoneStats.total }} 条,已生成任务 {{ ninetyoneStats.taskReadyCount }} 条。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-radio-group
|
||||
:model-value="ninetyoneStatus"
|
||||
size="small"
|
||||
@update:model-value="
|
||||
(v: string | number | boolean | undefined) =>
|
||||
handleNinetyoneStatusChange(v as 'pending_config' | 'all' | 'manual_failed')
|
||||
"
|
||||
>
|
||||
<el-radio-button value="pending_config">待补全</el-radio-button>
|
||||
<el-radio-button value="manual_failed">失败</el-radio-button>
|
||||
<el-radio-button value="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button
|
||||
:loading="ninetyoneLoading"
|
||||
@click="loadNinetyoneOrders(ninetyoneOrders.page)"
|
||||
>刷新</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
v-if="ninetyoneResultError"
|
||||
:title="ninetyoneResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<el-table
|
||||
:data="ninetyoneOrders.items"
|
||||
stripe
|
||||
size="small"
|
||||
v-loading="ninetyoneLoading"
|
||||
element-loading-text="正在读取 91卡券订单..."
|
||||
>
|
||||
<el-table-column label="订单" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div class="fw-600">{{ row.orderNo }}</div>
|
||||
<div class="text-muted-xs">
|
||||
{{ row.outTradeNo || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div class="fw-600">{{ row.productName || row.productNo || '-' }}</div>
|
||||
<div class="text-muted-xs">productNo: {{ row.productNo || '-' }}</div>
|
||||
<div class="text-muted-xs">数量:{{ row.buyNum || 0 }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-tag size="small">{{ row.orderStatus }}</el-tag>
|
||||
<div class="text-muted-xs mt-1">任务 {{ row.taskCount }}</div>
|
||||
<div v-if="row.failReason" class="text-xs text-danger">
|
||||
{{ row.failReason }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间" width="180">
|
||||
<template #default="{ row }">{{
|
||||
formatAdminDateTime(row.updatedAt || row.createdAt)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
:loading="ninetyoneActionLoadingId === row.orderId"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleNinetyoneRetryOrder(row)"
|
||||
>
|
||||
重试生成任务
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.orderStatus !== 'manual_failed'"
|
||||
:loading="ninetyoneActionLoadingId === row.orderId"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleNinetyoneFailOrder(row)"
|
||||
>
|
||||
标记失败
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="当前没有 91卡券订单。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
-623
@@ -1,623 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { useRememberedCollapse } from '../../composables/useRememberedCollapse'
|
||||
|
||||
type CloudtentaclesForm = {
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
code: string
|
||||
token: string
|
||||
vnKey: string
|
||||
vnId: number
|
||||
skuId: number
|
||||
skuCount: number
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesForm: CloudtentaclesForm
|
||||
cloudtentaclesSaving: boolean
|
||||
cloudtentaclesSendingSms: boolean
|
||||
cloudtentaclesTesting: boolean
|
||||
cloudtentaclesValidating: boolean
|
||||
cloudtentaclesResultError: string
|
||||
cloudtentaclesSmsResult: AdminCloudtentaclesSendSmsResult | null
|
||||
cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null
|
||||
cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null
|
||||
selectedSourceKey: string
|
||||
currentSourceEnabled: boolean
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
sessions: AdminCloudtentaclesSessionsMap
|
||||
currentSource: AdminCloudtentaclesSourceItem | null
|
||||
currentSession: AdminCloudtentaclesPersistedSession | null
|
||||
handleCloudtentaclesSaveSource: () => void | Promise<void>
|
||||
handleCloudtentaclesSendSmsCode: () => void | Promise<void>
|
||||
handleCloudtentaclesTestLogin: () => void | Promise<void>
|
||||
handleCloudtentaclesValidateSession: () => void | Promise<void>
|
||||
switchSource: (sourceKey: string) => void
|
||||
addSource: () => void
|
||||
removeSource: (sourceKey: string) => void
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const {
|
||||
activeNames: advancedCollapseNames,
|
||||
handleChange: handleAdvancedCollapseChange,
|
||||
} = useRememberedCollapse('admin:platform-config:cloudtentacles:config:collapse')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="cloud-config-card">
|
||||
<template #header>
|
||||
<div class="config-header">
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 履约平台配置</span>
|
||||
<p class="card-desc">维护账号、凭据与登录会话;调试字段默认折叠。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="cloudtentaclesSaving"
|
||||
@click="handleCloudtentaclesSaveSource"
|
||||
>
|
||||
保存配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="crud-layout">
|
||||
<aside class="account-column">
|
||||
<div class="account-column-head">
|
||||
<div>
|
||||
<strong>账号列表</strong>
|
||||
<p>{{ sources.length }} 个账号</p>
|
||||
</div>
|
||||
<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>{{
|
||||
sessions[source.key]
|
||||
? sessions[source.key].hasToken
|
||||
? '已登录'
|
||||
: '未登录'
|
||||
: '-'
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section 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
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="removeSource(selectedSourceKey)"
|
||||
>
|
||||
删除账号
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-panel">
|
||||
<el-form label-width="auto" label-position="top" class="editor-form">
|
||||
<div class="form-grid">
|
||||
<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-form-item label="系统 ID">
|
||||
<el-input :model-value="currentSource?.key || ''" readonly />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="登录账号">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.username"
|
||||
placeholder="cloudtentacles 登录账号"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="cloudtentaclesForm.phone" placeholder="接收短信验证码的手机号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="登录密码">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="cloudtentacles 登录密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Base URL">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.baseUrl"
|
||||
placeholder="https://123.207.217.176"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div class="session-panel">
|
||||
<div class="session-card-head">
|
||||
<div>
|
||||
<strong>登录与会话</strong>
|
||||
<p>发送验证码后测试登录;已有 token 可直接校验。</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-form label-position="top" class="session-form">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="短信验证码">
|
||||
<el-input v-model="cloudtentaclesForm.code" placeholder="收到验证码后填写" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手动 token">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.token"
|
||||
placeholder="仅在需要手动校验时填写"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-collapse
|
||||
:model-value="advancedCollapseNames"
|
||||
class="advanced-collapse"
|
||||
@update:model-value="handleAdvancedCollapseChange"
|
||||
@change="handleAdvancedCollapseChange"
|
||||
>
|
||||
<el-collapse-item name="advanced" title="高级参数与调试字段">
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="deviceId">
|
||||
<el-input
|
||||
v-model="cloudtentaclesForm.deviceId"
|
||||
placeholder="默认 08bc9d8c-fd15-48ea-bc00-8d754076cafc"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="deviceType">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.deviceType"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="debug-field-grid">
|
||||
<el-form-item label="VN Key">
|
||||
<el-input v-model="cloudtentaclesForm.vnKey" placeholder="例如 1" />
|
||||
</el-form-item>
|
||||
<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 label="SKU ID">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.skuId"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="购买数量">
|
||||
<el-input-number
|
||||
v-model="cloudtentaclesForm.skuCount"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="cloudtentaclesResultError"
|
||||
:title="cloudtentaclesResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<div class="status-strip mt-4">
|
||||
<div class="status-step" :class="{ 'status-step--done': cloudtentaclesSmsResult }">
|
||||
<span>短信</span>
|
||||
<strong>{{ cloudtentaclesSmsResult ? '已发送' : '未执行' }}</strong>
|
||||
<em>
|
||||
{{
|
||||
cloudtentaclesSmsResult
|
||||
? `${cloudtentaclesSmsResult.phoneMasked} · ${formatAdminDateTime(cloudtentaclesSmsResult.sentAt)}`
|
||||
: '先确认手机号可用'
|
||||
}}
|
||||
</em>
|
||||
</div>
|
||||
<div class="status-step" :class="{ 'status-step--done': cloudtentaclesLoginResult }">
|
||||
<span>登录</span>
|
||||
<strong>{{ cloudtentaclesLoginResult ? '成功' : '未执行' }}</strong>
|
||||
<em>
|
||||
{{
|
||||
cloudtentaclesLoginResult
|
||||
? `${cloudtentaclesLoginResult.phoneMasked} · ${formatAdminDateTime(cloudtentaclesLoginResult.loggedInAt)}`
|
||||
: '填写验证码后测试登录'
|
||||
}}
|
||||
</em>
|
||||
</div>
|
||||
<div class="status-step" :class="{ 'status-step--done': cloudtentaclesValidateResult }">
|
||||
<span>会话</span>
|
||||
<strong>{{ cloudtentaclesValidateResult ? '已校验' : '未执行' }}</strong>
|
||||
<em>
|
||||
{{
|
||||
cloudtentaclesValidateResult
|
||||
? `权限 ${cloudtentaclesValidateResult.session.permissionCount} 项 · 余额 ${cloudtentaclesValidateResult.asset}`
|
||||
: '登录成功后校验 token'
|
||||
}}
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cloud-config-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.crud-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.account-column,
|
||||
.editor-column {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
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: 4px 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 rgba(148, 163, 184, 0.18);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #f8fafc;
|
||||
padding: var(--space-3);
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
background 0.2s ease;
|
||||
}
|
||||
|
||||
.account-item:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.account-item--active {
|
||||
border-color: var(--el-color-primary);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
.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;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.account-item-head strong,
|
||||
.account-item-meta span,
|
||||
.account-item-foot span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.account-item-meta,
|
||||
.account-item-foot {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.account-item-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.account-item-foot {
|
||||
justify-content: space-between;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.editor-head {
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.editor-head > div:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.editor-head > div:first-child strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 2px var(--space-4);
|
||||
}
|
||||
|
||||
.session-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid rgba(59, 130, 246, 0.14);
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(180deg, rgba(239, 246, 255, 0.72), rgba(255, 255, 255, 0.92));
|
||||
}
|
||||
|
||||
.session-card-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.session-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.advanced-collapse {
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.advanced-collapse :deep(.el-collapse-item__header) {
|
||||
height: 38px;
|
||||
background: transparent;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.advanced-collapse :deep(.el-collapse-item__wrap) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.debug-field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 2px var(--space-3);
|
||||
}
|
||||
|
||||
.status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.status-step {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.status-step span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-step strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.status-step em {
|
||||
overflow: hidden;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-style: normal;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-step--done {
|
||||
border-color: rgba(34, 197, 94, 0.2);
|
||||
background: var(--color-success-light);
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.crud-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.account-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.config-header,
|
||||
.editor-head,
|
||||
.session-card-head {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-grid,
|
||||
.debug-field-grid,
|
||||
.status-strip,
|
||||
.account-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-267
@@ -1,267 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useRememberedCollapse } from '../../composables/useRememberedCollapse'
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesDebugLoading: boolean
|
||||
handleCloudtentaclesFetchAsset: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchCategories: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchSkuList: () => void | Promise<void>
|
||||
handleCloudtentaclesBuySku: () => void | Promise<void>
|
||||
handleCloudtentaclesUseSku: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchKnapsack: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnList: () => void | Promise<void>
|
||||
handleCloudtentaclesAppointVn: () => void | Promise<void>
|
||||
handleCloudtentaclesGenerateVnLoginCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesVerifyVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchBindUrl: () => void | Promise<void>
|
||||
handleCloudtentaclesBackVn: () => void | Promise<void>
|
||||
handleCloudtentaclesRunFullFlow: () => void | Promise<void>
|
||||
cloudtentaclesDebugResult: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const {
|
||||
activeNames: debugCollapseNames,
|
||||
handleChange: handleDebugCollapseChange,
|
||||
} = useRememberedCollapse('admin:platform-config:cloudtentacles:debug:collapse')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="debug-card">
|
||||
<el-collapse
|
||||
:model-value="debugCollapseNames"
|
||||
@update:model-value="handleDebugCollapseChange"
|
||||
@change="handleDebugCollapseChange"
|
||||
>
|
||||
<el-collapse-item name="debug">
|
||||
<template #title>
|
||||
<div class="debug-title">
|
||||
<strong>开发调试工具</strong>
|
||||
<span>分类、SKU、虚拟号和完整流程调试</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="debug-panel">
|
||||
<div class="debug-group">
|
||||
<span>基础</span>
|
||||
<div class="debug-buttons">
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesFetchAsset"
|
||||
>
|
||||
余额
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesFetchCategories"
|
||||
>
|
||||
分类
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesFetchSkuList"
|
||||
>
|
||||
SKU
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesFetchKnapsack"
|
||||
>
|
||||
背包
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debug-group">
|
||||
<span>履约</span>
|
||||
<div class="debug-buttons">
|
||||
<el-button :loading="cloudtentaclesDebugLoading" @click="handleCloudtentaclesBuySku">
|
||||
购买
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="primary"
|
||||
@click="handleCloudtentaclesUseSku"
|
||||
>
|
||||
发货
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="success"
|
||||
@click="handleCloudtentaclesRunFullFlow"
|
||||
>
|
||||
完整流程
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debug-group">
|
||||
<span>虚拟号</span>
|
||||
<div class="debug-buttons">
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesFetchVnList"
|
||||
>
|
||||
列表
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesAppointVn"
|
||||
>
|
||||
申请
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesGenerateVnLoginCode"
|
||||
>
|
||||
登录码
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesFetchVnCode"
|
||||
>
|
||||
验证码
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesVerifyVnCode"
|
||||
>
|
||||
校验
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
@click="handleCloudtentaclesFetchBindUrl"
|
||||
>
|
||||
兑换链接
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="cloudtentaclesDebugLoading"
|
||||
type="warning"
|
||||
@click="handleCloudtentaclesBackVn"
|
||||
>
|
||||
退还
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre v-if="cloudtentaclesDebugResult" class="debug-pre mt-4">{{
|
||||
cloudtentaclesDebugResult
|
||||
}}</pre>
|
||||
<div v-else class="debug-empty">执行调试动作后显示原始返回数据。</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.debug-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.debug-card :deep(.el-card__body) {
|
||||
padding: 0 var(--space-4);
|
||||
}
|
||||
|
||||
.debug-card :deep(.el-collapse) {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.debug-card :deep(.el-collapse-item__header) {
|
||||
height: 54px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.debug-card :deep(.el-collapse-item__wrap) {
|
||||
border-bottom: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.debug-title {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.debug-title strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.debug-title span {
|
||||
overflow: hidden;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.debug-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.debug-group {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.debug-group > span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 800;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.debug-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-buttons :deep(.el-button) {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.debug-pre {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.debug-empty {
|
||||
margin-bottom: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
border: 1px dashed rgba(148, 163, 184, 0.35);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.debug-title {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.debug-group {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-237
@@ -1,237 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
} from '@/types/admin'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesFilePath: string
|
||||
cloudtentaclesSessionFilePath: string
|
||||
cloudtentaclesPersistedSession: AdminCloudtentaclesPersistedSession | null
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
sessions: AdminCloudtentaclesSessionsMap
|
||||
selectedSourceKey: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
function resolveOrphanSessionEntries(
|
||||
sources: AdminCloudtentaclesSourceItem[],
|
||||
sessions: AdminCloudtentaclesSessionsMap,
|
||||
) {
|
||||
return Object.entries(sessions).filter(([key]) => !sources.some((source) => source.key === key))
|
||||
}
|
||||
|
||||
function countSavedTokens(sessions: AdminCloudtentaclesSessionsMap) {
|
||||
return Object.values(sessions).filter((session) => session?.hasToken).length
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="cloudtentacles-overview">
|
||||
<div class="overview-copy">
|
||||
<span class="eyebrow">履约账号</span>
|
||||
<strong>cloudtentacles</strong>
|
||||
<p>管理外部履约账号、短信登录和 token 会话。订单来源同步不在这里处理。</p>
|
||||
</div>
|
||||
|
||||
<div class="overview-metrics">
|
||||
<div class="metric-item">
|
||||
<span>账号</span>
|
||||
<strong>{{ sources.length }}</strong>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>已保存 token</span>
|
||||
<strong>{{ countSavedTokens(sessions) }}</strong>
|
||||
</div>
|
||||
<div class="metric-item metric-item--wide">
|
||||
<span>当前账号</span>
|
||||
<strong>{{ selectedSourceKey || '-' }}</strong>
|
||||
</div>
|
||||
<div class="metric-item metric-item--wide">
|
||||
<span>当前 token</span>
|
||||
<strong>
|
||||
{{
|
||||
cloudtentaclesPersistedSession?.hasToken
|
||||
? cloudtentaclesPersistedSession.tokenMasked
|
||||
: '未保存'
|
||||
}}
|
||||
</strong>
|
||||
<small v-if="cloudtentaclesPersistedSession?.hasToken">
|
||||
{{ formatAdminDateTime(cloudtentaclesPersistedSession.loggedInAt) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-strip">
|
||||
<span>配置 {{ cloudtentaclesFilePath || '-' }}</span>
|
||||
<span>会话 {{ cloudtentaclesSessionFilePath || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="sources.length > 0 || Object.keys(sessions).length > 0" class="session-strip">
|
||||
<span v-for="source in sources" :key="source.key" class="session-chip">
|
||||
<strong>{{ source.label || source.key }}</strong>
|
||||
<em>{{ sessions[source.key]?.hasToken ? sessions[source.key]?.tokenMasked : '未登录' }}</em>
|
||||
</span>
|
||||
<span
|
||||
v-for="[key, session] in resolveOrphanSessionEntries(sources, sessions)"
|
||||
:key="`orphan-${key}`"
|
||||
class="session-chip session-chip--orphan"
|
||||
>
|
||||
<strong>{{ key }}</strong>
|
||||
<em>{{ session.hasToken ? session.tokenMasked : '孤立会话' }}</em>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cloudtentacles-overview {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(420px, 1.5fr);
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.overview-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: block;
|
||||
color: var(--color-primary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.overview-copy strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-xl);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.overview-copy p {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.overview-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: 92px 128px minmax(120px, 1fr) minmax(180px, 1.2fr);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
align-content: center;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.metric-item span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-item strong {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.15;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metric-item small {
|
||||
overflow: hidden;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-strip,
|
||||
.session-strip {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.file-strip span {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: 5px 9px;
|
||||
border-radius: var(--radius-full);
|
||||
background: #eef3f8;
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.session-strip {
|
||||
padding-top: var(--space-1);
|
||||
}
|
||||
|
||||
.session-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 260px;
|
||||
padding: 4px 9px;
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success-dark);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.session-chip strong,
|
||||
.session-chip em {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-chip em {
|
||||
color: inherit;
|
||||
font-style: normal;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.session-chip--orphan {
|
||||
border-color: rgba(245, 158, 11, 0.24);
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning-dark);
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.cloudtentacles-overview,
|
||||
.overview-metrics {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.cloudtentacles-overview,
|
||||
.overview-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null
|
||||
cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card v-if="cloudtentaclesLoginResult || cloudtentaclesValidateResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">cloudtentacles 调试结果</span>
|
||||
<p class="card-desc">
|
||||
用于确认当前账号会话、权限和用户信息,后续发货执行器会复用这里的登录能力。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="result-grid">
|
||||
<AdminResultCard
|
||||
label="当前 token"
|
||||
:value="
|
||||
cloudtentaclesValidateResult?.session.tokenMasked ||
|
||||
cloudtentaclesLoginResult?.session.tokenMasked ||
|
||||
'-'
|
||||
"
|
||||
>
|
||||
<p>原始 token 已写入上方输入框,可继续用于校验或后续调试。</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="权限"
|
||||
:value="`${cloudtentaclesValidateResult?.session.permissionCount || cloudtentaclesLoginResult?.session.permissionCount || 0} 项`"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
(
|
||||
cloudtentaclesValidateResult?.session.permissions ||
|
||||
cloudtentaclesLoginResult?.session.permissions ||
|
||||
[]
|
||||
).join('、') || '暂无'
|
||||
}}
|
||||
</p>
|
||||
</AdminResultCard>
|
||||
<AdminResultCard
|
||||
label="余额"
|
||||
:value="cloudtentaclesValidateResult?.asset || cloudtentaclesLoginResult?.asset || 0"
|
||||
>
|
||||
<p>可用于确认当前履约账号状态。</p>
|
||||
</AdminResultCard>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
</style>
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
} from '@/types/admin'
|
||||
import AdminCloudtentaclesInfoCard from './AdminCloudtentaclesInfoCard.vue'
|
||||
import AdminCloudtentaclesConfigCard from './AdminCloudtentaclesConfigCard.vue'
|
||||
import AdminCloudtentaclesDebugCard from './AdminCloudtentaclesDebugCard.vue'
|
||||
import AdminCloudtentaclesResultCard from './AdminCloudtentaclesResultCard.vue'
|
||||
|
||||
type CloudtentaclesForm = {
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
code: string
|
||||
token: string
|
||||
vnKey: string
|
||||
vnId: number
|
||||
skuId: number
|
||||
skuCount: number
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
type CloudtentaclesStats = {
|
||||
hasCredential: boolean
|
||||
smsSent: boolean
|
||||
validated: boolean
|
||||
sourceCount: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
cloudtentaclesFilePath: string
|
||||
cloudtentaclesSessionFilePath: string
|
||||
cloudtentaclesForm: CloudtentaclesForm
|
||||
cloudtentaclesSaving: boolean
|
||||
cloudtentaclesSendingSms: boolean
|
||||
cloudtentaclesTesting: boolean
|
||||
cloudtentaclesValidating: boolean
|
||||
cloudtentaclesResultError: string
|
||||
cloudtentaclesSmsResult: AdminCloudtentaclesSendSmsResult | null
|
||||
cloudtentaclesLoginResult: AdminCloudtentaclesLoginTestResult | null
|
||||
cloudtentaclesValidateResult: AdminCloudtentaclesValidateSessionResult | null
|
||||
cloudtentaclesDebugLoading: boolean
|
||||
cloudtentaclesDebugResult: string
|
||||
cloudtentaclesPersistedSession: AdminCloudtentaclesPersistedSession | null
|
||||
cloudtentaclesStats: CloudtentaclesStats
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
sessions: AdminCloudtentaclesSessionsMap
|
||||
selectedSourceKey: string
|
||||
currentSourceEnabled: boolean
|
||||
currentSource: AdminCloudtentaclesSourceItem | null
|
||||
currentSession: AdminCloudtentaclesPersistedSession | null
|
||||
handleCloudtentaclesSaveSource: () => void | Promise<void>
|
||||
handleCloudtentaclesSendSmsCode: () => void | Promise<void>
|
||||
handleCloudtentaclesTestLogin: () => void | Promise<void>
|
||||
handleCloudtentaclesValidateSession: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchAsset: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchCategories: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchSkuList: () => void | Promise<void>
|
||||
handleCloudtentaclesBuySku: () => void | Promise<void>
|
||||
handleCloudtentaclesUseSku: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchKnapsack: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnList: () => void | Promise<void>
|
||||
handleCloudtentaclesAppointVn: () => void | Promise<void>
|
||||
handleCloudtentaclesGenerateVnLoginCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesVerifyVnCode: () => void | Promise<void>
|
||||
handleCloudtentaclesFetchBindUrl: () => void | Promise<void>
|
||||
handleCloudtentaclesBackVn: () => void | Promise<void>
|
||||
handleCloudtentaclesRunFullFlow: () => void | Promise<void>
|
||||
switchSource: (sourceKey: string) => void
|
||||
addSource: () => void
|
||||
removeSource: (sourceKey: string) => void
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-section">
|
||||
<AdminCloudtentaclesInfoCard
|
||||
:cloudtentacles-file-path="cloudtentaclesFilePath"
|
||||
:cloudtentacles-session-file-path="cloudtentaclesSessionFilePath"
|
||||
:cloudtentacles-persisted-session="cloudtentaclesPersistedSession"
|
||||
:sources="sources"
|
||||
:sessions="sessions"
|
||||
:selected-source-key="selectedSourceKey"
|
||||
/>
|
||||
|
||||
<AdminCloudtentaclesConfigCard
|
||||
:cloudtentacles-form="cloudtentaclesForm"
|
||||
:cloudtentacles-saving="cloudtentaclesSaving"
|
||||
:cloudtentacles-sending-sms="cloudtentaclesSendingSms"
|
||||
:cloudtentacles-testing="cloudtentaclesTesting"
|
||||
:cloudtentacles-validating="cloudtentaclesValidating"
|
||||
:cloudtentacles-result-error="cloudtentaclesResultError"
|
||||
:cloudtentacles-sms-result="cloudtentaclesSmsResult"
|
||||
:cloudtentacles-login-result="cloudtentaclesLoginResult"
|
||||
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
|
||||
:selected-source-key="selectedSourceKey"
|
||||
:current-source-enabled="currentSourceEnabled"
|
||||
:sources="sources"
|
||||
:sessions="sessions"
|
||||
:current-source="currentSource"
|
||||
:current-session="currentSession"
|
||||
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
|
||||
:handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode"
|
||||
:handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin"
|
||||
:handle-cloudtentacles-validate-session="handleCloudtentaclesValidateSession"
|
||||
:switch-source="switchSource"
|
||||
:add-source="addSource"
|
||||
:remove-source="removeSource"
|
||||
/>
|
||||
|
||||
<AdminCloudtentaclesDebugCard
|
||||
:cloudtentacles-debug-loading="cloudtentaclesDebugLoading"
|
||||
:cloudtentacles-debug-result="cloudtentaclesDebugResult"
|
||||
:handle-cloudtentacles-fetch-asset="handleCloudtentaclesFetchAsset"
|
||||
:handle-cloudtentacles-fetch-categories="handleCloudtentaclesFetchCategories"
|
||||
:handle-cloudtentacles-fetch-sku-list="handleCloudtentaclesFetchSkuList"
|
||||
:handle-cloudtentacles-buy-sku="handleCloudtentaclesBuySku"
|
||||
:handle-cloudtentacles-use-sku="handleCloudtentaclesUseSku"
|
||||
:handle-cloudtentacles-fetch-knapsack="handleCloudtentaclesFetchKnapsack"
|
||||
:handle-cloudtentacles-fetch-vn-list="handleCloudtentaclesFetchVnList"
|
||||
:handle-cloudtentacles-appoint-vn="handleCloudtentaclesAppointVn"
|
||||
:handle-cloudtentacles-generate-vn-login-code="handleCloudtentaclesGenerateVnLoginCode"
|
||||
:handle-cloudtentacles-fetch-vn-code="handleCloudtentaclesFetchVnCode"
|
||||
:handle-cloudtentacles-verify-vn-code="handleCloudtentaclesVerifyVnCode"
|
||||
:handle-cloudtentacles-fetch-bind-url="handleCloudtentaclesFetchBindUrl"
|
||||
:handle-cloudtentacles-back-vn="handleCloudtentaclesBackVn"
|
||||
:handle-cloudtentacles-run-full-flow="handleCloudtentaclesRunFullFlow"
|
||||
/>
|
||||
|
||||
<AdminCloudtentaclesResultCard
|
||||
:cloudtentacles-login-result="cloudtentaclesLoginResult"
|
||||
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
</style>
|
||||
-276
@@ -1,276 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
type KuaishouEticketDebugForm = {
|
||||
selectedShopId: string
|
||||
queryTicketCode: string
|
||||
eTicketId: string
|
||||
oid: string
|
||||
formToken: string
|
||||
num: number
|
||||
storeId: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
kuaishouEticketShops: { id: string; kshopName: string; shopId: string }[]
|
||||
kuaishouEticketForm: KuaishouEticketDebugForm
|
||||
kuaishouEticketDetailing: boolean
|
||||
kuaishouEticketConsuming: boolean
|
||||
kuaishouEticketDetailResult: AdminKuaishouEticketDetailResult | null
|
||||
kuaishouEticketConsumeResult: AdminKuaishouEticketConsumeResult | null
|
||||
handleKuaishouEticketQueryDetail: () => void | Promise<void>
|
||||
handleKuaishouEticketConsume: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
function formatDetailStatus(result: AdminKuaishouEticketDetailResult | null) {
|
||||
if (!result) return '未查询'
|
||||
if (result.ok) return result.alreadyConsumed ? '已核销' : '可核销'
|
||||
return '查询异常'
|
||||
}
|
||||
|
||||
function formatConsumeStatus(result: AdminKuaishouEticketConsumeResult | null) {
|
||||
if (!result) return '未执行'
|
||||
if (result.consumed) return '核销完成'
|
||||
if (result.alreadyConsumed) return '已核销'
|
||||
return result.ok ? '已返回' : '执行异常'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card debug-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">核销调试</span>
|
||||
<p class="card-desc">查询券码后自动回填 eTicketId、oid、formToken,再执行核销。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:loading="kuaishouEticketDetailing"
|
||||
@click="handleKuaishouEticketQueryDetail"
|
||||
>
|
||||
查询核销信息
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="kuaishouEticketConsuming"
|
||||
type="primary"
|
||||
@click="handleKuaishouEticketConsume"
|
||||
>
|
||||
执行核销
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="debug-grid">
|
||||
<section class="query-panel">
|
||||
<el-form label-position="top">
|
||||
<div class="query-grid">
|
||||
<el-form-item label="调试店铺">
|
||||
<el-select
|
||||
v-model="kuaishouEticketForm.selectedShopId"
|
||||
placeholder="请选择店铺"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="shop in kuaishouEticketShops"
|
||||
:key="shop.id"
|
||||
:label="shop.kshopName || shop.shopId || '未命名店铺'"
|
||||
:value="shop.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="查询券码">
|
||||
<el-input
|
||||
v-model="kuaishouEticketForm.queryTicketCode"
|
||||
placeholder="用于查询核销信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="核销数量">
|
||||
<el-input-number
|
||||
v-model="kuaishouEticketForm.num"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="storeId">
|
||||
<el-input v-model="kuaishouEticketForm.storeId" placeholder="默认 0" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div class="auto-fill-row">
|
||||
<div class="auto-fill-item">
|
||||
<span>eTicketId</span>
|
||||
<strong>{{ kuaishouEticketForm.eTicketId || '查询后回填' }}</strong>
|
||||
</div>
|
||||
<div class="auto-fill-item">
|
||||
<span>oid</span>
|
||||
<strong>{{ kuaishouEticketForm.oid || '查询后回填' }}</strong>
|
||||
</div>
|
||||
<div class="auto-fill-item">
|
||||
<span>formToken</span>
|
||||
<strong>{{ kuaishouEticketForm.formToken || '查询后回填' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="result-panel">
|
||||
<div class="status-card">
|
||||
<span>详情查询</span>
|
||||
<strong>{{ formatDetailStatus(kuaishouEticketDetailResult) }}</strong>
|
||||
<p v-if="kuaishouEticketDetailResult?.detail">
|
||||
{{
|
||||
kuaishouEticketDetailResult.goods?.itemTitle ||
|
||||
kuaishouEticketDetailResult.goods?.skuDesc ||
|
||||
'商品信息未返回'
|
||||
}}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{ kuaishouEticketDetailResult?.errorMessage || '先输入券码并查询详情。' }}
|
||||
</p>
|
||||
<div v-if="kuaishouEticketDetailResult?.detail" class="count-row">
|
||||
<span>剩余 {{ kuaishouEticketDetailResult.detail.leftCount }}</span>
|
||||
<span>总数 {{ kuaishouEticketDetailResult.detail.totalCount }}</span>
|
||||
<span>状态 {{ kuaishouEticketDetailResult.detail.status || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-card">
|
||||
<span>核销执行</span>
|
||||
<strong>{{ formatConsumeStatus(kuaishouEticketConsumeResult) }}</strong>
|
||||
<p>
|
||||
{{
|
||||
kuaishouEticketConsumeResult?.errorMessage ||
|
||||
(kuaishouEticketConsumeResult?.ok ? '接口已返回结果。' : '查询成功后再执行核销。')
|
||||
}}
|
||||
</p>
|
||||
<div v-if="kuaishouEticketConsumeResult" class="count-row">
|
||||
<span>数量 {{ kuaishouEticketConsumeResult.num }}</span>
|
||||
<span>requestId {{ kuaishouEticketConsumeResult.requestId || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.6fr) minmax(300px, 0.8fr);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.query-panel,
|
||||
.result-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.query-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1.2fr) minmax(220px, 1.6fr) minmax(120px, 0.8fr) minmax(
|
||||
120px,
|
||||
0.8fr
|
||||
);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.auto-fill-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.auto-fill-item,
|
||||
.status-card {
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.auto-fill-item {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.auto-fill-item span,
|
||||
.status-card > span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.auto-fill-item strong,
|
||||
.status-card strong {
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.status-card p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.count-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.count-row span {
|
||||
padding: 2px var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.debug-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.query-grid,
|
||||
.auto-fill-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
kuaishouEticketFilePath: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="info-strip">
|
||||
<div class="info-main">
|
||||
<span class="card-title">快手小店核销</span>
|
||||
<p class="card-desc">
|
||||
独立维护店铺 Cookie,选择目标店铺后查询券码并执行核销。
|
||||
</p>
|
||||
</div>
|
||||
<div class="info-meta">
|
||||
<span>配置文件</span>
|
||||
<strong>{{ kuaishouEticketFilePath || '-' }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.info-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.info-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.info-main .card-desc {
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.info-meta {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
min-width: 260px;
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.info-meta strong {
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.info-strip {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.info-meta {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
type Props = {
|
||||
kuaishouEticketDetailResult: AdminKuaishouEticketDetailResult | null
|
||||
kuaishouEticketConsumeResult: AdminKuaishouEticketConsumeResult | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 详情结果 -->
|
||||
<el-card v-if="kuaishouEticketDetailResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">核销详情返回</span>
|
||||
<p class="card-desc">成功查询后会自动回填 eTicketId、oid、formToken 和建议核销数量。</p>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="debug-pre">{{ JSON.stringify(kuaishouEticketDetailResult.raw, null, 2) }}</pre>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="kuaishouEticketConsumeResult" class="section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<span class="card-title">核销执行返回</span>
|
||||
<p class="card-desc">这里保留接口原始响应,方便核对 result、error_msg 和 requestId。</p>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="debug-pre">{{ JSON.stringify(kuaishouEticketConsumeResult.raw, null, 2) }}</pre>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.debug-pre {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
</style>
|
||||
-470
@@ -1,470 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { EditableKuaishouEticketShop } from '../../composables/types'
|
||||
import type { AdminKuaishouEticketShopInfoResult } from '@/types/admin'
|
||||
|
||||
type KuaishouEticketStats = {
|
||||
configuredShopCount: number
|
||||
cookieReadyCount: number
|
||||
selectedShopReady: boolean
|
||||
detailReady: boolean
|
||||
consumed: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
kuaishouEticketShops: EditableKuaishouEticketShop[]
|
||||
kuaishouEticketForm: {
|
||||
baseUrl: string
|
||||
selectedShopId: string
|
||||
}
|
||||
kuaishouEticketSaving: boolean
|
||||
kuaishouEticketResolvingShopId: string
|
||||
kuaishouEticketResultError: string
|
||||
kuaishouEticketShopInfoResult: AdminKuaishouEticketShopInfoResult | null
|
||||
activeKuaishouEticketShop: EditableKuaishouEticketShop | null
|
||||
kuaishouEticketStats: KuaishouEticketStats
|
||||
addKuaishouEticketShop: () => void
|
||||
removeKuaishouEticketShop: (id: string) => void
|
||||
setKuaishouEticketDebugShop: (id: string) => void
|
||||
describeKuaishouEticketShop: (shop: EditableKuaishouEticketShop) => string
|
||||
getKuaishouEticketShopInitial: (shop: EditableKuaishouEticketShop) => string
|
||||
handleKuaishouEticketResolveShopInfo: (shop: EditableKuaishouEticketShop) => void | Promise<void>
|
||||
handleKuaishouEticketSaveSource: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card shop-workspace">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">店铺配置</span>
|
||||
<p class="card-desc">维护多店铺 Cookie,当前选中店铺会直接用于下方查询与核销。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button @click="addKuaishouEticketShop">新增店铺</el-button>
|
||||
<el-button
|
||||
:loading="kuaishouEticketSaving"
|
||||
type="primary"
|
||||
@click="handleKuaishouEticketSaveSource"
|
||||
>
|
||||
保存配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="workspace-grid">
|
||||
<aside class="shop-sidebar">
|
||||
<div class="sidebar-head">
|
||||
<div>
|
||||
<strong>{{ kuaishouEticketStats.configuredShopCount }} 家店铺</strong>
|
||||
<p>Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} 家</p>
|
||||
</div>
|
||||
<el-tag
|
||||
:type="kuaishouEticketStats.selectedShopReady ? 'success' : 'warning'"
|
||||
effect="plain"
|
||||
>
|
||||
{{ kuaishouEticketStats.selectedShopReady ? '可调试' : '待补全' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div v-if="kuaishouEticketShops.length > 0" class="shop-list">
|
||||
<button
|
||||
v-for="shop in kuaishouEticketShops"
|
||||
:key="shop.id"
|
||||
type="button"
|
||||
class="shop-item"
|
||||
:class="{ 'shop-item--active': kuaishouEticketForm.selectedShopId === shop.id }"
|
||||
@click="setKuaishouEticketDebugShop(shop.id)"
|
||||
>
|
||||
<div class="shop-avatar">
|
||||
<img
|
||||
v-if="shop.userAvatar"
|
||||
:src="shop.userAvatar"
|
||||
:alt="shop.kshopName || shop.shopId || '店铺头像'"
|
||||
/>
|
||||
<span v-else>{{ getKuaishouEticketShopInitial(shop) }}</span>
|
||||
</div>
|
||||
<div class="shop-summary">
|
||||
<div class="shop-name">
|
||||
<strong>{{ shop.kshopName || shop.shopId || '未命名店铺' }}</strong>
|
||||
<el-tag :type="shop.enabled ? 'success' : 'info'" size="small">
|
||||
{{ shop.enabled ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p>{{ shop.shopId || '等待识别 shopId' }}</p>
|
||||
<div class="shop-flags">
|
||||
<span :class="{ 'is-ok': shop.cookie.trim() }">
|
||||
{{ shop.cookie.trim() ? 'Cookie 已填' : '缺 Cookie' }}
|
||||
</span>
|
||||
<span v-if="kuaishouEticketForm.selectedShopId === shop.id">当前调试</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="compact-empty">
|
||||
<strong>还没有店铺</strong>
|
||||
<p>新增店铺后粘贴 Cookie,再读取店铺信息确认归属。</p>
|
||||
<el-button type="primary" plain @click="addKuaishouEticketShop">新增店铺</el-button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="shop-editor">
|
||||
<el-form label-position="top" class="editor-form">
|
||||
<div class="base-row">
|
||||
<el-form-item label="Base URL">
|
||||
<el-input
|
||||
v-model="kuaishouEticketForm.baseUrl"
|
||||
placeholder="https://s.kwaixiaodian.com"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<template v-if="activeKuaishouEticketShop">
|
||||
<div class="editor-head">
|
||||
<div>
|
||||
<strong>
|
||||
{{
|
||||
activeKuaishouEticketShop.kshopName ||
|
||||
activeKuaishouEticketShop.shopId ||
|
||||
'未命名店铺'
|
||||
}}
|
||||
</strong>
|
||||
<p>{{ describeKuaishouEticketShop(activeKuaishouEticketShop) }}</p>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<el-switch
|
||||
v-model="activeKuaishouEticketShop.enabled"
|
||||
inline-prompt
|
||||
active-text="启用"
|
||||
inactive-text="停用"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="removeKuaishouEticketShop(activeKuaishouEticketShop.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<el-form-item label="kshopName">
|
||||
<el-input
|
||||
v-model="activeKuaishouEticketShop.kshopName"
|
||||
placeholder="例如:稚嫩游戏交易店"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="店铺 ID">
|
||||
<el-input
|
||||
v-model="activeKuaishouEticketShop.shopId"
|
||||
readonly
|
||||
placeholder="通过 info 接口自动识别"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item label="Cookie">
|
||||
<el-input
|
||||
v-model="activeKuaishouEticketShop.cookie"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="直接粘贴该店铺请求头中的完整 Cookie"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="editor-footer">
|
||||
<div class="recognize-result">
|
||||
<span>最近识别</span>
|
||||
<strong>
|
||||
{{
|
||||
kuaishouEticketShopInfoResult?.shop.kshopName ||
|
||||
kuaishouEticketShopInfoResult?.shop.shopId ||
|
||||
'未执行'
|
||||
}}
|
||||
</strong>
|
||||
<small v-if="kuaishouEticketShopInfoResult">
|
||||
结算状态 {{ kuaishouEticketShopInfoResult.shop.settleStatus }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="footer-actions">
|
||||
<el-button
|
||||
:loading="kuaishouEticketResolvingShopId === activeKuaishouEticketShop.id"
|
||||
@click="handleKuaishouEticketResolveShopInfo(activeKuaishouEticketShop)"
|
||||
>
|
||||
读取店铺信息
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="setKuaishouEticketDebugShop(activeKuaishouEticketShop.id)"
|
||||
>
|
||||
设为调试店铺
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="editor-empty">
|
||||
<strong>请选择或新增店铺</strong>
|
||||
<p>左侧选择店铺后,这里会显示 Cookie、店铺 ID 和识别结果。</p>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="kuaishouEticketResultError"
|
||||
:title="kuaishouEticketResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mt-4"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.shop-workspace :deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.workspace-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 320px minmax(0, 1fr);
|
||||
min-height: 460px;
|
||||
}
|
||||
|
||||
.shop-sidebar {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
border-right: 1px solid var(--border-default);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.sidebar-head,
|
||||
.editor-head,
|
||||
.editor-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.sidebar-head strong,
|
||||
.editor-head strong,
|
||||
.compact-empty strong,
|
||||
.editor-empty strong,
|
||||
.recognize-result strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sidebar-head p,
|
||||
.editor-head p,
|
||||
.compact-empty p,
|
||||
.editor-empty p {
|
||||
margin: var(--space-1) 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.shop-list {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
align-content: start;
|
||||
overflow-y: auto;
|
||||
padding-right: var(--space-1);
|
||||
}
|
||||
|
||||
.shop-item {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
width: 100%;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background 0.2s ease;
|
||||
}
|
||||
|
||||
.shop-item:hover,
|
||||
.shop-item--active {
|
||||
border-color: var(--border-hover);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.shop-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 1px solid var(--border-hover);
|
||||
border-radius: var(--radius-md);
|
||||
background: #eef6ff;
|
||||
color: var(--color-primary-dark);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.shop-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.shop-summary {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shop-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shop-name strong {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shop-summary p {
|
||||
margin: 2px 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shop-flags {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
margin-top: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.shop-flags .is-ok {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.compact-empty,
|
||||
.editor-empty {
|
||||
display: grid;
|
||||
place-items: start;
|
||||
align-content: center;
|
||||
gap: var(--space-2);
|
||||
min-height: 220px;
|
||||
padding: var(--space-4);
|
||||
border: 1px dashed var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.shop-editor {
|
||||
min-width: 0;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.editor-form {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.base-row {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.editor-head {
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.editor-actions,
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.editor-footer {
|
||||
align-items: flex-end;
|
||||
padding-top: var(--space-2);
|
||||
}
|
||||
|
||||
.recognize-result {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.recognize-result small {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.workspace-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.shop-sidebar {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.shop-editor {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.editor-head,
|
||||
.editor-footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { EditableKuaishouEticketShop } from '../../composables/types'
|
||||
import type {
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
} from '@/types/admin'
|
||||
import AdminKuaishouEticketInfoCard from './AdminKuaishouEticketInfoCard.vue'
|
||||
import AdminKuaishouEticketShopConfig from './AdminKuaishouEticketShopConfig.vue'
|
||||
import AdminKuaishouEticketDebug from './AdminKuaishouEticketDebug.vue'
|
||||
import AdminKuaishouEticketResults from './AdminKuaishouEticketResults.vue'
|
||||
|
||||
type KuaishouEticketStats = {
|
||||
configuredShopCount: number
|
||||
cookieReadyCount: number
|
||||
selectedShopReady: boolean
|
||||
detailReady: boolean
|
||||
consumed: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
kuaishouEticketFilePath: string
|
||||
kuaishouEticketShops: EditableKuaishouEticketShop[]
|
||||
kuaishouEticketForm: {
|
||||
baseUrl: string
|
||||
selectedShopId: string
|
||||
queryTicketCode: string
|
||||
eTicketId: string
|
||||
oid: string
|
||||
formToken: string
|
||||
num: number
|
||||
storeId: string
|
||||
}
|
||||
kuaishouEticketSaving: boolean
|
||||
kuaishouEticketResolvingShopId: string
|
||||
kuaishouEticketDetailing: boolean
|
||||
kuaishouEticketConsuming: boolean
|
||||
kuaishouEticketResultError: string
|
||||
kuaishouEticketShopInfoResult: AdminKuaishouEticketShopInfoResult | null
|
||||
kuaishouEticketDetailResult: AdminKuaishouEticketDetailResult | null
|
||||
kuaishouEticketConsumeResult: AdminKuaishouEticketConsumeResult | null
|
||||
activeKuaishouEticketShop: EditableKuaishouEticketShop | null
|
||||
kuaishouEticketStats: KuaishouEticketStats
|
||||
addKuaishouEticketShop: () => void
|
||||
removeKuaishouEticketShop: (id: string) => void
|
||||
setKuaishouEticketDebugShop: (id: string) => void
|
||||
describeKuaishouEticketShop: (shop: EditableKuaishouEticketShop) => string
|
||||
getKuaishouEticketShopInitial: (shop: EditableKuaishouEticketShop) => string
|
||||
handleKuaishouEticketResolveShopInfo: (shop: EditableKuaishouEticketShop) => void | Promise<void>
|
||||
handleKuaishouEticketSaveSource: () => void | Promise<void>
|
||||
handleKuaishouEticketQueryDetail: () => void | Promise<void>
|
||||
handleKuaishouEticketConsume: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="platform-section">
|
||||
<AdminKuaishouEticketInfoCard :kuaishou-eticket-file-path="kuaishouEticketFilePath" />
|
||||
|
||||
<AdminKuaishouEticketShopConfig
|
||||
:kuaishou-eticket-shops="kuaishouEticketShops"
|
||||
:kuaishou-eticket-form="kuaishouEticketForm"
|
||||
:kuaishou-eticket-saving="kuaishouEticketSaving"
|
||||
:kuaishou-eticket-resolving-shop-id="kuaishouEticketResolvingShopId"
|
||||
:kuaishou-eticket-result-error="kuaishouEticketResultError"
|
||||
:kuaishou-eticket-shop-info-result="kuaishouEticketShopInfoResult"
|
||||
:active-kuaishou-eticket-shop="activeKuaishouEticketShop"
|
||||
:kuaishou-eticket-stats="kuaishouEticketStats"
|
||||
:add-kuaishou-eticket-shop="addKuaishouEticketShop"
|
||||
:remove-kuaishou-eticket-shop="removeKuaishouEticketShop"
|
||||
:set-kuaishou-eticket-debug-shop="setKuaishouEticketDebugShop"
|
||||
:describe-kuaishou-eticket-shop="describeKuaishouEticketShop"
|
||||
:get-kuaishou-eticket-shop-initial="getKuaishouEticketShopInitial"
|
||||
:handle-kuaishou-eticket-resolve-shop-info="handleKuaishouEticketResolveShopInfo"
|
||||
:handle-kuaishou-eticket-save-source="handleKuaishouEticketSaveSource"
|
||||
/>
|
||||
|
||||
<AdminKuaishouEticketDebug
|
||||
:kuaishou-eticket-shops="kuaishouEticketShops"
|
||||
:kuaishou-eticket-form="kuaishouEticketForm"
|
||||
:kuaishou-eticket-detailing="kuaishouEticketDetailing"
|
||||
:kuaishou-eticket-consuming="kuaishouEticketConsuming"
|
||||
:kuaishou-eticket-detail-result="kuaishouEticketDetailResult"
|
||||
:kuaishou-eticket-consume-result="kuaishouEticketConsumeResult"
|
||||
:handle-kuaishou-eticket-query-detail="handleKuaishouEticketQueryDetail"
|
||||
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
|
||||
/>
|
||||
|
||||
<AdminKuaishouEticketResults
|
||||
:kuaishou-eticket-detail-result="kuaishouEticketDetailResult"
|
||||
:kuaishou-eticket-consume-result="kuaishouEticketConsumeResult"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.platform-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
</style>
|
||||
-814
@@ -1,814 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Check, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
createAdminKuaishouFeifeiTestOrder,
|
||||
fetchAdminKuaishouFeifeiConfig,
|
||||
fetchAdminKuaishouFeifeiProducts,
|
||||
queryAdminKuaishouFeifeiOrder,
|
||||
saveAdminKuaishouFeifeiConfig,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
AdminKuaishouFeifeiProductItem,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
} from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
|
||||
const emit = defineEmits<{
|
||||
loaded: [data: AdminKuaishouFeifeiConfigResponse]
|
||||
saved: [data: AdminKuaishouFeifeiConfigResponse]
|
||||
}>()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const productLoading = ref(false)
|
||||
const orderCreating = ref(false)
|
||||
const orderQuerying = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const productErrorMessage = ref('')
|
||||
const orderErrorMessage = ref('')
|
||||
const filePath = ref('')
|
||||
const products = ref<AdminKuaishouFeifeiProductItem[]>([])
|
||||
const productTotal = ref(0)
|
||||
const productRules = ref<AdminKuaishouFeifeiProductRule[]>([])
|
||||
const lastOrder = ref<AdminKuaishouFeifeiOrderResult | null>(null)
|
||||
const lastOrderAction = ref('')
|
||||
const effective = ref<AdminKuaishouFeifeiEffectiveConfig>({
|
||||
enabled: true,
|
||||
baseUrl: '',
|
||||
timeoutMs: 10000,
|
||||
notifyUrl: '',
|
||||
hasAppKey: false,
|
||||
hasAppSecret: false,
|
||||
productRuleCount: 0,
|
||||
})
|
||||
const form = reactive<Omit<AdminKuaishouFeifeiConfig, 'productRules'>>({
|
||||
enabled: true,
|
||||
baseUrl: '',
|
||||
appKey: '',
|
||||
appSecret: '',
|
||||
timeoutMs: 10000,
|
||||
notifyUrl: '',
|
||||
})
|
||||
const productQuery = reactive({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
status: 'on_sale',
|
||||
supplyProductName: '',
|
||||
})
|
||||
const testOrder = reactive({
|
||||
platformOrderNo: createTestOrderNo(),
|
||||
productCode: '',
|
||||
platformBuyNum: 1,
|
||||
platformAmount: null as number | null,
|
||||
playerAccount: '',
|
||||
playerGameRegion: '',
|
||||
playerGameSrv: '',
|
||||
playerGameRole: '',
|
||||
submitPlayer: false,
|
||||
notifyUrl: '',
|
||||
})
|
||||
const queryOrder = reactive({
|
||||
platformOrderNo: '',
|
||||
orderNo: '',
|
||||
})
|
||||
|
||||
const credentialReady = computed(() => Boolean(form.appKey.trim() && form.appSecret.trim()))
|
||||
const effectiveReady = computed(() => effective.value.enabled && effective.value.hasAppKey && effective.value.hasAppSecret)
|
||||
const selectedProduct = computed(
|
||||
() =>
|
||||
products.value.find((product: AdminKuaishouFeifeiProductItem) => {
|
||||
return product.productCode === testOrder.productCode
|
||||
}) || null,
|
||||
)
|
||||
const lastOrderJson = computed(() => {
|
||||
if (!lastOrder.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return JSON.stringify(lastOrder.value.raw || lastOrder.value, null, 2)
|
||||
})
|
||||
const metrics = computed(() => [
|
||||
{
|
||||
label: '运行状态',
|
||||
value: effectiveReady.value ? '已就绪' : '待配置',
|
||||
detail: form.enabled ? '启用中' : '已停用',
|
||||
},
|
||||
{
|
||||
label: 'API 凭据',
|
||||
value: credentialReady.value ? '已填写' : '缺失',
|
||||
detail: effective.value.hasAppKey && effective.value.hasAppSecret ? '生效配置可用' : '待补全',
|
||||
},
|
||||
{
|
||||
label: '联调商品',
|
||||
value: String(products.value.length),
|
||||
detail: productTotal.value > 0 ? `总计 ${productTotal.value} 条` : '未加载',
|
||||
},
|
||||
{
|
||||
label: '履约规则',
|
||||
value: String(productRules.value.filter((rule) => rule.enabled !== false).length),
|
||||
detail: `共 ${productRules.value.length} 条`,
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(loadConfig)
|
||||
|
||||
async function loadConfig() {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminKuaishouFeifeiConfig()
|
||||
hydrateConfig(response.data)
|
||||
emit('loaded', response.data)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 配置失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await saveAdminKuaishouFeifeiConfig(buildPayload())
|
||||
hydrateConfig(response.data)
|
||||
emit('saved', response.data)
|
||||
showSuccess('kuaishou-feifei 平台配置已保存')
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 配置失败'
|
||||
showError(errorMessage.value)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProducts() {
|
||||
productLoading.value = true
|
||||
productErrorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminKuaishouFeifeiProducts({
|
||||
page: productQuery.page,
|
||||
perPage: productQuery.perPage,
|
||||
status: productQuery.status,
|
||||
supplyProductName: productQuery.supplyProductName.trim(),
|
||||
})
|
||||
products.value = Array.isArray(response.data.items) ? response.data.items : []
|
||||
productTotal.value = Number(response.data.total || 0) || 0
|
||||
} catch (error) {
|
||||
products.value = []
|
||||
productTotal.value = 0
|
||||
productErrorMessage.value = error instanceof Error ? error.message : '查询 kuaishou-feifei 商品失败'
|
||||
showError(productErrorMessage.value)
|
||||
} finally {
|
||||
productLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createTestOrder() {
|
||||
if (!testOrder.platformOrderNo.trim()) {
|
||||
showError('请输入测试业务单号')
|
||||
return
|
||||
}
|
||||
|
||||
if (!testOrder.productCode.trim()) {
|
||||
showError('请输入 feifei 商品编码')
|
||||
return
|
||||
}
|
||||
|
||||
orderCreating.value = true
|
||||
orderErrorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await createAdminKuaishouFeifeiTestOrder({
|
||||
platformOrderNo: testOrder.platformOrderNo.trim(),
|
||||
productCode: testOrder.productCode.trim(),
|
||||
platformBuyNum: Number(testOrder.platformBuyNum || 1) || 1,
|
||||
platformAmount: testOrder.platformAmount,
|
||||
playerAccount: testOrder.playerAccount.trim(),
|
||||
playerGameRegion: testOrder.playerGameRegion.trim(),
|
||||
playerGameSrv: testOrder.playerGameSrv.trim(),
|
||||
playerGameRole: testOrder.playerGameRole.trim(),
|
||||
submitPlayer: testOrder.submitPlayer,
|
||||
notifyUrl: testOrder.notifyUrl.trim(),
|
||||
})
|
||||
applyOrderResult(response.data, '创建结果')
|
||||
showSuccess('kuaishou-feifei 测试单已创建')
|
||||
} catch (error) {
|
||||
orderErrorMessage.value = error instanceof Error ? error.message : '创建 kuaishou-feifei 测试单失败'
|
||||
showError(orderErrorMessage.value)
|
||||
} finally {
|
||||
orderCreating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function queryTestOrder() {
|
||||
const platformOrderNo = queryOrder.platformOrderNo.trim() || testOrder.platformOrderNo.trim()
|
||||
const orderNo = queryOrder.orderNo.trim()
|
||||
|
||||
if (!platformOrderNo && !orderNo) {
|
||||
showError('请输入 feifei 平台单号或测试业务单号')
|
||||
return
|
||||
}
|
||||
|
||||
orderQuerying.value = true
|
||||
orderErrorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await queryAdminKuaishouFeifeiOrder({
|
||||
platformOrderNo,
|
||||
orderNo,
|
||||
})
|
||||
applyOrderResult(response.data, '查询结果')
|
||||
showSuccess('kuaishou-feifei 订单查询完成')
|
||||
} catch (error) {
|
||||
orderErrorMessage.value = error instanceof Error ? error.message : '查询 kuaishou-feifei 订单失败'
|
||||
showError(orderErrorMessage.value)
|
||||
} finally {
|
||||
orderQuerying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function applyOrderResult(order: AdminKuaishouFeifeiOrderResult, action: string) {
|
||||
lastOrder.value = order
|
||||
lastOrderAction.value = action
|
||||
queryOrder.platformOrderNo = order.platformOrderNo || testOrder.platformOrderNo
|
||||
queryOrder.orderNo = order.orderNo || queryOrder.orderNo
|
||||
}
|
||||
|
||||
function useProductForTest(product: AdminKuaishouFeifeiProductItem) {
|
||||
testOrder.productCode = product.productCode
|
||||
}
|
||||
|
||||
function resetTestOrderNo() {
|
||||
testOrder.platformOrderNo = createTestOrderNo()
|
||||
}
|
||||
|
||||
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
|
||||
filePath.value = data.filePath || ''
|
||||
effective.value = data.effective
|
||||
form.enabled = data.source.enabled !== false
|
||||
form.baseUrl = data.source.baseUrl || ''
|
||||
form.appKey = data.source.appKey || ''
|
||||
form.appSecret = data.source.appSecret || ''
|
||||
form.timeoutMs = Number(data.source.timeoutMs || 10000) || 10000
|
||||
form.notifyUrl = data.source.notifyUrl || ''
|
||||
productRules.value = Array.isArray(data.source.productRules) ? data.source.productRules : []
|
||||
if (!testOrder.notifyUrl) {
|
||||
testOrder.notifyUrl = data.source.notifyUrl || ''
|
||||
}
|
||||
}
|
||||
|
||||
function buildPayload(): AdminKuaishouFeifeiConfig {
|
||||
return {
|
||||
enabled: form.enabled,
|
||||
baseUrl: form.baseUrl.trim(),
|
||||
appKey: form.appKey.trim(),
|
||||
appSecret: form.appSecret.trim(),
|
||||
timeoutMs: Number(form.timeoutMs || 10000) || 10000,
|
||||
notifyUrl: form.notifyUrl.trim(),
|
||||
productRules: productRules.value,
|
||||
}
|
||||
}
|
||||
|
||||
function formatPoints(value: unknown) {
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue.toLocaleString('zh-CN') : '-'
|
||||
}
|
||||
|
||||
function formatProductOption(product: AdminKuaishouFeifeiProductItem) {
|
||||
return `${product.productCode} · ${product.name || '-'}`
|
||||
}
|
||||
|
||||
function getProductTagType(product: AdminKuaishouFeifeiProductItem) {
|
||||
return product.status === 'on_sale' && product.supplyStatus === 'on_sale' ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
function getOrderStatusType(status: unknown) {
|
||||
const normalized = Number(status || 0)
|
||||
if (normalized === 30) return 'success'
|
||||
if ([40, 50, 60].includes(normalized)) return 'danger'
|
||||
if ([17, 20].includes(normalized)) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function createTestOrderNo() {
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[-:TZ.]/g, '')
|
||||
.slice(0, 14)
|
||||
|
||||
return `TEST-FEIFEI-${timestamp}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-loading="loading" class="feifei-platform-panel">
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
:title="errorMessage"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<section class="feifei-overview">
|
||||
<div v-for="metric in metrics" :key="metric.label" class="metric-card">
|
||||
<span>{{ metric.label }}</span>
|
||||
<strong>{{ metric.value }}</strong>
|
||||
<small>{{ metric.detail }}</small>
|
||||
</div>
|
||||
<div class="overview-actions">
|
||||
<el-button :icon="RefreshRight" :loading="loading" @click="loadConfig">刷新</el-button>
|
||||
<el-button type="primary" :icon="Check" :loading="saving" @click="saveConfig">
|
||||
保存平台配置
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="platform-layout">
|
||||
<aside class="config-panel">
|
||||
<div class="section-head">
|
||||
<h3>平台连接</h3>
|
||||
<p>{{ filePath || 'data/kuaishou-feifei-config.json' }}</p>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top" class="stack-form">
|
||||
<el-form-item label="启用">
|
||||
<el-switch
|
||||
v-model="form.enabled"
|
||||
inline-prompt
|
||||
active-text="启用"
|
||||
inactive-text="停用"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Base URL">
|
||||
<el-input v-model="form.baseUrl" placeholder="http://skin-exchange.yiquyou.icu" />
|
||||
</el-form-item>
|
||||
<el-form-item label="App Key">
|
||||
<el-input v-model="form.appKey" placeholder="kuaishou-feifei app key" />
|
||||
</el-form-item>
|
||||
<el-form-item label="App Secret">
|
||||
<el-input
|
||||
v-model="form.appSecret"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="kuaishou-feifei app secret"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="超时毫秒">
|
||||
<el-input-number
|
||||
v-model="form.timeoutMs"
|
||||
:min="1000"
|
||||
:step="1000"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="通知地址">
|
||||
<el-input v-model="form.notifyUrl" placeholder="可选,feifei 回调通知地址" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</aside>
|
||||
|
||||
<main class="test-panel">
|
||||
<div class="section-head">
|
||||
<h3>API 联调</h3>
|
||||
<p>{{ effective.baseUrl || form.baseUrl || '-' }}</p>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="productErrorMessage"
|
||||
:title="productErrorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="orderErrorMessage"
|
||||
:title="orderErrorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<div class="product-toolbar">
|
||||
<el-input
|
||||
v-model="productQuery.supplyProductName"
|
||||
clearable
|
||||
placeholder="供货商品名称"
|
||||
@keyup.enter="loadProducts"
|
||||
/>
|
||||
<el-select v-model="productQuery.status" class="status-select">
|
||||
<el-option label="在售" value="on_sale" />
|
||||
<el-option label="下架" value="off_sale" />
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="全部" value="all" />
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-model="productQuery.perPage"
|
||||
:min="1"
|
||||
:max="100"
|
||||
controls-position="right"
|
||||
/>
|
||||
<el-button :icon="Search" :loading="productLoading" @click="loadProducts">
|
||||
查询商品
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="productLoading"
|
||||
:data="products"
|
||||
border
|
||||
stripe
|
||||
height="320"
|
||||
empty-text="暂无商品"
|
||||
class="product-table"
|
||||
@row-click="useProductForTest"
|
||||
>
|
||||
<el-table-column label="商品" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="product-cell">
|
||||
<strong>{{ row.name || '-' }}</strong>
|
||||
<small>{{ row.productCode || '-' }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="渠道" prop="channel" width="120" />
|
||||
<el-table-column label="扣费积分" width="120">
|
||||
<template #default="{ row }">{{ formatPoints(row.unitCostPoints) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单次数量" prop="maxOrderQuantity" width="110" />
|
||||
<el-table-column label="状态" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getProductTagType(row)" effect="light">
|
||||
{{ row.status || '-' }} / {{ row.supplyStatus || '-' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" plain @click.stop="useProductForTest(row)">选用</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<section class="order-grid">
|
||||
<div class="order-panel">
|
||||
<div class="section-head">
|
||||
<h3>创建测试单</h3>
|
||||
</div>
|
||||
<el-form label-position="top" class="order-form">
|
||||
<el-form-item label="测试业务单号">
|
||||
<div class="inline-field">
|
||||
<el-input v-model="testOrder.platformOrderNo" />
|
||||
<el-button :icon="RefreshRight" @click="resetTestOrderNo" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="feifei 商品编码">
|
||||
<el-select
|
||||
v-model="testOrder.productCode"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="product_code"
|
||||
>
|
||||
<el-option
|
||||
v-for="product in products"
|
||||
:key="product.productCode"
|
||||
:label="formatProductOption(product)"
|
||||
:value="product.productCode"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="数量">
|
||||
<el-input-number
|
||||
v-model="testOrder.platformBuyNum"
|
||||
:min="1"
|
||||
:max="selectedProduct?.maxOrderQuantity || 999"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="记录金额">
|
||||
<el-input-number
|
||||
v-model="testOrder.platformAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="玩家 UID">
|
||||
<el-input v-model="testOrder.playerAccount" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="区服-系统">
|
||||
<el-input v-model="testOrder.playerGameRegion" clearable placeholder="安卓 / 苹果" />
|
||||
</el-form-item>
|
||||
<el-form-item label="区服-渠道">
|
||||
<el-input v-model="testOrder.playerGameSrv" clearable placeholder="微信 / QQ" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色名">
|
||||
<el-input v-model="testOrder.playerGameRole" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="直接提交玩家信息">
|
||||
<el-switch v-model="testOrder.submitPlayer" active-text="提交" inactive-text="不提交" />
|
||||
</el-form-item>
|
||||
<el-form-item label="通知地址">
|
||||
<el-input v-model="testOrder.notifyUrl" clearable />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
:loading="orderCreating"
|
||||
@click="createTestOrder"
|
||||
>
|
||||
创建测试单
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="order-panel">
|
||||
<div class="section-head">
|
||||
<h3>查询订单</h3>
|
||||
</div>
|
||||
<el-form label-position="top" class="order-form">
|
||||
<el-form-item label="测试业务单号">
|
||||
<el-input v-model="queryOrder.platformOrderNo" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="feifei 平台单号">
|
||||
<el-input v-model="queryOrder.orderNo" clearable />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button :icon="Search" :loading="orderQuerying" @click="queryTestOrder">
|
||||
查询订单
|
||||
</el-button>
|
||||
|
||||
<div v-if="lastOrder" class="order-result">
|
||||
<div class="result-head">
|
||||
<strong>{{ lastOrderAction || '订单结果' }}</strong>
|
||||
<el-tag :type="getOrderStatusType(lastOrder.rechargeStatus)" effect="light">
|
||||
{{ lastOrder.rechargeStatusLabel || lastOrder.rechargeStatus || '-' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>平台单号</dt>
|
||||
<dd>{{ lastOrder.orderNo || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>业务单号</dt>
|
||||
<dd>{{ lastOrder.platformOrderNo || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>商品</dt>
|
||||
<dd>{{ lastOrder.productName || lastOrder.productCode || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>扣除积分</dt>
|
||||
<dd>{{ formatPoints(lastOrder.pointsCharged) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<el-link
|
||||
v-if="lastOrder.h5?.rechargeUrl"
|
||||
:href="lastOrder.h5.rechargeUrl"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
打开领取链接
|
||||
</el-link>
|
||||
<pre v-if="lastOrderJson" class="json-preview">{{ lastOrderJson }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feifei-platform-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.feifei-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) minmax(160px, auto);
|
||||
gap: var(--space-3);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.metric-card,
|
||||
.config-panel,
|
||||
.test-panel,
|
||||
.order-panel {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.metric-card span,
|
||||
.metric-card small,
|
||||
.section-head p,
|
||||
.product-cell small,
|
||||
.order-result dt {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.metric-card strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-2xl);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.overview-actions,
|
||||
.product-toolbar,
|
||||
.inline-field,
|
||||
.result-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.overview-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.platform-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
|
||||
gap: var(--space-4);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.config-panel,
|
||||
.test-panel,
|
||||
.order-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.section-head h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-head p {
|
||||
margin: 0;
|
||||
font-size: var(--text-sm);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.stack-form,
|
||||
.order-form {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.stack-form :deep(.el-input-number),
|
||||
.order-form :deep(.el-input-number),
|
||||
.order-form :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-toolbar,
|
||||
.inline-field {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
width: 120px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.product-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-cell {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-cell strong,
|
||||
.product-cell small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
|
||||
gap: var(--space-4);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.order-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.inline-field .el-button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.order-result {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding-top: var(--space-2);
|
||||
border-top: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.result-head {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.order-result dl {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.order-result dl div {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.order-result dt,
|
||||
.order-result dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.order-result dd {
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.json-preview {
|
||||
max-height: 220px;
|
||||
margin: 0;
|
||||
padding: var(--space-3);
|
||||
overflow: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.feifei-overview,
|
||||
.platform-layout,
|
||||
.order-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.overview-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.product-toolbar,
|
||||
.overview-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.status-select,
|
||||
.order-form {
|
||||
width: 100%;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { EditableNotificationRecipient } from '../../composables/types'
|
||||
|
||||
type NotificationForm = {
|
||||
enabled: boolean
|
||||
barkEnabled: boolean
|
||||
barkServerUrl: string
|
||||
wpushEnabled: boolean
|
||||
testTitle: string
|
||||
testBody: string
|
||||
testUrl: string
|
||||
}
|
||||
|
||||
type NotificationStats = {
|
||||
barkConfiguredRecipientCount: number
|
||||
barkEnabledRecipientCount: number
|
||||
wpushConfiguredRecipientCount: number
|
||||
wpushEnabledRecipientCount: number
|
||||
barkReady: boolean
|
||||
wpushReady: boolean
|
||||
ready: boolean
|
||||
lastSuccessCount: number
|
||||
lastFailedCount: number
|
||||
scheduledJobEnabledCount: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
notificationForm: NotificationForm
|
||||
notificationRecipients: EditableNotificationRecipient[]
|
||||
notificationSaving: boolean
|
||||
notificationResultError: string
|
||||
notificationStats: NotificationStats
|
||||
addNotificationRecipient: () => void
|
||||
removeNotificationRecipient: (id: string) => void
|
||||
handleNotificationSaveConfig: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">Bark 通知</span>
|
||||
<p class="card-desc">
|
||||
已配置 {{ notificationStats.barkConfiguredRecipientCount }} 人,启用
|
||||
{{ notificationStats.barkEnabledRecipientCount }} 人。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button @click="addNotificationRecipient">新增接收人</el-button>
|
||||
<el-button
|
||||
:loading="notificationSaving"
|
||||
type="primary"
|
||||
@click="handleNotificationSaveConfig"
|
||||
>保存配置</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
v-if="notificationResultError"
|
||||
:title="notificationResultError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<el-form label-width="auto" label-position="top" inline>
|
||||
<el-form-item
|
||||
><el-switch v-model="notificationForm.enabled" active-text="启用内部通知"
|
||||
/></el-form-item>
|
||||
<el-form-item
|
||||
><el-switch v-model="notificationForm.barkEnabled" active-text="启用 Bark 通道"
|
||||
/></el-form-item>
|
||||
<el-form-item label="Bark 服务地址" class="w-full">
|
||||
<el-input v-model="notificationForm.barkServerUrl" placeholder="https://api.day.app" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="notificationRecipients" stripe size="small" class="mt-4">
|
||||
<el-table-column label="接收人" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.name" placeholder="例如:客服A" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Device Key" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.deviceKey" placeholder="Bark device key" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="danger" text @click="removeNotificationRecipient(row.id)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="还没有 Bark 接收人。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
notificationFilePath: string
|
||||
scheduledJobsFilePath: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span class="card-title">配置信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="职责"
|
||||
>内部运营通知,仅发送给后台管理员、客服或值班人员。</el-descriptions-item
|
||||
>
|
||||
<el-descriptions-item label="通道"
|
||||
>当前支持 Bark 与 WPush,两种通道可同时启用。</el-descriptions-item
|
||||
>
|
||||
<el-descriptions-item label="配置文件">{{
|
||||
notificationFilePath || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="监控任务">{{
|
||||
scheduledJobsFilePath || '-'
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
</style>
|
||||
-548
@@ -1,548 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type {
|
||||
AdminCloudtentaclesMonitorAccount,
|
||||
AdminScheduledJobAccountRuntime,
|
||||
AdminScheduledJobRuntimeState,
|
||||
} from '@/types/admin'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import type {
|
||||
EditableScheduledJob,
|
||||
EditableScheduledJobAccount,
|
||||
} from '../../composables/types'
|
||||
|
||||
const timeUnitOptions = [
|
||||
{ label: '秒', value: 1 },
|
||||
{ label: '分钟', value: 60 },
|
||||
{ label: '小时', value: 3600 },
|
||||
]
|
||||
|
||||
type Props = {
|
||||
scheduledJobsForm: {
|
||||
enabled: boolean
|
||||
}
|
||||
scheduledJobs: EditableScheduledJob[]
|
||||
scheduledJobRuntime: AdminScheduledJobRuntimeState[]
|
||||
cloudtentaclesMonitorAccounts: AdminCloudtentaclesMonitorAccount[]
|
||||
scheduledJobsSaving: boolean
|
||||
scheduledJobRunningId: string
|
||||
handleScheduledJobsSaveConfig: () => void | Promise<void>
|
||||
handleScheduledJobRunNow: (jobId: string) => void | Promise<void>
|
||||
getScheduledJobRuntime: (jobId: string) => AdminScheduledJobRuntimeState | null
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
function formatJobType(type: string) {
|
||||
if (type === 'cloudtentacles_health') return 'cloudtentacles 健康检查'
|
||||
return type || '-'
|
||||
}
|
||||
|
||||
function resolveTimeValue(seconds: number) {
|
||||
const normalized = Math.max(0, Number(seconds || 0))
|
||||
if (normalized >= 3600 && normalized % 3600 === 0) {
|
||||
return { amount: normalized / 3600, unit: 3600 }
|
||||
}
|
||||
if (normalized >= 60 && normalized % 60 === 0) {
|
||||
return { amount: normalized / 60, unit: 60 }
|
||||
}
|
||||
return { amount: normalized || 1, unit: 1 }
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number) {
|
||||
const value = resolveTimeValue(seconds)
|
||||
const unit = timeUnitOptions.find((item) => item.value === value.unit)?.label || '秒'
|
||||
return `${value.amount} ${unit}`
|
||||
}
|
||||
|
||||
function bindDuration(job: EditableScheduledJob, key: 'intervalSeconds' | 'cooldownSeconds') {
|
||||
const amountKey = `${key}Amount` as keyof EditableScheduledJob
|
||||
const unitKey = `${key}Unit` as keyof EditableScheduledJob
|
||||
|
||||
if (!Number(job[amountKey])) {
|
||||
const resolved = resolveTimeValue(Number(job[key] || 0))
|
||||
;(job[amountKey] as number) = resolved.amount
|
||||
;(job[unitKey] as number) = resolved.unit
|
||||
}
|
||||
|
||||
return computed<number>({
|
||||
get: () => Number(job[amountKey] || 1),
|
||||
set: (value: number) => {
|
||||
;(job[amountKey] as number) = Math.max(1, Number(value || 1))
|
||||
job[key] = Number(job[amountKey] || 1) * Number(job[unitKey] || 1)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function bindDurationUnit(job: EditableScheduledJob, key: 'intervalSeconds' | 'cooldownSeconds') {
|
||||
const amountKey = `${key}Amount` as keyof EditableScheduledJob
|
||||
const unitKey = `${key}Unit` as keyof EditableScheduledJob
|
||||
|
||||
if (!Number(job[unitKey])) {
|
||||
const resolved = resolveTimeValue(Number(job[key] || 0))
|
||||
;(job[amountKey] as number) = resolved.amount
|
||||
;(job[unitKey] as number) = resolved.unit
|
||||
}
|
||||
|
||||
return computed<number>({
|
||||
get: () => Number(job[unitKey] || 1),
|
||||
set: (value: number) => {
|
||||
;(job[unitKey] as number) = Math.max(1, Number(value || 1))
|
||||
job[key] = Number(job[amountKey] || 1) * Number(job[unitKey] || 1)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function getMonitorAccount(sourceKey: string) {
|
||||
return props.cloudtentaclesMonitorAccounts.find((item) => item.sourceKey === sourceKey) || null
|
||||
}
|
||||
|
||||
function getAccountRuntime(
|
||||
runtime: AdminScheduledJobRuntimeState | null,
|
||||
account: EditableScheduledJobAccount,
|
||||
) {
|
||||
return runtime?.lastAccounts?.find((item) => item.sourceKey === account.sourceKey) || null
|
||||
}
|
||||
|
||||
function formatStatus(status: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
if (normalized === 'ok') return '正常'
|
||||
if (normalized === 'asset_low') return '余额预警'
|
||||
if (normalized === 'auth_missing') return '未登录'
|
||||
if (normalized === 'source_missing') return '账号缺失'
|
||||
if (normalized === 'disabled') return '已停用'
|
||||
if (normalized === 'skipped') return '已跳过'
|
||||
if (normalized === 'running') return '执行中'
|
||||
if (normalized === 'failed') return '检查失败'
|
||||
if (normalized === 'pending') return '待执行'
|
||||
return normalized || '待执行'
|
||||
}
|
||||
|
||||
function resolveStatusType(status: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
if (normalized === 'ok') return 'success'
|
||||
if (normalized === 'asset_low' || normalized === 'running') return 'warning'
|
||||
if (['auth_missing', 'source_missing', 'failed'].includes(normalized)) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function formatAccountMeta(account: EditableScheduledJobAccount) {
|
||||
const source = getMonitorAccount(account.sourceKey)
|
||||
if (!source) return '账号配置不存在'
|
||||
|
||||
return [
|
||||
source.username ? `登录名 ${source.username}` : '',
|
||||
source.phoneMasked ? `手机 ${source.phoneMasked}` : '',
|
||||
source.hasToken ? 'Token 已保存' : '未登录',
|
||||
].filter(Boolean).join(' · ')
|
||||
}
|
||||
|
||||
function formatAccountRuntimeSummary(runtime: AdminScheduledJobAccountRuntime | null) {
|
||||
if (!runtime) return '尚未检查'
|
||||
if (runtime.skipped) return runtime.message || '已跳过'
|
||||
return runtime.message || formatStatus(runtime.status)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">监控任务</span>
|
||||
<p class="card-desc">
|
||||
监控任务会按账号分别检查 cloudtentacles 登录态和余额;每个账号可设置独立阈值。
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:loading="scheduledJobsSaving"
|
||||
type="primary"
|
||||
@click="handleScheduledJobsSaveConfig"
|
||||
>保存任务</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item>
|
||||
<el-switch v-model="scheduledJobsForm.enabled" active-text="启用监控任务系统" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="job-list">
|
||||
<section v-for="job in scheduledJobs" :key="job.id" class="job-panel">
|
||||
<div class="job-panel-head">
|
||||
<div>
|
||||
<div class="job-title-row">
|
||||
<span class="fw-600">{{ formatJobType(job.type) }}</span>
|
||||
<el-tag size="small" :type="job.enabled ? 'success' : 'info'">
|
||||
{{ job.enabled ? '已启用' : '已停用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="job-subtitle">
|
||||
{{ getScheduledJobRuntime(job.id)?.lastMessage || '等待首次执行' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-actions">
|
||||
<el-switch v-model="job.enabled" active-text="启用任务" />
|
||||
<el-button
|
||||
:loading="scheduledJobRunningId === job.id"
|
||||
size="small"
|
||||
@click="handleScheduledJobRunNow(job.id)"
|
||||
>
|
||||
立即检查
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="job-settings">
|
||||
<label class="duration-field">
|
||||
<span class="param-label">检查间隔</span>
|
||||
<span class="duration-controls">
|
||||
<el-input-number
|
||||
v-model.number="bindDuration(job, 'intervalSeconds').value"
|
||||
:min="1"
|
||||
class="duration-input"
|
||||
controls-position="right"
|
||||
/>
|
||||
<el-select
|
||||
v-model.number="bindDurationUnit(job, 'intervalSeconds').value"
|
||||
class="duration-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="unit in timeUnitOptions"
|
||||
:key="unit.value"
|
||||
:label="unit.label"
|
||||
:value="unit.value"
|
||||
>
|
||||
{{ unit.label }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</span>
|
||||
<span class="param-hint">每 {{ formatDuration(job.intervalSeconds) }} 检查一次</span>
|
||||
</label>
|
||||
|
||||
<label class="duration-field">
|
||||
<span class="param-label">通知冷却</span>
|
||||
<span class="duration-controls">
|
||||
<el-input-number
|
||||
v-model.number="bindDuration(job, 'cooldownSeconds').value"
|
||||
:min="1"
|
||||
class="duration-input"
|
||||
controls-position="right"
|
||||
/>
|
||||
<el-select
|
||||
v-model.number="bindDurationUnit(job, 'cooldownSeconds').value"
|
||||
class="duration-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="unit in timeUnitOptions"
|
||||
:key="unit.value"
|
||||
:label="unit.label"
|
||||
:value="unit.value"
|
||||
>
|
||||
{{ unit.label }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</span>
|
||||
<span class="param-hint"
|
||||
>同一账号同类异常 {{ formatDuration(job.cooldownSeconds) }} 内只通知一次</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="runtime-summary">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">任务状态</span>
|
||||
<el-tag
|
||||
:type="resolveStatusType(getScheduledJobRuntime(job.id)?.lastStatus || 'pending')"
|
||||
size="small"
|
||||
>
|
||||
{{ formatStatus(getScheduledJobRuntime(job.id)?.lastStatus || 'pending') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">账号</span>
|
||||
<span class="summary-value"
|
||||
><strong>{{ getScheduledJobRuntime(job.id)?.lastCheckedCount ?? 0 }}</strong>
|
||||
<span class="summary-muted">/ {{ job.accounts.length }} 已检查</span></span
|
||||
>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">预警</span>
|
||||
<span class="summary-value"
|
||||
><strong>{{ getScheduledJobRuntime(job.id)?.lastLowAssetCount ?? 0 }}</strong></span
|
||||
>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">异常</span>
|
||||
<span class="summary-value"
|
||||
><strong>{{ getScheduledJobRuntime(job.id)?.lastFailedCount ?? 0 }}</strong></span
|
||||
>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">上次</span>
|
||||
<span class="summary-value">{{
|
||||
formatAdminDateTime(getScheduledJobRuntime(job.id)?.lastRunAt || '')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">下次</span>
|
||||
<span class="summary-value">{{
|
||||
formatAdminDateTime(getScheduledJobRuntime(job.id)?.nextRunAt || '')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="job.accounts" size="small" class="accounts-table">
|
||||
<el-table-column label="账号" min-width="230">
|
||||
<template #default="{ row: account }">
|
||||
<div class="account-title">{{ account.label || account.sourceKey }}</div>
|
||||
<div class="account-meta">{{ account.sourceKey }}</div>
|
||||
<div class="account-meta">{{ formatAccountMeta(account) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="监控" width="110" align="center">
|
||||
<template #default="{ row: account }">
|
||||
<el-switch v-model="account.enabled" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="余额阈值" width="150">
|
||||
<template #default="{ row: account }">
|
||||
<el-input-number
|
||||
v-model.number="account.assetThreshold"
|
||||
:min="0"
|
||||
class="threshold-input"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近结果" min-width="260">
|
||||
<template #default="{ row: account }">
|
||||
<div
|
||||
v-if="getAccountRuntime(getScheduledJobRuntime(job.id), account)"
|
||||
class="account-runtime"
|
||||
>
|
||||
<div class="runtime-line">
|
||||
<el-tag
|
||||
:type="
|
||||
resolveStatusType(
|
||||
getAccountRuntime(getScheduledJobRuntime(job.id), account)?.status ||
|
||||
'pending',
|
||||
)
|
||||
"
|
||||
size="small"
|
||||
>
|
||||
{{
|
||||
formatStatus(
|
||||
getAccountRuntime(getScheduledJobRuntime(job.id), account)?.status ||
|
||||
'pending',
|
||||
)
|
||||
}}
|
||||
</el-tag>
|
||||
<span>
|
||||
余额
|
||||
{{ getAccountRuntime(getScheduledJobRuntime(job.id), account)?.asset ?? '-' }}
|
||||
/ 阈值
|
||||
{{
|
||||
getAccountRuntime(getScheduledJobRuntime(job.id), account)?.threshold ??
|
||||
account.assetThreshold
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="account-meta">
|
||||
{{ formatAccountRuntimeSummary(getAccountRuntime(getScheduledJobRuntime(job.id), account)) }}
|
||||
</div>
|
||||
<div class="account-meta">
|
||||
{{
|
||||
formatAdminDateTime(
|
||||
getAccountRuntime(getScheduledJobRuntime(job.id), account)?.checkedAt || '',
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="account-meta">尚未检查</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无 cloudtentacles 账号,请先在履约平台配置账号。" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-actions,
|
||||
.job-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.job-list {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.job-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.job-panel-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.job-title-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.job-subtitle,
|
||||
.param-hint,
|
||||
.summary-muted,
|
||||
.account-meta {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.job-settings {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.duration-field {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(230px, 300px) minmax(180px, 1fr);
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: var(--space-2);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-muted);
|
||||
}
|
||||
|
||||
.param-label,
|
||||
.summary-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.duration-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 144px 96px;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.duration-input {
|
||||
width: 144px;
|
||||
}
|
||||
|
||||
.duration-select {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
.runtime-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-muted);
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: grid;
|
||||
grid-template-rows: 18px minmax(24px, auto);
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
padding: var(--space-2);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.accounts-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.threshold-input {
|
||||
width: 144px;
|
||||
}
|
||||
|
||||
.account-runtime {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.runtime-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.job-panel-head {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.job-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.duration-field {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.duration-controls {
|
||||
grid-template-columns: minmax(0, 1fr) 96px;
|
||||
}
|
||||
|
||||
.duration-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminNotificationTestResult } from '@/types/admin'
|
||||
|
||||
type NotificationForm = {
|
||||
enabled: boolean
|
||||
barkEnabled: boolean
|
||||
barkServerUrl: string
|
||||
wpushEnabled: boolean
|
||||
testTitle: string
|
||||
testBody: string
|
||||
testUrl: string
|
||||
}
|
||||
|
||||
type NotificationStats = {
|
||||
barkConfiguredRecipientCount: number
|
||||
barkEnabledRecipientCount: number
|
||||
wpushConfiguredRecipientCount: number
|
||||
wpushEnabledRecipientCount: number
|
||||
barkReady: boolean
|
||||
wpushReady: boolean
|
||||
ready: boolean
|
||||
lastSuccessCount: number
|
||||
lastFailedCount: number
|
||||
scheduledJobEnabledCount: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
notificationForm: NotificationForm
|
||||
notificationTesting: boolean
|
||||
notificationTestResult: AdminNotificationTestResult | null
|
||||
notificationStats: NotificationStats
|
||||
handleNotificationTest: () => void | Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="card-title">测试发送</span>
|
||||
<p class="card-desc">测试会发送给所有已启用且已填写 Bark Device Key 或 WPush API Key 的接收人。</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:disabled="!notificationStats.ready"
|
||||
:loading="notificationTesting"
|
||||
type="primary"
|
||||
@click="handleNotificationTest"
|
||||
>
|
||||
发送测试
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="auto" label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="notificationForm.testTitle" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="内容">
|
||||
<el-input v-model="notificationForm.testBody" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="跳转链接">
|
||||
<el-input v-model="notificationForm.testUrl" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<div v-if="notificationTestResult" class="result-grid">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">成功</div>
|
||||
<div class="stat-value">{{ notificationTestResult.successCount }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">失败</div>
|
||||
<div class="stat-value">{{ notificationTestResult.failedCount }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">通道</div>
|
||||
<div class="stat-value">{{ notificationTestResult.channel || '-' }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-if="notificationTestResult"
|
||||
:data="notificationTestResult.results"
|
||||
stripe
|
||||
size="small"
|
||||
class="mt-4"
|
||||
>
|
||||
<el-table-column prop="channel" label="通道" width="100" />
|
||||
<el-table-column label="接收人">
|
||||
<template #default="{ row }">{{
|
||||
row.recipientName || row.recipientKeyMasked || '-'
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.ok ? 'success' : 'danger'" size="small">{{
|
||||
row.ok ? '成功' : '失败'
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态码" width="80" align="center" />
|
||||
<el-table-column label="说明">
|
||||
<template #default="{ row }">{{ row.errorMessage || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-card {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user