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>
124 lines
4.8 KiB
Vue
124 lines
4.8 KiB
Vue
<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>
|