devperf/frontend/src/components/roi/admin/MappingPanel.vue
zyc 4a2ed8d414 feat(ui+perf): Editorial Data Console 重设计 + 接口性能 + ROI 权限锁
UI 重设计 (Editorial Data Console 风):
- 设计令牌系统: OKLCH 色彩 + Newsreader/Geist/JetBrains Mono 字体 + exp easing
- 全局表格基线 (.n-data-table 统一 editorial 风 + .table-shell 卡片容器)
- DataCard / Naive UI 主题对齐新 token (深墨青主色 + 暖琥珀强调)
- RoiDashboard: 3 KPI 卡片同字号 + chip 多色筛选 + section editorial 节奏
- ProjectRoiBoard: hero 卡 highlight + ytd-strip 节奏化 (10/13/15px 三层字号)
- ProjectList: 自适应卡片 + 产品线 NSelect 筛选 + 拆出独立"类型"列 + 文本链接操作
- RevenuePieChart 重设计: donut + 中心总额 + 底部水平图例 (替代外部 callout 截断)
- 全部页面 width:100% + clamp() 流体 padding,断点驱动 auto-fit 网格
- AppSidebar 项目子菜单按产品线分组 + 可折叠 + localStorage 持久化

接口性能优化 (N+1 → 批量 + Map 索引):
- /api/overview: 8.5s → 0.5s (17×) - 消除 3 处循环 SQL 查询
- /api/okr:     11.3s → 0.3s (37×) - getOKRByPeriod 一次性 inArray 批量
- ROI 三处时间窗 (aggregate/timeseries/events) launchedAt 截断对齐

ROI 权限锁:
- 全部 ROI 端点统一 admin (roiRoutes 全局 requireRole)
- 路由 /roi + /projects/:id/roi meta.roles=['admin']
- 侧边栏 ROI 入口 + 项目详情打标按钮/分类标签全部 v-if isAdmin

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:28:48 +08:00

124 lines
4.8 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, onMounted, h } from 'vue';
import {
NSpin, NButton, NDataTable, NModal, NForm, NFormItem, NInput, NSelect, NSwitch, NTag, useMessage,
} from 'naive-ui';
import { listMapping, createMapping, deleteMapping, listUnmapped } from '@/api/roi';
import request from '@/api/request';
const message = useMessage();
const loading = ref(true);
const mappings = ref<any[]>([]);
const unmapped = ref<any[]>([]);
const projectOptions = ref<{ label: string; value: string }[]>([]);
const showModal = ref(false);
const form = ref({ projectId: '', businessProjectKey: '', enabled: true, notes: '' });
async function load() {
loading.value = true;
try {
const [m, u, p] = await Promise.all([
listMapping(),
listUnmapped(),
request.get('/api/projects'),
]);
mappings.value = m.data.data || [];
unmapped.value = u.data.data || [];
projectOptions.value = (p.data.data || []).map((x: any) => ({
label: `${x.identifier || x.id} - ${x.name}`,
value: x.id,
}));
} finally { loading.value = false; }
}
onMounted(load);
async function handleCreate() {
if (!form.value.projectId || !form.value.businessProjectKey) {
message.warning('请填写所有必填项');
return;
}
try {
await createMapping(form.value);
message.success('已新增映射');
showModal.value = false;
form.value = { projectId: '', businessProjectKey: '', enabled: true, notes: '' };
await load();
} catch (e: any) {
message.error('新增失败:' + (e?.response?.data?.message || e.message));
}
}
async function handleDelete(id: string) {
if (!confirm('确认删除该映射?')) return;
await deleteMapping(id);
message.success('已删除');
await load();
}
const mappingColumns = [
{ title: '业务方 Key', key: 'businessProjectKey' },
{ title: 'DevPerf 项目', key: 'projectId' },
{ title: '启用', key: 'enabled', render: (row: any) => row.enabled ? '✅' : '⛔' },
{ title: '备注', key: 'notes' },
{ title: '操作', key: 'actions', render: (row: any) => h(NButton, {
size: 'tiny', type: 'error', onClick: () => handleDelete(row.id),
}, () => '删除') },
];
const unmappedColumns = [
{ title: '业务方 Key', key: 'businessProjectKey' },
{ title: '日期', key: 'eventDate', render: (row: any) => row.eventDate?.slice(0, 10) },
{ title: '金额', key: 'amount', render: (row: any) => `¥${Number(row.amount).toLocaleString()}` },
{ title: '类型', key: 'revenueType' },
{ title: '状态', key: 'status' },
];
</script>
<template>
<NSpin :show="loading">
<div style="margin-bottom:12px;color:var(--color-text-muted);font-size:13px">
把外部业务系统的"项目 key"映射到 DevPerf 项目新增映射后,未来抓到的营收数据自动归到对应项目;之前堆在"未映射"里的数据需手动处理
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<strong>当前映射 ({{ mappings.length }})</strong>
<NButton type="primary" size="small" @click="showModal = true">+ 添加映射</NButton>
</div>
<div class="table-shell">
<NDataTable :columns="mappingColumns" :data="mappings" size="small" :bordered="false" />
</div>
<div style="margin-top:24px">
<strong style="color:var(--color-text-muted)"> 未映射的营收事件 ({{ unmapped.length }})</strong>
<div style="font-size:12px;color:var(--color-text-muted);margin:6px 0">
外部 API 拉到但未匹配到 DevPerf 项目的营收事件,先放在收容表里待处理新增对应映射后,后续数据会自动归类
</div>
<div class="table-shell">
<NDataTable :columns="unmappedColumns" :data="unmapped" size="small" :bordered="false" :max-height="300" />
</div>
</div>
<NModal v-model:show="showModal" preset="card" title="新增项目映射" style="width:500px">
<NForm label-placement="top">
<NFormItem label="业务方项目 Key(外部系统的 key)">
<NInput v-model:value="form.businessProjectKey" placeholder="如 PROD-A001" />
</NFormItem>
<NFormItem label="对应 DevPerf 项目">
<NSelect v-model:value="form.projectId" :options="projectOptions" filterable />
</NFormItem>
<NFormItem label="启用">
<NSwitch v-model:value="form.enabled" />
</NFormItem>
<NFormItem label="备注(可选)">
<NInput v-model:value="form.notes" type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" />
</NFormItem>
<div style="display:flex;justify-content:flex-end;gap:8px">
<NButton @click="showModal = false">取消</NButton>
<NButton type="primary" @click="handleCreate">新增</NButton>
</div>
</NForm>
</NModal>
</NSpin>
</template>