Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yd-csf-h5
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
jianan
yd-csf-h5
Commits
a7ad962a
Commit
a7ad962a
authored
Nov 02, 2026
by
Sweet Zhang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
解决本地联调问题
parent
5491ae58
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
285 additions
and
49 deletions
+285
-49
.env.example
+0
-26
DEV-LAN-ACCESS.md
+155
-0
src/App.vue
+15
-2
src/__tests__/App.spec.ts
+6
-0
src/components/dev/DevBrokerSwitcher.vue
+2
-0
src/components/feature/LoginBadge.vue
+19
-5
src/locales/zh-Hans.ts
+4
-0
src/locales/zh-Hant.ts
+4
-0
src/router/index.ts
+5
-3
src/stores/auth.ts
+22
-6
src/views/LoggedOutView.vue
+29
-3
vite.config.ts
+24
-4
No files found.
.env.example
deleted
100644 → 0
View file @
5491ae58
# 复制为 .env 供本地开发使用;真实 .env 不入库。
# OIDC 变量在「构建期」固化进产物,测试/生产由流水线 --build-arg 分别注入。
# IdP(Casdoor)地址
VITE_OIDC_AUTHORITY=https://center.zuihuibi.cn
# 在 IdP 为本站登记的应用 clientId
VITE_OIDC_CLIENT_ID=<测试应用-clientId>
VITE_OIDC_SCOPE=openid profile phone
# 仅本地开发使用:vite dev server 代理的后端地址。
# 不设置时指向 http://localhost:9202(本机起的 yd-csf-api),
# 需要连测试环境时再显式改成测试域名,避免本地开发误写测试库数据。
# VITE_API_TARGET=http://localhost:9202
# 置为 true 才启用接口 mock(当前仅影响首页待办的假延迟),默认关闭。
# VITE_USE_MOCK=false
# mock-only 本地核对模式:整条 OIDC 链路停用,用 localStorage 里的转介人身份直连后端。
# 只在本地 .env.development.local 里临时打开,切勿写进测试/生产构建。
# VITE_MOCK_ONLY=true
# 说明:
# redirect_uri / post_logout_redirect_uri 由代码按当前域名自动拼出,无需配置。
# 上线 https://center.zuihuibi.cn 前,需在 IdP 应用里登记:
# 允许的回调:https://center.zuihuibi.cn/auth/callback
# 登出回跳: https://center.zuihuibi.cn/logged-out
DEV-LAN-ACCESS.md
0 → 100644
View file @
a7ad962a
# 局域网 IP 访问开发服务器的可访问性说明
> 结论先行:**用 `http://localhost:5173` 能登录、用 `http://10.0.10.x:5173` 报 403,不是前端 bug,是登录服务(auth.anjibao.cn)网关的 Origin 白名单不含局域网地址。** 前端改不掉,必须让登录服务管理员登记,或改用 localhost 隧道。
---
## 1. 现象
从另一台电脑打开
`http://10.0.10.22:5173`
时:
```
GET https://auth.anjibao.cn/.well-known/openid-configuration
net::ERR_FAILED 403 (Forbidden)
at UserManager.signinRedirect (oidc.ts:53)
```
## 2. 实测证据
对同一个 discovery 地址,只改
`Origin`
请求头:
| Origin | 响应 |
|---|---|
| 不发送 Origin |
`200`
|
|
`http://localhost:5173`
|
`200`
|
|
**`http://10.0.10.22:5173`**
|
**`403`**
|
|
`https://center.zuihuibi.cn`
|
`200`
|
403 的响应头:
```
HTTP/1.1 403 Forbidden
Server: nginx
Vary: Origin
Content-Security-Policy: frame-ancestors 'self' http://localhost:5173
https://mdev.anjibao.cn https://mdev.zuihuibi.cn
```
`Vary: Origin`
+ 明确的白名单列表说明:
**auth.anjibao.cn 前面的 nginx 按 Origin 放行**
。
白名单里目前有
`http://localhost:5173`
、
`https://mdev.anjibao.cn`
、
`https://mdev.zuihuibi.cn`
,
**没有局域网 IP**
。
oidc-client-ts 在
`signinRedirect`
时会先 fetch discovery 文档(
`getMetadata`
),这一步被 403,整个授权流程就中断了 —— 所以点登录/登出都没反应。
## 3. 解决方案(三选一)
### 方案 A:让登录服务管理员登记(唯一彻底的办法)
需要登记
**两项**
,缺一项仍然登录不了:
1.
**网关 Origin 白名单**
(nginx)加入
`http://10.0.10.22:5173`
—— 解决 discovery / token 端点的 403。
2.
**Casdoor 应用的回调地址白名单**
加入
`http://10.0.10.22:5173/auth/callback`
——
`redirect_uri`
是代码按
`window.location.origin`
自动拼的,用 IP 访问时就是 IP 地址。
只改第 1 项不改这项,会在输完账号密码后报
`redirect_uri`
不匹配。
> 可以直接把下面这段发给管理员:
>
> 需要在登录服务 auth.anjibao.cn 登记一个开发联调用来源 `http://10.0.10.22:5173`:
> 1. nginx 的 Origin 白名单(以及 CSP frame-ancestors)加这一项;
> 2. 对应 Casdoor 应用的回调地址白名单加 `http://10.0.10.22:5173/auth/callback`。
登记完成后无需改任何代码,重启 dev server 即可。
### 方案 B:localhost 隧道(不用等管理员,立刻可用)
让
**浏览器地址栏保持 `localhost:5173`**
,这样 Origin 就在白名单里,请求通过隧道转发到真正跑 dev server 的机器。
-
用 VS Code 的「端口转发」把远端 5173 映射到本地 5173;
-
或 SSH:
`ssh -L 5173:127.0.0.1:5173 <user>@10.0.10.22`
,然后本机浏览器开
`http://localhost:5173`
;
-
Windows 无 SSH 时可用
`netsh interface portproxy`
做转发,但转发后浏览器仍需访问
`localhost`
。
注意:隧道转发的是 dev server 端口,页面里的
`/csf/api`
仍走 vite 代理,不受影响。
### 方案 C:本地核对模式(跳过整条 OIDC)
项目已内置
`VITE_MOCK_ONLY`
开关,置为
`true`
后路由守卫直接放行、不再发起 OIDC。
在
`.env.development.local`
中配置(
`VITE_MOCK_ONLY=true`
),配合左下角的转介人切换面板使用。
**仅用于本地核对,切勿写进测试/生产构建。**
## 4. 附带发现并已修复的问题
### 4.1 本地开发实际连的是正式域名(危险,已修)
`vite.config.ts`
里
`loadEnv(mode, '.', 'http://10.0.10.40:9002')`
的第三个参数是
**变量前缀**
,不是后端地址。传成地址后:
```
当前写法读到 VITE_API_TARGET = undefined (实测只加载到 0 个变量)
正确写法(prefixes='') 读到 = "http://10.0.10.40:9002"
```
结果:
`VITE_API_TARGET`
恒为 undefined,
`/csf/api`
静默回退到
`https://center.zuihuibi.cn`
——
**本地开发一直在连正式域名,有写库风险**
,而你
`.env`
/
`.env.example`
里配的
`10.0.10.40:9002`
从未生效。
已改为
`loadEnv(mode, '.', '')`
,并补了本地
`.env`
。修复后实测:
```
GET http://127.0.0.1:5173/csf/api/c/dev/brokers
→ 200 {"code":200,"msg":"操作成功","data":[{"brokerBizId":...}]}
```
未配置
`VITE_API_TARGET`
时启动时会有醒目警告,不再静默连正式库。
### 4.2 dev 面板硬编码后端地址(已修)
`src/components/dev/DevBrokerSwitcher.vue`
里直连
`http://10.0.10.40:9002/...`
,绕过了 vite 代理,从其他机器访问时该地址未必可达。已改为走
`/csf/api`
。
### 4.3 动态导入失败的报错(已加固)
```
TypeError: Failed to fetch dynamically imported module:
http://10.0.10.22:5173/src/components/dev/DevBrokerSwitcher.vue
```
**已排除 Vite 的 host 校验**
:用
`Host: 10.0.10.22`
请求该模块,返回
`200`
+
`Content-Type: text/javascript`
,Vite 6 默认放行 IP 形式的 Host。
真实原因通常是
**dev server 正在重启或重新预构建依赖**
(改代码触发、或外部机器首次访问触发 optimize),期间模块请求返回的是 HTML 兜底而非 JS,浏览器就报这个错。
已做的加固:
-
该 dev 面板改为异步加载失败时降级渲染为空,不再抛未捕获异常;
-
`vite.config.ts`
增加
`server.warmup`
,减少首次访问触发重优化的概率。
自查命令(在
**跑 dev server 的那台机器**
上执行):
```
bash
curl
-i
http://127.0.0.1:5173/src/components/dev/DevBrokerSwitcher.vue
# 正常应为 200 且 Content-Type: text/javascript
# 若返回 200 但是 text/html,说明该服务跑的是 preview/dist 而不是 dev server
```
若另一台机器上跑的是
`vite preview`
或 nginx 托管的
`dist/`
,
`/src/...`
路径根本不存在,SPA 兜底返回 index.html,同样会报这个错 —— 这种情况属于预期,dev 面板不会出现在生产产物中。
### 4.4 登录失败现在有可读提示(已加)
原先 IdP 不可达时守卫直接跳
`/logged-out`
,页面只显示「已退出登录」,看不出原因。
现在会带
`?reason=idp-unreachable`
,登出页显示黄色提示条,写明「当前访问地址可能未在登录服务登记白名单」,并给出改用 localhost / 联系管理员的建议。
## 5. 快速排查清单
```
bash
# 1. 确认当前 Origin 是否被登录服务放行(在被访问的那台机器上执行)
curl
-s
-o
/dev/null
-w
"%{http_code}
\n
"
\
-H
"Origin: http://10.0.10.22:5173"
\
https://auth.anjibao.cn/.well-known/openid-configuration
# 200 = 已登记;403 = 未登记,走方案 A 或 B
# 2. 确认后端代理目标正确
curl
-s
http://127.0.0.1:5173/csf/api/c/dev/brokers | head
-c
120
# 3. 确认 dev server 监听在 0.0.0.0(vite.config.ts 的 server.host)
# 启动日志应同时出现 Local 与 Network 两行地址
```
---
*最后更新:2026-11-02*
src/App.vue
View file @
a7ad962a
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
defineAsyncComponent
}
from
'vue'
import
{
computed
,
defineAsyncComponent
,
defineComponent
}
from
'vue'
import
{
useRoute
}
from
'vue-router'
import
{
useRoute
}
from
'vue-router'
import
{
useI18n
}
from
'vue-i18n'
import
{
useI18n
}
from
'vue-i18n'
import
LoginBadge
from
'@/components/feature/LoginBadge.vue'
import
LoginBadge
from
'@/components/feature/LoginBadge.vue'
...
@@ -8,8 +8,21 @@ const route = useRoute()
...
@@ -8,8 +8,21 @@ const route = useRoute()
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
// 生产构建中 import.meta.env.DEV 被替换为 false,死分支连同动态导入一起被 tree-shake
// 生产构建中 import.meta.env.DEV 被替换为 false,死分支连同动态导入一起被 tree-shake
// dev 面板加载失败(vite 重启、依赖重优化、跨机器网络抖动)时降级渲染为空,
// 不再抛出「Failed to fetch dynamically imported module」的未捕获异常。
const
DevFallback
=
defineComponent
({
render
:
()
=>
null
})
const
DevBrokerSwitcher
=
import
.
meta
.
env
.
DEV
const
DevBrokerSwitcher
=
import
.
meta
.
env
.
DEV
?
defineAsyncComponent
(()
=>
import
(
'@/components/dev/DevBrokerSwitcher.vue'
))
?
defineAsyncComponent
({
loader
:
()
=>
import
(
'@/components/dev/DevBrokerSwitcher.vue'
),
timeout
:
10
_000
,
errorComponent
:
DevFallback
,
onError
(
err
,
_retry
,
fail
)
{
console
.
warn
(
'[dev] 转介人切换面板加载失败,已跳过:'
,
err
)
// 必须调用 fail(),否则 promise 悬停,errorComponent 不会生效
fail
()
}
})
:
null
:
null
const
tabs
=
[
const
tabs
=
[
...
...
src/__tests__/App.spec.ts
View file @
a7ad962a
...
@@ -12,6 +12,12 @@ vi.mock('@/utils/oidc', () => ({
...
@@ -12,6 +12,12 @@ vi.mock('@/utils/oidc', () => ({
signoutRedirect
:
vi
.
fn
()
signoutRedirect
:
vi
.
fn
()
}))
}))
// 测试环境里 import.meta.env.DEV 同样为 true,App 会真的加载 dev 面板并发起请求,
// jsdom 无法连接后端地址,会产生 XHR 的 AggregateError 噪音。这里隔离掉。
vi
.
mock
(
'@/components/dev/DevBrokerSwitcher.vue'
,
()
=>
({
default
:
{
render
:
()
=>
null
}
}))
import
App
from
'../App.vue'
import
App
from
'../App.vue'
const
i18n
=
createI18n
({
const
i18n
=
createI18n
({
...
...
src/components/dev/DevBrokerSwitcher.vue
View file @
a7ad962a
...
@@ -34,6 +34,8 @@ const filtered = computed(() => {
...
@@ -34,6 +34,8 @@ const filtered = computed(() => {
onMounted
(
async
()
=>
{
onMounted
(
async
()
=>
{
try
{
try
{
// 走 vite 代理(/csf/api)而不是硬编码后端地址:
// 硬编码时从局域网其他机器访问会绕过代理直连该地址,且后端 IP 一变就失效。
brokers
.
value
=
await
get
<
BrokerOption
[]
>
(
'/c/dev/brokers'
)
brokers
.
value
=
await
get
<
BrokerOption
[]
>
(
'/c/dev/brokers'
)
loaded
.
value
=
true
loaded
.
value
=
true
}
catch
(
e
)
{
}
catch
(
e
)
{
...
...
src/components/feature/LoginBadge.vue
View file @
a7ad962a
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'vue'
import
{
computed
,
ref
}
from
'vue'
import
{
storeToRefs
}
from
'pinia'
import
{
storeToRefs
}
from
'pinia'
import
{
useI18n
}
from
'vue-i18n'
import
{
useI18n
}
from
'vue-i18n'
import
{
useAuthStore
}
from
'@/stores/auth'
import
{
useAuthStore
}
from
'@/stores/auth'
...
@@ -15,6 +15,18 @@ const mockOnly = import.meta.env.VITE_MOCK_ONLY === 'true'
...
@@ -15,6 +15,18 @@ const mockOnly = import.meta.env.VITE_MOCK_ONLY === 'true'
const
isHant
=
computed
(()
=>
preference
.
locale
===
'zh-Hant'
)
const
isHant
=
computed
(()
=>
preference
.
locale
===
'zh-Hant'
)
// 登录/登出发起失败(IdP 不可达、origin 未登记白名单)时按钮转为「重试」,
// 避免点击后毫无反应、只有一个控制台里的 unhandled rejection
const
failed
=
ref
(
false
)
async
function
onLogin
():
Promise
<
void
>
{
failed
.
value
=
!
(
await
auth
.
login
())
}
async
function
onLogout
():
Promise
<
void
>
{
failed
.
value
=
!
(
await
auth
.
logout
())
}
function
toggleLocale
()
{
function
toggleLocale
()
{
preference
.
setLocale
(
isHant
.
value
?
'zh-Hans'
:
'zh-Hant'
)
preference
.
setLocale
(
isHant
.
value
?
'zh-Hans'
:
'zh-Hant'
)
}
}
...
@@ -36,15 +48,16 @@ function toggleLocale() {
...
@@ -36,15 +48,16 @@ function toggleLocale() {
<span
class=
"max-w-[7rem] truncate text-sm font-medium"
>
{{
user
.
name
}}
</span>
<span
class=
"max-w-[7rem] truncate text-sm font-medium"
>
{{
user
.
name
}}
</span>
<button
<button
class=
"shrink-0 text-xs text-slate-400 transition-colors hover:text-slate-600"
class=
"shrink-0 text-xs text-slate-400 transition-colors hover:text-slate-600"
@
click=
"
auth.logout()
"
@
click=
"
onLogout
"
>
{{
t
(
'auth.logout'
)
}}
</button>
>
{{
t
(
'auth.logout'
)
}}
</button>
</div>
</div>
<button
<button
v-else-if=
"!mockOnly"
v-else-if=
"!mockOnly"
class=
"rounded-full bg-brand-600 px-4 py-1.5 text-sm font-medium text-white transition-colors hover:bg-brand-700"
class=
"rounded-full px-4 py-1.5 text-sm font-medium text-white transition-colors"
@
click=
"auth.login()"
:class=
"failed ? 'bg-red-500 hover:bg-red-600' : 'bg-brand-600 hover:bg-brand-700'"
@
click=
"onLogin"
>
>
{{
t
(
'auth.login'
)
}}
{{
failed
?
t
(
'auth.retry'
)
:
t
(
'auth.login'
)
}}
</button>
</button>
</div>
</div>
</
template
>
</
template
>
\ No newline at end of file
src/locales/zh-Hans.ts
View file @
a7ad962a
...
@@ -110,9 +110,13 @@ export default {
...
@@ -110,9 +110,13 @@ export default {
'auth.callbackDesc'
:
'登录成功,正在跳转…'
,
'auth.callbackDesc'
:
'登录成功,正在跳转…'
,
'auth.callbackError'
:
'登录失败或授权已失效,请重试。'
,
'auth.callbackError'
:
'登录失败或授权已失效,请重试。'
,
'auth.login'
:
'登录'
,
'auth.login'
:
'登录'
,
'auth.retry'
:
'重试'
,
'auth.logout'
:
'退出'
,
'auth.logout'
:
'退出'
,
'auth.loggedOutTitle'
:
'已退出登录'
,
'auth.loggedOutTitle'
:
'已退出登录'
,
'auth.loggedOutDesc'
:
'本门户登录态已清除,不影响其他系统。'
,
'auth.loggedOutDesc'
:
'本门户登录态已清除,不影响其他系统。'
,
'auth.idpUnreachableTitle'
:
'无法连接登录服务'
,
'auth.idpUnreachableDesc'
:
'常见原因:当前访问地址未在该登录服务登记白名单(例如用局域网 IP 打开)。请改用 localhost 访问,或联系管理员登记当前地址后重试。'
,
'lang.zhHant'
:
'繁'
,
'lang.zhHant'
:
'繁'
,
'lang.zhHans'
:
'简'
,
'lang.zhHans'
:
'简'
,
'common.pullHint'
:
'下拉即可重新整理'
'common.pullHint'
:
'下拉即可重新整理'
...
...
src/locales/zh-Hant.ts
View file @
a7ad962a
...
@@ -110,9 +110,13 @@ export default {
...
@@ -110,9 +110,13 @@ export default {
'auth.callbackDesc'
:
'登入成功,正在跳轉…'
,
'auth.callbackDesc'
:
'登入成功,正在跳轉…'
,
'auth.callbackError'
:
'登入失敗或授權已失效,請重試。'
,
'auth.callbackError'
:
'登入失敗或授權已失效,請重試。'
,
'auth.login'
:
'登入'
,
'auth.login'
:
'登入'
,
'auth.retry'
:
'重試'
,
'auth.logout'
:
'退出'
,
'auth.logout'
:
'退出'
,
'auth.loggedOutTitle'
:
'已登出'
,
'auth.loggedOutTitle'
:
'已登出'
,
'auth.loggedOutDesc'
:
'本門戶登入態已清除,不影響其他系統。'
,
'auth.loggedOutDesc'
:
'本門戶登入態已清除,不影響其他系統。'
,
'auth.idpUnreachableTitle'
:
'無法連接登入服務'
,
'auth.idpUnreachableDesc'
:
'常見原因:當前訪問地址未在該登入服務登記白名單(例如用局域網 IP 打開)。請改用 localhost 訪問,或聯絡管理員登記當前地址後重試。'
,
'lang.zhHant'
:
'繁'
,
'lang.zhHant'
:
'繁'
,
'lang.zhHans'
:
'简'
,
'lang.zhHans'
:
'简'
,
'common.pullHint'
:
'下拉即可重新整理'
'common.pullHint'
:
'下拉即可重新整理'
...
...
src/router/index.ts
View file @
a7ad962a
...
@@ -31,9 +31,11 @@ router.beforeEach(async (to) => {
...
@@ -31,9 +31,11 @@ router.beforeEach(async (to) => {
try
{
try
{
await
signinRedirect
(
to
.
fullPath
)
await
signinRedirect
(
to
.
fullPath
)
}
catch
{
}
catch
(
e
)
{
// IdP 不可达时 signinRedirect 会 reject;不兜住会导致导航中断、整页空白
// IdP 不可达 / 当前 origin 未登记(403)时 signinRedirect 会 reject;
return
{
name
:
'logged-out'
}
// 不兜住会导致导航中断、整页空白。带上 reason 让登出页给出可读提示。
console
.
error
(
'[auth] 无法发起 OIDC 授权:'
,
e
)
return
{
name
:
'logged-out'
,
query
:
{
reason
:
'idp-unreachable'
}
}
}
}
// 已发起整页跳转,终止本次导航
// 已发起整页跳转,终止本次导航
return
false
return
false
...
...
src/stores/auth.ts
View file @
a7ad962a
...
@@ -33,9 +33,19 @@ export const useAuthStore = defineStore('auth', () => {
...
@@ -33,9 +33,19 @@ export const useAuthStore = defineStore('auth', () => {
user
.
value
=
toAuthUser
(
await
userManager
.
getUser
())
user
.
value
=
toAuthUser
(
await
userManager
.
getUser
())
}
}
/** 发起 OIDC 登录;当前路径随 state 携带,回调后跳回。 */
/**
function
login
(
returnPath
?:
string
):
Promise
<
void
>
{
* 发起 OIDC 登录;当前路径随 state 携带,回调后跳回。
return
signinRedirect
(
returnPath
)
* 返回 false 表示发起失败(如 IdP 不可达、当前 origin 未登记白名单返回 403)。
* 原先直接透传 promise,失败时是 unhandled rejection —— 界面上表现为「点了没反应」。
*/
async
function
login
(
returnPath
?:
string
):
Promise
<
boolean
>
{
try
{
await
signinRedirect
(
returnPath
)
return
true
}
catch
(
e
)
{
console
.
error
(
'[auth] 发起登录失败:'
,
e
)
return
false
}
}
}
/** 处理授权码回调,装载登录态,返回登录前路径。 */
/** 处理授权码回调,装载登录态,返回登录前路径。 */
...
@@ -45,9 +55,15 @@ export const useAuthStore = defineStore('auth', () => {
...
@@ -45,9 +55,15 @@ export const useAuthStore = defineStore('auth', () => {
return
returnPath
return
returnPath
}
}
/** 登出:跳转 IdP 结束 SSO 会话,回 /logged-out。 */
/** 登出:跳转 IdP 结束 SSO 会话,回 /logged-out。返回 false 表示 IdP 不可达。 */
function
logout
():
Promise
<
void
>
{
async
function
logout
():
Promise
<
boolean
>
{
return
signoutRedirect
()
try
{
await
signoutRedirect
()
return
true
}
catch
(
e
)
{
console
.
error
(
'[auth] 发起登出失败:'
,
e
)
return
false
}
}
}
return
{
user
,
init
,
login
,
handleCallback
,
logout
}
return
{
user
,
init
,
login
,
handleCallback
,
logout
}
...
...
src/views/LoggedOutView.vue
View file @
a7ad962a
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
ref
}
from
'vue'
import
{
useRoute
}
from
'vue-router'
import
{
useI18n
}
from
'vue-i18n'
import
{
useI18n
}
from
'vue-i18n'
import
{
useAuthStore
}
from
'@/stores/auth'
import
{
useAuthStore
}
from
'@/stores/auth'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
auth
=
useAuthStore
()
const
auth
=
useAuthStore
()
const
route
=
useRoute
()
/** 登录失败回跳时带的 reason,用于在页面上给出可操作的排查方向 */
const
unreachable
=
computed
(()
=>
route
.
query
.
reason
===
'idp-unreachable'
)
// 点「登录」再次发起失败时(IdP 仍不可达)也要能看出原因
const
loginFailed
=
ref
(
false
)
const
showHint
=
computed
(()
=>
unreachable
.
value
||
loginFailed
.
value
)
async
function
onLogin
():
Promise
<
void
>
{
loginFailed
.
value
=
!
(
await
auth
.
login
(
'/'
))
}
</
script
>
</
script
>
<
template
>
<
template
>
<div
class=
"py-16 text-center"
>
<div
class=
"py-16 text-center"
>
<h1
class=
"text-lg font-semibold"
>
{{
t
(
'auth.loggedOutTitle'
)
}}
</h1>
<h1
class=
"text-lg font-semibold"
>
{{
t
(
'auth.loggedOutTitle'
)
}}
</h1>
<p
class=
"mt-2 text-sm text-slate-500"
>
{{
t
(
'auth.loggedOutDesc'
)
}}
</p>
<p
class=
"mt-2 text-sm text-slate-500"
>
{{
t
(
'auth.loggedOutDesc'
)
}}
</p>
<div
v-if=
"showHint"
class=
"mx-auto mt-5 max-w-sm rounded-xl border border-amber-300 bg-amber-50 px-4 py-3 text-left"
>
<p
class=
"text-sm font-medium text-amber-800"
>
{{
t
(
'auth.idpUnreachableTitle'
)
}}
</p>
<p
class=
"mt-1 text-xs leading-relaxed text-amber-700"
>
{{
t
(
'auth.idpUnreachableDesc'
)
}}
</p>
</div>
<button
<button
class=
"mt-4 rounded-lg bg-brand-600 px-4 py-2 text-sm text-white"
class=
"mt-4 rounded-lg px-4 py-2 text-sm text-white transition-colors"
@
click=
"auth.login('/')"
:class=
"loginFailed ? 'bg-red-500 hover:bg-red-600' : 'bg-brand-600 hover:bg-brand-700'"
@
click=
"onLogin"
>
>
{{
t
(
'auth.login'
)
}}
{{
loginFailed
?
t
(
'auth.retry'
)
:
t
(
'auth.login'
)
}}
</button>
</button>
</div>
</div>
</
template
>
</
template
>
vite.config.ts
View file @
a7ad962a
...
@@ -3,9 +3,21 @@ import vue from '@vitejs/plugin-vue'
...
@@ -3,9 +3,21 @@ import vue from '@vitejs/plugin-vue'
import
{
fileURLToPath
,
URL
}
from
'node:url'
import
{
fileURLToPath
,
URL
}
from
'node:url'
export
default
defineConfig
(({
mode
})
=>
{
export
default
defineConfig
(({
mode
})
=>
{
// 第三个参数传 '' 表示不限制 VITE_ 前缀,VITE_API_TARGET 也一并读取
// 第三个参数是「变量前缀」,必须传 '' 表示不限制前缀。
// 曾误传成后端地址,导致 loadEnv 只匹配以该地址开头的 key —— VITE_* 一个都读不到(实测 0 个),
// VITE_API_TARGET 恒为 undefined,本地开发被静默代理到正式域名。
const
env
=
loadEnv
(
mode
,
'.'
,
''
)
const
env
=
loadEnv
(
mode
,
'.'
,
''
)
// 未显式配置时给出醒目警告:默认目标指向正式域名,本地开发误连会有写库风险
const
apiTarget
=
env
.
VITE_API_TARGET
||
'https://center.zuihuibi.cn'
if
(
!
env
.
VITE_API_TARGET
)
{
// 用 stdout 直接写:vite 加载配置期间 console 可能被日志器接管,警告会静默丢失
process
.
stdout
.
write
(
'
\
n[vite] 警告:未设置 VITE_API_TARGET,/csf/api 将代理到正式域名 https://center.zuihuibi.cn
\
n'
+
' 本地开发请在 .env 中设置为本机后端,例如 VITE_API_TARGET=http://localhost:9202
\
n
\
n'
)
}
return
{
return
{
plugins
:
[
vue
()],
plugins
:
[
vue
()],
resolve
:
{
resolve
:
{
...
@@ -17,12 +29,20 @@ export default defineConfig(({ mode }) => {
...
@@ -17,12 +29,20 @@ export default defineConfig(({ mode }) => {
proxy
:
{
proxy
:
{
// 后端 context-path 即 /csf/api,代理时不 rewrite
// 后端 context-path 即 /csf/api,代理时不 rewrite
'/csf/api'
:
{
'/csf/api'
:
{
// 原先硬编码测试环境域名,本地一跑 npm run dev 就直接打测试库,容易误操作数据。
// 需要连测试/正式环境时在 .env 里显式设置 VITE_API_TARGET,避免本地开发误写远端库数据。
// 需要连测试环境时在 .env.local 里显式设置 VITE_API_TARGET。
target
:
apiTarget
,
target
:
env
.
VITE_API_TARGET
||
'https://center.zuihuibi.cn'
,
changeOrigin
:
true
,
changeOrigin
:
true
,
secure
:
false
secure
:
false
}
}
},
// 允许局域网其他机器通过本机 IP 访问(如 http://10.0.10.22:5173)。
// Vite 默认放行 localhost 与 IP 形式的 Host;若改用内网域名访问,需显式配置
// allowedHosts: ['your.intranet.host'](不要设 true,会关闭 host 校验带来 DNS rebinding 风险)。
host
:
'0.0.0.0'
,
port
:
5173
,
// 局域网联调时从其他机器首次打开可能触发依赖重优化,预热后更稳
warmup
:
{
clientFiles
:
[
'./src/main.ts'
,
'./src/App.vue'
]
}
}
},
},
build
:
{
build
:
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment