cc пре 3 недеља
родитељ
комит
b2374c8a31

+ 107 - 0
.workbuddy-ai/memory/2026-09-18.md

@@ -707,3 +707,110 @@ exit code 0,但不建 loose ref、也不改 `packed-refs` 旧值,`git rev-pa
   `clipTop = card.top` 均为正、`clipBottom = footer.bottom - innerHeight` 均为负。
   卡片高 439(正常)/ 395(矮屏压缩)/ 402(窄屏);页脚高 48(单行)/ 72(窄屏两行)。
 - 验证:eslint ✅、stylelint ✅、`vue-tsc` 89 个错误(与基线一致,零新增)。
+
+### 追加:登录页头部改为「logo + 文字同排居中」
+
+文件:`ai-frontend/src/core/layouts/views/login/index.vue`。
+
+- 老爷要求:**logo 和标题文字在同一排**,整组在卡片内水平居中(不是上下堆叠)。
+  实现:`.login-card__head` 保持 `display:flex; gap:14px; align-items:center`,
+  只加 `justify-content: center`;文字块内部仍左对齐。logo 回到 42px/radius 10,
+  标题 `margin: 0`、副标题 `margin: 5px 0 0`(卡片高度回到 439)。
+- **窄屏隐患(已修)**:`justify-content: center` + 不可压缩的 flex 项 ⇒ 内容超宽时会
+  **从左边溢出**(logo 被顶出卡片),且因为全局禁滚动没法滚动看。
+  修法:给文字块加类 `.login-card__meta { min-width: 0 }`,让它可被压缩换行;
+  并在 `@media (width <= 480px)` 里把 head gap 收到 10px、logo 缩到 38px。
+- **实测**(`headCx === cardCx` 且 `logo.left > card.left` 才算过):
+  1440×900 / 1366×768 / 390×844 / 360×740 全部单行居中不溢出;
+  320×640 副标题自动折成 2 行(`min-width:0` 生效),卡片 429 仍不裁切。
+- 页脚文案:老爷已自行填入 `中数矩阵(成都)科技有限公司` / `18908093240`;
+  **`footerEmail` 仍是 `service@example.com` 占位**,代码里留了 TODO。
+- 验证:eslint ✅、stylelint ✅、`vue-tsc` 89 个错误(与基线一致)。
+
+## 系统授权页:原生 file 控件裸露(CSS 选择器挂错父级)
+
+文件:`ai-frontend/src/plat/views/authorization/index.vue`(路由 `/authorization`)。
+
+**根因**:模板里 `<input type="file" class="upload-panel__file-input">`,
+但 `display: none` 那条规则嵌在 `.auth-card { }` 里,编译出来是 **`.auth-card__file-input`** —— 永远匹配不上。
+后果:浏览器原生的「选择文件 | 未选择任何文件」控件直接渲染在虚线 dropzone 上方(实测 294×31px,
+`getComputedStyle(input).display === 'inline-block'`),面板高度虚高 146px。
+
+**修法**:把 `&__file-input { display: none }` 挪进 `.upload-panel { }`(编译成 `.upload-panel__file-input`)。
+修完 `inputDisplay: 'none'`,面板高度 **146 → 115**(正好少掉原生控件的 31px)。
+
+**顺带优化**:
+- dropzone 加选中态 `--has-file`(实线蓝框 `#93c5fd` + 浅蓝底 + 文件名 `#2563eb` 加粗),
+  之前选完文件只有文字变化、没有视觉反馈。
+- 三处重复的 `licenseFileInputRef?.click()` 抽成 `openLicenseFilePicker()`,
+  并给 dropzone 补 `role="button"` / `tabindex="0"` / Enter、Space 键支持(原生控件被隐藏后键盘要能用)。
+
+**同类 bug 在另一处也有,已一并修**:`src/trans/views/aiBaseStation/index.vue`
+的 `.side-actions__file-input` **完全没有样式**,原生控件裸露在「批量转换基站」按钮旁边。
+补了 `&__file-input { display: none }`。该页在 `routeHelper.ts:57` 有映射,是活页面。
+
+**验证手法(可复用)**:
+- 编译后选择器直接查 Vite dev server:
+  `curl -s "http://localhost:3100/<path>.vue?vue&type=style&index=0&lang.less" | grep -o "<类名>[^{]*{[^}]*}"`
+  —— 比截图快,且能确认 Less 嵌套编译结果。
+- 真实上传链路:`agent-browser upload ".upload-panel__file-input" /tmp/x.xlts`,
+  **隐藏 input 也能被 setInputFiles 命中**。实测选完文件后「离线授权」按钮 `disabled` 从 true 变 false。
+- 授权页进不去的问题:**不能直接 `open /authorization`**(app 启动时的 license 校验会 401 → 跳 /login)。
+  正确姿势:登录 admin/admin123 → app 自己 replace 到 `/authorization?skipAuthCheck=1`,
+  **此时不要再 open 一次 URL**,直接在同一个会话里 eval + 截图。
+- stylelint:该文件有 **112 个存量错误**(rgba/alpha/inset 等老写法),用 `git show HEAD:<path>` 导出
+  临时文件对比,确认本次改动**零新增**(改动前也是 112)。
+
+## 登录页合并进授权页(/login 彻底删除,/authorization 成为唯一入口)
+
+老爷要求:登录页 + 其路由全部删除,不保留重定向;未授权显示授权卡,已授权未登录显示登录卡;
+授权判断方式**不变**(`checkAuthApi()` + `isAuthorizationExpired()`)。
+
+**改动文件**
+- `src/core/layouts/views/login/index.vue` —— 删除(目录一并删除)。
+- `src/core/router/routes/index.ts` —— 删 `LoginRoute`,`basicRoutes` 只留 `AuthorizationRoute`。
+- `src/core/enums/pageEnum.ts` —— 删 `BASE_LOGIN`,新增 `LEGACY_LOGIN = '/login'`
+  (**只用于识别老书签/老回跳地址,不注册为路由**)。
+- `src/plat/views/authorization/index.vue` —— 加 `viewMode: 'license' | 'login' | ''`,
+  空串表示未判定(避免先闪错误形态)。新增 `resolveEntryMode()` / `isDeviceAuthorized()` /
+  `handleLoginSubmit()` / `getPostLoginTarget()`。登录卡走 `accountStore.login()`。
+  左侧品牌区随状态切换:授权态=平台能力三条,登录态=`loginTips` 三条(账号来源/案件隔离/会话保护)。
+- 跳转落点改 `/authorization`:`main.ts`、`utils/http/axios/unauthorized.ts`、
+  `guard/permissionGuard.ts`、`guard/stateGuard.ts`、`store/modules/multipleTab.ts`、
+  `user-dropdown`、`Exception.vue`、`LockPage.vue`、`plat/views/case/case.vue`。
+
+**踩到的两个真坑(都不是"看起来对不对"能发现的)**
+
+1. **`stateGuard` 在进入口页时清 token,把"已登录"分支变成死代码。**
+   原逻辑:`to.path === BASE_LOGIN` → `accountStore.resetState()`(内部 `setAuthCache(TOKEN_KEY, null)`)。
+   合并后入口页同时承担登录职责,于是**已登录用户整页刷新 /authorization 会被强制重新登录**。
+   实测证据:登录后 token 有 → 重载 /authorization → token 变无。
+   修法:入口页分支**不再清 account/token**(凭据失效的清理已由 `unauthorized.ts`
+   的 `clearLocalState()` 兜底;三处主动登出都先调 `accountStore.logout()` 再跳转)。
+   修完:登录后重载 /authorization,token 保留。
+
+2. **老书签 `/login` 会带着 `redirect=/login` 进入口页,登录后落到已删除路由。**
+   `permissionGuard` 未登录分支会 `query: { redirect: to.fullPath }`,`to` 是 `/login` 就把它记下来。
+   修法:`permissionGuard` / `main.ts#ensureLoggedIn` / `getPostLoginTarget()` 三处都把
+   `LEGACY_LOGIN` 视为「无来源」,`main.ts#getAuthorizedTarget()` 也把 `/login` 归到 `/case`。
+
+**验证手法(可复用,踩过坑)**
+- token 存储位置:**不是** `localStorage['TOKEN__']`。真实键是
+  `<APP_PREFIX>__COMMON_LOCAL__`(dev 下形如 `QINGJIAN__DEVELOPMENT__5.15.0__COMMON_LOCAL__`,
+  见 `cacheEnum.APP_LOCAL_CACHE_KEY` + `cache/persistent.ts`),结构是
+  `{"value":{"TOKEN__":{"value":"...","time":...,"expire":...}}, ...}`。
+  读法:`JSON.parse(localStorage.getItem(K)).value['TOKEN__'].value`。
+  dev 下 `enableStorageEncryption = !isDevMode()` 为 false,所以是明文 JSON。
+- 想验证「已授权 + 未登录」卡片:本机后端设备**未授权**,`checkAuthApi()` 不返回 200,
+  所以正常路径进不去登录态。用组件实例强切:
+  `el.__vueParentComponent` 向上找 `setupState` 里有 `viewMode` 的那个,直接 `setupState.viewMode='login'`。
+  授权摘要同理:`setupState.userStore.setPageCache('authorizationUnit', ...)`。
+- 判定「整页重载」必须用 `agent-browser open <url>`(会真重载),不能用 client 侧 push。
+
+**验证结果**:eslint ✅(改动文件全清)、stylelint 112 存量错误零新增、
+`vue-tsc` 89 = 基线。实测:未授权→授权卡;登录态→登录卡(摘要/左侧文案均切换);
+登录 admin/admin123 → `/case`;重载 /authorization token 保留;/login → `/authorization?skipAuthCheck=1`(不再带 redirect)。
+
+**补充:入口页窄屏实测(登录卡)**。`@media (max-width: 1279px)` 会隐藏整个左侧品牌区,
+右侧卡片居中。实测 1024×700 / 900×700 / 768×700 / 600×800 / 390×844:
+均无纵向裁切、无横向溢出;390 下卡片收到 358 宽(左右各 16 边距),内容完整。

