This commit is contained in:
shijing 2026-07-01 15:57:24 +08:00
commit 4eb54c63f6
7 changed files with 148 additions and 50 deletions

View File

@ -21,8 +21,11 @@ VUE_APP_PJ = 'gx'
# VUE_APP_BASEURL = http://10.50.211.228:2250/
VUE_APP_API_BASEURL = http://127.0.0.1:2226/api
VUE_APP_BASEURL = http://127.0.0.1:2226
# VUE_APP_API_BASEURL = http://127.0.0.1:2226/api
# VUE_APP_BASEURL = http://127.0.0.1:2226
VUE_APP_API_BASEURL = http://10.0.11.51:2250/api
VUE_APP_BASEURL = http://10.0.11.51:2250
# 托克逊
# VUE_APP_API_BASEURL = http://10.50.211.228:2250/api

View File

@ -6,11 +6,24 @@ export default {
name: "列表",
req: async function(data){
return await http.post(
`${config.HOST_URL}/hfnf_api/mplogx/`,
// 同源路径,由 nginx(/factory_api/) 反向代理到统计服务 10.0.11.52:5800
`${config.HOST_URL}/factory_api/mplogx/`,
// "http://10.0.11.52:5800/mplogx/",
data
);
}
},
},
stats: {
list: {
name: "数据统计",
req: async function(params){
return await http.get(
// 同源路径,由 nginx(/factory_api/) 反向代理到统计服务 10.0.11.52:5800
`${config.HOST_URL}/factory_api/stats`,
params
);
}
},
}
}

View File

