增加feifei履约配置界面
This commit is contained in:
Vendored
+2
@@ -47,6 +47,8 @@ declare module 'vue' {
|
||||
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']
|
||||
|
||||
@@ -2,4 +2,5 @@ export * from './notifications'
|
||||
export * from './scheduled-jobs'
|
||||
export * from './ninetyone'
|
||||
export * from './kuaishou-eticket'
|
||||
export * from './kuaishou-feifei'
|
||||
export * from './cloudtentacles'
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
} 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 },
|
||||
)
|
||||
}
|
||||
@@ -50,6 +50,12 @@ export type {
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiProductMatch,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
|
||||
@@ -30,6 +30,15 @@ export type {
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
} from './kuaishou-eticket'
|
||||
|
||||
export type {
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiProductMatch,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
} from './kuaishou-feifei'
|
||||
|
||||
export type {
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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 AdminKuaishouFeifeiProductMatch {
|
||||
matchMode: string
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
productCode: string
|
||||
skuName: string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiMatchResult {
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
matched: boolean
|
||||
match: AdminKuaishouFeifeiProductMatch | null
|
||||
}
|
||||
@@ -1,15 +1,27 @@
|
||||
<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 履约资源的映射规则。"
|
||||
description="维护 91 卡券快手商品到 Cloud 与 kuaishou-feifei 的履约映射规则。"
|
||||
/>
|
||||
|
||||
<AdminKuaishouCloudFulfillmentView />
|
||||
<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>
|
||||
|
||||
@@ -18,4 +30,8 @@ import AdminKuaishouCloudFulfillmentView from './kuaishou-cloud/AdminKuaishouClo
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.fulfillment-tabs {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
+432
@@ -0,0 +1,432 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Check, Delete, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminKuaishouFeifeiConfig,
|
||||
matchAdminKuaishouFeifeiProduct,
|
||||
saveAdminKuaishouFeifeiConfig,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
} from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
|
||||
type EditableRule = AdminKuaishouFeifeiProductRule
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const matching = ref(false)
|
||||
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>({
|
||||
enabled: true,
|
||||
baseUrl: '',
|
||||
appKey: '',
|
||||
appSecret: '',
|
||||
timeoutMs: 10000,
|
||||
notifyUrl: '',
|
||||
productRules: [],
|
||||
})
|
||||
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)
|
||||
|
||||
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 saveConfig() {
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await saveAdminKuaishouFeifeiConfig(buildPayload())
|
||||
hydrateConfig(response.data)
|
||||
showSuccess(`kuaishou-feifei 配置已保存,启用规则 ${enabledRuleCount.value} 条`)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 配置失败'
|
||||
showError(errorMessage.value)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runMatchTest() {
|
||||
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 || ''
|
||||
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))
|
||||
: []
|
||||
}
|
||||
|
||||
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: rules.value.map((rule) => normalizeEditableRule(rule)),
|
||||
}
|
||||
}
|
||||
|
||||
function addRule() {
|
||||
rules.value.unshift(
|
||||
normalizeEditableRule({
|
||||
id: createRuleId(),
|
||||
enabled: true,
|
||||
productName: matchInput.value.trim(),
|
||||
normalizedProductName: '',
|
||||
productCode: '',
|
||||
skuName: matchInput.value.trim(),
|
||||
notes: '',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function removeRule(index: number) {
|
||||
rules.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function normalizeEditableRule(rule: Partial<AdminKuaishouFeifeiProductRule>): EditableRule {
|
||||
const productName = String(rule.productName || '').trim()
|
||||
const productCode = String(rule.productCode || '').trim()
|
||||
|
||||
return {
|
||||
id: String(rule.id || createRuleId()).trim(),
|
||||
enabled: rule.enabled !== false,
|
||||
productName,
|
||||
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||
productCode,
|
||||
skuName: String(rule.skuName || productName || productCode).trim(),
|
||||
notes: String(rule.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function createRuleId() {
|
||||
return `feifei-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-loading="loading" class="feifei-panel">
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
:title="errorMessage"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<section class="config-section">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h3>平台连接</h3>
|
||||
<p>{{ filePath || 'data/kuaishou-feifei-config.json' }}</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :icon="RefreshRight" @click="loadConfig">刷新</el-button>
|
||||
<el-button type="primary" :icon="Check" :loading="saving" @click="saveConfig">
|
||||
保存配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feifei-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.summary-item,
|
||||
.config-section,
|
||||
.rule-item {
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.summary-item span,
|
||||
.summary-item p,
|
||||
.section-head p {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.summary-item strong {
|
||||
display: block;
|
||||
margin: 6px 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.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,
|
||||
.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);
|
||||
}
|
||||
|
||||
.match-bar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.match-result {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
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-list {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.rule-head {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.summary-grid,
|
||||
.config-form,
|
||||
.rule-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-head,
|
||||
.match-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user