+ 3 - 2
ai-frontend/src/core/enums/pageEnum.ts

@@ -1,7 +1,8 @@
 export enum PageEnum {
-  // basic login path
-  BASE_LOGIN = '/login',
+  // 授权 / 登录 唯一入口页(原 /login 已合并进本页)
   BASE_AUTHORIZATION = '/authorization',
+  // 已废弃:登录页已删除,这里仅用于识别老书签 / 老回跳地址,不再注册为路由
+  LEGACY_LOGIN = '/login',
   BASE_CASE = '/case',
   // basic home path
   BASE_HOME = '/data/index',

+ 1 - 1
ai-frontend/src/core/layouts/default/header/components/user-dropdown/index.vue

@@ -89,7 +89,7 @@
       async function doLogout() {
         caseStore.resetState();
         await accountStore.logout();
-        await router.replace({ path: PageEnum.BASE_LOGIN });
+        await router.replace({ path: PageEnum.BASE_AUTHORIZATION });
       }
 
       function handleMenuClick({ key }: { key: string | number }) {

+ 2 - 2
ai-frontend/src/core/layouts/views/exception/Exception.vue

@@ -71,7 +71,7 @@
         status: `${ExceptionEnum.PAGE_NOT_ACCESS}`,
         subTitle: t('sys.exception.subTitle403'),
         btnText: props.full ? backLoginI18n : backHomeI18n,
-        handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()),
+        handler: () => (props.full ? go(PageEnum.BASE_AUTHORIZATION) : go()),
       });
 
       unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_FOUND, {
@@ -79,7 +79,7 @@
         status: `${ExceptionEnum.PAGE_NOT_FOUND}`,
         subTitle: t('sys.exception.subTitle404'),
         btnText: props.full ? backLoginI18n : backHomeI18n,
-        handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()),
+        handler: () => (props.full ? go(PageEnum.BASE_AUTHORIZATION) : go()),
       });
 
       unref(statusMapRef).set(ExceptionEnum.ERROR, {

+ 1 - 1
ai-frontend/src/core/layouts/views/lock/LockPage.vue

@@ -116,7 +116,7 @@
     lockStore.resetLockInfo();
     // 锁屏页的「去登录」= 注销当前账号(旧实现调的是 userStore.logout,语义是退出案件)
     await accountStore.logout();
-    await router.replace({ path: PageEnum.BASE_LOGIN });
+    await router.replace({ path: PageEnum.BASE_AUTHORIZATION });
   }
 
   function handleShowForm(show = false) {

+ 0 - 379
ai-frontend/src/core/layouts/views/login/index.vue

@@ -1,379 +0,0 @@
-<template>
-  <div class="login-page">
-    <main class="login-card">
-      <header class="login-card__head">
-        <img class="login-card__logo" src="/resource/img/logo.png" alt="" />
-        <div>
-          <h1 class="login-card__title">清鉴线索调查工具</h1>
-          <p class="login-card__subtitle">线索调查与资金数据研判平台</p>
-        </div>
-      </header>
-
-      <a-form :model="form" layout="vertical" @submit.prevent>
-        <a-form-item name="username" :rules="[{ required: true, message: '请输入用户名' }]">
-          <label class="field__label" for="login-username">用户名</label>
-          <a-input
-            id="login-username"
-            v-model:value="form.username"
-            placeholder="请输入用户名"
-            autocomplete="username"
-            allow-clear
-          />
-        </a-form-item>
-
-        <a-form-item name="password" :rules="[{ required: true, message: '请输入密码' }]">
-          <label class="field__label" for="login-password">密码</label>
-          <a-input-password
-            id="login-password"
-            v-model:value="form.password"
-            placeholder="请输入密码"
-            autocomplete="current-password"
-            @press-enter="handleSubmit"
-          />
-        </a-form-item>
-
-        <a-button class="login-card__submit" type="primary" block :loading="loading" @click="handleSubmit">
-          登录
-        </a-button>
-      </a-form>
-
-      <p class="login-card__tip">账号由管理员创建,如需开通请联系管理员</p>
-    </main>
-
-    <footer class="login-foot">
-      <p class="login-foot__org">{{ footerOrg }}</p>
-      <p class="login-foot__contact">
-        <span class="login-foot__item">
-          <Icon icon="i-mdi:phone-outline" :size="13" />
-          <span>{{ footerPhone }}</span>
-        </span>
-        <span class="login-foot__sep" aria-hidden="true"></span>
-        <span class="login-foot__item">
-          <Icon icon="i-mdi:email-outline" :size="13" />
-          <span>{{ footerEmail }}</span>
-        </span>
-      </p>
-    </footer>
-  </div>
-</template>
-
-<script setup lang="ts">
-  import { reactive, ref } from 'vue';
-  import { useRoute, useRouter } from 'vue-router';
-  import { PageEnum } from '@/core/enums/pageEnum';
-  import { useAccountStore } from '@/core/store/modules/account';
-  import { Icon } from '@/core/components/Icon';
-
-  /**
-   * 页脚展示信息。
-   *
-   * ⚠️ 目前是占位文案,替换时只改这三行即可。
-   */
-  const footerOrg = '中数矩阵(成都)科技有限公司';
-  const footerPhone = '18908093240';
-  const footerEmail = 'service@example.com';
-
-  const route = useRoute();
-  const router = useRouter();
-  const accountStore = useAccountStore();
-
-  const form = reactive({
-    username: '',
-    password: '',
-  });
-  const loading = ref(false);
-
-  async function handleSubmit() {
-    // 防重入:重复登录会因 is-concurrent=false 把前一个 token 顶掉,导致刚拿到的凭据立即失效
-    if (loading.value) {
-      return;
-    }
-    const username = form.username.trim();
-    if (!username || !form.password) {
-      return;
-    }
-    loading.value = true;
-    try {
-      await accountStore.login({ username, password: form.password });
-      // 登录后回到被拦截前的页面;没有则进案件列表
-      const redirect = (route.query.redirect as string) || PageEnum.BASE_CASE;
-      await router.replace(redirect);
-    } catch {
-      // 错误提示已由 defHttp 统一弹出,这里只负责不跳转
-    } finally {
-      loading.value = false;
-    }
-  }
-</script>
-
-<style lang="less" scoped>
-  // 全局 `html, body { overflow: hidden !important }`,登录页无法滚动,
-  // 所以卡片 + 页脚整体作为一个居中块,靠短屏 media query 压缩间距来避免裁切。
-  .login-page {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    width: 100%;
-    height: 100vh;
-    height: 100dvh;
-    padding: 24px;
-    overflow: hidden;
-    background: @content-bg;
-  }
-
-  .login-card {
-    position: relative;
-    flex: none;
-    width: 420px;
-    max-width: 100%;
-    padding: 44px 40px 36px;
-    background: #fff;
-    border: 1px solid #e8eaee;
-    border-radius: 14px;
-    box-shadow:
-      0 1px 2px rgb(16 24 40 / 4%),
-      0 18px 40px -18px rgb(16 24 40 / 16%);
-    animation: login-rise 0.42s cubic-bezier(25%, 100%, 50%, 100%) both;
-
-    // logo 与标题文字同一排,整组在卡片内水平居中(文字块内部仍左对齐)
-    &__head {
-      display: flex;
-      gap: 14px;
-      align-items: center;
-      justify-content: center;
-      margin-bottom: 38px;
-    }
-
-    &__logo {
-      flex: none;
-      width: 42px;
-      height: 42px;
-      border-radius: 10px;
-    }
-
-    &__title {
-      margin: 0;
-      font-size: 20px;
-      font-weight: 600;
-      letter-spacing: 0.01em;
-      color: @cb-text-primary;
-    }
-
-    &__subtitle {
-      margin: 5px 0 0;
-      font-size: 13px;
-      color: @cb-text-secondary;
-    }
-
-    &__submit {
-      height: 46px;
-      margin-top: 12px;
-      font-size: 14.5px;
-      font-weight: 500;
-    }
-
-    &__tip {
-      margin: 30px 0 0;
-      font-size: 12px;
-      color: #98a2b3;
-      text-align: center;
-    }
-  }
-
-  // 页脚:公司名称 + 联系方式,位于卡片外、随整体居中
-  .login-foot {
-    flex: none;
-    margin-top: 26px;
-    text-align: center;
-
-    &__org {
-      margin: 0;
-      font-size: 13px;
-      font-weight: 500;
-      letter-spacing: 0.02em;
-      color: @cb-text-secondary;
-    }
-
-    &__contact {
-      display: flex;
-      gap: 10px;
-      align-items: center;
-      justify-content: center;
-      margin: 9px 0 0;
-      font-size: 12px;
-      color: #98a2b3;
-    }
-
-    &__item {
-      display: inline-flex;
-      gap: 5px;
-      align-items: center;
-      white-space: nowrap;
-    }
-
-    &__sep {
-      flex: none;
-      width: 1px;
-      height: 11px;
-      background: #d8dde5;
-    }
-  }
-
-  .field__label {
-    display: block;
-    margin-bottom: 9px;
-    font-size: 13px;
-    color: #344054;
-    cursor: pointer;
-  }
-
-  // ---------- antd 覆盖:与卡片视觉语言对齐 ----------
-  .login-card {
-    :deep(.ant-form-item) {
-      margin-bottom: 24px;
-    }
-
-    :deep(.ant-input),
-    :deep(.ant-input-affix-wrapper) {
-      height: 46px;
-      padding: 0 15px;
-      font-size: 14px;
-      color: @cb-text-primary;
-      background: #fff;
-      border: 1px solid #d5d9e0;
-      border-radius: 8px;
-      box-shadow: none;
-      transition:
-        border-color 0.18s ease-out,
-        box-shadow 0.18s ease-out;
-
-      &::placeholder {
-        color: #98a2b3;
-      }
-
-      &:hover {
-        border-color: #b9c0cb;
-      }
-
-      &:focus,
-      &:focus-within,
-      &-focused {
-        border-color: @primary-color;
-        box-shadow: 0 0 0 3px fade(@primary-color, 12%);
-      }
-    }
-
-    // 外壳与内层 input 叠加会重复画边框,这里抹平
-    :deep(.ant-input-affix-wrapper > .ant-input) {
-      height: auto;
-      padding: 0;
-      color: inherit;
-      background: transparent;
-      border: 0;
-      border-radius: 0;
-      box-shadow: none;
-
-      &:hover,
-      &:focus {
-        background: transparent;
-        border: 0;
-        box-shadow: none;
-      }
-    }
-
-    :deep(.ant-input-clear-icon),
-    :deep(.ant-input-suffix .anticon) {
-      color: #98a2b3;
-      transition: color 0.18s ease-out;
-
-      &:hover {
-        color: @cb-text-secondary;
-      }
-    }
-
-    :deep(.ant-form-item-explain-error) {
-      padding-top: 4px;
-      font-size: 12px;
-    }
-
-    // 全局 `.ant-btn.ant-btn` 把圆角改成了 15px,这里用更高优先级拉回卡片语言
-    :deep(.ant-btn.ant-btn-primary) {
-      border-radius: 8px;
-      box-shadow: none;
-      transition:
-        background-color 0.18s ease-out,
-        box-shadow 0.18s ease-out;
-
-      &:hover:not([disabled]) {
-        background: darken(@primary-color, 8%);
-        box-shadow: 0 8px 18px -8px fade(@primary-color, 70%);
-      }
-    }
-  }
-
-  @keyframes login-rise {
-    from {
-      opacity: 0;
-      transform: translateY(12px);
-    }
-
-    to {
-      opacity: 1;
-      transform: none;
-    }
-  }
-
-  @media (width <= 480px) {
-    .login-page {
-      padding: 16px;
-    }
-
-    .login-card {
-      padding: 26px 20px 22px;
-    }
-
-    .login-card__title {
-      font-size: 17px;
-    }
-
-    // 窄屏放不下「电话 | 邮箱」一行,改成上下两行并去掉分隔线
-    .login-foot__contact {
-      flex-direction: column;
-      gap: 5px;
-    }
-
-    .login-foot__sep {
-      display: none;
-    }
-  }
-
-  // 矮屏(1366×768 缩放后、投影仪等)压缩纵向间距,避免卡片 + 页脚被裁切
-  @media (height <= 720px) {
-    .login-page {
-      padding: 16px;
-    }
-
-    .login-card {
-      padding: 30px 36px 26px;
-
-      &__head {
-        margin-bottom: 26px;
-      }
-
-      &__tip {
-        margin-top: 22px;
-      }
-    }
-
-    .login-foot {
-      margin-top: 16px;
-    }
-  }
-
-  @media (prefers-reduced-motion: reduce) {
-    .login-card {
-      animation-duration: 0.01ms;
-    }
-  }
-</style>

+ 7 - 5
ai-frontend/src/core/router/guard/permissionGuard.ts

@@ -17,18 +17,20 @@ function ensurePageNotFoundRoute(router: Router) {
 export function createPermissionGuard(router: Router) {
   const permissionStore = usePermissionStoreWithOut();
   router.beforeEach(async (to, from, next) => {
-    // 登录页与授权页在 basicRoutes 中始终存在,不需要权限检查
-    if (to.path === PageEnum.BASE_LOGIN || to.path === PageEnum.BASE_AUTHORIZATION) {
+    // 授权 / 登录入口页在 basicRoutes 中始终存在,不需要权限检查
+    if (to.path === PageEnum.BASE_AUTHORIZATION) {
       next();
       return;
     }
 
-    // 未登录:跳登录页并记录来源,登录成功后回跳
+    // 未登录:跳入口页并记录来源,登录成功后回跳
     // (后端对未登录返回 body.code=401,由 defHttp 拦截器兜底;这里拦住的是「首次进入/直链」场景)
     if (!getToken()) {
+      // 老书签 / 老回跳地址 /login 已不存在,带上它会让登录后落到 404,所以不作为来源记录
+      const source = to.fullPath && to.fullPath !== '/' && to.path !== PageEnum.LEGACY_LOGIN ? to.fullPath : '';
       next({
-        path: PageEnum.BASE_LOGIN,
-        query: to.fullPath && to.fullPath !== '/' ? { redirect: to.fullPath } : undefined,
+        path: PageEnum.BASE_AUTHORIZATION,
+        query: source ? { redirect: source } : undefined,
         replace: true,
       });
       return;

+ 6 - 5
ai-frontend/src/core/router/guard/stateGuard.ts

@@ -2,7 +2,6 @@ import type { Router } from 'vue-router';
 import { useAppStore } from '@/core/store/modules/app';
 import { useMultipleTabStore } from '@/core/store/modules/multipleTab';
 import { usePermissionStore } from '@/core/store/modules/permission';
-import { useAccountStore } from '@/core/store/modules/account';
 import { PageEnum } from '@/core/enums/pageEnum';
 import { removeTabChangeListener } from '@/core/logics/mitt/routeChange';
 
@@ -11,10 +10,12 @@ export function createStateGuard(router: Router) {
     const tabStore = useMultipleTabStore();
     const appStore = useAppStore();
     const permissionStore = usePermissionStore();
-    // 进入登录页说明凭据已失效(被顶下线 / 过期)或用户主动登出,
-    // 清掉登录用户信息与 token,避免带着坏凭据继续请求。
-    if (to.path === PageEnum.BASE_LOGIN) {
-      useAccountStore().resetState();
+    // 进入授权/登录入口页时**不能**清 token:
+    // 这一页现在同时承担「登录」职责,已登录用户刷新/直链到这里时要能直接回业务页
+    // (见 authorization/index.vue 的 resolveEntryMode)。
+    // 凭据失效(401 / 授权过期)的清理已由 unauthorized.ts 的 clearLocalState() 完成;
+    // 主动登出(user-dropdown / case.vue / LockPage)都先调 accountStore.logout() 再跳转。
+    if (to.path === PageEnum.BASE_AUTHORIZATION) {
       appStore.resetAllState();
       permissionStore.resetState();
       tabStore.resetState();

+ 8 - 16
ai-frontend/src/core/router/routes/index.ts

@@ -15,6 +15,14 @@ export const RootRoute: AppRouteRecordRaw = {
   },
 };
 
+/**
+ * 授权 / 登录 入口页(唯一入口)。
+ *
+ * <p>原先 `/login` 与 `/authorization` 是两个页面,现已合并为这一页:
+ * 未授权时展示离线授权卡片,已授权但未登录时展示登录卡片。
+ * 必须放进 {@code basicRoutes}(静态路由),否则首次进入时
+ * 动态路由尚未注入,本页自身会 404。
+ */
 export const AuthorizationRoute: AppRouteRecordRaw = {
   path: PageEnum.BASE_AUTHORIZATION,
   name: 'Authorization',
@@ -24,21 +32,6 @@ export const AuthorizationRoute: AppRouteRecordRaw = {
   },
 };
 
-/**
- * 登录页。
- *
- * <p>必须放进 {@code basicRoutes}(静态路由),否则首次进入时
- * 动态路由尚未注入,登录页本身会 404。
- */
-export const LoginRoute: AppRouteRecordRaw = {
-  path: PageEnum.BASE_LOGIN,
-  name: 'Login',
-  component: () => import('@/core/layouts/views/login/index.vue'),
-  meta: {
-    title: '登录',
-  },
-};
-
 export const CaseRoute: AppRouteRecordRaw = {
   path: PageEnum.BASE_CASE,
   name: 'Case',
@@ -136,7 +129,6 @@ export const CaseCleanProgressRoute: AppRouteRecordRaw = {
 
 // Basic routing without permission
 export const basicRoutes = [
-  LoginRoute,
   AuthorizationRoute,
   CaseRoute,
   CaseUploadRoute,

+ 1 - 1
ai-frontend/src/core/store/modules/multipleTab.ts

@@ -194,7 +194,7 @@ export const useMultipleTabStore = defineStore('app-multiple-tab', {
       // 404  The page does not need to add a tab
       if (
         path === PageEnum.ERROR_PAGE ||
-        path === PageEnum.BASE_LOGIN ||
+        path === PageEnum.BASE_AUTHORIZATION ||
         route.meta?.hideTab ||
         !name ||
         [REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes(name as string)

+ 2 - 2
ai-frontend/src/core/utils/http/axios/index.ts

@@ -93,8 +93,8 @@ const transform: AxiosTransform = {
     //         showMessage(data.message, undefined, 180);
     //       }
     //       if (config.url?.indexOf('__notUpdateSession=true') == -1) {
-    //         let path = PageEnum.BASE_LOGIN as string;
-    //         if (currentRoute.path !== '/' && currentRoute.path !== PageEnum.BASE_LOGIN) {
+    //         let path = PageEnum.BASE_AUTHORIZATION as string;
+    //         if (currentRoute.path !== '/' && currentRoute.path !== PageEnum.BASE_AUTHORIZATION) {
     //           path = path + '?redirect=' + currentRoute.fullPath;
     //         }
     //         router.replace(path);

+ 3 - 2
ai-frontend/src/core/utils/http/axios/unauthorized.ts

@@ -25,10 +25,11 @@ function redirectTo(path: string, query?: Record<string, string>) {
 }
 
 /**
- * code=401:未登录 / token 失效 / 被顶下线 → 跳登录页。
+ * code=401:未登录 / token 失效 / 被顶下线 → 跳授权/登录入口页。
  *
  * <p>后端对认证失败刻意返回 **HTTP 200 + body.code=401**(见 GlobalExceptionHandler),
  * 因此这里由 transformRequestHook 触发,而不是 axios 的 catch 分支。
+ * <p>`/login` 已合并进 `/authorization`,入口页会根据授权状态自行显示登录卡片。
  */
 export function handleUnauthorizedRedirect(message = '登录状态已失效,请重新登录') {
   if (isHandlingUnauthorized) return;
@@ -42,7 +43,7 @@ export function handleUnauthorizedRedirect(message = '登录状态已失效,
       title: '提示',
       content: message,
       onOk: async () => {
-        redirectTo(PageEnum.BASE_LOGIN, { redirect });
+        redirectTo(PageEnum.BASE_AUTHORIZATION, { redirect });
         return Promise.resolve();
       },
     });

+ 16 - 6
ai-frontend/src/main.ts

@@ -86,7 +86,13 @@ function shouldRunStartupAuthCheck(path: string) {
 }
 
 function getAuthorizedTarget(fullPath: string, path: string) {
-  if (path === '/' || path === '/test/testData/list' || path === PageEnum.BASE_AUTHORIZATION) {
+  // 首页、旧的测试页、入口页自身、以及已删除的 /login,统一落到案件页
+  if (
+    path === '/' ||
+    path === '/test/testData/list' ||
+    path === PageEnum.BASE_AUTHORIZATION ||
+    path === PageEnum.LEGACY_LOGIN
+  ) {
     return PageEnum.BASE_CASE;
   }
   return fullPath || PageEnum.BASE_CASE;
@@ -172,20 +178,24 @@ async function resolveStartupRoute() {
 }
 
 /**
- * 登录门禁:授权校验通过后,若本地没有 token 则进登录页。
+ * 登录门禁:授权校验通过后,若本地没有 token 则进授权/登录入口页。
  *
  * <p>放在 license 校验<b>之后</b>:license 未通过时应停在 /authorization,
- * 不能反过来把用户推到登录页。所以这里遇到授权页直接返回。
+ * 不能反过来把用户推到别处。所以这里遇到入口页直接返回。
+ * <p>入口页自身会根据 `checkAuthApi()` 结果决定显示授权卡片还是登录卡片。
  */
 async function ensureLoggedIn() {
   const current = router.currentRoute.value;
-  if (current.path === PageEnum.BASE_LOGIN || current.path === PageEnum.BASE_AUTHORIZATION) {
+  if (current.path === PageEnum.BASE_AUTHORIZATION) {
     return;
   }
   if (!getToken()) {
+    // /login 已删除,不作为回跳来源,否则登录后会落到 404
+    const source =
+      current.fullPath && current.fullPath !== '/' && current.path !== PageEnum.LEGACY_LOGIN ? current.fullPath : '';
     await router.replace({
-      path: PageEnum.BASE_LOGIN,
-      query: current.fullPath && current.fullPath !== '/' ? { redirect: current.fullPath } : undefined,
+      path: PageEnum.BASE_AUTHORIZATION,
+      query: source ? { redirect: source } : undefined,
     });
   }
 }

+ 390 - 97
ai-frontend/src/plat/views/authorization/index.vue

@@ -1,21 +1,21 @@
 <template>
   <div :class="prefixCls" class="authorization-page">
-    <div class="case-header-bar flex items-center justify-between">
-      <AppLogo :showTitle="true" />
-      <div class="flex items-center"> </div>
-    </div>
     <div class="authorization-content">
       <section class="left-pane">
         <div class="left-pane__content">
           <div class="left-pane__header">
             <div class="left-pane__badge">
               <SafetyCertificateOutlined />
-              <span>系统授权入口</span>
+              <span>{{ isLoginView ? '已授权设备' : '系统授权入口' }}</span>
             </div>
 
-            <h1 class="left-pane__title">智能研判分析平台</h1>
+            <h1 class="left-pane__title">{{ isLoginView ? '欢迎回来' : '智能研判分析平台' }}</h1>
             <p class="left-pane__subtitle">
-              面向案件管理、对象关联和多源数据研判的专业系统,帮助团队更高效地完成线索梳理、关系挖掘与专题分析。
+              {{
+                isLoginView
+                  ? '请使用管理员分配的账号登录,登录后即可进入案件工作台,开展线索梳理与关系研判。'
+                  : '面向案件管理、对象关联和多源数据研判的专业系统,帮助团队更高效地完成线索梳理、关系挖掘与专题分析。'
+              }}
             </p>
           </div>
 
@@ -63,7 +63,7 @@
           </div>
 
           <div class="left-pane__features">
-            <div v-for="item in capabilityList" :key="item.title" class="feature-item">
+            <div v-for="item in leftFeatures" :key="item.title" class="feature-item">
               <div class="feature-item__icon">
                 <component :is="item.icon" />
               </div>
@@ -75,7 +75,7 @@
           </div>
 
           <div class="left-pane__footer">
-            <p class="left-pane__footer-line">© 2026 中数矩阵(成都)科技有限公司 | 向量探索团队</p>
+            <p class="left-pane__footer-line">© 2026 中数矩阵(成都)科技有限公司</p>
             <p class="left-pane__footer-line">联系电话:18908093240</p>
           </div>
         </div>
@@ -87,7 +87,7 @@
         </svg>
 
         <div class="right-pane__scroll">
-          <div class="auth-card">
+          <div v-if="viewMode === 'license'" class="auth-card">
             <div class="auth-card__header">
               <span class="auth-card__eyebrow">离线授权流程</span>
               <h2 class="auth-card__title">未授权时请先完成离线授权</h2>
@@ -144,7 +144,15 @@
                   accept=".xlts"
                   @change="handleLicenseFileChange"
                 />
-                <div class="upload-panel__dropzone" @click="licenseFileInputRef?.click()">
+                <div
+                  class="upload-panel__dropzone"
+                  :class="{ 'upload-panel__dropzone--has-file': !!licenseFile }"
+                  role="button"
+                  tabindex="0"
+                  @click="openLicenseFilePicker"
+                  @keydown.enter.prevent="openLicenseFilePicker"
+                  @keydown.space.prevent="openLicenseFilePicker"
+                >
                   <UploadOutlined class="upload-panel__dropzone-icon" />
                   <span class="upload-panel__dropzone-text">
                     {{ licenseFileName || '请选择离线授权文件' }}
@@ -152,7 +160,7 @@
                 </div>
 
                 <div class="upload-panel__actions">
-                  <Button class="ui-btn ui-btn--ghost" @click="licenseFileInputRef?.click()">选择授权文件</Button>
+                  <Button class="ui-btn ui-btn--ghost" @click="openLicenseFilePicker">选择授权文件</Button>
                   <Button
                     type="primary"
                     class="ui-btn ui-btn--primary"
@@ -166,6 +174,66 @@
               </div>
             </div>
           </div>
+
+          <div v-else-if="viewMode === 'login'" class="auth-card auth-card--login">
+            <div class="auth-card__header">
+              <span class="auth-card__eyebrow">账号登录</span>
+              <h2 class="auth-card__title">登录进入案件工作台</h2>
+            </div>
+
+            <div v-if="hasAuthorizationSummary" class="license-summary">
+              <div class="license-summary__head">
+                <SafetyCertificateOutlined />
+                <span>当前设备授权信息</span>
+              </div>
+              <dl class="license-summary__list">
+                <div v-if="authorizationUnit" class="license-summary__row">
+                  <dt>授权单位</dt>
+                  <dd>{{ authorizationUnit }}</dd>
+                </div>
+                <div v-if="authorizationOverDateTime" class="license-summary__row">
+                  <dt>授权到期</dt>
+                  <dd>{{ authorizationOverDateTime }}</dd>
+                </div>
+              </dl>
+            </div>
+
+            <Form :model="loginForm" layout="vertical" class="login-form" @submit.prevent>
+              <FormItem name="username" :rules="[{ required: true, message: '请输入用户名' }]">
+                <label class="login-form__label" for="login-username">用户名</label>
+                <Input
+                  id="login-username"
+                  v-model:value="loginForm.username"
+                  placeholder="请输入用户名"
+                  autocomplete="username"
+                  allow-clear
+                />
+              </FormItem>
+
+              <FormItem name="password" :rules="[{ required: true, message: '请输入密码' }]">
+                <label class="login-form__label" for="login-password">密码</label>
+                <Input.Password
+                  id="login-password"
+                  v-model:value="loginForm.password"
+                  placeholder="请输入密码"
+                  autocomplete="current-password"
+                  @press-enter="handleLoginSubmit"
+                />
+              </FormItem>
+
+              <Button
+                type="primary"
+                block
+                class="login-form__submit"
+                :loading="loginLoading"
+                @click="handleLoginSubmit"
+              >
+                登录
+              </Button>
+            </Form>
+
+            <p class="login-form__tip">账号由管理员创建,如需开通请联系管理员</p>
+          </div>
         </div>
       </section>
     </div>
@@ -173,14 +241,15 @@
 </template>
 
 <script lang="ts" setup>
-  import { computed, onMounted, ref } from 'vue';
+  import { computed, onMounted, reactive, ref } from 'vue';
   import { useRoute, useRouter } from 'vue-router';
-  import { Button } from 'ant-design-vue';
+  import { Button, Form, FormItem, Input } from 'ant-design-vue';
   import {
     ApartmentOutlined,
     CopyOutlined,
     DatabaseOutlined,
     DesktopOutlined,
+    LockOutlined,
     NotificationOutlined,
     RightOutlined,
     SafetyCertificateOutlined,
@@ -191,8 +260,9 @@
   import { useCopyToClipboard } from '@/core/hooks/web/useCopyToClipboard';
   import { useDesign } from '@/core/hooks/web/useDesign';
   import { useMessage } from '@/core/hooks/web/useMessage';
+  import { useAccountStore } from '@/core/store/modules/account';
   import { useUserStore } from '@/core/store/modules/case';
-  import { AppLogo } from '@/core/components/Application';
+  import { getToken } from '@/core/utils/auth';
   import { initGlobalCaseConfig } from '@/core/logics/caseConfigGuide';
   import {
     extractAuthorizationCache,
@@ -206,6 +276,17 @@
   const { showMessage } = useMessage();
   const { clipboardRef, isSuccessRef } = useCopyToClipboard();
   const userStore = useUserStore();
+  const accountStore = useAccountStore();
+
+  /**
+   * 页面形态。
+   *
+   * <p>`license` = 设备未授权(或授权已过期)→ 展示离线授权卡片;
+   * `login` = 设备已授权但未登录 → 展示登录卡片;
+   * `''` = 尚未判定,此时右侧不渲染卡片,避免先闪一下错误的形态。
+   */
+  type ViewMode = 'license' | 'login';
+  const viewMode = ref<ViewMode | ''>('');
 
   const machineCode = ref('');
   const machineCodeLoading = ref(false);
@@ -216,6 +297,16 @@
   const licenseFileName = computed(() => licenseFile.value?.name || '');
   const authorizationWarningMessage = ref('');
 
+  const loginForm = reactive({ username: '', password: '' });
+  const loginLoading = ref(false);
+
+  /** 登录卡片上的授权摘要(授权单位 / 到期时间),来源与授权页一致 */
+  const authorizationUnit = computed(() => String(userStore.getPageCacheByKey('authorizationUnit', '') || ''));
+  const authorizationOverDateTime = computed(() =>
+    String(userStore.getPageCacheByKey('licenseOverDateTime', '') || ''),
+  );
+  const hasAuthorizationSummary = computed(() => !!(authorizationUnit.value || authorizationOverDateTime.value));
+
   const capabilityList = [
     {
       title: '多源数据融合',
@@ -234,6 +325,28 @@
     },
   ];
 
+  /** 登录态的左侧说明。刻意不复述卡片上的内容(授权信息、账号来源提示都在卡片里)。 */
+  const loginTips = [
+    {
+      title: '账号来源',
+      desc: '账号由管理员统一创建,忘记密码请联系管理员重置。',
+      icon: SafetyCertificateOutlined,
+    },
+    {
+      title: '案件隔离',
+      desc: '不同案件的数据源相互独立,切换案件会重新加载对应数据源。',
+      icon: DatabaseOutlined,
+    },
+    {
+      title: '会话保护',
+      desc: '长时间无操作会自动锁屏,需重新验证身份后继续。',
+      icon: LockOutlined,
+    },
+  ];
+
+  const isLoginView = computed(() => viewMode.value === 'login');
+  const leftFeatures = computed(() => (isLoginView.value ? loginTips : capabilityList));
+
   const authorizationSteps = [
     {
       title: '获取机器码',
@@ -311,6 +424,11 @@
     }
   }
 
+  /** 统一入口:dropzone 点击 / 键盘、以及「选择授权文件」按钮都走这里 */
+  function openLicenseFilePicker() {
+    licenseFileInputRef.value?.click();
+  }
+
   function handleLicenseFileChange(event: Event) {
     const input = event.target as HTMLInputElement;
     const file = input.files?.[0] || null;
@@ -333,9 +451,14 @@
       const res = await offlineAuthorizationApi(licenseFile.value);
       authorizationWarningMessage.value = '';
       if (res?.code === 200) {
-        const entered = await checkAuthAndEnterSystem();
-        if (entered) {
-          showMessage('离线授权成功', 'success');
+        showMessage('离线授权成功', 'success');
+        // 授权成功后不再直接进业务页:先刷新授权摘要,再按登录态决定去哪
+        await resolveAuthorizationCache();
+        if (getToken()) {
+          await initGlobalCaseConfig();
+          await router.replace(getPostLoginTarget());
+        } else {
+          viewMode.value = 'login';
         }
       } else {
         showMessage(res?.message || '离线授权失败');
@@ -347,60 +470,98 @@
     }
   }
 
-  async function checkAuthAndEnterSystem(showUnauthorizedMessage = true) {
+  /**
+   * 判定当前设备的授权状态。
+   *
+   * <p>判定方式与合并前**完全一致**(`checkAuthApi()` + `isAuthorizationExpired()`),
+   * 只是不再负责跳转:这里只回答「授权是否有效」,展示哪张卡片由 `resolveEntryMode()` 决定。
+   */
+  async function isDeviceAuthorized(): Promise<boolean> {
     try {
       const res = await checkAuthApi();
-      const code = res?.code;
-      if (code === 401) {
-        clearAuthorizationCache();
-        if (showUnauthorizedMessage) {
-          showMessage('当前设备未授权,请先完成授权!');
-        }
-        return false;
-      }
-      if (code === 200) {
+      if (res?.code === 200) {
         const cache = await resolveAuthorizationCache(res);
         if (isAuthorizationExpired(cache.overDateTime)) {
           showExpiredAuthorizationMessage(cache.overDateTime);
           return false;
         }
-        await initGlobalCaseConfig();
-        await router.replace(PageEnum.BASE_CASE);
         return true;
       }
       clearAuthorizationCache();
-      if (showUnauthorizedMessage) {
-        showMessage('授权校验失败,请稍后重试!');
-      }
       return false;
     } catch (error: any) {
       const status = error?.response?.status || error?.status;
-      if (status === 401) {
-        clearAuthorizationCache();
-        if (showUnauthorizedMessage) {
-          showMessage('当前设备未授权,请先完成授权!');
-        }
-        return false;
-      }
-      clearAuthorizationCache();
-      if (showUnauthorizedMessage) {
+      if (status !== 401) {
         showMessage(error?.message || '授权校验失败,请稍后重试!');
       }
+      clearAuthorizationCache();
       return false;
     }
   }
 
-  onMounted(() => {
+  /** 登录 / 已登录后的落点:优先回跳来源页,且不能落回入口页自身(或已删除的旧登录页) */
+  function getPostLoginTarget(): string {
+    const redirect = String(route.query.redirect || '');
+    if (redirect && redirect !== PageEnum.BASE_AUTHORIZATION && redirect !== PageEnum.LEGACY_LOGIN) {
+      return redirect;
+    }
+    return PageEnum.BASE_CASE;
+  }
+
+  /**
+   * 决定入口页展示哪张卡片。
+   *
+   * <p>· 带 `forceAuth` / `skipAuthCheck`:授权已被明确判定失效,直接进授权态;
+   * <p>· 已授权 + 有 token:直接进业务页;
+   * <p>· 已授权 + 无 token:登录卡片;
+   * <p>· 未授权:授权卡片。
+   */
+  async function resolveEntryMode() {
+    if (route.query.forceAuth === '1' || route.query.skipAuthCheck === '1') {
+      viewMode.value = 'license';
+      return;
+    }
+    const authorized = await isDeviceAuthorized();
+    if (!authorized) {
+      viewMode.value = 'license';
+      return;
+    }
+    if (getToken()) {
+      await initGlobalCaseConfig();
+      await router.replace(getPostLoginTarget());
+      return;
+    }
+    viewMode.value = 'login';
+  }
+
+  async function handleLoginSubmit() {
+    // 防重入:重复登录会因 is-concurrent=false 把前一个 token 顶掉,导致刚拿到的凭据立即失效
+    if (loginLoading.value) {
+      return;
+    }
+    const username = loginForm.username.trim();
+    if (!username || !loginForm.password) {
+      return;
+    }
+    loginLoading.value = true;
+    try {
+      await accountStore.login({ username, password: loginForm.password });
+      await router.replace(getPostLoginTarget());
+    } catch {
+      // 错误提示已由 defHttp 统一弹出,这里只负责不跳转
+    } finally {
+      loginLoading.value = false;
+    }
+  }
+
+  onMounted(async () => {
     if (route.query.expired === '1') {
       authorizationWarningMessage.value = String(
         route.query.message || '当前授权已过期,请重新导入有效授权文件或联系管理员。',
       );
       showMessage(authorizationWarningMessage.value, 'warning');
     }
-    if (route.query.forceAuth === '1' || route.query.skipAuthCheck === '1') {
-      return;
-    }
-    void checkAuthAndEnterSystem(false);
+    await resolveEntryMode();
   });
 </script>
 
@@ -424,54 +585,6 @@
       height: 100vh;
       overflow: hidden;
     }
-    .case-header-bar {
-      height: 48px;
-      padding: 0 12px 0 0;
-      background: #fff;
-      flex-shrink: 0;
-      -webkit-app-region: drag;
-
-      .jeesite-app-logo {
-        -webkit-app-region: no-drag;
-        &__title {
-          display: inline-flex;
-          align-items: baseline;
-          gap: 6px;
-          font-size: 18px;
-          font-weight: bold;
-          color: #555;
-          font-family: Arial, 'Microsoft YaHei', sans-serif;
-          cursor: default;
-          user-select: none;
-          transition: all 0.5s;
-        }
-      }
-
-      .jeesite-app-logo img {
-        width: 32px;
-        height: 32px;
-        margin-right: 2px;
-      }
-
-      .jeesite-header-action-button {
-        display: inline-flex;
-        align-items: center;
-        justify-content: center;
-        width: 36px;
-        height: 36px;
-        border-radius: 10px;
-        color: #333;
-        font-size: 18px;
-        cursor: pointer;
-        transition: background-color 0.2s ease;
-        -webkit-app-region: no-drag;
-
-        &:hover {
-          background-color: #f6f6f6;
-        }
-      }
-    }
-
     .authorization-content {
       flex: 1;
       display: flex;
@@ -924,9 +1037,170 @@
         font-weight: 500;
         color: #9ca3af;
       }
+    }
 
-      &__file-input {
-        display: none;
+    // ---------- 登录卡片(设备已授权、但未登录) ----------
+    // 用 .auth-card.auth-card--login 提权,压住后面媒体查询里的 .auth-card 规则
+    .auth-card.auth-card--login {
+      max-width: 480px;
+
+      .auth-card__header {
+        margin-bottom: 18px;
+      }
+    }
+
+    .license-summary {
+      padding: 12px 14px;
+      margin-bottom: 20px;
+      background: #f8fafc;
+      border: 1px solid #e5e7eb;
+      border-radius: 12px;
+
+      &__head {
+        display: flex;
+        gap: 6px;
+        align-items: center;
+        margin-bottom: 9px;
+        font-size: 11px;
+        font-weight: 700;
+        letter-spacing: 0.04em;
+        color: #6b7280;
+      }
+
+      &__list {
+        display: grid;
+        gap: 7px;
+        margin: 0;
+      }
+
+      &__row {
+        display: flex;
+        gap: 12px;
+        align-items: baseline;
+        font-size: 12.5px;
+        line-height: 1.5;
+
+        dt {
+          flex: none;
+          width: 58px;
+          color: #9ca3af;
+        }
+
+        dd {
+          margin: 0;
+          font-weight: 600;
+          color: #374151;
+          word-break: break-all;
+        }
+      }
+    }
+
+    .login-form {
+      .ant-form-item {
+        margin-bottom: 16px;
+      }
+
+      &__label {
+        display: block;
+        margin-bottom: 7px;
+        font-size: 12.5px;
+        font-weight: 500;
+        color: #4b5563;
+        cursor: pointer;
+      }
+
+      .ant-input,
+      .ant-input-affix-wrapper {
+        height: 42px;
+        padding: 0 12px;
+        font-size: 13.5px;
+        color: #111827;
+        background: #fff;
+        border: 1px solid #d1d5db;
+        border-radius: 9px;
+        box-shadow: none;
+        transition:
+          border-color 0.18s ease-out,
+          box-shadow 0.18s ease-out;
+
+        &::placeholder {
+          color: #9ca3af;
+        }
+
+        &:hover {
+          border-color: #93c5fd;
+        }
+
+        &:focus,
+        &:focus-within,
+        &-focused {
+          border-color: #165dff;
+          box-shadow: 0 0 0 3px rgb(22 93 255 / 12%);
+        }
+      }
+
+      // 外壳与内层 input 叠加会重复画边框,这里抹平
+      .ant-input-affix-wrapper > .ant-input {
+        height: auto;
+        padding: 0;
+        color: inherit;
+        background: transparent;
+        border: 0;
+        border-radius: 0;
+        box-shadow: none;
+
+        &:hover,
+        &:focus {
+          background: transparent;
+          border: 0;
+          box-shadow: none;
+        }
+      }
+
+      .ant-input-clear-icon,
+      .ant-input-suffix .anticon {
+        color: #9ca3af;
+
+        &:hover {
+          color: #6b7280;
+        }
+      }
+
+      .ant-form-item-explain-error {
+        padding-top: 3px;
+        font-size: 12px;
+      }
+
+      &__submit {
+        &.ant-btn {
+          height: 44px;
+          padding: 0 18px;
+          margin-top: 4px;
+          font-size: 14px;
+          font-weight: 600;
+          border-radius: 9px;
+          box-shadow: none;
+        }
+
+        &.ant-btn.ant-btn-primary {
+          color: #fff;
+          background: #165dff;
+          border-color: #165dff;
+
+          &:hover:not([disabled]) {
+            background: #0f56f0;
+            border-color: #0f56f0;
+            box-shadow: 0 10px 22px -10px rgb(22 93 255 / 60%);
+          }
+        }
+      }
+
+      &__tip {
+        margin: 14px 0 0;
+        font-size: 12px;
+        line-height: 1.6;
+        color: #9ca3af;
+        text-align: center;
       }
     }
 
@@ -1025,6 +1299,13 @@
     }
 
     .upload-panel {
+      // 原生 file 控件只用于触发选择,视觉上完全由下方 dropzone 承担。
+      // 注意:选择器必须挂在 .upload-panel 下(曾经误挂在 .auth-card 里,
+      // 编译成 .auth-card__file-input 永远匹配不到,导致原生控件裸露在页面上)。
+      &__file-input {
+        display: none;
+      }
+
       &__dropzone {
         display: flex;
         flex-direction: column;
@@ -1073,6 +1354,18 @@
         transition: color 0.2s ease;
       }
 
+      // 已选中文件:实线蓝框 + 文件名高亮,给出明确的选中反馈
+      &__dropzone--has-file {
+        background: #f8fbff;
+        border-color: #93c5fd;
+        border-style: solid;
+
+        .upload-panel__dropzone-text {
+          font-weight: 600;
+          color: #2563eb;
+        }
+      }
+
       &__actions {
         display: flex;
         justify-content: flex-end;

+ 1 - 1
ai-frontend/src/plat/views/case/case.vue

@@ -169,7 +169,7 @@
       onOk: async () => {
         userStore.resetState();
         await accountStore.logout();
-        await router.replace({ path: PageEnum.BASE_LOGIN });
+        await router.replace({ path: PageEnum.BASE_AUTHORIZATION });
       },
     });
   }

+ 6 - 0
ai-frontend/src/trans/views/aiBaseStation/index.vue

@@ -373,6 +373,12 @@
 
     .side-actions {
       margin-left: auto;
+
+      // 原生 file 控件只用于触发选择,视觉上由「批量转换基站」按钮承担。
+      // 之前这个类没有任何样式,导致原生控件直接裸露在按钮旁边。
+      &__file-input {
+        display: none;
+      }
     }
 
     .table-card {

+ 1 - 1
ai-server/src/main/java/com/zsjz/ai/module/plat/service/SystemService.java

@@ -400,7 +400,7 @@ public class SystemService extends ServiceImpl<SystemInfoMapper, SystemInfo> {
      * 如果未授权则抛出异常
      */
     public String checkAuth() {
-       /* CheckAuthStatus checked = LicenseUtil.checkAuthStatus();
+        /*CheckAuthStatus checked = LicenseUtil.checkAuthStatus();
         Assert.isTrue(checked.status(), () -> ServerException.spe(checked.message()));
         SystemInfo info = info();
         if (info == null) {