@ -414,8 +414,8 @@ export default {
}
this.pmenu = this.$route.meta.breadcrumb
? this.$route.meta.breadcrumb[0]
: {};
let lists = this.pmenu.children;
: { meta: {}, children: [] };
let lists = this.pmenu.children || [];
let perms = this.$TOOL.data.get("PERMISSIONS") || [];
for (var i = 0; i < lists.length; i++) {
if (lists[i].meta && lists[i].meta.perms) {

View File

@ -55,7 +55,14 @@ const _ResizeObserver = window.ResizeObserver;
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
constructor(callback) {
callback = debounce(callback, 16);
super(callback);
// 先包 try/catch 再 debouncedebounce 用 setTimeout 延迟执行,
// 必须让真正执行回调时也在 try/catch 内,否则元素卸载瞬间
// getComputedStyle 拿到非 Element 的错误会逃出 setTimeout 成为未捕获异常
const safe = (entries, observer) => {
try {
callback(entries, observer);
} catch (e) {}
};
super(debounce(safe, 16));
}
};

View File

@ -7,7 +7,21 @@ const routes = [
path: "/",
component: () => import(/* webpackChunkName: "layout" */ '@/layout'),
redirect: config.DASHBOARD_URL || getBaseDashboard(),
children: []
//固定页:通过按钮跳转、不依赖菜单(动态/静态)的页面
children: [
{
name: "hfnf_mplogx",
path: "/hfnf_mplogx",
meta: { title: "测点数据明细", hidden: true },
component: () => import('@/views/fac_cal/hfnf_mplogx'),
},
{
name: "cement_data_template",
path: "/cement_data_template",
meta: { title: "数据模板", hidden: true },
component: () => import('@/views/fac_cal/cement_data_template'),
},
]
},
{
path: "/login",

View File

@ -18,6 +18,8 @@ axios.interceptors.request.use(
let token = tool.data.get("TOKEN");
if(token){
config.headers[sysConfig.TOKEN_NAME] = sysConfig.TOKEN_PREFIX + token
// 记录本次请求实际使用的 token用于 401 时判断是否已被并发刷新过
config._tokenUsed = token;
}
if(!sysConfig.REQUEST_CACHE && config.method == 'get'){
config.params = config.params || {};
@ -49,13 +51,43 @@ axios.interceptors.response.use(
});
}else{
if (error.config.url.indexOf('auth/token/refresh/') != -1) {
// 刷新接口自身 401refresh token 已失效,清除登录态并跳登录
isRefreshing = false;
requests=[];
requests = [];
tool.data.remove("TOKEN");
tool.data.remove("TOKEN_REFRESH");
console.warn('[auth] refresh token 失效,跳转登录页');
router.push('/login');
return
return Promise.reject(error.response);
}
const currentToken = tool.data.get("TOKEN");
// 已无 tokenrefresh 失败已清登录态并跳登录):并发请求直接拒绝,
// 不再重试/刷新,避免误报“无权限”
if (!currentToken) {
return Promise.reject(error.response);
}
// 本次请求用的 token 已和当前 localStorage 里的不一致,
// 说明期间已被并发刷新过,直接用最新 token 重试一次即可,不再触发刷新
// (避免快速点击时多个晚到 401 引发刷新风暴)
if (error.config._tokenUsed && error.config._tokenUsed !== currentToken && !error.config._retriedAfterRefresh) {
error.config._tokenUsed = currentToken;
error.config._retriedAfterRefresh = true;
return axios(error.config);
}
// 已刷新过 token 仍然 401说明不是 token 过期,而是无权限/持久 401。
// 不再刷新、不跳登录,只提示无权限,避免刷新循环把会话耗到失效。
if (error.config._retriedAfterRefresh) {
console.warn('[auth] 刷新后仍 401判定为无权限不跳登录', error.config.url);
ElNotification.warning({
title: '无权限',
message: (error.response.data && error.response.data.err_msg) || '当前账号无权限访问该资源'
});
return Promise.reject(error.response);
}
if (!isRefreshing) {
isRefreshing = true;
// 必须 return 刷新 Promise否则即便刷新成功原始请求也会被 reject重试结果被丢弃
return new Promise((resolve, reject) => {
axios({
method: 'post',
url: sysConfig.API_URL+'/auth/token/refresh/',
@ -67,17 +99,34 @@ axios.interceptors.response.use(
// 替换本地的token
tool.data.set("TOKEN", res.data.access);
tool.data.set("TOKEN_TIME", new Date().getTime());
// 若后端开启了 refresh token 轮换,需同步更新本地 refresh token
// 否则旧的被拉黑后,下一次刷新就会 401 → 跳登录admin 也会偶发被踢)
if (res.data && res.data.refresh) {
tool.data.set("TOKEN_REFRESH", res.data.refresh);
}
error.config.headers[sysConfig.TOKEN_NAME] = sysConfig.TOKEN_PREFIX + res.data.access;
error.config._retriedAfterRefresh = true; // 标记:重试后若仍 401 则视为无权限
isRefreshing = false;
requests.forEach(cb => cb())
//发起后将requests置空
requests=[];
return axios(error.config)
const queue = requests; requests = [];
queue.forEach(cb => cb())
resolve(axios(error.config))
}).catch(err=>{
// 刷新失败:复位状态,清登录态,跳登录
isRefreshing = false;
requests = [];
tool.data.remove("TOKEN");
tool.data.remove("TOKEN_REFRESH");
console.warn('[auth] token 刷新失败,跳转登录页', err);
router.push('/login');
reject(err);
})
})
}else{
//如果正在刷新token将发起的请求暂存在requests中
return new Promise((resolve)=>{
requests.push(()=>{
error.config._retriedAfterRefresh = true;
resolve(axios(error.config));
})
})

View File

@ -23,7 +23,7 @@
<!-- 工厂数据卡片 -->
<el-row :gutter="16" class="factory-row">
<el-col :xs="24" :sm="12" :md="6" v-for="(item, index) in factories" :key="item.name">
<div class="factory-card" :style="{ borderTopColor: item.color }" @click="goDetail">
<div class="factory-card" :style="{ borderTopColor: item.color }">
<div class="card-header">
<div class="card-icon" :style="{ backgroundColor: item.bgColor }">
<el-icon :size="24" :color="item.color"><OfficeBuilding /></el-icon>
@ -193,30 +193,32 @@ let pieChart = null
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c']
const bgColors = ['#ecf5ff', '#f0f9eb', '#fdf6ec', '#fef0f0']
//
const baseFactories = [
{ name: '合肥南方', ratio: 0.4382 },
{ name: '中联万吨', ratio: 0.3081 },
{ name: '铜梁水泥', ratio: 0.1424 },
{ name: '槐坎南方水泥', ratio: 0.1113 },
//
const factoryDefs = [
{ name: '合肥南方', match: (t) => t === 'mplogx' }, //
{ name: '中联万吨', match: (t) => t === 'mplogx_xzzl' }, //
{ name: '铜梁水泥', match: (t) => t === 'mplogx_tlxn' }, //
{ name: '槐坎南方水泥', match: (t) => t.startsWith('mplogx_hknf') }, //
]
const totalCount = ref(0)
const tables = ref([])
const factories = computed(() => {
const total = totalCount.value
// 33
const counts = baseFactories.map(f => Math.round(total * f.ratio))
const sumFirst3 = counts[0] + counts[1] + counts[2]
counts[3] = total - sumFirst3
return baseFactories.map((f, i) => ({
return factoryDefs.map((f, i) => {
//
const count = tables.value
.filter((t) => f.match(t.table_name))
.reduce((sum, t) => sum + (t.approximate_row_count || 0), 0)
return {
name: f.name,
count: counts[i],
count,
color: colors[i],
bgColor: bgColors[i],
percentage: total > 0 ? (counts[i] / total) * 100 : 0,
}))
percentage: total > 0 ? (count / total) * 100 : 0,
}
})
})
const maxFactory = computed(() => {
@ -336,19 +338,29 @@ const handleResize = () => {
pieChart?.resize()
}
// 退
const handlePopState = () => {
router.push('/cement_data_template')
}
onMounted(() => {
API.hfnf.mplogx.list.req({ page: 1, page_size: 1 }).then(res => {
totalCount.value = res.count
API.hfnf.stats.list.req().then(res => {
tables.value = res.tables || []
totalCount.value = res.summary?.approximate_row_count_total || 0
nextTick(() => {
initBarChart()
initPieChart()
})
})
window.addEventListener('resize', handleResize)
// 使退 popstate
history.pushState(null, '', location.href)
window.addEventListener('popstate', handlePopState)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('popstate', handlePopState)
barChart?.dispose()
pieChart?.dispose()
})