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:
@@ -72,38 +72,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.login-entry-veil {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 380;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
backdrop-filter: blur(3px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-entry-veil-enter-active {
|
||||
transition: opacity 180ms var(--ease);
|
||||
}
|
||||
|
||||
.login-entry-veil-leave-active {
|
||||
transition: opacity 260ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.login-entry-veil-enter-from,
|
||||
.login-entry-veil-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.app.login-entry-active .app-sidebar {
|
||||
animation: loginEntrySidebarIn 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.app.login-entry-active > .main {
|
||||
animation: loginEntryMainIn 620ms 90ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.boot-state {
|
||||
min-height: var(--desktop-stage-height, 100dvh);
|
||||
display: grid;
|
||||
@@ -234,6 +202,28 @@
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.document-detail-loading {
|
||||
min-height: 280px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 14px;
|
||||
text-align: center;
|
||||
color: #475569;
|
||||
}
|
||||
.document-detail-loading i {
|
||||
font-size: 30px;
|
||||
color: var(--theme-primary-active);
|
||||
}
|
||||
.document-detail-loading strong {
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
}
|
||||
.document-detail-loading p {
|
||||
margin: 6px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
.workarea.settings-workarea {
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
@@ -299,10 +289,6 @@
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.app.login-entry-active .app-sidebar {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.app > .main {
|
||||
flex: 1 1 100%;
|
||||
width: 100vw;
|
||||
@@ -374,14 +360,4 @@
|
||||
flex-basis 120ms ease-out !important;
|
||||
transition-duration: 120ms, 120ms !important;
|
||||
}
|
||||
|
||||
.app.login-entry-active .app-sidebar,
|
||||
.app.login-entry-active > .main {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.login-entry-veil-enter-active,
|
||||
.login-entry-veil-leave-active {
|
||||
transition: opacity 120ms ease-out !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1556,12 +1556,28 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.application-preview-row.is-disabled {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.application-preview-row.is-disabled .application-preview-label,
|
||||
.application-preview-row.is-disabled .application-preview-value {
|
||||
background: rgba(248, 250, 252, 0.84);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.application-preview-row.editable:hover,
|
||||
.application-preview-row.editable:hover .application-preview-label,
|
||||
.application-preview-row.editable:hover .application-preview-value {
|
||||
background: rgba(239, 246, 255, 0.58);
|
||||
}
|
||||
|
||||
.application-preview-row.is-disabled:hover,
|
||||
.application-preview-row.is-disabled:hover .application-preview-label,
|
||||
.application-preview-row.is-disabled:hover .application-preview-value {
|
||||
background: rgba(248, 250, 252, 0.84);
|
||||
}
|
||||
|
||||
.application-preview-row.editable:focus-visible {
|
||||
z-index: 1;
|
||||
outline: 2px solid rgba(37, 99, 235, 0.42);
|
||||
@@ -1655,6 +1671,12 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.application-preview-edit-btn:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.46;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.application-preview-footer {
|
||||
color: #334155;
|
||||
font-size: 15px;
|
||||
@@ -1748,11 +1770,17 @@
|
||||
transform 160ms ease;
|
||||
}
|
||||
|
||||
.workbench-ai-suggested-actions button:hover {
|
||||
.workbench-ai-suggested-actions button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.workbench-ai-suggested-actions button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.6;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.workbench-ai-message-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1940,6 +1968,12 @@
|
||||
box-shadow: 0 12px 22px rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
.workbench-ai-confirm-actions .primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
box-shadow: 0 12px 22px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.workbench-ai-confirm-fade-enter-active,
|
||||
.workbench-ai-confirm-fade-leave-active {
|
||||
transition: opacity 180ms ease;
|
||||
|
||||
@@ -559,11 +559,72 @@
|
||||
|
||||
.submit-btn:disabled,
|
||||
.sso-btn:disabled {
|
||||
opacity: .6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 登录中:SSO 按钮置灰,登录按钮保持主色并显示 spinner */
|
||||
.sso-btn:disabled {
|
||||
opacity: .6;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 1;
|
||||
box-shadow: 0 16px 30px rgba(var(--theme-primary-rgb, 58, 124, 165), .20);
|
||||
}
|
||||
|
||||
/*
|
||||
* 登录中表单态:用户名 / 密码 / 租户 / 记住账号 / 忘记密码全部置灰禁用,
|
||||
* 让视觉焦点集中在正在校验的登录按钮上
|
||||
*/
|
||||
.login-form.is-submitting .field input,
|
||||
.login-form.is-submitting .field select {
|
||||
background: #f1f5f9;
|
||||
border-color: #e2e8f0;
|
||||
color: #94a3b8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-form.is-submitting .field input::placeholder {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.login-form.is-submitting .field > .mdi,
|
||||
.login-form.is-submitting .field-icon-btn,
|
||||
.login-form.is-submitting .field-select-chevron {
|
||||
color: #cbd5e1;
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-form.is-submitting .remember,
|
||||
.login-form.is-submitting .link-btn {
|
||||
opacity: .55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 登录按钮内的旋转 loading */
|
||||
.submit-btn__spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2.5px solid rgba(255, 255, 255, .4);
|
||||
border-top-color: #fff;
|
||||
border-radius: 999px;
|
||||
animation: loginSubmitSpin 720ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes loginSubmitSpin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.submit-btn__spinner {
|
||||
animation-duration: 1800ms;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
display: grid;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -158,6 +158,10 @@ export function useAppShell() {
|
||||
].some((value) => String(value || '').trim() === normalizedId)
|
||||
}
|
||||
|
||||
function isDetailLookupOnlyPayload(payload = {}) {
|
||||
return Boolean(payload?.detailLookupOnly || payload?.detail_lookup_only)
|
||||
}
|
||||
|
||||
function resolveRequestDetailLookupId(requestOrId = selectedRequestSnapshot.value) {
|
||||
if (typeof requestOrId === 'string') {
|
||||
return requestOrId.trim()
|
||||
@@ -168,6 +172,8 @@ export function useAppShell() {
|
||||
|| requestOrId?.id
|
||||
|| requestOrId?.claimNo
|
||||
|| requestOrId?.claim_no
|
||||
|| requestOrId?.documentNo
|
||||
|| requestOrId?.document_no
|
||||
|| ''
|
||||
).trim()
|
||||
}
|
||||
@@ -564,13 +570,18 @@ export function useAppShell() {
|
||||
}
|
||||
|
||||
function openRequestDetail(request, options = {}) {
|
||||
selectedRequestSnapshot.value = request || null
|
||||
const requestId = resolveRequestDetailLookupId(request)
|
||||
if (!requestId) {
|
||||
return
|
||||
}
|
||||
const isDetailLookupOnlyRequest = isDetailLookupOnlyPayload(request)
|
||||
selectedRequestSnapshot.value = isDetailLookupOnlyRequest ? null : request || null
|
||||
router.push({
|
||||
name: 'app-document-detail',
|
||||
params: { requestId: request.claimId || request.id },
|
||||
params: { requestId },
|
||||
query: buildDocumentDetailQuery(options)
|
||||
})
|
||||
void refreshSelectedRequestDetail(request)
|
||||
void refreshSelectedRequestDetail(isDetailLookupOnlyRequest ? requestId : request)
|
||||
}
|
||||
|
||||
function closeRequestDetail() {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { resolveDefaultAuthorizedRoute } from '../utils/accessControl.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import { fetchSettings } from '../services/settings.js'
|
||||
import { setThemeSkin } from './useThemeSkin.js'
|
||||
import { normalizeAuthUserSnapshot } from '../utils/authUser.js'
|
||||
import { normalizeAuthUserSnapshot, resolveAuthUserAdminFlag } from '../utils/authUser.js'
|
||||
import {
|
||||
clearAuthSessionMetrics,
|
||||
finalizeAuthSession,
|
||||
@@ -142,18 +142,7 @@ function buildLegacyAdminUser(username = '') {
|
||||
}
|
||||
|
||||
function resolvePlatformAdminFlag(payload, roleCodes = []) {
|
||||
const username = String(payload?.username || payload?.account || '').trim().toLowerCase()
|
||||
const role = String(payload?.role || '').trim().toLowerCase()
|
||||
const normalizedRoleCodes = roleCodes.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
|
||||
|
||||
return (
|
||||
Boolean(payload?.isAdmin)
|
||||
|| username === 'admin'
|
||||
|| role === 'admin'
|
||||
|| role === '管理员'
|
||||
|| role === '系统管理员'
|
||||
|| normalizedRoleCodes.includes('admin')
|
||||
)
|
||||
return resolveAuthUserAdminFlag(payload, roleCodes)
|
||||
}
|
||||
|
||||
function normalizeStoredAuthUser(payload = {}) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiRequest } from './api.js'
|
||||
import { runOrchestrator } from './orchestrator.js'
|
||||
import {
|
||||
buildApplicationPreviewRows,
|
||||
@@ -126,11 +127,20 @@ export function buildAiApplicationPreviewActionPayload({
|
||||
}
|
||||
|
||||
export function runAiApplicationPreviewAction(params = {}, options = {}) {
|
||||
return runOrchestrator(buildAiApplicationPreviewActionPayload(params), {
|
||||
timeoutMs: params.actionType === AI_APPLICATION_ACTION_SUBMIT ? 120000 : 75000,
|
||||
timeoutMessage: params.actionType === AI_APPLICATION_ACTION_SUBMIT
|
||||
? '申请提交处理超时,请稍后重试。'
|
||||
: '申请草稿保存超时,请稍后重试。',
|
||||
const payload = buildAiApplicationPreviewActionPayload(params)
|
||||
if (params.actionType === AI_APPLICATION_ACTION_SUBMIT) {
|
||||
return apiRequest('/reimbursements/application-preview-action', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
timeoutMs: 45000,
|
||||
timeoutMessage: '申请提交处理超时,请稍后重试。',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
return runOrchestrator(payload, {
|
||||
timeoutMs: 75000,
|
||||
timeoutMessage: '申请草稿保存超时,请稍后重试。',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { normalizeAuthUserSnapshot } from '../utils/authUser.js'
|
||||
import { normalizeAuthUserSnapshot, resolveAuthUserAdminFlag } from '../utils/authUser.js'
|
||||
|
||||
const API_BASE_STORAGE_KEY = 'x-financial-api-base-url'
|
||||
const AUTH_USER_STORAGE_KEY = 'x-financial-auth-user'
|
||||
@@ -49,7 +49,7 @@ function readCurrentUserHeaders() {
|
||||
const username = user.username
|
||||
const name = user.name || username
|
||||
const roleCodes = user.roleCodes
|
||||
const isAdmin = resolveStoredUserAdminFlag(payload, roleCodes)
|
||||
const isAdmin = resolveAuthUserAdminFlag(payload, roleCodes)
|
||||
const department = user.department || user.departmentName
|
||||
const costCenter = user.costCenter
|
||||
const position = user.position
|
||||
@@ -112,22 +112,7 @@ function readCurrentUserHeaders() {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveStoredUserAdminFlag(payload, roleCodes = []) {
|
||||
const username = String(payload?.username || payload?.account || '').trim().toLowerCase()
|
||||
const role = String(payload?.role || '').trim().toLowerCase()
|
||||
const normalizedRoleCodes = roleCodes.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
|
||||
|
||||
return (
|
||||
Boolean(payload?.isAdmin)
|
||||
|| username === 'admin'
|
||||
|| role === 'admin'
|
||||
|| role === '管理员'
|
||||
|| role === '系统管理员'
|
||||
|| normalizedRoleCodes.includes('admin')
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeApiBaseUrl(value) {
|
||||
function normalizeApiBaseUrl(value) {
|
||||
return String(value || '/api/v1').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { resolveAuthUserAdminFlag } from './authUser.js'
|
||||
|
||||
export const DEFAULT_APP_VIEW_ORDER = [
|
||||
'workbench',
|
||||
'documents',
|
||||
@@ -81,18 +83,7 @@ function hasPlatformAdminIdentity(user) {
|
||||
return false
|
||||
}
|
||||
|
||||
const username = String(user.username || user.account || '').trim().toLowerCase()
|
||||
const role = String(user.role || '').trim().toLowerCase()
|
||||
const roleCodes = normalizedRoleCodes(user)
|
||||
|
||||
return (
|
||||
Boolean(user.isAdmin)
|
||||
|| username === 'admin'
|
||||
|| role === 'admin'
|
||||
|| role === '管理员'
|
||||
|| role === '系统管理员'
|
||||
|| roleCodes.includes('admin')
|
||||
)
|
||||
return resolveAuthUserAdminFlag(user, normalizedRoleCodes(user))
|
||||
}
|
||||
|
||||
export function isManagerUser(user) {
|
||||
|
||||
@@ -159,6 +159,10 @@ function isBlockingPrecheck(precheck = {}) {
|
||||
return precheck?.overlap?.status === 'warning'
|
||||
}
|
||||
|
||||
export function isAiApplicationPrecheckBlocking(precheck = {}) {
|
||||
return isBlockingPrecheck(precheck)
|
||||
}
|
||||
|
||||
function buildOverlapMatchTable(matches = []) {
|
||||
const rows = Array.isArray(matches) ? matches : []
|
||||
if (!rows.length) {
|
||||
@@ -343,3 +347,32 @@ export function buildAiApplicationPrecheckMessage(preview = {}, precheck = {}) {
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function buildAiApplicationSubmitConflictMessage(preview = {}, precheck = {}) {
|
||||
const matchTable = buildOverlapMatchTable(precheck?.overlap?.matches)
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const fields = normalized.fields || {}
|
||||
const currentRange = resolveDateRange(fields.time, fields.days)
|
||||
const currentRangeText = currentRange
|
||||
? `${currentRange.startText} 至 ${currentRange.endText}`
|
||||
: normalizeText(fields.time) || '待确认'
|
||||
const lines = [
|
||||
'### 发现相同日期已有申请单',
|
||||
'',
|
||||
'**我已完成提交前的单据重叠核查**,发现相同或重叠日期已有差旅申请单,当前不能继续提交。',
|
||||
'',
|
||||
`> **相同日期提醒**:${precheck?.overlap?.summary || '发现相同日期已有申请单,请先核对后再提交。'}`,
|
||||
'',
|
||||
`> **本次申请时间**:${currentRangeText}`,
|
||||
]
|
||||
if (matchTable) {
|
||||
lines.push('', matchTable)
|
||||
}
|
||||
lines.push(
|
||||
'',
|
||||
'> **请先核对**:请先核对申请时间是否填写正确。若日期填错,请直接回复正确的出发时间和返回时间,我会重新查询;若日期无误,请先查看或处理已有申请单,避免重复申请。',
|
||||
'',
|
||||
'我会先暂停本次提交,不会生成新的审批流。'
|
||||
)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
@@ -8,10 +8,45 @@ function pickText(payload = {}, keys = [], fallback = '') {
|
||||
return String(fallback || '').trim()
|
||||
}
|
||||
|
||||
const PLATFORM_ADMIN_IDENTITIES = new Set(['admin', 'superadmin'])
|
||||
const PLATFORM_ADMIN_ROLES = new Set(['admin', 'superadmin', '管理员', '系统管理员'])
|
||||
|
||||
function isTruthyAdminFlag(value) {
|
||||
if (value === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value === 1
|
||||
}
|
||||
|
||||
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase())
|
||||
}
|
||||
|
||||
function normalizeRoleCodes(payload = {}) {
|
||||
return Array.isArray(payload.roleCodes)
|
||||
? payload.roleCodes.map((item) => String(item || '').trim()).filter(Boolean)
|
||||
: []
|
||||
const rawRoleCodes = Array.isArray(payload.roleCodes)
|
||||
? payload.roleCodes
|
||||
: Array.isArray(payload.role_codes)
|
||||
? payload.role_codes
|
||||
: typeof payload.roleCodes === 'string'
|
||||
? payload.roleCodes.split(',')
|
||||
: []
|
||||
|
||||
return rawRoleCodes.map((item) => String(item || '').trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
export function resolveAuthUserAdminFlag(payload = {}, roleCodes = []) {
|
||||
const username = String(payload?.username || payload?.account || '').trim().toLowerCase()
|
||||
const role = String(payload?.role || '').trim().toLowerCase()
|
||||
const normalizedRoleCodes = roleCodes.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
|
||||
|
||||
return (
|
||||
isTruthyAdminFlag(payload?.isAdmin)
|
||||
|| isTruthyAdminFlag(payload?.is_admin)
|
||||
|| PLATFORM_ADMIN_IDENTITIES.has(username)
|
||||
|| PLATFORM_ADMIN_ROLES.has(role)
|
||||
|| normalizedRoleCodes.some((item) => PLATFORM_ADMIN_IDENTITIES.has(item))
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeAuthUserSnapshot(payload = {}, defaults = {}) {
|
||||
@@ -47,6 +82,7 @@ export function normalizeAuthUserSnapshot(payload = {}, defaults = {}) {
|
||||
'leaderName',
|
||||
'leader_name'
|
||||
])
|
||||
const roleCodes = normalizeRoleCodes(payload)
|
||||
|
||||
return {
|
||||
username,
|
||||
@@ -62,9 +98,9 @@ export function normalizeAuthUserSnapshot(payload = {}, defaults = {}) {
|
||||
costCenter,
|
||||
financeOwnerName,
|
||||
riskProfile: payload.riskProfile && typeof payload.riskProfile === 'object' ? payload.riskProfile : {},
|
||||
roleCodes: normalizeRoleCodes(payload),
|
||||
roleCodes,
|
||||
email: pickText(payload, ['email'], username),
|
||||
avatar: pickText(payload, ['avatar'], name.slice(0, 1).toUpperCase()),
|
||||
isAdmin: Boolean(payload.isAdmin)
|
||||
isAdmin: resolveAuthUserAdminFlag(payload, roleCodes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user