Commit 4a4af674 by Sweet Zhang

解决冲突

parents 0936166d 0cac32e9
<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"
......
...@@ -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' })
>>>>>>> 0cac32e9c611857dfc32d14724490175aac1f693
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') {
>>>>>>> 0cac32e9c611857dfc32d14724490175aac1f693
redirecting = true redirecting = true
// 跳转失败(IdP 不可达)时必须复位,否则此后再也不重定向,页面卡死在错误态 // 跳转失败(IdP 不可达)时必须复位,否则此后再也不重定向,页面卡死在错误态
signinRedirect(window.location.pathname + window.location.search).catch(() => { signinRedirect(window.location.pathname + window.location.search).catch(() => {
......
<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>
...@@ -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()"
> >
......
...@@ -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"
......
/// <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
}
>>>>>>> 0cac32e9c611857dfc32d14724490175aac1f693
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()
......
...@@ -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())
} }
......
// 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)
}
}
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 也一并读取
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment