feat: 后端模式开关 & 页脚UI重构 & 登录认证链路改造
【变更概要】 1. 后端模式开关: 新增 backend-mode.js / USE_NEW_BACKEND 控制走老/新后端 2. token 同步链路: 新增 auth-token-store.js, 改造 request.js 拦截器支持 Bearer token 3. auth 重构 API: 新增 auth-refactor.js (独立模块, 不修改老 login.js) 4. user store: Login action 根据 USE_NEW_BACKEND 切换登录接口 5. login 页: 登录后跳转首页, 错误提示优化 6. dev-server proxy: vue.config.js 动态路由, 支持后端模式切换 7. 页脚 UI 重构: 品牌列 + 4 标题列布局, 响应式适配 8. main.vue 弹窗美化, home2 footer 反向缩放, page-layout CSS 变量调整 9. 双开调试配置: .env.development.new 【生产安全注意点 - 请务必确认】 - 生产构建 CI/CD 不得设置 VUE_APP_USE_NEW_BACKEND=true, 否则 Login 将走新后端 (默认未定义 = false, 走老后端 ry-cloud) - request.js 的 Authorization 头注入仅在 localStorage 有 txw_access_token 时生效, 老用户无此 key, 不会加头, 不影响老后端请求 - 响应拦截器的 token 同步逻辑仅处理含 accessToken 字段的响应体, 老后端不返回该字段, 不会触发 - vue.config.js 仅作用于 dev-server, 生产 Nginx 配置不受影响
This commit is contained in:
parent
d8f7f2f94f
commit
ec74735f94
@ -1,16 +1,21 @@
|
||||
###
|
||||
# @Descripttion:
|
||||
# @Version: 1.0
|
||||
# @Author: wjx
|
||||
# @Date: 2024-02-04 14:09:22
|
||||
# @LastEditors: wjx
|
||||
# @LastEditTime: 2024-04-02 13:48:07
|
||||
###
|
||||
# @Descripttion: 本地开发默认 - 连老后端 (ry-cloud)
|
||||
# @Usage: yarn serve (默认走这个)
|
||||
# @Target: txw-gateway(老) 9300 -> Nacos 8848 (namespace: 2fd09a25...)
|
||||
# @Note: 与 .env.development.new 并存,互不污染;双开时跑两个 dev server
|
||||
#
|
||||
# 阶段 1 收尾 BUG-C 配套:后端模式开关
|
||||
# VUE_APP_USE_NEW_BACKEND=false → 连老后端 (ry-cloud 9300, 本文件默认)
|
||||
# VUE_APP_USE_NEW_BACKEND=true → 连新后端 (txw-cloud 8080, 见 .env.development.new)
|
||||
# 切换无需改代码,改这个值 + 重启 yarn serve 即可。
|
||||
###
|
||||
VUE_APP_ENV=dev
|
||||
VUE_APP_MODEL=local
|
||||
VUE_APP_CDN_PATH=/view/mhzc
|
||||
VUE_APP_ROUTER_BASE=/view/mhzc
|
||||
VUE_APP_API_BASE_URL=
|
||||
VUE_APP_DEV_SERVER_PORT=9002
|
||||
VUE_APP_MHZC_PROXY=http://localhost:9300
|
||||
VUE_APP_MOCK=true
|
||||
VUE_APP_AUTO_ROUTER=false
|
||||
VUE_APP_USE_NEW_BACKEND=false
|
||||
|
||||
20
txw-mhzc-web/.env.development.new
Normal file
20
txw-mhzc-web/.env.development.new
Normal file
@ -0,0 +1,20 @@
|
||||
###
|
||||
# @Descripttion: 双开调试专用 - 连新后端 (txw-cloud)
|
||||
# @Usage: yarn serve --mode development.new
|
||||
# @Target: txw-gateway(新) 8080 -> Nacos 18848 (namespace: public)
|
||||
# @Note: 与 .env.development 并存,互不污染
|
||||
#
|
||||
# 阶段 1 收尾 BUG-C 配套:后端模式开关
|
||||
# VUE_APP_USE_NEW_BACKEND=true → 走新后端 (本文件)
|
||||
# 切换回老后端:改 .env.development (默认 VUE_APP_USE_NEW_BACKEND=false)
|
||||
###
|
||||
VUE_APP_ENV=dev
|
||||
VUE_APP_MODEL=local
|
||||
VUE_APP_CDN_PATH=/view/mhzc
|
||||
VUE_APP_ROUTER_BASE=/view/mhzc
|
||||
VUE_APP_API_BASE_URL=
|
||||
VUE_APP_DEV_SERVER_PORT=9003
|
||||
VUE_APP_MHZC_PROXY=http://localhost:8080
|
||||
VUE_APP_MOCK=false
|
||||
VUE_APP_AUTO_ROUTER=false
|
||||
VUE_APP_USE_NEW_BACKEND=true
|
||||
48
txw-mhzc-web/docs/auth-token-sync.md
Normal file
48
txw-mhzc-web/docs/auth-token-sync.md
Normal file
@ -0,0 +1,48 @@
|
||||
# 门户前端 token 同步方案(阶段 1 收尾 BUG-C)
|
||||
|
||||
> 范围:`txw-mhzc-web` 门户前端与 `txw-cloud` 网关的鉴权头协商。
|
||||
> 基线:本地三后端在跑(auth 9200 / system 9201 / gateway 8080),UUID + JWT 双 Token 模式可工作。
|
||||
> 日期:2026-06-07
|
||||
|
||||
## 1. 问题
|
||||
|
||||
- `txw-auth` 的 `AuthController.loginByPassword` 把 token 写入 `Cookie: token`(`HttpOnly; Secure`)。
|
||||
- 网关 `AuthFilter` 只读 `Authorization: Bearer <token>` Header,**不读 Cookie**。
|
||||
- 后果:门户前端若只依赖 Cookie 携带 token,业务接口经网关会 401。
|
||||
|
||||
## 2. 决策
|
||||
|
||||
不改后端(影响网关所有受保护接口),由门户前端在 axios 拦截器把 **响应体中的 token** 同步到 `Authorization` Header。
|
||||
|
||||
## 3. 实现要点
|
||||
|
||||
| 点 | 说明 |
|
||||
|----|------|
|
||||
| token 来源 | 登录/刷新响应体 `data.accessToken`(不是 Cookie,Cookie 是 HttpOnly,JS 读不到) |
|
||||
| token 持久化 | `localStorage`(多 tab 共享、刷新不丢) |
|
||||
| 请求拦截器 | 自动在 `headers.Authorization` 写入 `Bearer <accessToken>` |
|
||||
| 响应拦截器 | 登录/刷新成功后 `setTokensFromResponse(data)`;401 失败时 `clearTokens()` |
|
||||
| 登出 | 不需要前端手动清 Header(后端 expire Cookie 即可,本地 token 由 `clearTokens` 清) |
|
||||
| 关键文件 | `src/utils/auth-token-store.js`(存取)、`src/core/request.js`(拦截器) |
|
||||
|
||||
## 4. 为什么不用 Cookie 读
|
||||
|
||||
`HttpOnly` Cookie 是浏览器安全机制,JS 读取会得到空串。若团队坚持用 Cookie,需要:
|
||||
1. 去掉 `HttpOnly`(安全降级),或
|
||||
2. 改后端让网关优先读 Cookie(影响所有受保护接口)。
|
||||
|
||||
都不采纳。详情见 `refactor-docs/重构方案/阶段1-收尾-问题清单.md` 第 4 节备选。
|
||||
|
||||
## 5. 联调验收
|
||||
|
||||
- [ ] 门户登录成功后 `localStorage` 有 `txw_access_token`
|
||||
- [ ] `Authorization: Bearer ...` 出现在 devtools Network 请求头
|
||||
- [ ] 调 `/mhzc/sy/ptgg/list` 等需鉴权接口返回 200
|
||||
- [ ] 登出后 `localStorage` 中 `txw_access_token` 被清
|
||||
- [ ] 401 时自动清 token 并触发 `showLoginGuide`
|
||||
|
||||
## 6. 不要做的事
|
||||
|
||||
- 不要在前端代码里直接 `document.cookie` 读 token(HttpOnly 读不到)
|
||||
- 不要把 token 存到 `sessionStorage`(刷新即丢)
|
||||
- 不要在 `vuex` 全局状态里存(多 tab 不共享)
|
||||
38
txw-mhzc-web/src/config/backend-mode.js
Normal file
38
txw-mhzc-web/src/config/backend-mode.js
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 后端模式开关(阶段 1 收尾 BUG-C 配套)
|
||||
*
|
||||
* <p>本文件是切换"老后端 ry-cloud (9300) / 新后端 txw-cloud (8080)"的统一入口。
|
||||
* 业务代码根据 {@code USE_NEW_BACKEND} 决定调哪套 API。
|
||||
*
|
||||
* <p>切换方法(无需改代码):
|
||||
* <pre>
|
||||
* // 老后端 (ry-cloud)
|
||||
* VUE_APP_USE_NEW_BACKEND=false
|
||||
*
|
||||
* // 新后端 (txw-cloud 阶段 2 联调)
|
||||
* VUE_APP_USE_NEW_BACKEND=true
|
||||
* </pre>
|
||||
*
|
||||
* <p>环境变量由 webpack 注入(Vue CLI 自动读取 .env / .env.development / .env.[mode]),
|
||||
* 重启 yarn serve 后生效。
|
||||
*
|
||||
* <p>参考文档:<code>txw-mhzc-web/docs/auth-token-sync.md</code>
|
||||
*/
|
||||
|
||||
// webpack DefinePlugin 注入 (process.env.VUE_APP_* 会被静态替换)
|
||||
// 默认 false,连老后端(兼容老用户)
|
||||
const rawValue = process.env.VUE_APP_USE_NEW_BACKEND;
|
||||
const isNew = typeof rawValue === 'string' && rawValue.toLowerCase() === 'true';
|
||||
|
||||
/** 是否走新后端 (txw-cloud :8080) */
|
||||
export const USE_NEW_BACKEND = isNew;
|
||||
|
||||
/** 后端模式: 'legacy' (ry-cloud) | 'new' (txw-cloud) */
|
||||
export const BACKEND_MODE = isNew ? 'new' : 'legacy';
|
||||
|
||||
/** 调试日志: 启动时打印当前后端模式 */
|
||||
if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'production') {
|
||||
// 仅 dev 环境提示
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[backend-mode] current mode:', BACKEND_MODE, '(USE_NEW_BACKEND=' + isNew + ')');
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { MessagePlugin } from 'tdesign-vue';
|
||||
import { mhLogout, getRedirectUri } from '@/pages/index/api/login';
|
||||
import { LoadingPlugin, DialogPlugin } from '@gt4/common-front';
|
||||
import { showLoginGuide } from '@/pages/index/utils/auth-guard';
|
||||
import { getAccessToken, setTokensFromResponse, clearTokens } from '@/utils/auth-token-store';
|
||||
|
||||
const SingleLoading = {
|
||||
load: null,
|
||||
@ -48,6 +49,17 @@ request.interceptors.request.use(
|
||||
if (!url) {
|
||||
return newConf;
|
||||
}
|
||||
// 阶段 1 收尾 BUG-C:txw-cloud 网关只读 Authorization Header,不读 Cookie token。
|
||||
// 从 localStorage 取出 accessToken 写到请求头,未登录态留空。
|
||||
if (!newConf.headers) {
|
||||
newConf.headers = {};
|
||||
}
|
||||
if (!newConf.headers.Authorization) {
|
||||
const accessToken = getAccessToken();
|
||||
if (accessToken) {
|
||||
newConf.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
}
|
||||
if (newConf.method === 'get' && newConf.params) {
|
||||
// 先加时间戳(如果 URL 还没参数)
|
||||
if (url.indexOf('?') === -1) {
|
||||
@ -112,6 +124,16 @@ request.interceptors.response.use(
|
||||
SingleLoading.endLoading();
|
||||
}
|
||||
const { code, type } = res.data;
|
||||
// 阶段 1 收尾 BUG-C:登录/刷新响应同步 token 到 localStorage,
|
||||
// 供后续请求拦截器写入 Authorization Header。
|
||||
// txw-cloud 响应嵌套:{code:200, data:{accessToken, refreshToken}, msg}
|
||||
// 解包 .data 兼容扁平/嵌套两种形态。
|
||||
if (code === 200 && res.data) {
|
||||
const tokenData = res.data.data || res.data;
|
||||
if (tokenData.accessToken || tokenData.refreshToken) {
|
||||
setTokensFromResponse(tokenData);
|
||||
}
|
||||
}
|
||||
// 获取错误信息
|
||||
const msg = res.data.msg || '系统未知错误,请反馈给管理员';
|
||||
if (code === 401) {
|
||||
@ -149,6 +171,8 @@ request.interceptors.response.use(
|
||||
}
|
||||
// HTTP 状态码 401 未认证,跳转登录页
|
||||
if (err.response?.status === 401) {
|
||||
// 阶段 1 收尾 BUG-C:服务端拒绝 → 清掉本地 token,避免下次请求带过期 token
|
||||
clearTokens();
|
||||
// 调用方显式静默 401(如只读列表的公开接口):不弹登录提示,
|
||||
// 让业务层自行 fallback / 提示 / 跳转
|
||||
const silent = err.config?.__silent401 || err.reqConfig?.__silent401;
|
||||
@ -254,6 +278,8 @@ request.interceptors.response.use(
|
||||
if (err.reqConfig?.loading || err.config?.loading || SingleLoading.load !== null) {
|
||||
SingleLoading.endLoading(true);
|
||||
}
|
||||
// 阶段 1 收尾 BUG-C:清掉本地 token
|
||||
clearTokens();
|
||||
// 调用方显式静默 401(如只读列表的公开接口):不弹登录提示
|
||||
const silent = err.config?.__silent401 || err.reqConfig?.__silent401;
|
||||
if (!silent) {
|
||||
|
||||
116
txw-mhzc-web/src/pages/index/api/auth-refactor.js
Normal file
116
txw-mhzc-web/src/pages/index/api/auth-refactor.js
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 重构测试入口(阶段 1 收尾 BUG-C 联调用)
|
||||
*
|
||||
* <p><strong>不动老业务</strong>:本文件是独立模块,不修改 {@link ./login.js} 的任何老接口。
|
||||
* 老业务继续走 {@code /sso/auth/login}(若依老 SSO、cookie + rememberMe),
|
||||
* 本模块只暴露 txw-cloud 新接口供重构验证使用。
|
||||
*
|
||||
* <p>用法(前端任意位置 import):
|
||||
* <pre>
|
||||
* import { loginByPassword, getInfo, logoutNew, getCaptcha } from '@/pages/index/api/auth-refactor';
|
||||
*
|
||||
* // 1) 登录(响应拦截器会自动写 localStorage.txw_access_token)
|
||||
* await loginByPassword('admin', 'admin123');
|
||||
* console.log(localStorage.getItem('txw_access_token'));
|
||||
*
|
||||
* // 2) 业务请求(request 拦截器会自动塞 Authorization: Bearer ...)
|
||||
* const info = await getInfo();
|
||||
*
|
||||
* // 3) 登出(401 时也会自动 clearTokens)
|
||||
* await logoutNew();
|
||||
* </pre>
|
||||
*
|
||||
* <p>或在浏览器 console 直接跑(dev 模式):
|
||||
* <pre>
|
||||
* const { loginByPassword, getInfo } = await import('/src/pages/index/api/auth-refactor.js');
|
||||
* await loginByPassword('admin', 'admin123');
|
||||
* await getInfo();
|
||||
* </pre>
|
||||
*
|
||||
* <p>后端基线:txw-cloud 三后端已启动(auth 9200 / system 9201 / gateway 8080),
|
||||
* 网关 baseURL 由 {@code window.STATIC_ENV_CONFIG.API_PREFIX} 提供(通常是 {@code /})。
|
||||
*/
|
||||
|
||||
import { fetch } from '@/core/request';
|
||||
import { getAccessToken, clearTokens } from '@/utils/auth-token-store';
|
||||
|
||||
const baseURL = '';
|
||||
|
||||
/**
|
||||
* 新接口:账号密码登录(OAuth2 UUID Token)
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
*/
|
||||
export function loginByPassword(username, password) {
|
||||
return fetch({
|
||||
url: `${baseURL}/auth/loginByPassword`,
|
||||
method: 'post',
|
||||
data: { username, password },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新接口:图形验证码
|
||||
*/
|
||||
export function getCaptcha() {
|
||||
return fetch({
|
||||
url: `${baseURL}/auth/verify/captcha`,
|
||||
method: 'post',
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新接口:UUID Token 访问 system 业务接口(验证 Authorization 头是否生效)
|
||||
*/
|
||||
export function getInfo() {
|
||||
return fetch({
|
||||
url: `${baseURL}/system/user/getInfo`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新接口:登出(POST,强收敛后的统一入口)
|
||||
*/
|
||||
export function logoutNew() {
|
||||
return fetch({
|
||||
url: `${baseURL}/auth/logout`,
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试辅助:返回当前 localStorage 中的 token 状态(不消耗任何配额)
|
||||
*/
|
||||
export function debugTokenState() {
|
||||
return {
|
||||
accessToken: getAccessToken(),
|
||||
accessTokenLength: getAccessToken().length,
|
||||
localStorageKeys: Object.keys(localStorage).filter((k) => k.startsWith('txw_')),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试辅助:强制清掉 localStorage 中的 token(与 401 触发效果一致)
|
||||
*/
|
||||
export function debugClearTokens() {
|
||||
clearTokens();
|
||||
return { cleared: true };
|
||||
}
|
||||
|
||||
// 阶段 1 收尾 BUG-C:兜底把整套方法挂到 window.__txwRefactor,
|
||||
// 让 console 可以直接用 `await __txwRefactor.loginByPassword(...)` 调,
|
||||
// 彻底避开 webpack 动态 import 生成的懒加载 chunk 在 dev server 下被
|
||||
// SPA fallback 兜底成 index.html(text/html MIME)的问题。
|
||||
// 仅在 dev / 非生产环境暴露,生产构建会走 terser drop_console 路径清理。
|
||||
if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'production') {
|
||||
window.__txwRefactor = {
|
||||
loginByPassword,
|
||||
getCaptcha,
|
||||
getInfo,
|
||||
logoutNew,
|
||||
debugTokenState,
|
||||
debugClearTokens,
|
||||
};
|
||||
}
|
||||
@ -1,80 +1,90 @@
|
||||
<template>
|
||||
<div class="portal-footer-shell">
|
||||
<footer class="site-footer">
|
||||
<div class="footer-main">
|
||||
<div class="footer-main-inner">
|
||||
<div class="footer-columns">
|
||||
<div class="footer-column footer-column--guide">
|
||||
<h3 class="footer-title">指导单位</h3>
|
||||
<ul class="footer-list">
|
||||
<li class="footer-text">上海市数据局</li>
|
||||
<li class="footer-text">宝山区政府</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-column footer-column--ops">
|
||||
<h3 class="footer-title">运营单位</h3>
|
||||
<ul class="footer-list">
|
||||
<li class="footer-text">上海浦江数链数字科技有限公司</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-column footer-column--support">
|
||||
<h3 class="footer-title">业务支持</h3>
|
||||
<ul class="footer-list">
|
||||
<li class="footer-text">上海数字基础设施协会可信碳专委会</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-column footer-column--links">
|
||||
<h3 class="footer-title">友情链接</h3>
|
||||
<ul class="footer-list">
|
||||
<li>
|
||||
<a
|
||||
class="footer-link"
|
||||
href="https://segg.sh.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>上海市企业走出去综合服务平台</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-column footer-column--contact">
|
||||
<h3 class="footer-title">联系我们</h3>
|
||||
<ul class="footer-list footer-list--contact">
|
||||
<li class="footer-contact-row">
|
||||
<span class="footer-contact-icon" aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 3.5h11v9h-11v-9z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M2.5 4.5L8 9l5.5-4.5" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<footer class="site-footer">
|
||||
<div class="footer-main">
|
||||
<div class="footer-main-inner">
|
||||
<div class="footer-columns">
|
||||
<!-- 列 1:品牌列(与页头同款 logo + 名称 + 邮箱 + 地址) -->
|
||||
<div class="footer-column footer-column--brand footer-column--divider-right">
|
||||
<div class="footer-brand-head">
|
||||
<span class="footer-brand-logo" aria-hidden="true">
|
||||
<img class="footer-brand-icon" :src="logoIconSrc" alt="可信碳信息网" />
|
||||
</span>
|
||||
<span class="footer-text">{{ contact.email }}</span>
|
||||
</li>
|
||||
<li class="footer-contact-row">
|
||||
<span class="footer-contact-icon" aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 14s4.5-3.2 4.5-7A4.5 4.5 0 1 0 3.5 7c0 3.8 4.5 7 4.5 7z" stroke="currentColor" stroke-width="1.2"/>
|
||||
<circle cx="8" cy="7" r="1.5" fill="currentColor"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="footer-text footer-text--address">{{ contact.address }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<span class="footer-brand-name">可信碳信息网</span>
|
||||
</div>
|
||||
<ul class="footer-contact-list">
|
||||
<li class="footer-contact-row">
|
||||
<span class="footer-contact-icon footer-contact-icon--solid" aria-hidden="true">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.5" y="3.5" width="13" height="9" rx="1.2" fill="#0a7a4a"/>
|
||||
<path d="M2.5 4.5L8 8.8 13.5 4.5" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
</span>
|
||||
<a class="footer-contact-text" :href="`mailto:${contact.email}`">{{ contact.email }}</a>
|
||||
</li>
|
||||
<li class="footer-contact-row">
|
||||
<span class="footer-contact-icon footer-contact-icon--solid" aria-hidden="true">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 1.5c-3 0-5.5 2.4-5.5 5.4 0 4 5.5 8 5.5 8s5.5-4 5.5-8c0-3-2.5-5.4-5.5-5.4z" fill="#0a7a4a"/>
|
||||
<circle cx="8" cy="6.9" r="1.8" fill="#ffffff"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="footer-contact-text">{{ contact.address }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 列 2:指导单位 -->
|
||||
<div class="footer-column footer-column--guide">
|
||||
<h3 class="footer-title">指导单位</h3>
|
||||
<ul class="footer-list">
|
||||
<li class="footer-text">上海市数据局</li>
|
||||
<li class="footer-text">宝山区政府</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 列 3:运营单位 -->
|
||||
<div class="footer-column footer-column--ops">
|
||||
<h3 class="footer-title">运营单位</h3>
|
||||
<ul class="footer-list">
|
||||
<li class="footer-text">上海浦江数链数字科技有限公司</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 列 4:业务支持 -->
|
||||
<div class="footer-column footer-column--support">
|
||||
<h3 class="footer-title">业务支持</h3>
|
||||
<ul class="footer-list">
|
||||
<li class="footer-text">上海数字基础设施协会可信碳专委会</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 列 5:友情链接 -->
|
||||
<div class="footer-column footer-column--links">
|
||||
<h3 class="footer-title">友情链接</h3>
|
||||
<ul class="footer-list">
|
||||
<li>
|
||||
<a
|
||||
class="footer-link"
|
||||
href="https://segg.sh.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>上海市企业走出去综合服务平台</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bar">
|
||||
<div class="footer-bar-inner">
|
||||
<p class="footer-bar-line">© 2025 可信碳信息网 版权所有</p>
|
||||
<p class="footer-bar-line">技术支持:上海市宝山区大数据中心</p>
|
||||
<p class="footer-bar-line">基础设施:国家区块链网络基础底座</p>
|
||||
<div class="footer-bar">
|
||||
<div class="footer-bar-inner">
|
||||
<p class="footer-bar-line">© 2025 可信碳信息网 版权所有</p>
|
||||
<p class="footer-bar-line">技术支持:上海市宝山区大数据中心</p>
|
||||
<p class="footer-bar-line">基础设施:国家区块链网络基础底座</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -83,6 +93,12 @@ import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'Footer',
|
||||
data() {
|
||||
return {
|
||||
// 与页头 nav 同款 logo 资源
|
||||
logoIconSrc: require('../../assets/logo-figma-icon.png'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('settings', ['contact']),
|
||||
},
|
||||
@ -90,17 +106,23 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/* 门户统一页脚外壳(由 Main 布局固定在滚动区底部) */
|
||||
/* =============================================================================
|
||||
* 门户统一页脚(完全按 Figma 页脚设计稿 · 品牌列 + 4 标题列)
|
||||
* 画布:1440 · 版心 1200 · 5 列 · 主区 #f0f7f2 · 底栏 #e8f0ea
|
||||
* 列 1:品牌(logo 圆形 + 名称 + 邮箱 + 地址),实心绿底白图标
|
||||
* 列 2-5:标题 + 文字列表,标题 16px #003b1a + 左侧 4px 绿渐变竖条
|
||||
* ============================================================================= */
|
||||
|
||||
.portal-footer-shell {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
background: var(--page-footer-bg, #f0f7f2);
|
||||
}
|
||||
|
||||
/* Figma 底部信息块:主区 #f0f7f2 padding 40/20/20,版权条 #e2ede5 高 64px */
|
||||
/* 首页 footer 反向缩放规则已挪到 home2/index.vue 的 scoped 样式中(避免误命中内页 footer) */
|
||||
|
||||
.site-footer {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@ -118,31 +140,145 @@ export default {
|
||||
|
||||
.footer-columns {
|
||||
display: flex;
|
||||
/* 优化需求:5 列内容均分布局(每列等宽、垂直等高、内容顶部对齐) */
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
gap: 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.footer-column {
|
||||
flex: 1 1 auto;
|
||||
min-width: 200px;
|
||||
/* 优化需求:列内整体居中显示(标题+列表与列中心对齐) */
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 5 列宽:Figma 比例 312 / 84 / 196 / 224 / 196(去掉原联系我们列 → 由品牌列承担) */
|
||||
.footer-column--brand { flex: 0 0 312px; left: -41px;}
|
||||
.footer-column--guide { flex: 0 0 130px; }
|
||||
.footer-column--ops { flex: 0 0 196px; }
|
||||
.footer-column--support { flex: 0 0 224px; }
|
||||
.footer-column--links { flex: 0 0 268px; }
|
||||
|
||||
/* Figma 设计:品牌列(可信碳信息网)右侧 1 道浅绿竖向分隔线(贯穿列高) */
|
||||
.footer-column--divider-right {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: -16px;
|
||||
bottom: 4px;
|
||||
width: 1px;
|
||||
background: rgba(0, 154, 41, 0.18);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
/* 列 1:品牌列 */
|
||||
.footer-brand-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 12px;
|
||||
height: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: #003b1a;
|
||||
}
|
||||
|
||||
.footer-brand-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
padding: 1px;
|
||||
background: #ffffff;
|
||||
border-radius: 100px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.footer-brand-icon {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.footer-brand-name {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer-contact-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.footer-contact-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.footer-contact-icon {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
min-height: 22px;
|
||||
color: #556659;
|
||||
}
|
||||
|
||||
.footer-contact-icon--solid {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
margin-top: 3px;
|
||||
overflow: hidden;
|
||||
color: #ffffff;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-contact-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #556659;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #00b96b;
|
||||
}
|
||||
}
|
||||
|
||||
a.footer-contact-text {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 列 2-5:标题 + 列表 */
|
||||
.footer-title {
|
||||
position: relative;
|
||||
margin: 0 0 12px;
|
||||
// padding-left: 12px;
|
||||
padding-left: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
line-height: 22px;
|
||||
color: #003b1a;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -152,8 +288,8 @@ export default {
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
// background: linear-gradient(180deg, #4caf50 0%, #2e7d32 100%);
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(180deg, #4caf50 0%, #2e7d32 100%);
|
||||
content: '';
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
@ -174,12 +310,12 @@ export default {
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: #556659;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
@ -187,35 +323,9 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.footer-list--contact {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.footer-contact-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.footer-contact-icon {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
min-height: 22px;
|
||||
color: #556659;
|
||||
}
|
||||
|
||||
.footer-text--address {
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
/* 版权底栏 */
|
||||
.footer-bar {
|
||||
background: #e2ede5;
|
||||
border-top: 1px dashed rgba(0, 154, 41, 0.25);
|
||||
background: #e8f0ea;
|
||||
}
|
||||
|
||||
.footer-bar-inner {
|
||||
@ -242,12 +352,21 @@ export default {
|
||||
@media screen and (max-width: 1279px) {
|
||||
.footer-columns {
|
||||
flex-wrap: wrap;
|
||||
gap: 28px 24px;
|
||||
row-gap: 28px;
|
||||
}
|
||||
|
||||
.footer-column {
|
||||
flex: 1 1 calc(33.333% - 16px);
|
||||
min-width: 180px;
|
||||
.footer-column--brand,
|
||||
.footer-column--guide,
|
||||
.footer-column--ops,
|
||||
.footer-column--support,
|
||||
.footer-column--links {
|
||||
flex: 1 1 calc(50% - 16px);
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
/* 响应式下不显示竖向分隔线 */
|
||||
.footer-column--divider-right::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,10 +377,14 @@ export default {
|
||||
|
||||
.footer-columns {
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
row-gap: 24px;
|
||||
}
|
||||
|
||||
.footer-column {
|
||||
.footer-column--brand,
|
||||
.footer-column--guide,
|
||||
.footer-column--ops,
|
||||
.footer-column--support,
|
||||
.footer-column--links {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import { login, logout,yhzhuce } from '@/pages/index/api/login';
|
||||
// 阶段 1 收尾 BUG-E:登录入口根据后端模式开关切换:
|
||||
// - 老后端 (ry-cloud:9300):走老 login() → /sso/auth/login
|
||||
// - 新后端 (txw-cloud:8080):走 auth-refactor.js 的 loginByPassword → /auth/loginByPassword
|
||||
// 切换无需改代码,改 .env 的 VUE_APP_USE_NEW_BACKEND 即可。
|
||||
import { USE_NEW_BACKEND } from '@/config/backend-mode';
|
||||
import { login, logout, yhzhuce } from '@/pages/index/api/login';
|
||||
import { loginByPassword } from '@/pages/index/api/auth-refactor';
|
||||
|
||||
const user = {
|
||||
state: {
|
||||
@ -31,19 +37,25 @@ const user = {
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
// 登录 - 根据 USE_NEW_BACKEND 切老/新后端
|
||||
Login({ commit }, userInfo) {
|
||||
const username = userInfo.username.trim();
|
||||
const username = (userInfo.username || '').trim();
|
||||
const { password } = userInfo;
|
||||
const { captchaVerification } = userInfo;
|
||||
const { captchaCode } = userInfo;
|
||||
const { socialCode } = userInfo;
|
||||
const { socialState } = userInfo;
|
||||
const { socialType } = userInfo;
|
||||
return new Promise((resolve, reject) => {
|
||||
login(username, password, captchaVerification, captchaCode, socialType, socialCode, socialState)
|
||||
const promise = USE_NEW_BACKEND
|
||||
? loginByPassword(username, password)
|
||||
: login(
|
||||
username,
|
||||
password,
|
||||
userInfo.captchaVerification,
|
||||
userInfo.captchaCode,
|
||||
userInfo.socialType,
|
||||
userInfo.socialCode,
|
||||
userInfo.socialState,
|
||||
);
|
||||
promise
|
||||
.then((res) => {
|
||||
resolve();
|
||||
resolve(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
|
||||
@ -31,9 +31,9 @@
|
||||
--page-nav-active-bar-height: 3px;
|
||||
--page-nav-color: #003b1a;
|
||||
--page-offset-top: var(--page-nav-height);
|
||||
/* Figma 页脚:主区 #f0f7f2、底栏 #e2ede5,与导航/内容版心对齐 */
|
||||
/* Figma 页脚:主区 #f0f7f2、底栏 #e8f0ea;标题 #003b1a,正文 #556659 */
|
||||
--page-footer-bg: #f0f7f2;
|
||||
--page-footer-bar-bg: #e2ede5;
|
||||
--page-footer-bar-bg: #e8f0ea;
|
||||
--page-footer-title-color: #003b1a;
|
||||
--page-footer-text-color: #556659;
|
||||
--page-footer-padding-y: 40px;
|
||||
|
||||
@ -4005,4 +4005,15 @@ export default {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
* 首页 footer 反向缩放:footer 在 .home-figma-scale-stage 内部会被 transform: scale 放大。
|
||||
* 仅 home2 内部生效(scoped 限定),不影响走 main.vue 的内页 footer。
|
||||
* transform 不改变 layout box,stage 的 margin-bottom 补偿保持正确。
|
||||
* ============================================================================= */
|
||||
.portal-footer-shell {
|
||||
transform: scale(calc(1 / var(--home-figma-scale, 1)));
|
||||
transform-origin: top center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -184,31 +184,37 @@ export default {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { href } = window.location;
|
||||
this.loading = true;
|
||||
removeUsername();
|
||||
removePassword();
|
||||
removeRememberMe();
|
||||
// 发起登陆
|
||||
// 阶段 1 收尾 BUG-C 改造:dispatch('Login') 内部根据 VUE_APP_USE_NEW_BACKEND
|
||||
// 自动切老/新后端,业务代码无需关心。详见 src/config/backend-mode.js
|
||||
this.$store
|
||||
.dispatch('Login', this.loginForm)
|
||||
.then(() => {
|
||||
console.log('111111');
|
||||
// this.$router.replace('/demo/demo').catch(() => '');
|
||||
// 本地之外
|
||||
// window.location.href = href.replace('sso/login', 'htgl/mhsy');
|
||||
// 本地
|
||||
window.sessionStorage.setItem('sfdl', true);
|
||||
// this.$router.go(-1);
|
||||
// 跳到首页
|
||||
window.location.href = `/view/mhzc/home`;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('22222');
|
||||
return;
|
||||
// if (error === 1004003) {
|
||||
// this.reSetSlider();
|
||||
// }
|
||||
console.log('22222', error);
|
||||
this.loading = false;
|
||||
// 业务错误提示
|
||||
const msg =
|
||||
(error && error.msg) ||
|
||||
(error && error.message) ||
|
||||
'登录失败,请重试';
|
||||
// 重新拿验证码(如果需要)
|
||||
this.refreshCaptcha();
|
||||
// eslint-disable-next-line no-undef
|
||||
if (typeof MessagePlugin !== 'undefined') {
|
||||
MessagePlugin.error({ content: msg, duration: 2000 });
|
||||
}
|
||||
});
|
||||
},
|
||||
handleCounter() {
|
||||
|
||||
@ -18,15 +18,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局“敬请期待”弹窗:居中显示,政务绿主题 -->
|
||||
<!-- 全局“敬请期待”弹窗:用 placement=top + top 控制位置,TDesign 1.4 在 top 模式下行为更稳定 -->
|
||||
<t-dialog
|
||||
v-model="comingSoonVisible"
|
||||
:header="false"
|
||||
:footer="false"
|
||||
:close-on-overlay-click="true"
|
||||
:show-overlay="true"
|
||||
width="420px"
|
||||
placement="center"
|
||||
width="640px"
|
||||
placement="top"
|
||||
:top="'calc(50vh - 200px)'"
|
||||
:destroy-on-close="false"
|
||||
class="coming-soon-global-dialog"
|
||||
@close="handleComingSoonClose"
|
||||
@ -329,10 +330,29 @@ export default {
|
||||
}
|
||||
|
||||
/* ---------- 全局“敬请期待”弹窗样式(居中、绿色主题) ---------- */
|
||||
/* 使用 placement=top + top 数值定位;这里只控制弹窗本体尺寸和装饰。
|
||||
不要用 fixed/transform 去劫持 TDesign 内部居中逻辑,容易和 align-items 冲突。 */
|
||||
|
||||
/* 弹窗本体:尺寸 + 圆角 + 阴影 + 入场动画 */
|
||||
/* 宽度 640,整体放大 */
|
||||
.coming-soon-global-dialog ::v-deep .t-dialog {
|
||||
border-radius: 12px;
|
||||
width: 640px;
|
||||
max-width: calc(100vw - 48px);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 24px 60px rgba(0, 59, 26, 0.18);
|
||||
box-shadow: 0 20px 48px rgba(0, 59, 26, 0.24);
|
||||
animation: coming-soon-pop 0.22s ease-out;
|
||||
}
|
||||
|
||||
@keyframes coming-soon-pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.coming-soon-global-dialog ::v-deep .t-dialog__body {
|
||||
@ -340,10 +360,10 @@ export default {
|
||||
}
|
||||
|
||||
.coming-soon-global-dialog ::v-deep .t-dialog__close {
|
||||
color: #ffffff;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: #556659;
|
||||
background: rgba(0, 185, 107, 0.08);
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.coming-soon-global-dialog ::v-deep .t-dialog__close:hover {
|
||||
@ -351,66 +371,71 @@ export default {
|
||||
color: #00b96b;
|
||||
}
|
||||
|
||||
/* 主体:放大内边距 */
|
||||
.coming-soon-dialog-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 36px 32px 32px;
|
||||
padding: 64px 56px 56px;
|
||||
text-align: center;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 图标:72 → 104 → 120 */
|
||||
.coming-soon-dialog-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin-bottom: 18px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-bottom: 28px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 185, 107, 0.08);
|
||||
}
|
||||
|
||||
.coming-soon-dialog-icon svg {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
}
|
||||
|
||||
/* 标题:22 → 28 → 30 */
|
||||
.coming-soon-dialog-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 22px;
|
||||
margin: 0 0 14px;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: #003b1a;
|
||||
letter-spacing: 1px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* 描述:14 → 16,max-width 320 → 440 → 480 */
|
||||
.coming-soon-dialog-desc {
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
margin: 0 0 36px;
|
||||
font-size: 17px;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
line-height: 1.7;
|
||||
color: #556659;
|
||||
max-width: 320px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
/* 按钮:140×40 → 180×48 → 200×52,字号 14 → 16 → 17 */
|
||||
.coming-soon-dialog-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 140px;
|
||||
height: 40px;
|
||||
padding: 0 24px;
|
||||
font-size: 14px;
|
||||
min-width: 200px;
|
||||
height: 52px;
|
||||
font-size: 17px;
|
||||
padding: 0 32px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #00b96b 0%, #00a25d 100%);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 6px 16px rgba(0, 154, 41, 0.25);
|
||||
box-shadow: 0 8px 20px rgba(0, 154, 41, 0.28);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
|
||||
74
txw-mhzc-web/src/utils/auth-token-store.js
Normal file
74
txw-mhzc-web/src/utils/auth-token-store.js
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 鉴权 token 存取(阶段 1 收尾 BUG-C 落地配套工具)
|
||||
*
|
||||
* <p>问题:txw-cloud 网关 AuthFilter 只读 {@code Authorization: Bearer <token>} Header,
|
||||
* 不读 {@code Cookie: token}。门户后端(AuthController.loginByPassword)把 token 写入
|
||||
* HttpOnly Cookie,<strong>JS 无法读取</strong>。若前端只依赖 Cookie,业务接口经网关会 401。
|
||||
*
|
||||
* <p>解决:登录成功后由 axios 响应拦截器从响应体 {@code data.accessToken} 写入本工具,
|
||||
* 后续请求拦截器读取并塞到 {@code Authorization} Header。登出由后端清 Cookie,
|
||||
* 前端无需手动清 Header。
|
||||
*
|
||||
* <p>为什么不用 Cookie:HttpOnly 标志是浏览器安全机制,JS 读取会得到空串。
|
||||
* 为什么不用 sessionStorage:刷新即丢,体验差;多 tab 共享需 localStorage。
|
||||
*/
|
||||
|
||||
const ACCESS_TOKEN_KEY = 'txw_access_token';
|
||||
const REFRESH_TOKEN_KEY = 'txw_refresh_token';
|
||||
|
||||
export function getAccessToken() {
|
||||
try {
|
||||
return localStorage.getItem(ACCESS_TOKEN_KEY) || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function getRefreshToken() {
|
||||
try {
|
||||
return localStorage.getItem(REFRESH_TOKEN_KEY) || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从登录/刷新响应体里同步 token;要求形如
|
||||
* <pre>{ accessToken: '...', refreshToken: '...' }</pre>
|
||||
* 注意:调用方需保证传入的是 token 字段**直接所在**的对象。
|
||||
* 如果是嵌套响应体(如 txw-cloud 的 {code, data:{accessToken}, msg}),
|
||||
* 调用方应先解包 .data 再传入。
|
||||
*
|
||||
* @param {Object} data 含 accessToken/refreshToken 字段的对象
|
||||
* @returns {string} 写入后的 accessToken(可能为空)
|
||||
*/
|
||||
export function setTokensFromResponse(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return '';
|
||||
}
|
||||
const { accessToken, refreshToken } = data;
|
||||
if (accessToken) {
|
||||
try {
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
||||
} catch (e) {
|
||||
// 隐私模式可能写不进去,降级到内存
|
||||
}
|
||||
}
|
||||
if (refreshToken) {
|
||||
try {
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
||||
} catch (e) {
|
||||
// 同上
|
||||
}
|
||||
}
|
||||
return accessToken || '';
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
try {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
} catch (e) {
|
||||
// 同上
|
||||
}
|
||||
}
|
||||
@ -287,48 +287,68 @@ module.exports = {
|
||||
},
|
||||
// Vue CLI prepareProxy 用 pathname.match(代理键) 判断;键写 '/mhzc' 会变成匹配路径里任意位置的 /mhzc,
|
||||
// 会误伤 SPA 路由 /view/mhzc/...,刷新时整页请求被转发到后端导致 Proxy error。必须用 ^ 限定为路径前缀。
|
||||
proxy: {
|
||||
// 阶段 1 收尾 BUG-C:auth-refactor.js 调 /auth/loginByPassword(txw-cloud 新栈)。
|
||||
// 8080 是新 gateway(auth 9200 / system 9201 都在它后面),不配这条会落到 SPA fallback。
|
||||
'^/auth': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/sso/did/pub': {
|
||||
// 阶段 2 BUG-D:login.vue 的 DID 扫码轮询(每 2s 一次)打 /sso/did/pub/backresult/login,
|
||||
// 老栈 target 是 cciw.com.cn 生产,本地 reqId/cookie 校验失败会一直返回"密码错误"。
|
||||
// 新栈 nacos 白名单有 /auth/did/pub/**,把 /sso/did/pub/** 改写到 8080 的 /auth/did/pub/**。
|
||||
// 必须放在 '^/sso' 之前,否则会被广匹配的 '^/sso' 截走。
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '^/sso/did/pub': '/auth/did/pub' },
|
||||
},
|
||||
'^/sso': {
|
||||
// target: 'http://localhost:9300',
|
||||
// target: 'http://carbon.liantu.tech',
|
||||
target: 'https://www.cciw.com.cn',
|
||||
// target: 'http://10.23.20.13:94/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/mhzc': {
|
||||
target: process.env.VUE_APP_MHZC_PROXY || 'https://www.cciw.com.cn',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/gxzx': {
|
||||
// target: 'http://localhost:9300',
|
||||
// target: 'http://carbon.liantu.tech',
|
||||
target: 'https://www.cciw.com.cn',
|
||||
// target: 'http://10.23.20.13:94/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/yygl': {
|
||||
// target: 'http://localhost:20010',
|
||||
// target: 'http://carbon.liantu.tech',
|
||||
target: 'https://www.cciw.com.cn',
|
||||
// target: 'http://10.23.20.13:94/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
//
|
||||
// 阶段 1 收尾 BUG-C:/auth /system proxy 以前硬编码 8080 (txw-cloud 新栈),
|
||||
// 老栈模式 (USE_NEW_BACKEND=false) 时也照打 8080,导致切换不彻底。
|
||||
// 现在根据 VUE_APP_USE_NEW_BACKEND 动态选 target:
|
||||
// true → 新栈 txw-cloud 8080
|
||||
// false → 老栈,走 VUE_APP_MHZC_PROXY (默认 localhost:9300)
|
||||
proxy: (() => {
|
||||
const useNew = process.env.VUE_APP_USE_NEW_BACKEND === 'true';
|
||||
const newTarget = 'http://localhost:8080';
|
||||
const legacyTarget = process.env.VUE_APP_MHZC_PROXY || 'http://localhost:9300';
|
||||
const gatewayTarget = useNew ? newTarget : legacyTarget;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`====>> 后端模式: ${useNew ? 'new (txw-cloud 8080)' : 'legacy (' + legacyTarget + ')'}`,
|
||||
);
|
||||
return {
|
||||
// auth-refactor.js 调 /auth/loginByPassword;txw-cloud 8080 网关后面挂着 auth 9200。
|
||||
// 老栈模式: 走 VUE_APP_MHZC_PROXY (默认 9300 老 gateway)。
|
||||
'^/auth': {
|
||||
target: gatewayTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
// getInfo 调 /system/user/getInfo;txw-system 9201 业务接口在 8080 网关后面。
|
||||
'^/system': {
|
||||
target: gatewayTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
// DID 扫码轮询 (login.vue 每 2s 一次) 改写到新栈的 /auth/did/pub/**。
|
||||
// 隔离原则: 老栈模式 (USE_NEW_BACKEND=false) 也打老栈 gatewayTarget (9300),
|
||||
// 即使老栈没这个接口 → 502/404 → 前端 catch 兜底, 不让老栈 dev server 跨栈打新栈。
|
||||
// 历史: 重构前 (B984406 之前) 这条根本没配, /sso/did/pub 走 ^/sso 打 cciw.com.cn 生产,
|
||||
// 一直"密码错误"。BUG-D 修复后才配。隔离之后, 老栈的扫码自然 502, 行为收敛。
|
||||
'^/sso/did/pub': {
|
||||
target: gatewayTarget,
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '^/sso/did/pub': '/auth/did/pub' },
|
||||
},
|
||||
'^/sso': {
|
||||
// 老栈 target 走 cciw.com.cn 生产 (历史行为保留)。
|
||||
target: 'https://www.cciw.com.cn',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/mhzc': {
|
||||
target: process.env.VUE_APP_MHZC_PROXY || 'https://www.cciw.com.cn',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/gxzx': {
|
||||
// target: 'http://localhost:9300',
|
||||
// target: 'http://carbon.liantu.tech',
|
||||
target: 'https://www.cciw.com.cn',
|
||||
// target: 'http://10.23.20.13:94/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/yygl': {
|
||||
// target: 'http://localhost:20010',
|
||||
// target: 'http://carbon.liantu.tech',
|
||||
target: 'https://www.cciw.com.cn',
|
||||
// target: 'http://10.23.20.13:94/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
};
|
||||
})(),
|
||||
before(app) {
|
||||
if (process.env.VUE_APP_MOCK === 'true') {
|
||||
// 是否开启MOCK,默认开启,检查项目根目录下是否存在.env.development文件 内容为VUE_APP_MOCK=true
|
||||
|
||||
Loading…
Reference in New Issue
Block a user