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
4a4af674
Commit
4a4af674
authored
Oct 29, 2026
by
Sweet Zhang
Browse files
Options
Browse Files
Download
Plain Diff
解决冲突
parents
0936166d
0cac32e9
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
216 additions
and
4 deletions
+216
-4
src/App.vue
+8
-1
src/api/http.ts
+26
-0
src/components/dev/DevBrokerSwitcher.vue
+137
-0
src/components/feature/LoginBadge.vue
+5
-2
src/components/feature/TodoCard.vue
+4
-0
src/env.d.ts
+11
-0
src/router/index.ts
+3
-0
src/stores/auth.ts
+3
-0
src/utils/devMock.ts
+18
-0
vite.config.ts
+1
-1
No files found.
src/App.vue
View file @
4a4af674
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'vue'
import
{
computed
,
defineAsyncComponent
}
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'
...
@@ -7,6 +7,11 @@ import LoginBadge from '@/components/feature/LoginBadge.vue'
...
@@ -7,6 +7,11 @@ import LoginBadge from '@/components/feature/LoginBadge.vue'
const
route
=
useRoute
()
const
route
=
useRoute
()
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
// 生产构建中 import.meta.env.DEV 被替换为 false,死分支连同动态导入一起被 tree-shake
const
DevBrokerSwitcher
=
import
.
meta
.
env
.
DEV
?
defineAsyncComponent
(()
=>
import
(
'@/components/dev/DevBrokerSwitcher.vue'
))
:
null
const
tabs
=
[
const
tabs
=
[
{
name
:
'home'
,
path
:
'/'
,
labelKey
:
'nav.home'
},
{
name
:
'home'
,
path
:
'/'
,
labelKey
:
'nav.home'
},
{
name
:
'statement'
,
path
:
'/statement'
,
labelKey
:
'nav.statement'
},
{
name
:
'statement'
,
path
:
'/statement'
,
labelKey
:
'nav.statement'
},
...
@@ -40,6 +45,8 @@ function isActive(path: string): boolean {
...
@@ -40,6 +45,8 @@ function isActive(path: string): boolean {
<RouterView
/>
<RouterView
/>
</main>
</main>
<DevBrokerSwitcher
v-if=
"DevBrokerSwitcher"
/>
<nav
<nav
v-if=
"!hideTabBar"
v-if=
"!hideTabBar"
class=
"fixed inset-x-0 bottom-0 z-20 border-t border-silver-200/70 bg-white/90 pb-safe backdrop-blur-lg"
class=
"fixed inset-x-0 bottom-0 z-20 border-t border-silver-200/70 bg-white/90 pb-safe backdrop-blur-lg"
...
...
src/api/http.ts
View file @
4a4af674
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
// 仅 Spring Security 层未认证会返回真实 401。
// 仅 Spring Security 层未认证会返回真实 401。
import
axios
from
'axios'
import
axios
from
'axios'
import
{
userManager
,
signinRedirect
}
from
'@/utils/oidc'
import
{
userManager
,
signinRedirect
}
from
'@/utils/oidc'
import
{
getMockBrokerBizId
,
isMockOnly
}
from
'@/utils/devMock'
interface
ApiResult
<
T
>
{
interface
ApiResult
<
T
>
{
code
:
number
code
:
number
...
@@ -10,6 +11,7 @@ interface ApiResult<T> {
...
@@ -10,6 +11,7 @@ interface ApiResult<T> {
data
:
T
data
:
T
}
}
<<<<<<<
HEAD
/** 错误分类:界面据此决定文案与是否可重试(认证类跳登录,其余可重试)。 */
/** 错误分类:界面据此决定文案与是否可重试(认证类跳登录,其余可重试)。 */
export
type
ApiErrorKind
=
'auth'
|
'timeout'
|
'network'
|
'server'
|
'business'
export
type
ApiErrorKind
=
'auth'
|
'timeout'
|
'network'
|
'server'
|
'business'
...
@@ -40,8 +42,25 @@ const instance = axios.create({
...
@@ -40,8 +42,25 @@ const instance = axios.create({
// 不加超时:后端 hang 住时页面 loading 会永久转圈
// 不加超时:后端 hang 住时页面 loading 会永久转圈
timeout
:
15000
timeout
:
15000
})
})
=======
const
HEADER_MOCK_BROKER
=
'X-Mock-Broker-Biz-Id'
// mock-only:本地生产核对环境(.env.development.local),整条 OIDC 链路停用
const
mockOnly
=
isMockOnly
const
instance
=
axios
.
create
({
baseURL
:
'/csf/api'
})
>>>>>>>
0
cac32e9c611857dfc32d14724490175aac1f693
instance
.
interceptors
.
request
.
use
(
async
(
config
)
=>
{
instance
.
interceptors
.
request
.
use
(
async
(
config
)
=>
{
// DEV 模拟身份优先于 OIDC(仅在 dev 构建中可能有值)
const
mockBrokerBizId
=
import
.
meta
.
env
.
DEV
?
getMockBrokerBizId
()
:
''
if
(
mockBrokerBizId
)
{
config
.
headers
[
HEADER_MOCK_BROKER
]
=
mockBrokerBizId
return
config
}
if
(
mockOnly
)
{
return
config
}
const
user
=
await
userManager
.
getUser
()
const
user
=
await
userManager
.
getUser
()
if
(
user
&&
!
user
.
expired
)
{
if
(
user
&&
!
user
.
expired
)
{
config
.
headers
.
Authorization
=
`Bearer
${
user
.
access_token
}
`
config
.
headers
.
Authorization
=
`Bearer
${
user
.
access_token
}
`
...
@@ -54,10 +73,17 @@ let redirecting = false
...
@@ -54,10 +73,17 @@ let redirecting = false
instance
.
interceptors
.
response
.
use
(
instance
.
interceptors
.
response
.
use
(
(
response
)
=>
response
,
(
response
)
=>
response
,
(
error
)
=>
{
(
error
)
=>
{
<<<<<<<
HEAD
const
status
:
number
|
undefined
=
error
.
response
?.
status
const
status
:
number
|
undefined
=
error
.
response
?.
status
if
(
status
===
401
)
{
if
(
status
===
401
)
{
if
(
!
redirecting
&&
window
.
location
.
pathname
!==
'/auth/callback'
)
{
if
(
!
redirecting
&&
window
.
location
.
pathname
!==
'/auth/callback'
)
{
=======
if
(
error
.
response
?.
status
===
401
)
{
// mock-only / 已选模拟身份时 401 交给页面处理,不跳 OIDC
const
mockActive
=
mockOnly
||
(
import
.
meta
.
env
.
DEV
&&
!!
getMockBrokerBizId
())
if
(
!
mockActive
&&
!
redirecting
&&
window
.
location
.
pathname
!==
'/auth/callback'
)
{
>>>>>>>
0
cac32e9c611857dfc32d14724490175aac1f693
redirecting
=
true
redirecting
=
true
// 跳转失败(IdP 不可达)时必须复位,否则此后再也不重定向,页面卡死在错误态
// 跳转失败(IdP 不可达)时必须复位,否则此后再也不重定向,页面卡死在错误态
signinRedirect
(
window
.
location
.
pathname
+
window
.
location
.
search
).
catch
(()
=>
{
signinRedirect
(
window
.
location
.
pathname
+
window
.
location
.
search
).
catch
(()
=>
{
...
...
src/components/dev/DevBrokerSwitcher.vue
0 → 100644
View file @
4a4af674
<
script
setup
lang=
"ts"
>
import
{
computed
,
onMounted
,
ref
}
from
'vue'
import
{
get
}
from
'@/api/http'
import
{
getMockBrokerBizId
,
setMockBrokerBizId
}
from
'@/utils/devMock'
interface
BrokerOption
{
brokerBizId
:
string
brokerName
:
string
hasStatement
:
boolean
}
const
brokers
=
ref
<
BrokerOption
[]
>
([])
const
selectedBizId
=
ref
(
getMockBrokerBizId
())
const
loaded
=
ref
(
false
)
const
error
=
ref
(
''
)
const
open
=
ref
(
false
)
const
keyword
=
ref
(
''
)
const
selectedName
=
computed
(
()
=>
brokers
.
value
.
find
((
b
)
=>
b
.
brokerBizId
===
selectedBizId
.
value
)?.
brokerName
??
''
)
const
filtered
=
computed
(()
=>
{
const
kw
=
keyword
.
value
.
trim
().
toLowerCase
()
const
all
=
kw
?
brokers
.
value
.
filter
((
b
)
=>
b
.
brokerName
.
toLowerCase
().
includes
(
kw
))
:
brokers
.
value
// 后端已按 hasStatement 倒序返回,这里分组保持顺序
return
{
withStatement
:
all
.
filter
((
b
)
=>
b
.
hasStatement
),
rest
:
all
.
filter
((
b
)
=>
!
b
.
hasStatement
)
}
})
onMounted
(
async
()
=>
{
try
{
brokers
.
value
=
await
get
<
BrokerOption
[]
>
(
'/c/dev/brokers'
)
loaded
.
value
=
true
}
catch
(
e
)
{
error
.
value
=
e
instanceof
Error
?
e
.
message
:
'error'
}
})
function
toggle
()
{
open
.
value
=
!
open
.
value
keyword
.
value
=
''
}
function
pick
(
broker
:
BrokerOption
)
{
open
.
value
=
false
if
(
broker
.
brokerBizId
===
selectedBizId
.
value
)
return
selectedBizId
.
value
=
broker
.
brokerBizId
setMockBrokerBizId
(
broker
.
brokerBizId
)
// 回首页再以新身份加载:避免停留在上一人的详情/列表 URL 触发归属校验
if
(
window
.
location
.
pathname
===
'/'
)
{
window
.
location
.
reload
()
}
else
{
window
.
location
.
href
=
'/'
}
}
</
script
>
<
template
>
<div
class=
"fixed bottom-20 left-3 z-30 w-56 text-xs"
>
<!-- 下拉面板:向上展开,自带滚动,不被 tab 栏遮挡 -->
<div
v-if=
"open"
class=
"absolute bottom-12 left-0 w-full overflow-hidden rounded-xl border border-amber-300 bg-white shadow-lg"
>
<input
v-model=
"keyword"
type=
"text"
placeholder=
"搜索姓名…"
class=
"w-full border-b border-amber-200 px-2.5 py-2 text-xs outline-none placeholder:text-slate-400"
>
<ul
class=
"max-h-[50vh] overflow-y-auto py-1"
>
<li
class=
"cursor-pointer px-2.5 py-1.5 text-slate-600 hover:bg-amber-50"
@
click=
"pick(
{ brokerBizId: '', brokerName: '', hasStatement: false })"
>
— 未选择 —
</li>
<li
v-if=
"filtered.withStatement.length"
class=
"px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-amber-700"
>
有月结单 ·
{{
filtered
.
withStatement
.
length
}}
</li>
<li
v-for=
"b in filtered.withStatement"
:key=
"b.brokerBizId"
class=
"cursor-pointer px-2.5 py-1.5 hover:bg-amber-50"
:class=
"b.brokerBizId === selectedBizId ? 'font-medium text-amber-800' : 'text-slate-700'"
@
click=
"pick(b)"
>
{{
b
.
brokerName
}}
</li>
<li
v-if=
"filtered.rest.length"
class=
"px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-slate-400"
>
其他有保单归属 ·
{{
filtered
.
rest
.
length
}}
</li>
<li
v-for=
"b in filtered.rest"
:key=
"b.brokerBizId"
class=
"cursor-pointer px-2.5 py-1.5 hover:bg-amber-50"
:class=
"b.brokerBizId === selectedBizId ? 'font-medium text-amber-800' : 'text-slate-700'"
@
click=
"pick(b)"
>
{{
b
.
brokerName
}}
</li>
<li
v-if=
"filtered.withStatement.length + filtered.rest.length === 0"
class=
"px-2.5 py-2 text-slate-400"
>
无匹配
</li>
</ul>
</div>
<button
class=
"flex w-full items-center justify-between rounded-xl border border-amber-300 bg-amber-50/95 px-2.5 py-2 font-medium text-amber-800 shadow-lg backdrop-blur"
@
click=
"toggle"
>
<span>
DEV ·
{{
selectedName
||
'模拟转介人'
}}
</span>
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"2"
stroke-linecap=
"round"
stroke-linejoin=
"round"
class=
"h-3.5 w-3.5 transition-transform"
:class=
"open ? 'rotate-180' : ''"
>
<polyline
points=
"18 15 12 9 6 15"
/>
</svg>
</button>
<div
v-if=
"!loaded && !error"
class=
"mt-1 text-amber-700"
>
加载中…
</div>
<div
v-if=
"error"
class=
"mt-1 text-red-600"
>
{{
error
}}
</div>
</div>
</
template
>
src/components/feature/LoginBadge.vue
View file @
4a4af674
...
@@ -10,6 +10,9 @@ const preference = usePreferenceStore()
...
@@ -10,6 +10,9 @@ const preference = usePreferenceStore()
const
{
user
}
=
storeToRefs
(
auth
)
const
{
user
}
=
storeToRefs
(
auth
)
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
// mock-only 核对环境不显示登录/用户区(语言切换保留)
const
mockOnly
=
import
.
meta
.
env
.
VITE_MOCK_ONLY
===
'true'
const
isHant
=
computed
(()
=>
preference
.
locale
===
'zh-Hant'
)
const
isHant
=
computed
(()
=>
preference
.
locale
===
'zh-Hant'
)
function
toggleLocale
()
{
function
toggleLocale
()
{
...
@@ -26,7 +29,7 @@ function toggleLocale() {
...
@@ -26,7 +29,7 @@ function toggleLocale() {
{{
isHant
?
t
(
'lang.zhHans'
)
:
t
(
'lang.zhHant'
)
}}
{{
isHant
?
t
(
'lang.zhHans'
)
:
t
(
'lang.zhHant'
)
}}
</button>
</button>
<div
v-if=
"user"
class=
"flex items-center gap-2"
>
<div
v-if=
"user
&& !mockOnly
"
class=
"flex items-center gap-2"
>
<div
class=
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand-600 text-sm font-medium text-white"
>
<div
class=
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand-600 text-sm font-medium text-white"
>
{{
user
.
name
.
slice
(
0
,
1
)
}}
{{
user
.
name
.
slice
(
0
,
1
)
}}
</div>
</div>
...
@@ -37,7 +40,7 @@ function toggleLocale() {
...
@@ -37,7 +40,7 @@ function toggleLocale() {
>
{{
t
(
'auth.logout'
)
}}
</button>
>
{{
t
(
'auth.logout'
)
}}
</button>
</div>
</div>
<button
<button
v-else
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 bg-brand-600 px-4 py-1.5 text-sm font-medium text-white transition-colors hover:bg-brand-700"
@
click=
"auth.login()"
@
click=
"auth.login()"
>
>
...
...
src/components/feature/TodoCard.vue
View file @
4a4af674
...
@@ -18,6 +18,9 @@ const typeLabel = computed(() => {
...
@@ -18,6 +18,9 @@ const typeLabel = computed(() => {
return
t
(
map
[
props
.
todo
.
type
])
return
t
(
map
[
props
.
todo
.
type
])
})
})
// DEV 核对环境下隐藏假的「标记完成」按钮(仅本地状态、不落库,避免干扰核对)
const
isDev
=
import
.
meta
.
env
.
DEV
const
done
=
ref
(
false
)
const
done
=
ref
(
false
)
const
doneDate
=
ref
(
''
)
const
doneDate
=
ref
(
''
)
...
@@ -39,6 +42,7 @@ function markDone() {
...
@@ -39,6 +42,7 @@ function markDone() {
<div
class=
"mt-1 text-xs text-slate-500"
>
{{
todo
.
customerName
}}
·
{{
todo
.
policyNo
}}
</div>
<div
class=
"mt-1 text-xs text-slate-500"
>
{{
todo
.
customerName
}}
·
{{
todo
.
policyNo
}}
</div>
</div>
</div>
<button
<button
v-if=
"!isDev"
class=
"rounded-full px-3.5 py-1.5 text-xs font-medium transition-all active:scale-95"
class=
"rounded-full px-3.5 py-1.5 text-xs font-medium transition-all active:scale-95"
:class=
"done ? 'bg-emerald-50 text-emerald-700' : 'bg-brand-600 text-white hover:bg-brand-700 shadow-sm'"
:class=
"done ? 'bg-emerald-50 text-emerald-700' : 'bg-brand-600 text-white hover:bg-brand-700 shadow-sm'"
@
click=
"markDone"
@
click=
"markDone"
...
...
src/env.d.ts
View file @
4a4af674
/// <reference types="vite/client" />
/// <reference types="vite/client" />
<<<<<<<
HEAD
/**
/**
* 构建期注入的环境变量(Vite 只暴露 VITE_ 前缀)。
* 构建期注入的环境变量(Vite 只暴露 VITE_ 前缀)。
* 集中声明后 `import.meta.env.XXX` 有补全与拼写校验,避免变量名打错后静默取到 undefined。
* 集中声明后 `import.meta.env.XXX` 有补全与拼写校验,避免变量名打错后静默取到 undefined。
...
@@ -16,3 +17,13 @@ interface ImportMetaEnv {
...
@@ -16,3 +17,13 @@ interface ImportMetaEnv {
/** 本地开发代理后端地址,未设置时指向本机 9202 */
/** 本地开发代理后端地址,未设置时指向本机 9202 */
readonly
VITE_API_TARGET
?:
string
readonly
VITE_API_TARGET
?:
string
}
}
=======
interface
ImportMetaEnv
{
/** mock-only 模式(本地生产数据核对):禁用 OIDC 跳转与 Bearer 附加 */
readonly
VITE_MOCK_ONLY
?:
string
}
interface
ImportMeta
{
readonly
env
:
ImportMetaEnv
}
>>>>>>>
0
cac32e9c611857dfc32d14724490175aac1f693
src/router/index.ts
View file @
4a4af674
import
{
createRouter
,
createWebHistory
}
from
'vue-router'
import
{
createRouter
,
createWebHistory
}
from
'vue-router'
import
{
userManager
,
signinRedirect
}
from
'@/utils/oidc'
import
{
userManager
,
signinRedirect
}
from
'@/utils/oidc'
import
{
isMockOnly
}
from
'@/utils/devMock'
const
router
=
createRouter
({
const
router
=
createRouter
({
history
:
createWebHistory
(),
history
:
createWebHistory
(),
...
@@ -20,7 +21,9 @@ const router = createRouter({
...
@@ -20,7 +21,9 @@ const router = createRouter({
})
})
// 认证守卫:除回调/已退出页外,无有效登录态即发起 OIDC 授权(当前路径随授权 state 携带,回跳后还原)。
// 认证守卫:除回调/已退出页外,无有效登录态即发起 OIDC 授权(当前路径随授权 state 携带,回跳后还原)。
// mock-only(本地生产核对)下整条 OIDC 链路停用,守卫直接放行。
router
.
beforeEach
(
async
(
to
)
=>
{
router
.
beforeEach
(
async
(
to
)
=>
{
if
(
isMockOnly
)
return
true
if
(
to
.
name
===
'auth-callback'
||
to
.
name
===
'logged-out'
)
return
true
if
(
to
.
name
===
'auth-callback'
||
to
.
name
===
'logged-out'
)
return
true
const
user
=
await
userManager
.
getUser
()
const
user
=
await
userManager
.
getUser
()
...
...
src/stores/auth.ts
View file @
4a4af674
...
@@ -7,6 +7,7 @@ import {
...
@@ -7,6 +7,7 @@ import {
handleRedirectCallback
,
handleRedirectCallback
,
signoutRedirect
signoutRedirect
}
from
'@/utils/oidc'
}
from
'@/utils/oidc'
import
{
isMockOnly
}
from
'@/utils/devMock'
export
interface
AuthUser
{
export
interface
AuthUser
{
name
:
string
name
:
string
...
@@ -27,6 +28,8 @@ export const useAuthStore = defineStore('auth', () => {
...
@@ -27,6 +28,8 @@ export const useAuthStore = defineStore('auth', () => {
/** 应用启动时从内存登录态同步到 store。 */
/** 应用启动时从内存登录态同步到 store。 */
async
function
init
():
Promise
<
void
>
{
async
function
init
():
Promise
<
void
>
{
// mock-only(本地生产核对):OIDC 链路停用,跳过登录态装载
if
(
isMockOnly
)
return
user
.
value
=
toAuthUser
(
await
userManager
.
getUser
())
user
.
value
=
toAuthUser
(
await
userManager
.
getUser
())
}
}
...
...
src/utils/devMock.ts
0 → 100644
View file @
4a4af674
// DEV 专用:模拟转介人身份。仅被 import.meta.env.DEV 守卫的代码引用,生产构建不打包。
const
STORAGE_KEY
=
'dev.mockBrokerBizId'
/** mock-only:本地生产核对环境(.env.development.local),整条 OIDC 链路停用。 */
export
const
isMockOnly
=
import
.
meta
.
env
.
VITE_MOCK_ONLY
===
'true'
export
function
getMockBrokerBizId
():
string
{
return
localStorage
.
getItem
(
STORAGE_KEY
)
??
''
}
export
function
setMockBrokerBizId
(
brokerBizId
:
string
):
void
{
if
(
brokerBizId
)
{
localStorage
.
setItem
(
STORAGE_KEY
,
brokerBizId
)
}
else
{
localStorage
.
removeItem
(
STORAGE_KEY
)
}
}
vite.config.ts
View file @
4a4af674
import
{
defineConfig
,
loadEnv
}
from
'vite'
import
{
defineConfig
,
loadEnv
}
from
'vite'
import
vue
from
'@vitejs/plugin-vue'
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 也一并读取
// 第三个参数传 '' 表示不限制 VITE_ 前缀,VITE_API_TARGET 也一并读取
...
...
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