拆分feifei配置并修复商品查询
This commit is contained in:
Vendored
+1
@@ -33,6 +33,7 @@ declare module 'vue' {
|
||||
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']
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
AdminKuaishouFeifeiProductListResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminKuaishouFeifeiConfig() {
|
||||
@@ -24,3 +26,43 @@ export function matchAdminKuaishouFeifeiProduct(productName: string) {
|
||||
{ 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 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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ export type {
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiProductMatch,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductItem,
|
||||
AdminKuaishouFeifeiProductListResult,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
|
||||
@@ -37,6 +37,9 @@ export type {
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiProductMatch,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductItem,
|
||||
AdminKuaishouFeifeiProductListResult,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
} from './kuaishou-feifei'
|
||||
|
||||
export type {
|
||||
|
||||
@@ -48,3 +48,48 @@ export interface AdminKuaishouFeifeiMatchResult {
|
||||
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>
|
||||
}
|
||||
|
||||
+192
-217
@@ -11,7 +11,6 @@ import {
|
||||
import type {
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
} from '@/types/admin'
|
||||
@@ -26,16 +25,7 @@ const errorMessage = ref('')
|
||||
const filePath = ref('')
|
||||
const matchInput = ref('')
|
||||
const matchResult = ref<AdminKuaishouFeifeiMatchResult | null>(null)
|
||||
const effective = ref<AdminKuaishouFeifeiEffectiveConfig>({
|
||||
enabled: true,
|
||||
baseUrl: '',
|
||||
timeoutMs: 10000,
|
||||
notifyUrl: '',
|
||||
hasAppKey: false,
|
||||
hasAppSecret: false,
|
||||
productRuleCount: 0,
|
||||
})
|
||||
const form = reactive<AdminKuaishouFeifeiConfig>({
|
||||
const sourceConfig = reactive<AdminKuaishouFeifeiConfig>({
|
||||
enabled: true,
|
||||
baseUrl: '',
|
||||
appKey: '',
|
||||
@@ -47,8 +37,27 @@ const form = reactive<AdminKuaishouFeifeiConfig>({
|
||||
const rules = ref<EditableRule[]>([])
|
||||
|
||||
const enabledRuleCount = computed(() => rules.value.filter((rule) => rule.enabled !== false).length)
|
||||
const credentialReady = computed(() => Boolean(form.appKey.trim() && form.appSecret.trim()))
|
||||
const effectiveReady = computed(() => effective.value.enabled && effective.value.hasAppKey && effective.value.hasAppSecret)
|
||||
const configuredRuleCount = computed(
|
||||
() => rules.value.filter((rule) => rule.productName.trim() && rule.productCode.trim()).length,
|
||||
)
|
||||
const incompleteRuleCount = computed(() => Math.max(0, rules.value.length - configuredRuleCount.value))
|
||||
const metrics = computed(() => [
|
||||
{
|
||||
label: '启用规则',
|
||||
value: String(enabledRuleCount.value),
|
||||
detail: `共 ${rules.value.length} 条`,
|
||||
},
|
||||
{
|
||||
label: '已配置映射',
|
||||
value: String(configuredRuleCount.value),
|
||||
detail: incompleteRuleCount.value > 0 ? `待补全 ${incompleteRuleCount.value} 条` : '无缺失',
|
||||
},
|
||||
{
|
||||
label: '配置文件',
|
||||
value: filePath.value ? 'GUI' : '默认',
|
||||
detail: filePath.value || 'data/kuaishou-feifei-config.json',
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(loadConfig)
|
||||
|
||||
@@ -65,29 +74,29 @@ async function loadConfig() {
|
||||
const response = await fetchAdminKuaishouFeifeiConfig()
|
||||
hydrateConfig(response.data)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 配置失败'
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 履约配置失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
async function saveRules() {
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await saveAdminKuaishouFeifeiConfig(buildPayload())
|
||||
hydrateConfig(response.data)
|
||||
showSuccess(`kuaishou-feifei 配置已保存,启用规则 ${enabledRuleCount.value} 条`)
|
||||
showSuccess(`kuaishou-feifei 履约规则已保存,启用 ${enabledRuleCount.value} 条`)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 配置失败'
|
||||
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 履约配置失败'
|
||||
showError(errorMessage.value)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runMatchTest() {
|
||||
async function previewMatch() {
|
||||
const productName = matchInput.value.trim()
|
||||
if (!productName) {
|
||||
showError('请输入 91 商品名')
|
||||
@@ -101,7 +110,7 @@ async function runMatchTest() {
|
||||
const response = await matchAdminKuaishouFeifeiProduct(productName)
|
||||
matchResult.value = response.data
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '匹配 kuaishou-feifei 商品失败')
|
||||
showError(error instanceof Error ? error.message : '预览 kuaishou-feifei 映射失败')
|
||||
} finally {
|
||||
matching.value = false
|
||||
}
|
||||
@@ -109,26 +118,24 @@ async function runMatchTest() {
|
||||
|
||||
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 || ''
|
||||
rules.value = Array.isArray(data.source.productRules)
|
||||
? data.source.productRules.map((rule) => normalizeEditableRule(rule))
|
||||
: []
|
||||
sourceConfig.enabled = data.source.enabled !== false
|
||||
sourceConfig.baseUrl = data.source.baseUrl || ''
|
||||
sourceConfig.appKey = data.source.appKey || ''
|
||||
sourceConfig.appSecret = data.source.appSecret || ''
|
||||
sourceConfig.timeoutMs = Number(data.source.timeoutMs || 10000) || 10000
|
||||
sourceConfig.notifyUrl = data.source.notifyUrl || ''
|
||||
sourceConfig.productRules = Array.isArray(data.source.productRules) ? data.source.productRules : []
|
||||
rules.value = sourceConfig.productRules.map((rule) => normalizeEditableRule(rule))
|
||||
}
|
||||
|
||||
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(),
|
||||
enabled: sourceConfig.enabled,
|
||||
baseUrl: sourceConfig.baseUrl,
|
||||
appKey: sourceConfig.appKey,
|
||||
appSecret: sourceConfig.appSecret,
|
||||
timeoutMs: Number(sourceConfig.timeoutMs || 10000) || 10000,
|
||||
notifyUrl: sourceConfig.notifyUrl,
|
||||
productRules: rules.value.map((rule) => normalizeEditableRule(rule)),
|
||||
}
|
||||
}
|
||||
@@ -172,217 +179,180 @@ function createRuleId() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-loading="loading" class="feifei-panel">
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
:title="errorMessage"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<section v-loading="loading" class="feifei-fulfillment-page">
|
||||
<el-result v-if="!hasAdminRole('admin')" icon="warning" title="仅管理员可以查看履约配置" />
|
||||
|
||||
<div class="summary-grid">
|
||||
<div class="summary-item">
|
||||
<span>运行状态</span>
|
||||
<strong>{{ effectiveReady ? '已就绪' : '待配置' }}</strong>
|
||||
<p>{{ form.enabled ? '启用中' : '已停用' }}</p>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>商品规则</span>
|
||||
<strong>{{ enabledRuleCount }}</strong>
|
||||
<p>共 {{ rules.length }} 条</p>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>凭据</span>
|
||||
<strong>{{ credentialReady ? '已填写' : '缺失' }}</strong>
|
||||
<p>生效配置 {{ effective.hasAppKey && effective.hasAppSecret ? '可用' : '待补全' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
:title="errorMessage"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<section class="config-section">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h3>平台连接</h3>
|
||||
<p>{{ filePath || 'data/kuaishou-feifei-config.json' }}</p>
|
||||
<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="section-actions">
|
||||
<el-button :icon="RefreshRight" @click="loadConfig">刷新</el-button>
|
||||
<el-button type="primary" :icon="Check" :loading="saving" @click="saveConfig">
|
||||
保存配置
|
||||
<div class="overview-actions">
|
||||
<el-button :icon="RefreshRight" :loading="loading" @click="loadConfig">刷新</el-button>
|
||||
<el-button type="primary" :icon="Check" :loading="saving" @click="saveRules">
|
||||
保存规则
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-form label-position="top" class="config-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>
|
||||
</section>
|
||||
|
||||
<section class="config-section">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h3>商品规则</h3>
|
||||
<p>91 商品名命中后会创建 kuaishou-feifei 履约任务。</p>
|
||||
</div>
|
||||
<el-button type="primary" plain :icon="Plus" @click="addRule">新增规则</el-button>
|
||||
</div>
|
||||
|
||||
<div class="match-bar">
|
||||
<el-input
|
||||
v-model="matchInput"
|
||||
clearable
|
||||
placeholder="输入 91 商品名,例如 套装-Alan Walker"
|
||||
@keyup.enter="runMatchTest"
|
||||
/>
|
||||
<el-button :icon="Search" :loading="matching" @click="runMatchTest">匹配测试</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 商品规则" />
|
||||
|
||||
<div v-else class="rule-list">
|
||||
<article v-for="(rule, index) in rules" :key="rule.id" class="rule-item">
|
||||
<div class="rule-head">
|
||||
<el-switch
|
||||
v-model="rule.enabled"
|
||||
inline-prompt
|
||||
active-text="启用"
|
||||
inactive-text="停用"
|
||||
/>
|
||||
<el-button type="danger" plain :icon="Delete" @click="removeRule(index)">
|
||||
删除
|
||||
</el-button>
|
||||
<section class="rule-panel">
|
||||
<div class="section-head rule-head-title">
|
||||
<div>
|
||||
<h3>商品履约规则</h3>
|
||||
<p>91 商品名命中后会创建 kuaishou-feifei 履约任务。</p>
|
||||
</div>
|
||||
<el-button type="primary" plain :icon="Plus" @click="addRule">新增规则</el-button>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top" class="rule-form">
|
||||
<el-form-item label="91 商品名">
|
||||
<el-input v-model="rule.productName" placeholder="套装-Alan Walker" />
|
||||
</el-form-item>
|
||||
<el-form-item label="feifei 商品编码">
|
||||
<el-input v-model="rule.productCode" placeholder="product_code" />
|
||||
</el-form-item>
|
||||
<el-form-item label="展示名称">
|
||||
<el-input v-model="rule.skuName" placeholder="后台展示名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="rule.notes" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<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 商品规则" />
|
||||
|
||||
<div v-else class="rule-list">
|
||||
<article v-for="(rule, index) in rules" :key="rule.id" class="rule-item">
|
||||
<div class="rule-head">
|
||||
<el-switch
|
||||
v-model="rule.enabled"
|
||||
inline-prompt
|
||||
active-text="启用"
|
||||
inactive-text="停用"
|
||||
/>
|
||||
<el-button type="danger" plain :icon="Delete" @click="removeRule(index)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top" class="rule-form">
|
||||
<el-form-item label="91 商品名">
|
||||
<el-input v-model="rule.productName" placeholder="套装-Alan Walker" />
|
||||
</el-form-item>
|
||||
<el-form-item label="feifei 商品编码">
|
||||
<el-input v-model="rule.productCode" placeholder="product_code" />
|
||||
</el-form-item>
|
||||
<el-form-item label="展示名称">
|
||||
<el-input v-model="rule.skuName" placeholder="后台展示名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="rule.notes" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feifei-panel {
|
||||
.feifei-fulfillment-page {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
.rule-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(150px, auto);
|
||||
gap: var(--space-3);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.summary-item,
|
||||
.config-section,
|
||||
.metric-card,
|
||||
.rule-panel,
|
||||
.rule-item {
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
background: var(--el-bg-color);
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
.metric-card {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.summary-item span,
|
||||
.summary-item p,
|
||||
.metric-card span,
|
||||
.metric-card small,
|
||||
.section-head p {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.summary-item strong {
|
||||
display: block;
|
||||
margin: 6px 0;
|
||||
font-size: 22px;
|
||||
.metric-card strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-2xl);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.summary-item p,
|
||||
.section-head p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.section-head h3 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.section-actions,
|
||||
.match-bar,
|
||||
.overview-actions,
|
||||
.rule-head-title,
|
||||
.match-checker,
|
||||
.rule-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.config-form,
|
||||
.rule-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
.overview-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.match-bar {
|
||||
.rule-panel,
|
||||
.rule-item {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -391,7 +361,7 @@ function createRuleId() {
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
@@ -406,25 +376,30 @@ function createRuleId() {
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.rule-head {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.rule-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.rule-form :deep(.el-input),
|
||||
.rule-form :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.summary-grid,
|
||||
.config-form,
|
||||
.rule-overview,
|
||||
.rule-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-head,
|
||||
.match-bar {
|
||||
.overview-actions,
|
||||
.rule-head-title,
|
||||
.match-checker {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
fetchAdminScheduledJobsConfig,
|
||||
} from '@/services/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import AdminKuaishouFeifeiFulfillmentView from '../fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue'
|
||||
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'
|
||||
@@ -392,8 +392,10 @@ onMounted(loadConfigs)
|
||||
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
|
||||
/>
|
||||
|
||||
<AdminKuaishouFeifeiFulfillmentView
|
||||
<AdminPlatformKuaishouFeifeiSection
|
||||
v-else-if="activePlatform === 'kuaishouFeifei'"
|
||||
@loaded="kuaishouFeifeiConfig = $event"
|
||||
@saved="kuaishouFeifeiConfig = $event"
|
||||
/>
|
||||
|
||||
<AdminPlatformCloudtentaclesSection
|
||||
|
||||
+814
@@ -0,0 +1,814 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user