feat(web): 统一平台管理员判定与 AI 工作台申请预览动作接入

- authUser 抽出 resolveAuthUserAdminFlag,统一 isAdmin 解析(含 superadmin、role_codes、中英文角色名),accessControl 复用同一逻辑
- 登录态、应用外壳路由、系统状态接入统一管理员判定,LoginView 与相关 composable 配套调整
- AI 工作台申请提交改为调用新的 /application-preview-action 接口,草稿保存仍走 orchestrator;预审模型补充重叠冲突提示与阻断判断
- 同步更新 accessControl/api-request/ai 预览动作等前端测试
This commit is contained in:
caoxiaozhu
2026-06-20 14:42:04 +08:00
parent 729d833edb
commit 96c2e1099a
21 changed files with 1364 additions and 331 deletions

View File

@@ -4,8 +4,7 @@
:class="{
'sidebar-collapsed': sidebarCollapsed,
'workbench-ai-sidebar-active': isAiShellMode,
'mobile-sidebar-open': mobileSidebarOpen,
'login-entry-active': loginEntryAnimating
'mobile-sidebar-open': mobileSidebarOpen
}"
>
<div class="mobile-overlay" aria-hidden="true" @click="mobileSidebarOpen = false"></div>
@@ -18,17 +17,6 @@
>
<i class="mdi mdi-menu" aria-hidden="true"></i>
</button>
<Transition name="login-entry-veil">
<div v-if="loginEntryAnimating" class="login-entry-veil" aria-live="polite" aria-label="登录成功,正在进入工作台">
<FloatingLightBandWindow
icon="mdi mdi-shield-check-outline"
message="正在进入工作台"
motion="entry"
title="登录成功"
variant="entry"
/>
</div>
</Transition>
<div class="app-sidebar">
<Transition name="sidebar-mode-fade" mode="out-in">
<AiSidebarRail
@@ -169,6 +157,18 @@
@request-deleted="handleRequestDeleted"
/>
<section
v-else-if="activeView === 'documents' && detailMode && !selectedRequest"
class="document-detail-loading panel"
aria-live="polite"
>
<i class="mdi mdi-loading mdi-spin" aria-hidden="true"></i>
<div>
<strong>正在加载完整单据详情</strong>
<p>正在读取申请表审批进度和详情字段加载完成后再展示详情表格</p>
</div>
</section>
<DocumentsCenterView
v-else-if="activeView === 'documents'"
:filtered-requests="requests"
@@ -236,13 +236,12 @@
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import AiSidebarRail from '../components/layout/AiSidebarRail.vue'
import SidebarRail from '../components/layout/SidebarRail.vue'
import TopBar from '../components/layout/TopBar.vue'
import FilterBar from '../components/layout/FilterBar.vue'
import FloatingLightBandWindow from '../components/shared/FloatingLightBandWindow.vue'
import AuditView from './AuditView.vue'
import BudgetCenterView from './BudgetCenterView.vue'
import DigitalEmployeesView from './DigitalEmployeesView.vue'
@@ -258,9 +257,8 @@ import TravelRequestDetailView from './TravelRequestDetailView.vue'
import { useAppShell } from '../composables/useAppShell.js'
import { useSystemState } from '../composables/useSystemState.js'
import { filterNavItemsByAccess } from '../utils/accessControl.js'
import { filterNavItemsByAccess, isPlatformAdminUser } from '../utils/accessControl.js'
import { loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation } from '../utils/aiWorkbenchConversationStore.js'
import { consumeLoginEntryTransition } from '../utils/loginEntryTransition.js'
const employeeSummary = ref(null)
const knowledgeSummary = ref(null)
@@ -271,7 +269,6 @@ const auditDetailOpen = ref(false)
const digitalEmployeeDetailOpen = ref(false)
const receiptFolderDetailOpen = ref(false)
const budgetDetailOpen = ref(false)
const loginEntryAnimating = ref(false)
const sidebarCollapsed = ref(false)
const sidebarCollapsedBeforeAiMode = ref(false)
const mobileSidebarOpen = ref(false)
@@ -281,25 +278,6 @@ const aiSidebarCommandSeq = ref(0)
const aiSidebarCommand = ref({ seq: 0, type: '', payload: null })
const aiActiveConversationId = ref('')
const aiConversationHistory = ref([])
let loginEntryTimer = null
function stopLoginEntryAnimation() {
if (loginEntryTimer) {
window.clearTimeout(loginEntryTimer)
loginEntryTimer = null
}
loginEntryAnimating.value = false
}
function playLoginEntryAnimation() {
if (!consumeLoginEntryTransition()) {
return
}
loginEntryAnimating.value = true
loginEntryTimer = window.setTimeout(stopLoginEntryAnimation, 920)
}
function toggleSidebarCollapsed() {
sidebarCollapsed.value = !sidebarCollapsed.value
@@ -414,7 +392,7 @@ const resolvedDetailKpis = computed(() => (
))
function openWorkbenchDocument(payload = {}) {
const requestId = String(payload.claimId || payload.id || payload.claimNo || '').trim()
const requestId = String(payload.claimId || payload.id || payload.claimNo || payload.documentNo || '').trim()
if (!requestId) {
return
}
@@ -423,6 +401,7 @@ function openWorkbenchDocument(payload = {}) {
String(item.claimId || '').trim() === requestId
|| String(item.id || '').trim() === requestId
|| String(item.claimNo || '').trim() === requestId
|| String(item.documentNo || '').trim() === requestId
))
const returnTo = (
String(payload.returnTo || '').trim() === 'workbench'
@@ -431,7 +410,15 @@ function openWorkbenchDocument(payload = {}) {
)
? 'workbench'
: ''
openRequestDetail(request || payload, { returnTo })
const detailPayload = request || {
...payload,
id: payload.id || requestId,
claimId: payload.claimId || requestId,
claimNo: payload.claimNo || payload.documentNo || requestId,
documentNo: payload.documentNo || requestId,
detailLookupOnly: true
}
openRequestDetail(detailPayload, { returnTo })
}
function dispatchAiSidebarCommand(type, payload = null) {
@@ -509,12 +496,4 @@ watch(
},
{ immediate: true }
)
onMounted(() => {
playLoginEntryAnimation()
})
onBeforeUnmount(() => {
stopLoginEntryAnimation()
})
</script>

View File

@@ -74,11 +74,22 @@
<p>使用员工邮箱或管理员账号进入系统</p>
</header>
<form class="login-form" @submit.prevent="emit('login', { username, password })">
<form
class="login-form"
:class="{ 'is-submitting': submitting }"
@submit.prevent="emit('login', { username, password })"
>
<label class="field">
<span class="sr-only">账号</span>
<i class="mdi mdi-account-outline"></i>
<input v-model="username" type="text" placeholder="请输入员工邮箱 / 管理员账号" autocomplete="username" required />
<input
v-model="username"
type="text"
placeholder="请输入员工邮箱 / 管理员账号"
autocomplete="username"
:disabled="submitting"
required
/>
</label>
<label class="field">
@@ -89,11 +100,13 @@
:type="showPassword ? 'text' : 'password'"
placeholder="请输入登录密码"
autocomplete="current-password"
:disabled="submitting"
required
/>
<button
class="field-icon-btn"
type="button"
:disabled="submitting"
:aria-label="showPassword ? '隐藏密码' : '显示密码'"
@click="showPassword = !showPassword"
>
@@ -104,7 +117,12 @@
<label class="field">
<span class="sr-only">企业或租户</span>
<i class="mdi mdi-office-building"></i>
<select v-model="tenant" class="tenant-select" aria-label="请选择企业或租户">
<select
v-model="tenant"
class="tenant-select"
aria-label="请选择企业或租户"
:disabled="submitting"
>
<option value="远光软件股份有限公司">远光软件股份有限公司</option>
</select>
<span class="field-select-chevron" aria-hidden="true">
@@ -114,16 +132,17 @@
<div class="form-meta">
<label class="remember">
<input v-model="remember" type="checkbox" />
<input v-model="remember" type="checkbox" :disabled="submitting" />
<span>记住账号</span>
</label>
<button type="button" class="link-btn" @click="emit('recover-password')">忘记密码?</button>
<button type="button" class="link-btn" :disabled="submitting" @click="emit('recover-password')">忘记密码?</button>
</div>
<p v-if="errorMessage" class="login-error">{{ errorMessage }}</p>
<button class="submit-btn" type="submit" :disabled="submitting">
{{ submitting ? '登录中...' : '登录' }}
<span v-if="submitting" class="submit-btn__spinner" aria-hidden="true"></span>
<span class="submit-btn__label">{{ submitting ? '登录中' : '登录' }}</span>
</button>
<div class="divider"><span></span></div>