|
|
@@ -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;
|