优化了一些文件 增加多店铺
This commit is contained in:
@@ -68,6 +68,10 @@ const router = createRouter({
|
||||
path: 'webhook-events',
|
||||
component: () => import('@/views/admin/AdminWebhookEventsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'platform-shops',
|
||||
component: () => import('@/views/admin/AdminPlatformShopsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'webhook-events/:eventId',
|
||||
component: () => import('@/views/admin/AdminWebhookEventDetailView.vue'),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { apiGet, apiGetBlob, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminAgisoObservedShopItem,
|
||||
AdminAgisoShopConfigItem,
|
||||
AdminAuditLogItem,
|
||||
AdminTaskActionResponse,
|
||||
AdminCdkListItem,
|
||||
@@ -57,6 +59,21 @@ export function fetchAdminAuditLogs(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminAuditLogItem[]; pagination: AdminPagination }>('/api/v1/admin/audit-logs', params)
|
||||
}
|
||||
|
||||
export function fetchAdminAgisoShopConfigs() {
|
||||
return apiGet<{
|
||||
filePath: string
|
||||
shops: AdminAgisoShopConfigItem[]
|
||||
observedShops: AdminAgisoObservedShopItem[]
|
||||
}>('/api/v1/admin/platform-config/agiso-shops')
|
||||
}
|
||||
|
||||
export function saveAdminAgisoShopConfigs(payload: { shops: Array<Record<string, unknown>> }) {
|
||||
return apiPost<{
|
||||
filePath: string
|
||||
shops: AdminAgisoShopConfigItem[]
|
||||
}>('/api/v1/admin/platform-config/agiso-shops', payload)
|
||||
}
|
||||
|
||||
export function fetchAdminOrders(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminOrderListItem[]; pagination: AdminPagination }>('/api/v1/admin/orders', params)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,27 @@ export interface AdminAuditLogItem {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AdminAgisoShopConfigItem {
|
||||
shopId: string
|
||||
shopName: string
|
||||
accessToken: string
|
||||
accessTokenMasked: string
|
||||
enabled: boolean | null
|
||||
messageTemplate: string
|
||||
appSecretConfigured: boolean
|
||||
apiVersion: string
|
||||
sendMessageEndpoint: string
|
||||
}
|
||||
|
||||
export interface AdminAgisoObservedShopItem {
|
||||
shopId: string
|
||||
detectedShopName: string
|
||||
displayShopName: string
|
||||
latestSeenAt: string | null
|
||||
webhookEventCount: number
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
export interface AdminDashboardSummary {
|
||||
todayOrders: number
|
||||
paidPendingClaim: number
|
||||
|
||||
@@ -28,6 +28,7 @@ export function formatAuditAction(action: string) {
|
||||
inventory_cdk_released: '释放库存 CDK',
|
||||
inventory_cdk_invalidated: '作废库存 CDK',
|
||||
webhook_replayed: '重放 Webhook',
|
||||
platform_shop_config_updated: '更新店铺配置',
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(action, labelMap)
|
||||
@@ -39,6 +40,7 @@ export function formatAuditTargetType(targetType: string) {
|
||||
task: '交付任务',
|
||||
cdk: 'CDK 库存',
|
||||
webhook_event: 'Webhook 事件',
|
||||
platform_config: '平台配置',
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(targetType, labelMap)
|
||||
|
||||
@@ -20,6 +20,7 @@ const navItems = computed(() => {
|
||||
|
||||
if (isAdmin.value) {
|
||||
baseItems.splice(1, 0, { to: '/admin/users', label: '用户' })
|
||||
baseItems.push({ to: '/admin/platform-shops', label: '店铺配置' })
|
||||
baseItems.push({ to: '/admin/audit-logs', label: '审计' })
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { fetchAdminAgisoShopConfigs, saveAdminAgisoShopConfigs } from '@/services/admin'
|
||||
import type { AdminAgisoObservedShopItem, AdminAgisoShopConfigItem } from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type EditableShop = {
|
||||
id: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
accessToken: string
|
||||
enabled: boolean
|
||||
messageTemplate: string
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const filePath = ref('')
|
||||
const shops = ref<EditableShop[]>([])
|
||||
const observedShops = ref<AdminAgisoObservedShopItem[]>([])
|
||||
|
||||
function createEmptyShop(): EditableShop {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
shopId: '',
|
||||
shopName: '',
|
||||
accessToken: '',
|
||||
enabled: true,
|
||||
messageTemplate: '',
|
||||
}
|
||||
}
|
||||
|
||||
function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
shopId: item.shopId,
|
||||
shopName: item.shopName,
|
||||
accessToken: item.accessToken,
|
||||
enabled: item.enabled !== false,
|
||||
messageTemplate: item.messageTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfigs() {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminAgisoShopConfigs()
|
||||
filePath.value = response.data.filePath
|
||||
shops.value = response.data.shops.map(mapEditableShop)
|
||||
observedShops.value = response.data.observedShops
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取店铺配置失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function addShop() {
|
||||
shops.value.unshift(createEmptyShop())
|
||||
}
|
||||
|
||||
function removeShop(id: string) {
|
||||
shops.value = shops.value.filter((item) => item.id !== id)
|
||||
}
|
||||
|
||||
function importObservedShop(item: AdminAgisoObservedShopItem) {
|
||||
shops.value.unshift({
|
||||
id: crypto.randomUUID(),
|
||||
shopId: item.shopId,
|
||||
shopName: item.detectedShopName || item.displayShopName || '',
|
||||
accessToken: '',
|
||||
enabled: true,
|
||||
messageTemplate: '',
|
||||
})
|
||||
}
|
||||
|
||||
async function saveConfigs() {
|
||||
const payload = shops.value
|
||||
.map((item) => ({
|
||||
shopId: item.shopId.trim(),
|
||||
shopName: item.shopName.trim(),
|
||||
accessToken: item.accessToken.trim(),
|
||||
enabled: item.enabled,
|
||||
messageTemplate: item.messageTemplate.trim(),
|
||||
}))
|
||||
.filter((item) => item.shopId && item.accessToken)
|
||||
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const response = await saveAdminAgisoShopConfigs({ shops: payload })
|
||||
filePath.value = response.data.filePath
|
||||
shops.value = response.data.shops.map(mapEditableShop)
|
||||
ElMessage.success('店铺配置已保存')
|
||||
await loadConfigs()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存店铺配置失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadConfigs)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-panel">
|
||||
<header class="panel-header">
|
||||
<div>
|
||||
<h1>店铺配置</h1>
|
||||
<p>Agiso 多店铺配置改为文件维护。保存后立即生效,不需要再手改 `.env`。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button round @click="loadConfigs">刷新</el-button>
|
||||
<el-button round type="primary" @click="addShop">新增店铺</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以维护店铺配置。</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">配置文件</span>
|
||||
<code>{{ filePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">说明</span>
|
||||
<span>店铺级 `accessToken` 优先于全局 `AGISO_ACCESS_TOKEN`。未配置的店铺仍会继续使用全局兜底。</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
|
||||
<div v-if="loading" class="empty-block">店铺配置加载中</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>已配置店铺</h3>
|
||||
<p>至少填写 `shopId` 和 `accessToken`。`shopName` 为空时,界面会退回显示店铺 ID。</p>
|
||||
</div>
|
||||
<el-button :loading="saving" round type="primary" @click="saveConfigs">保存配置</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="shops.length === 0" class="empty-inline">当前还没有店铺配置,先新增一条。</div>
|
||||
|
||||
<div v-for="shop in shops" :key="shop.id" class="shop-card">
|
||||
<div class="shop-grid">
|
||||
<label class="field-block">
|
||||
<span>店铺 ID</span>
|
||||
<input v-model="shop.shopId" class="text-input" placeholder="例如 693760716" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>店铺名称</span>
|
||||
<input v-model="shop.shopName" class="text-input" placeholder="例如 大锤商行" />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>AccessToken</span>
|
||||
<input v-model="shop.accessToken" class="text-input" placeholder="AldsIdle..." />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>消息模板</span>
|
||||
<textarea
|
||||
v-model="shop.messageTemplate"
|
||||
class="text-area"
|
||||
placeholder="留空则沿用全局 AGISO_MESSAGE_TEMPLATE"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="shop-actions">
|
||||
<label class="checkbox-line">
|
||||
<input v-model="shop.enabled" type="checkbox" />
|
||||
<span>启用店铺级覆盖</span>
|
||||
</label>
|
||||
<el-button link type="danger" @click="removeShop(shop.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>近期识别到的店铺</h3>
|
||||
<p>从 webhook 自动识别出的店铺,可一键导入配置列表。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>店铺</th>
|
||||
<th>Webhook 次数</th>
|
||||
<th>最近出现时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in observedShops" :key="item.shopId">
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.displayShopName || item.shopId }}</strong>
|
||||
<span class="cell-subtle">ID: {{ item.shopId }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.webhookEventCount }}</td>
|
||||
<td>{{ formatAdminDateTime(item.latestSeenAt) }}</td>
|
||||
<td>{{ item.configured ? '已配置' : '未配置' }}</td>
|
||||
<td>
|
||||
<el-button
|
||||
v-if="!item.configured"
|
||||
link
|
||||
type="primary"
|
||||
@click="importObservedShop(item)"
|
||||
>
|
||||
导入到配置
|
||||
</el-button>
|
||||
<span v-else class="cell-subtle">已存在</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="observedShops.length === 0">
|
||||
<td colspan="5" class="empty-inline">最近还没有识别到可用店铺。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-panel {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-header,
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.panel-header h1,
|
||||
.section-title-row h3 {
|
||||
margin: 0;
|
||||
color: #1d3555;
|
||||
}
|
||||
|
||||
.panel-header p,
|
||||
.section-title-row p {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.meta-card,
|
||||
.table-card,
|
||||
.empty-block,
|
||||
.error-copy {
|
||||
padding: 18px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(86, 108, 138, 0.1);
|
||||
}
|
||||
|
||||
.meta-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.meta-line {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.shop-card {
|
||||
margin-top: 14px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.shop-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.field-block {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-block span {
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.text-input,
|
||||
.text-area {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(86, 108, 138, 0.18);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.text-area {
|
||||
min-height: 90px;
|
||||
padding: 12px 14px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.shop-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-line {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 12px 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(86, 108, 138, 0.08);
|
||||
color: #334155;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cell-stack {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cell-subtle {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty-block,
|
||||
.empty-inline {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.error-copy {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.panel-header,
|
||||
.section-title-row,
|
||||
.shop-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.shop-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user