refactor(cma-frontend): route all API calls through shared axios instance (cma-frontend-axios-unify-20260915)

api/index.ts: add permissionApi.getModules/getConfig/saveConfig + alignmentApi.config/tree.

views: replace 13 raw fetch calls and 6 local axios.create instances (each hand-building the Authorization header, without the shared 401 redirect / timeout toast) with the shared instance from api/index.ts.

KPIAlignment: fixes a live bug - the header value was the string literal 'Bearer ' + token, so /alignment/tree always returned 401 (reproduced against the production bundle).

Unwrap semantics preserved: the shared response interceptor returns response.data, so call sites that used res.data were adjusted accordingly.

Verified: scan-frontend-tokenless RAW-TOKENLESS=0; vite build ok; headless A/B (new dist vs production bundle, 8 affected pages, real login) -> identical rendered rows and identical pre-existing error profile, /alignment 401->200.

Push not executed: main is ahead 4 with other tickets' gated commits (see cma-main-ahead3-push-gate-notice-20260915).
This commit is contained in:
Hermes CI Fix
2026-09-15 22:21:56 +08:00
parent e5eb092d20
commit 7b74736980
9 changed files with 73 additions and 133 deletions
+10
View File
@@ -339,10 +339,20 @@ export const roleApi = {
export const permissionApi = {
getMenuTree: () => api.get('/permissions/menu-tree'),
getModules: () => api.get('/permissions/modules'),
getConfig: () => api.get('/permissions/config'),
saveConfig: (data: any) => api.put('/permissions/config', data),
getUserRoles: () => api.get('/user-roles'),
setUserRoles: (data: any) => api.post('/user-roles', data),
}
// ── KPI目标对齐(/alignment/* 由路由级 require_auth 保护,统一走本实例)──
export const alignmentApi = {
getConfig: () => api.get('/alignment/config'),
saveConfig: (data: any) => api.post('/alignment/config', data),
getTree: (params?: any) => api.get('/alignment/tree', { params }),
}
export const kpiCausalityApi = {
list: (params?: any) => api.get('/kpi-causality', { params }),
get: (id: number) => api.get(`/kpi-causality/${id}`),
+9 -16
View File
@@ -271,8 +271,8 @@ import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Loading, ArrowRight } from '@element-plus/icons-vue'
import { dashboardApi, alertApi, aiSuggestionApi } from '../api/index'
import axios from 'axios'
import { dashboardApi, alertApi, aiSuggestionApi, notificationApi } from '../api/index'
import api from '../api/index'
import CountUp from '../components/CountUp.vue'
import GrowthQuality from '../components/GrowthQuality.vue'
import BEPBreakEvenCard from '../components/BEPBreakEvenCard.vue'
@@ -346,12 +346,7 @@ async function loadSuggestions() {
suggestionsLoading.value = false
}
const aiApi = axios.create({ baseURL: '/api/cma', timeout: 30000 })
aiApi.interceptors.request.use((config: any) => {
const token = localStorage.getItem('cma_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// AI 分析等接口统一走 api/index.ts 的共享实例(此前本地 aiApi 手拼 Bearer 已移除)
const dimConfig: Record<string, { icon: string; color: string; name: string }> = {
finance: { icon: '💰', color: '#409eff', name: '财务维度' },
@@ -423,8 +418,8 @@ async function loadKPIAnalysis() {
if (!selectedKPI.value?.id) return
loadingDetail.value = true
try {
const r: any = await aiApi.get(`/ai/kpi-analysis/${selectedKPI.value.id}`)
selectedKPIDetail.value = r.data?.analysis || r.analysis || '暂无分析结果'
const r: any = await api.get(`/ai/kpi-analysis/${selectedKPI.value.id}`)
selectedKPIDetail.value = r?.analysis || '暂无分析结果'
} catch { selectedKPIDetail.value = 'AI分析请求失败' }
loadingDetail.value = false
}
@@ -432,8 +427,8 @@ async function loadKPIAnalysis() {
async function refreshAnalysis() {
aiLoading.value = true
try {
const r: any = await aiApi.get('/ai/dashboard-analysis', { params: { role: userRole.value } })
aiAnalysis.value = r.data?.analysis || r.analysis || '暂无分析'
const r: any = await api.get('/ai/dashboard-analysis', { params: { role: userRole.value } })
aiAnalysis.value = r?.analysis || '暂无分析'
// 建议列表从服务端拉取(含dashboard分析自动生成的规则建议)
await loadSuggestions()
} catch { aiAnalysis.value = 'AI分析暂时不可用' }
@@ -482,10 +477,8 @@ async function loadData() {
finSummary.value = (r as any)?.summary || {}
dataRange.value = (r as any)?.period || ''
} else if (userRole.value === 'it') {
// 鉴权收口(cma-api-auth-surface-audit-20260914):/notifications/channels 已要求 Bearer
const channelRes = await axios.get('/api/cma/notifications/channels', {
headers: { 'Authorization': `Bearer ${localStorage.getItem('cma_token')}` },
}).then(r => r.data)
// 鉴权收口(cma-api-auth-surface-audit-20260914)+ 实例统一(2026-09-15):走共享实例,Bearer 由拦截器注入
const channelRes: any = await notificationApi.list()
enabledChannels.value = channelRes.data?.filter((c: any) => c.enabled).length || 0
}
} catch (e) { console.error('加载数据失败:', e) }
+6 -12
View File
@@ -176,14 +176,8 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const api = axios.create({ baseURL: '/api/cma', timeout: 30000 })
api.interceptors.request.use((config: any) => {
const token = localStorage.getItem('cma_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// 统一走 api/index.ts 的 axios 实例(拦截器自动带 Bearer / 401 跳登录)
import api from '../api/index'
const userRole = computed(() => (localStorage.getItem('cma_role') || 'finance'))
const canWrite = computed(() => ['ceo', 'finance', 'it'].includes(userRole.value))
@@ -221,7 +215,7 @@ function dimLabel(d: string) {
async function loadStats() {
try {
const r = await api.get('/data-classification/stats')
stats.value = (r as any).data || {}
stats.value = (r as any) || {}
} catch (e) {
stats.value = {}
}
@@ -236,7 +230,7 @@ async function loadInventory() {
if (onlyImportant.value) params.important = 1
if (keyword.value) params.keyword = keyword.value
const r = await api.get('/data-classification/inventory', { params })
const d = (r as any).data || {}
const d = (r as any) || {}
items.value = d.items || []
totalItems.value = d.total || 0
markedCount.value = (d.stats || {}).marked || 0
@@ -251,7 +245,7 @@ async function loadInventory() {
async function loadIndustryRef() {
try {
const r = await api.get('/data-classification/industry-reference')
industryRef.value = (r as any).data?.items || []
industryRef.value = (r as any)?.items || []
} catch (e) {
industryRef.value = []
}
@@ -322,7 +316,7 @@ async function doExport() {
if (filterLevel.value) params.data_level = filterLevel.value
if (onlyImportant.value) params.important = 1
const r: any = await api.get('/data-classification/export', { params, responseType: 'blob' })
const blob = r.data
const blob = r
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
+2 -9
View File
@@ -112,21 +112,14 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { sessionEntityName } from '../utils/entity'
// 统一走 api/index.ts 的 axios 实例(拦截器自动带 Bearer / 401 跳登录;语义同为「已剥层」)
import api from '../api/index'
const loading = ref(false)
const entityLabel = ref(sessionEntityName())
const data = ref<any>({})
const api = axios.create({ baseURL: '/api/cma', timeout: 15000 })
api.interceptors.request.use((config: any) => {
const token = localStorage.getItem('cma_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
api.interceptors.response.use((r: any) => r.data, (e: any) => Promise.reject(e))
function formatPct(val: number): string {
if (val === null || val === undefined) return '-'
return val.toFixed(2)
+9 -21
View File
@@ -137,6 +137,8 @@
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
// 统一走 api/index.ts 的 axios 实例(拦截器自动带 Bearer / 401 跳登录)
import { alignmentApi, mapApi } from '../api/index'
const route = useRoute()
const router = useRouter()
@@ -156,10 +158,8 @@ const selectedMapId = ref<number | null>(null)
async function loadMapList() {
try {
const token = localStorage.getItem('cma_token')
const r = await fetch('/api/cma/maps', {
headers: { 'Authorization': `Bearer ${token}` },
}).then(r => r.json())
// 统一走 api 实例(Bearer 由拦截器注入,不再手拼 header)
const r: any = await mapApi.list()
mapList.value = r.data || []
} catch {}
}
@@ -271,11 +271,8 @@ const devClass = (d: any) => {
async function loadConfig() {
try {
// 鉴权收口(cma-api-auth-surface-audit-20260914):/alignment/* 已要求 Bearer,补 token
const token = localStorage.getItem('cma_token')
const r = await fetch('/api/cma/alignment/config', {
headers: { 'Authorization': `Bearer ${token}` },
}).then(r => r.json())
// 统一走 api 实例(Bearer 由拦截器注入)
const r: any = await alignmentApi.getConfig()
alignModes.value = r.modes || []
alignConfigured.value = r.configured || false
if (r.configured && r.mode) {
@@ -291,12 +288,8 @@ async function saveAlignConfig() {
if (!selectedMode.value) return
saving.value = true
try {
const token = localStorage.getItem('cma_token')
await fetch('/api/cma/alignment/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `'Bearer ' + token` },
body: JSON.stringify({ mode: selectedMode.value }),
}).then(r => r.json())
// 修复:原 `'Bearer ' + token` 模板字面量 bug(header 值被写死成字面量 → 必 401)
await alignmentApi.saveConfig({ mode: selectedMode.value })
ElMessage.success('对齐模式已设置')
currentMode.value = alignModes.value.find(m => m.key === selectedMode.value) || null
alignConfigured.value = true
@@ -309,12 +302,7 @@ async function saveAlignConfig() {
async function loadTree() {
loading.value = true
try {
const token = localStorage.getItem('cma_token')
let url = '/api/cma/alignment/tree'
if (selectedMapId.value) url += `?map_id=${selectedMapId.value}`
const r = await fetch(url, {
headers: { 'Authorization': `'Bearer ' + token` },
}).then(r => r.json())
const r: any = await alignmentApi.getTree(selectedMapId.value ? { map_id: selectedMapId.value } : undefined)
treeData.value = r
} catch (e: any) {
ElMessage.error(e.message || '加载失败')
+3 -9
View File
@@ -155,14 +155,8 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const api = axios.create({ baseURL: '/api/cma', timeout: 30000 })
api.interceptors.request.use((config: any) => {
const token = localStorage.getItem('cma_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// 统一走 api/index.ts 的 axios 实例(拦截器自动带 Bearer / 401 跳登录)
import api from '../api/index'
// ── 状态 ──
const period = ref('2026-06')
@@ -243,7 +237,7 @@ async function calculate() {
})),
}
const r = await api.post('/reports/mpm-calculate', payload)
result.value = (r as any).data || {}
result.value = (r as any) || {}
} catch (e) {
ElMessage.error('计算失败,请检查参数')
result.value = null
+17 -22
View File
@@ -619,7 +619,8 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
// 统一走 api/index.ts 的 axios 实例(拦截器自动带 Bearer / 统一 401 跳登录 / 超时提示)
import api from '../api/index'
import { getFixedEntityId } from '../utils/entity'
// ── 状态 ──
@@ -644,13 +645,7 @@ function getEntityId() {
return Number(localStorage.getItem('cma_entity_id') || currentEntityId.value || 1)
}
// API 配置
const api = axios.create({ baseURL: '/api/cma', timeout: 30000 })
api.interceptors.request.use((config: any) => {
const token = localStorage.getItem('cma_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// API 配置:统一使用 api/index.ts 的共享实例(本地 axios 实例 + 手拼 Bearer 已移除)
// ── 报表1:管理利润表 ──
const profitItems = ref<any[]>([])
@@ -688,7 +683,7 @@ async function loadNew30() {
params.format = 'new'
}
const r = await api.get('/reports/profit-statement', { params })
const d = (r as any).data || {}
const d = (r as any) || {}
if (new30Format.value === 'dual') {
dualComparisonData.value = d
@@ -749,7 +744,7 @@ async function loadStatutory() {
statutoryLoading.value = true
try {
const r = await api.get('/reports/statutory', { params: { period: reportPeriod.value, entity_id: getEntityId() } })
const d = (r as any).data || {}
const d = (r as any) || {}
statutoryData.value = d
statutoryDemoMode.value = !d.cash_flow?.all_items_have_data
} catch (e) {
@@ -762,11 +757,11 @@ async function loadStatutory() {
async function exportStatutory() {
exporting.value = true
try {
const token = localStorage.getItem('cma_token') || ''
const url = `/api/cma/reports/statutory/export?period=${reportPeriod.value}&entity_id=${getEntityId()}`
const resp = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } })
if (!resp.ok) throw new Error('导出失败')
const blob = await resp.blob()
// 统一走 api 实例(responseType=blob;Bearer 由拦截器注入,非 2xx 直接 reject)
const blob: any = await api.get('/reports/statutory/export', {
params: { period: reportPeriod.value, entity_id: getEntityId() },
responseType: 'blob',
})
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = `对外法定报表_${reportPeriod.value}.xlsx`
@@ -811,7 +806,7 @@ async function loadRestatement() {
loadingRestatement.value = true
try {
const r = await api.get('/reports/restatement', { params: { period: reportPeriod.value, entity_id: getEntityId() } })
const d = (r as any).data || {}
const d = (r as any) || {}
restatementItems.value = d.items || []
restatementSummary.value = d.summary || null
restatementNetProfit.value = d.net_profit_comparison || null
@@ -864,7 +859,7 @@ function onPeriodChange() {
async function loadProfit() {
try {
const r = await api.get('/reports/profit-summary', { params: { period: reportPeriod.value, entity_id: getEntityId() } })
profitItems.value = (r as any).data?.items || []
profitItems.value = (r as any)?.items || []
} catch { profitItems.value = [] }
}
@@ -874,7 +869,7 @@ async function loadBudget() {
if (budgetDim.value) params.dimension = budgetDim.value
if (budgetLevel.value) params.alert_level = budgetLevel.value
const r = await api.get('/reports/budget-execution', { params })
const d = (r as any).data || {}
const d = (r as any) || {}
budgetSummary.value = d.summary || {}
budgetItems.value = d.items || []
} catch { budgetItems.value = [] }
@@ -925,7 +920,7 @@ async function loadProforma() {
cash: '/reports/proforma/cash-flow',
}
const r = await api.get(map[proformaType.value], { params: { period: reportPeriod.value, entity_id: getEntityId() } })
proformaData.value = (r as any).data || {}
proformaData.value = (r as any) || {}
} catch (e) {
proformaData.value = null
ElMessage.error('加载预编报表失败')
@@ -953,7 +948,7 @@ async function loadTrends() {
if (trendKpiId.value) params.kpi_id = trendKpiId.value
else if (trendDim.value) params.dimension = trendDim.value
const r = await api.get('/reports/kpi-trends', { params })
trendData.value = (r as any).data?.data || []
trendData.value = (r as any)?.data || []
nextTick(() => renderTrendCharts())
} catch { trendData.value = [] }
}
@@ -961,14 +956,14 @@ async function loadTrends() {
async function loadBsc() {
try {
const r = await api.get('/reports/bsc-scorecard', { params: { period: reportPeriod.value, entity_id: getEntityId() } })
bscData.value = (r as any).data || {}
bscData.value = (r as any) || {}
} catch { bscData.value = {} }
}
async function loadKpiOptions() {
try {
const r = await api.get('/kpis', { params: { page_size: 100, entity_id: getEntityId() } })
kpiOptions.value = (r as any).data?.data || []
kpiOptions.value = (r as any)?.data || []
} catch {}
}
+14 -35
View File
@@ -141,6 +141,8 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 统一走 api/index.ts 的 axios 实例(拦截器自动带 Bearer / 401 跳登录)
import { permissionApi, alignmentApi } from '../api/index'
// ── Tab 控制 ──
const activeTab = ref('permissions')
@@ -200,14 +202,10 @@ function toggleAction(role: string, actionKey: string, value: boolean) {
async function savePermConfig() {
saving.value = true
try {
const token = localStorage.getItem('cma_token')
await fetch('/api/cma/permissions/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({
route_permissions: routePerms.value,
action_permissions: actionPerms.value,
}),
// 统一走 api 实例(Bearer 由拦截器注入,不再手拼 header)
await permissionApi.saveConfig({
route_permissions: routePerms.value,
action_permissions: actionPerms.value,
})
ElMessage.success('权限配置已保存')
permSaved.value = true
@@ -217,13 +215,8 @@ async function savePermConfig() {
async function resetPermConfig() {
try { await ElMessageBox.confirm('确定恢复默认权限配置?', '确认', { center: true }) } catch { return }
const token = localStorage.getItem('cma_token')
try {
await fetch('/api/cma/permissions/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({})
})
await permissionApi.saveConfig({})
ElMessage.success('已恢复默认')
loadPermData()
} catch { ElMessage.error('恢复失败') }
@@ -231,12 +224,9 @@ async function resetPermConfig() {
async function loadPermData() {
try {
// 鉴权收口(cma-api-auth-surface-audit-20260914):/permissions/* 已要求 Bearer,补 token
const token = localStorage.getItem('cma_token')
const authHeaders = { 'Authorization': `Bearer ${token}` }
const [modRes, cfgRes] = await Promise.all([
fetch('/api/cma/permissions/modules', { headers: authHeaders }).then(r => r.json()),
fetch('/api/cma/permissions/config', { headers: authHeaders }).then(r => r.json()),
const [modRes, cfgRes]: any[] = await Promise.all([
permissionApi.getModules(),
permissionApi.getConfig(),
])
modules.value = modRes.modules || []
actions.value = modRes.actions || []
@@ -275,11 +265,8 @@ function feedsLabel(feeds: string[] | null) {
async function loadAlignmentConfig() {
try {
// 鉴权收口(cma-api-auth-surface-audit-20260914):/alignment/* 已要求 Bearer,补 token
const token = localStorage.getItem('cma_token')
const r = await fetch('/api/cma/alignment/config', {
headers: { 'Authorization': `Bearer ${token}` },
}).then(r => r.json())
// 统一走 api 实例(Bearer 由拦截器注入)
const r: any = await alignmentApi.getConfig()
alignmentModes.value = r.modes || []
alignConfigured.value = r.configured || false
currentMode.value = null
@@ -295,12 +282,7 @@ async function saveAlignmentConfig() {
if (!selectedMode.value) return
savingAlign.value = true
try {
const token = localStorage.getItem('cma_token')
const r = await fetch('/api/cma/alignment/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ mode: selectedMode.value }),
}).then(r => r.json())
const r: any = await alignmentApi.saveConfig({ mode: selectedMode.value })
ElMessage.success(r.message || '对齐模式已设置')
loadAlignmentConfig()
showSelector.value = false
@@ -311,10 +293,7 @@ async function saveAlignmentConfig() {
async function previewTree() {
loadingTree.value = true
try {
const token = localStorage.getItem('cma_token')
const r = await fetch('/api/cma/alignment/tree', {
headers: { 'Authorization': `Bearer ${token}` },
}).then(r => r.json())
const r: any = await alignmentApi.getTree()
treeData.value = r
} catch (e: any) {
ElMessage.error(e.message || '加载失败')
+3 -9
View File
@@ -99,14 +99,8 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const api = axios.create({ baseURL: '/api/cma', timeout: 30000 })
api.interceptors.request.use((config: any) => {
const token = localStorage.getItem('cma_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// 统一走 api/index.ts 的 axios 实例(拦截器自动带 Bearer / 401 跳登录)
import api from '../api/index'
const loading = ref(false)
const subjects = ref<any[]>([])
@@ -127,7 +121,7 @@ async function loadSubjects() {
if (filterCategory.value) params.category = filterCategory.value
if (keyword.value) params.keyword = keyword.value
const r = await api.get('/subjects', { params })
const d = (r as any).data || {}
const d = (r as any) || {}
subjects.value = d.data || []
totalSubjects.value = d.total || 0
} catch (e) {