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:
liulong 2026-06-08 18:00:21 +08:00
parent d8f7f2f94f
commit ec74735f94
14 changed files with 741 additions and 217 deletions

View File

@ -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

View 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

View File

@ -0,0 +1,48 @@
# 门户前端 token 同步方案(阶段 1 收尾 BUG-C
> 范围:`txw-mhzc-web` 门户前端与 `txw-cloud` 网关的鉴权头协商。
> 基线本地三后端在跑auth 9200 / system 9201 / gateway 8080UUID + 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`(不是 CookieCookie 是 HttpOnlyJS 读不到) |
| 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` 读 tokenHttpOnly 读不到)
- 不要把 token 存到 `sessionStorage`(刷新即丢)
- 不要在 `vuex` 全局状态里存(多 tab 不共享)

View 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 + ')');
}

View File

@ -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-Ctxw-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) {

View File

@ -0,0 +1,116 @@
/**
* 重构测试入口阶段 1 收尾 BUG-C 联调用
*
* <p><strong>不动老业务</strong> {@link ./login.js}
* 老业务继续走 {@code /sso/auth/login}若依老 SSOcookie + 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.htmltext/html MIME的问题。
// 仅在 dev / 非生产环境暴露,生产构建会走 terser drop_console 路径清理。
if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'production') {
window.__txwRefactor = {
loginByPassword,
getCaptcha,
getInfo,
logoutNew,
debugTokenState,
debugClearTokens,
};
}

View File

@ -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%;
}

View File

@ -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);

View File

@ -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;

View File

@ -4005,4 +4005,15 @@ export default {
max-width: none;
}
}
/* =============================================================================
* 首页 footer 反向缩放footer .home-figma-scale-stage 内部会被 transform: scale 放大
* home2 内部生效scoped 限定不影响走 main.vue 的内页 footer
* transform 不改变 layout boxstage margin-bottom 补偿保持正确
* ============================================================================= */
.portal-footer-shell {
transform: scale(calc(1 / var(--home-figma-scale, 1)));
transform-origin: top center;
width: 100%;
}
</style>

View File

@ -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() {

View File

@ -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;
}

View 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>为什么不用 CookieHttpOnly 标志是浏览器安全机制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) {
// 同上
}
}

View File

@ -287,48 +287,68 @@ module.exports = {
},
// Vue CLI prepareProxy 用 pathname.match(代理键) 判断;键写 '/mhzc' 会变成匹配路径里任意位置的 /mhzc
// 会误伤 SPA 路由 /view/mhzc/...,刷新时整页请求被转发到后端导致 Proxy error。必须用 ^ 限定为路径前缀。
proxy: {
// 阶段 1 收尾 BUG-Cauth-refactor.js 调 /auth/loginByPasswordtxw-cloud 新栈)。
// 8080 是新 gatewayauth 9200 / system 9201 都在它后面),不配这条会落到 SPA fallback。
'^/auth': {
target: 'http://localhost:8080',
changeOrigin: true,
},
'^/sso/did/pub': {
// 阶段 2 BUG-Dlogin.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/loginByPasswordtxw-cloud 8080 网关后面挂着 auth 9200。
// 老栈模式: 走 VUE_APP_MHZC_PROXY (默认 9300 老 gateway)。
'^/auth': {
target: gatewayTarget,
changeOrigin: true,
},
// getInfo 调 /system/user/getInfotxw-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