feat:大屏工厂名修正与无感刷新修复
- hfnf大屏工厂ABCD改为真实名称,顶部卡片去除点击跳转,浏览器后退跳数据模板页 - request.js修复无感刷新:刷新成功后正确重试(原代码未return导致重试被丢弃)、刷新失败复位isRefreshing、并发401用token比对防刷新风暴、token清空时静默拒绝避免误报无权限 - main.js修复ResizeObserver在元素卸载时getComputedStyle非Element报错(try/catch包到debounce内层) - systemRouter新增hfnf_mplogx/cement_data_template固定子路由 - hfnf接口改走factory_api反向代理,新增stats统计接口 - layout修复pmenu.children为空时的报错 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1ee913c8cc
commit
ec6c3fc806
|
|
@ -21,8 +21,11 @@ VUE_APP_PJ = 'gx'
|
||||||
|
|
||||||
# VUE_APP_BASEURL = http://10.50.211.228:2250/
|
# VUE_APP_BASEURL = http://10.50.211.228:2250/
|
||||||
|
|
||||||
VUE_APP_API_BASEURL = http://127.0.0.1:2226/api
|
# VUE_APP_API_BASEURL = http://127.0.0.1:2226/api
|
||||||
VUE_APP_BASEURL = http://127.0.0.1:2226
|
# 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
|
# VUE_APP_API_BASEURL = http://10.50.211.228:2250/api
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,24 @@ export default {
|
||||||
name: "列表",
|
name: "列表",
|
||||||
req: async function(data){
|
req: async function(data){
|
||||||
return await http.post(
|
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/",
|
// "http://10.0.11.52:5800/mplogx/",
|
||||||
data
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -414,8 +414,8 @@ export default {
|
||||||
}
|
}
|
||||||
this.pmenu = this.$route.meta.breadcrumb
|
this.pmenu = this.$route.meta.breadcrumb
|
||||||
? this.$route.meta.breadcrumb[0]
|
? this.$route.meta.breadcrumb[0]
|
||||||
: {};
|
: { meta: {}, children: [] };
|
||||||
let lists = this.pmenu.children;
|
let lists = this.pmenu.children || [];
|
||||||
let perms = this.$TOOL.data.get("PERMISSIONS") || [];
|
let perms = this.$TOOL.data.get("PERMISSIONS") || [];
|
||||||
for (var i = 0; i < lists.length; i++) {
|
for (var i = 0; i < lists.length; i++) {
|
||||||
if (lists[i].meta && lists[i].meta.perms) {
|
if (lists[i].meta && lists[i].meta.perms) {
|
||||||
|
|
|
||||||
11
src/main.js
11
src/main.js
|
|
@ -55,7 +55,14 @@ const _ResizeObserver = window.ResizeObserver;
|
||||||
|
|
||||||
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
|
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
|
||||||
constructor(callback) {
|
constructor(callback) {
|
||||||
callback = debounce(callback, 16);
|
// 先包 try/catch 再 debounce:debounce 用 setTimeout 延迟执行,
|
||||||
super(callback);
|
// 必须让真正执行回调时也在 try/catch 内,否则元素卸载瞬间
|
||||||
|
// getComputedStyle 拿到非 Element 的错误会逃出 setTimeout 成为未捕获异常
|
||||||
|
const safe = (entries, observer) => {
|
||||||
|
try {
|
||||||
|
callback(entries, observer);
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
super(debounce(safe, 16));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,21 @@ const routes = [
|
||||||
path: "/",
|
path: "/",
|
||||||
component: () => import(/* webpackChunkName: "layout" */ '@/layout'),
|
component: () => import(/* webpackChunkName: "layout" */ '@/layout'),
|
||||||
redirect: config.DASHBOARD_URL || getBaseDashboard(),
|
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",
|
path: "/login",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ axios.interceptors.request.use(
|
||||||
let token = tool.data.get("TOKEN");
|
let token = tool.data.get("TOKEN");
|
||||||
if(token){
|
if(token){
|
||||||
config.headers[sysConfig.TOKEN_NAME] = sysConfig.TOKEN_PREFIX + token
|
config.headers[sysConfig.TOKEN_NAME] = sysConfig.TOKEN_PREFIX + token
|
||||||
|
// 记录本次请求实际使用的 token,用于 401 时判断是否已被并发刷新过
|
||||||
|
config._tokenUsed = token;
|
||||||
}
|
}
|
||||||
if(!sysConfig.REQUEST_CACHE && config.method == 'get'){
|
if(!sysConfig.REQUEST_CACHE && config.method == 'get'){
|
||||||
config.params = config.params || {};
|
config.params = config.params || {};
|
||||||
|
|
@ -49,13 +51,43 @@ axios.interceptors.response.use(
|
||||||
});
|
});
|
||||||
}else{
|
}else{
|
||||||
if (error.config.url.indexOf('auth/token/refresh/') != -1) {
|
if (error.config.url.indexOf('auth/token/refresh/') != -1) {
|
||||||
|
// 刷新接口自身 401:refresh token 已失效,清除登录态并跳登录
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
requests=[];
|
requests = [];
|
||||||
|
tool.data.remove("TOKEN");
|
||||||
|
tool.data.remove("TOKEN_REFRESH");
|
||||||
|
console.warn('[auth] refresh token 失效,跳转登录页');
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
return
|
return Promise.reject(error.response);
|
||||||
|
}
|
||||||
|
const currentToken = tool.data.get("TOKEN");
|
||||||
|
// 已无 token(refresh 失败已清登录态并跳登录):并发请求直接拒绝,
|
||||||
|
// 不再重试/刷新,避免误报“无权限”
|
||||||
|
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) {
|
if (!isRefreshing) {
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
|
// 必须 return 刷新 Promise,否则即便刷新成功,原始请求也会被 reject,重试结果被丢弃
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
axios({
|
axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: sysConfig.API_URL+'/auth/token/refresh/',
|
url: sysConfig.API_URL+'/auth/token/refresh/',
|
||||||
|
|
@ -67,17 +99,34 @@ axios.interceptors.response.use(
|
||||||
// 替换本地的token
|
// 替换本地的token
|
||||||
tool.data.set("TOKEN", res.data.access);
|
tool.data.set("TOKEN", res.data.access);
|
||||||
tool.data.set("TOKEN_TIME", new Date().getTime());
|
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.headers[sysConfig.TOKEN_NAME] = sysConfig.TOKEN_PREFIX + res.data.access;
|
||||||
|
error.config._retriedAfterRefresh = true; // 标记:重试后若仍 401 则视为无权限
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
requests.forEach(cb => cb())
|
|
||||||
//发起后将requests置空
|
//发起后将requests置空
|
||||||
requests=[];
|
const queue = requests; requests = [];
|
||||||
return axios(error.config)
|
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{
|
}else{
|
||||||
//如果正在刷新token,将发起的请求暂存在requests中
|
//如果正在刷新token,将发起的请求暂存在requests中
|
||||||
return new Promise((resolve)=>{
|
return new Promise((resolve)=>{
|
||||||
requests.push(()=>{
|
requests.push(()=>{
|
||||||
|
error.config._retriedAfterRefresh = true;
|
||||||
resolve(axios(error.config));
|
resolve(axios(error.config));
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
<!-- 工厂数据卡片 -->
|
<!-- 工厂数据卡片 -->
|
||||||
<el-row :gutter="16" class="factory-row">
|
<el-row :gutter="16" class="factory-row">
|
||||||
<el-col :xs="24" :sm="12" :md="6" v-for="(item, index) in factories" :key="item.name">
|
<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-header">
|
||||||
<div class="card-icon" :style="{ backgroundColor: item.bgColor }">
|
<div class="card-icon" :style="{ backgroundColor: item.bgColor }">
|
||||||
<el-icon :size="24" :color="item.color"><OfficeBuilding /></el-icon>
|
<el-icon :size="24" :color="item.color"><OfficeBuilding /></el-icon>
|
||||||
|
|
@ -193,30 +193,32 @@ let pieChart = null
|
||||||
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c']
|
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c']
|
||||||
const bgColors = ['#ecf5ff', '#f0f9eb', '#fdf6ec', '#fef0f0']
|
const bgColors = ['#ecf5ff', '#f0f9eb', '#fdf6ec', '#fef0f0']
|
||||||
|
|
||||||
// 各工厂基础比例
|
// 各工厂与数据表的映射关系
|
||||||
const baseFactories = [
|
const factoryDefs = [
|
||||||
{ name: '合肥南方', ratio: 0.4382 },
|
{ name: '合肥南方', match: (t) => t === 'mplogx' }, // 合肥南方
|
||||||
{ name: '中联万吨', ratio: 0.3081 },
|
{ name: '中联万吨', match: (t) => t === 'mplogx_xzzl' }, // 中联万吨
|
||||||
{ name: '铜梁水泥', ratio: 0.1424 },
|
{ name: '铜梁水泥', match: (t) => t === 'mplogx_tlxn' }, // 铜梁水泥
|
||||||
{ name: '槐坎南方水泥', ratio: 0.1113 },
|
{ name: '槐坎南方水泥', match: (t) => t.startsWith('mplogx_hknf') }, // 槁坎南方水泥
|
||||||
]
|
]
|
||||||
|
|
||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
|
const tables = ref([])
|
||||||
|
|
||||||
const factories = computed(() => {
|
const factories = computed(() => {
|
||||||
const total = totalCount.value
|
const total = totalCount.value
|
||||||
// 前3个按比例取整,最后一个用总量减去前3个保证总和精确
|
return factoryDefs.map((f, i) => {
|
||||||
const counts = baseFactories.map(f => Math.round(total * f.ratio))
|
// 同一工厂可能对应多张数据表,按表名匹配后累加行数
|
||||||
const sumFirst3 = counts[0] + counts[1] + counts[2]
|
const count = tables.value
|
||||||
counts[3] = total - sumFirst3
|
.filter((t) => f.match(t.table_name))
|
||||||
|
.reduce((sum, t) => sum + (t.approximate_row_count || 0), 0)
|
||||||
return baseFactories.map((f, i) => ({
|
return {
|
||||||
name: f.name,
|
name: f.name,
|
||||||
count: counts[i],
|
count,
|
||||||
color: colors[i],
|
color: colors[i],
|
||||||
bgColor: bgColors[i],
|
bgColor: bgColors[i],
|
||||||
percentage: total > 0 ? (counts[i] / total) * 100 : 0,
|
percentage: total > 0 ? (count / total) * 100 : 0,
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const maxFactory = computed(() => {
|
const maxFactory = computed(() => {
|
||||||
|
|
@ -336,19 +338,29 @@ const handleResize = () => {
|
||||||
pieChart?.resize()
|
pieChart?.resize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 拦截浏览器后退:在大屏页按“返回”时跳转到数据模板页
|
||||||
|
const handlePopState = () => {
|
||||||
|
router.push('/cement_data_template')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
API.hfnf.mplogx.list.req({ page: 1, page_size: 1 }).then(res => {
|
API.hfnf.stats.list.req().then(res => {
|
||||||
totalCount.value = res.count
|
tables.value = res.tables || []
|
||||||
|
totalCount.value = res.summary?.approximate_row_count_total || 0
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initBarChart()
|
initBarChart()
|
||||||
initPieChart()
|
initPieChart()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
|
// 多压一条相同的历史记录,使浏览器后退可被 popstate 拦截
|
||||||
|
history.pushState(null, '', location.href)
|
||||||
|
window.addEventListener('popstate', handlePopState)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
|
window.removeEventListener('popstate', handlePopState)
|
||||||
barChart?.dispose()
|
barChart?.dispose()
|
||||||
pieChart?.dispose()
|
pieChart?.dispose()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue