20260717170047
This commit is contained in:
1 parent
49dafe524c
commit
35aa108a34
40 files changed
+6218
-413
No files matched your search
@@ -23,9 +23,9 @@ const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: "/home",
|
||||
key: "/dashboard",
|
||||
icon: menuIcon(House),
|
||||
label: "首页",
|
||||
label: "工作台",
|
||||
},
|
||||
{
|
||||
key: "business",
|
||||
|
||||
@@ -145,7 +145,9 @@ const router = createRouter({
|
||||
|
||||
router.beforeEach((to) => {
|
||||
NProgress.start();
|
||||
document.title = to.meta.title ? `${to.meta.title} - FMS 管理系统` : "FMS 管理系统";
|
||||
document.title = to.meta.title
|
||||
? `${to.meta.title} - FMS 货代物流管理系统`
|
||||
: "FMS 货代物流管理系统";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { post } from "./http";
|
||||
* @param {string} orgId 机构码。
|
||||
* @param {string} userId 用户 ID。
|
||||
* @param {string} password 密码。
|
||||
* @returns {Promise<{token: string, orgid: string, user: {id: string, name: string}}>} 登录会话。
|
||||
* @returns {Promise<{success: boolean, code: number, message: string, data: {token: string, orgid: string, user: {id: string, name: string}}}>} 登录响应。
|
||||
*/
|
||||
export function loginApi(orgId, userId, password) {
|
||||
return post("/auth/login", {
|
||||
@@ -21,7 +21,7 @@ export function loginApi(orgId, userId, password) {
|
||||
* @param {string} [searchCondition] 查询条件,不包含 WHERE。
|
||||
* @param {string} [orderField] 排序字段,可带 ASC 或 DESC。
|
||||
* @param {string[]} [searchColumns] 需要返回的字段列表,省略时返回全部字段。
|
||||
* @returns {Promise<object[]>} 查询结果行。
|
||||
* @returns {Promise<{success: boolean, code: number, message: string, data: object[]}>} 查询响应。
|
||||
*/
|
||||
export function loadDataApi(viewName, searchCondition, orderField, searchColumns) {
|
||||
return post("/data/loaddata", {
|
||||
@@ -35,7 +35,7 @@ export function loadDataApi(viewName, searchCondition, orderField, searchColumns
|
||||
/**
|
||||
* 执行只读 SQL 查询。
|
||||
* @param {string} sql 完整的 SELECT 或 WITH 查询语句。
|
||||
* @returns {Promise<object[]>} 查询结果行。
|
||||
* @returns {Promise<{success: boolean, code: number, message: string, data: object[]}>} 查询响应。
|
||||
*/
|
||||
export function loadDataBySqlApi(sql) {
|
||||
return post("/data/loaddatabysql", { sql });
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import axios from "axios";
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
|
||||
const AUTH_ERROR_CODE = 401;
|
||||
const SESSION_REPLACED_CODE = 40101;
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
|
||||
timeout: 15000,
|
||||
@@ -12,19 +15,25 @@ const http = axios.create({
|
||||
|
||||
let redirectingToLogin = false;
|
||||
|
||||
function redirectToLogin() {
|
||||
function redirectToLogin(reason) {
|
||||
const currentPath = window.location.pathname.replace(/\/+$/, "") || "/";
|
||||
if (currentPath === "/login" || redirectingToLogin) return;
|
||||
|
||||
redirectingToLogin = true;
|
||||
const redirect = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
window.location.assign(`/login?redirect=${encodeURIComponent(redirect)}`);
|
||||
const query = new URLSearchParams({ redirect });
|
||||
if (reason) query.set("reason", reason);
|
||||
window.location.assign(`/login?${query.toString()}`);
|
||||
}
|
||||
|
||||
function handleUnauthorized() {
|
||||
function isAuthenticationError(code) {
|
||||
return code === AUTH_ERROR_CODE || code === SESSION_REPLACED_CODE;
|
||||
}
|
||||
|
||||
function handleUnauthorized(code) {
|
||||
const authStore = useAuthStore();
|
||||
authStore.clearSession();
|
||||
redirectToLogin();
|
||||
redirectToLogin(code === SESSION_REPLACED_CODE ? "session-replaced" : undefined);
|
||||
}
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
@@ -42,25 +51,34 @@ http.interceptors.response.use(
|
||||
const payload = response.data;
|
||||
|
||||
if (payload?.code === undefined) return payload;
|
||||
if (payload.code === 0) return payload.data;
|
||||
const result = {
|
||||
...payload,
|
||||
success: payload.code === 0,
|
||||
};
|
||||
if (result.success) return result;
|
||||
|
||||
if (payload.code === 401) handleUnauthorized();
|
||||
if (isAuthenticationError(result.code)) handleUnauthorized(result.code);
|
||||
|
||||
const error = new Error(payload.message || "请求失败");
|
||||
error.code = payload.code;
|
||||
const error = new Error(result.message || "请求失败");
|
||||
error.code = result.code;
|
||||
error.data = result.data;
|
||||
error.success = result.success;
|
||||
throw error;
|
||||
},
|
||||
(error) => {
|
||||
const status = error.response?.status;
|
||||
const payload = error.response?.data;
|
||||
|
||||
if (status === 401 || payload?.code === 401) handleUnauthorized();
|
||||
const authenticationCode = payload?.code ?? status;
|
||||
if (isAuthenticationError(authenticationCode)) handleUnauthorized(authenticationCode);
|
||||
|
||||
const requestError = new Error(
|
||||
payload?.message ||
|
||||
(error.code === "ECONNABORTED" ? "请求超时,请稍后重试" : "网络异常,请检查服务是否正常"),
|
||||
);
|
||||
requestError.code = payload?.code ?? status;
|
||||
requestError.data = payload?.data;
|
||||
requestError.success = false;
|
||||
throw requestError;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
try {
|
||||
// Remove sessions persisted by earlier versions; authentication is session-only now.
|
||||
localStorage.removeItem("fms-auth");
|
||||
} catch {
|
||||
// Storage can be unavailable in restricted browser contexts.
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore(
|
||||
"auth",
|
||||
() => {
|
||||
@@ -33,6 +40,7 @@ export const useAuthStore = defineStore(
|
||||
{
|
||||
persist: {
|
||||
key: "fms-auth",
|
||||
storage: sessionStorage,
|
||||
pick: ["token", "orgId", "user"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script setup>
|
||||
import { CircleCheck, Clock3, DollarSign, FileText, Plus } from "@lucide/vue";
|
||||
import { ref } from "vue";
|
||||
import { CircleCheck, Clock3, DollarSign, FileText, Plus, RefreshCw } from "@lucide/vue";
|
||||
import { App } from "antdv-next";
|
||||
import { loadDataApi } from "../services/api";
|
||||
|
||||
const { message } = App.useApp();
|
||||
const usersLoading = ref(false);
|
||||
const users = ref([]);
|
||||
|
||||
const metrics = [
|
||||
{ label: "今日新增", value: "0", tone: "blue", icon: FileText },
|
||||
@@ -14,6 +21,25 @@ const columns = [
|
||||
{ title: "业务状态", dataIndex: "status", key: "status" },
|
||||
{ title: "更新时间", dataIndex: "updatedAt", key: "updatedAt" },
|
||||
];
|
||||
|
||||
const userColumns = [
|
||||
{ title: "用户ID", dataIndex: "b_id", key: "b_id" },
|
||||
{ title: "用户名称", dataIndex: "b_name", key: "b_name" },
|
||||
];
|
||||
|
||||
async function loadUsers() {
|
||||
usersLoading.value = true;
|
||||
try {
|
||||
const response = await loadDataApi("b_user", null, "b_id ASC", ["b_id", "b_name"]);
|
||||
users.value = response.data;
|
||||
} catch (error) {
|
||||
if (error.code !== 401 && error.code !== 40101) {
|
||||
message.error(error.message || "用户查询失败");
|
||||
}
|
||||
} finally {
|
||||
usersLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -74,6 +100,27 @@ const columns = [
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel session-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>登录会话测试</h2>
|
||||
<span>b_user 用户列表</span>
|
||||
</div>
|
||||
<a-button :loading="usersLoading" @click="loadUsers">
|
||||
<template #icon><RefreshCw /></template>
|
||||
查询用户
|
||||
</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
:columns="userColumns"
|
||||
:data-source="users"
|
||||
:loading="usersLoading"
|
||||
:pagination="false"
|
||||
row-key="b_id"
|
||||
size="small"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -194,6 +241,10 @@ const columns = [
|
||||
padding: 0 2px 2px;
|
||||
}
|
||||
|
||||
.session-panel :deep(.ant-table-wrapper) {
|
||||
padding: 0 2px 2px;
|
||||
}
|
||||
|
||||
.quick-links {
|
||||
display: grid;
|
||||
padding: 4px;
|
||||
|
||||
@@ -188,7 +188,8 @@ async function loadData() {
|
||||
loadError.value = "";
|
||||
|
||||
try {
|
||||
const result = await loadDataApi(viewName);
|
||||
const response = await loadDataApi(viewName);
|
||||
const result = response.data;
|
||||
if (currentSequence !== loadSequence) return;
|
||||
sourceRows.value = (Array.isArray(result) ? result : []).map(normalizeRow);
|
||||
applyLocalFilters();
|
||||
|
||||
@@ -1,22 +1,54 @@
|
||||
<script setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Building2, LockKeyhole, LogIn, MapPin, Ship, Truck, UserRound } from "@lucide/vue";
|
||||
import { App } from "antdv-next";
|
||||
import { loginApi } from "../services/api";
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
|
||||
const LOGIN_IDENTITY_KEY = "fms-login-identity";
|
||||
|
||||
function readRememberedIdentity() {
|
||||
try {
|
||||
const identity = JSON.parse(localStorage.getItem(LOGIN_IDENTITY_KEY) || "{}");
|
||||
return {
|
||||
orgId: typeof identity.orgId === "string" ? identity.orgId : "",
|
||||
userId: typeof identity.userId === "string" ? identity.userId : "",
|
||||
};
|
||||
} catch {
|
||||
return { orgId: "", userId: "" };
|
||||
}
|
||||
}
|
||||
|
||||
function rememberIdentity(orgId, userId) {
|
||||
try {
|
||||
localStorage.setItem(LOGIN_IDENTITY_KEY, JSON.stringify({ orgId, userId }));
|
||||
} catch {
|
||||
// Login remains available when browser storage is restricted.
|
||||
}
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const { message } = App.useApp();
|
||||
const loading = ref(false);
|
||||
const rememberedIdentity = readRememberedIdentity();
|
||||
const form = reactive({
|
||||
orgId: "",
|
||||
userId: "",
|
||||
orgId: rememberedIdentity.orgId,
|
||||
userId: rememberedIdentity.userId,
|
||||
password: "",
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.reason !== "session-replaced") return;
|
||||
|
||||
message.warning("账号已在其他设备登录,请重新登录");
|
||||
const query = { ...route.query };
|
||||
delete query.reason;
|
||||
router.replace({ name: "login", query });
|
||||
});
|
||||
|
||||
function getRedirectPath() {
|
||||
const redirect = route.query.redirect;
|
||||
return typeof redirect === "string" && redirect.startsWith("/") && !redirect.startsWith("//")
|
||||
@@ -25,11 +57,16 @@ function getRedirectPath() {
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (loading.value) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const session = await loginApi(form.orgId, form.userId, form.password);
|
||||
const response = await loginApi(form.orgId, form.userId, form.password);
|
||||
const session = response.data;
|
||||
authStore.setSession(session, form.orgId);
|
||||
rememberIdentity(form.orgId, form.userId);
|
||||
message.success("登录成功,正在进入工作台");
|
||||
await router.replace(getRedirectPath());
|
||||
} catch (error) {
|
||||
message.error(error.message || "登录失败");
|
||||
@@ -41,56 +78,49 @@ async function submit() {
|
||||
|
||||
<template>
|
||||
<main class="login-page">
|
||||
<section class="login-shell" aria-label="FMS 登录">
|
||||
<aside class="login-overview">
|
||||
<section class="login-shell" aria-label="FMS 货代物流管理系统登录">
|
||||
<aside class="login-overview" aria-label="货代物流业务概览">
|
||||
<div class="overview-brand">
|
||||
<div class="overview-brand__mark">F</div>
|
||||
<div class="overview-brand__mark"><Ship /></div>
|
||||
<div class="overview-brand__copy">
|
||||
<strong>FMS</strong>
|
||||
<span>Freight Management System</span>
|
||||
<span>Freight & Logistics</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-copy">
|
||||
<span class="overview-copy__eyebrow">一体化业务工作台</span>
|
||||
<h1>FMS 管理系统</h1>
|
||||
<p>让业务、费用与数据在同一工作台高效流转。</p>
|
||||
<span class="overview-copy__eyebrow">货代 · 物流 · 费用一体化</span>
|
||||
<h1>货代物流管理系统</h1>
|
||||
<p>贯通订舱委托、运输跟踪与费用结算,让每一票业务清晰可控。</p>
|
||||
</div>
|
||||
|
||||
<div class="route-visual" aria-hidden="true">
|
||||
<div class="route-line route-line--first"></div>
|
||||
<div class="route-line route-line--second"></div>
|
||||
<div class="route-node route-node--origin"><Building2 /></div>
|
||||
<div class="route-node route-node--truck"><Truck /></div>
|
||||
<div class="route-node route-node--ship"><Ship /></div>
|
||||
<div class="route-node route-node--destination"><MapPin /></div>
|
||||
</div>
|
||||
|
||||
<div class="overview-footer">
|
||||
<span>多机构协同</span>
|
||||
<span>业务数据一体化</span>
|
||||
<div class="overview-scope">
|
||||
<span><Ship /> 海运订舱</span>
|
||||
<span><Truck /> 运输跟踪</span>
|
||||
<span><MapPin /> 业务协同</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="login-panel">
|
||||
<div class="mobile-brand">
|
||||
<div class="mobile-brand__mark">F</div>
|
||||
<strong>FMS 管理系统</strong>
|
||||
</div>
|
||||
|
||||
<header class="login-heading">
|
||||
<span>ACCOUNT ACCESS</span>
|
||||
<span>业务工作台</span>
|
||||
<h2>账号登录</h2>
|
||||
<p>欢迎回来,请登录所属机构</p>
|
||||
<p>欢迎回来,请使用您的业务账号登录</p>
|
||||
</header>
|
||||
|
||||
<a-form layout="vertical" :model="form" class="login-form" @finish="submit">
|
||||
<a-form layout="vertical" :model="form" @finish="submit">
|
||||
<a-form-item
|
||||
label="机构码"
|
||||
name="orgId"
|
||||
:rules="[{ required: true, message: '请输入机构码' }]"
|
||||
>
|
||||
<a-input v-model:value="form.orgId" size="large" autocomplete="organization">
|
||||
<a-input
|
||||
v-model:value="form.orgId"
|
||||
size="large"
|
||||
autocomplete="organization"
|
||||
placeholder="请输入机构码"
|
||||
autofocus
|
||||
>
|
||||
<template #prefix><Building2 /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
@@ -99,7 +129,12 @@ async function submit() {
|
||||
name="userId"
|
||||
:rules="[{ required: true, message: '请输入账号' }]"
|
||||
>
|
||||
<a-input v-model:value="form.userId" size="large" autocomplete="username">
|
||||
<a-input
|
||||
v-model:value="form.userId"
|
||||
size="large"
|
||||
autocomplete="username"
|
||||
placeholder="请输入账号"
|
||||
>
|
||||
<template #prefix><UserRound /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
@@ -108,6 +143,7 @@ async function submit() {
|
||||
v-model:value="form.password"
|
||||
size="large"
|
||||
autocomplete="current-password"
|
||||
placeholder="请输入密码"
|
||||
>
|
||||
<template #prefix><LockKeyhole /></template>
|
||||
</a-input-password>
|
||||
@@ -117,7 +153,6 @@ async function submit() {
|
||||
html-type="submit"
|
||||
size="large"
|
||||
block
|
||||
class="login-button"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #icon><LogIn /></template>
|
||||
@@ -125,7 +160,7 @@ async function submit() {
|
||||
</a-button>
|
||||
</a-form>
|
||||
|
||||
<footer class="login-footer">FMS · 机构业务管理平台</footer>
|
||||
<footer class="login-footer">FMS · 货代 / 物流管理系统</footer>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -133,39 +168,33 @@ async function submit() {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
place-items: center;
|
||||
padding: 32px;
|
||||
background: #e8edf2;
|
||||
background: #edf1f3;
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: min(100%, 920px);
|
||||
min-height: 560px;
|
||||
grid-template-columns: minmax(0, 1.08fr) minmax(380px, 0.92fr);
|
||||
width: 880px;
|
||||
min-height: 520px;
|
||||
grid-template-columns: 380px 500px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d8e0e8;
|
||||
border: 1px solid #d5dee2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 22px 60px rgb(31 45 61 / 14%);
|
||||
box-shadow: 0 18px 48px rgb(25 47 55 / 12%);
|
||||
}
|
||||
|
||||
.login-overview {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
padding: 38px 42px 34px;
|
||||
overflow: hidden;
|
||||
color: #f7fafc;
|
||||
background: #22313f;
|
||||
padding: 42px;
|
||||
color: #f5faf9;
|
||||
background: #173a42;
|
||||
}
|
||||
|
||||
.overview-brand {
|
||||
@@ -178,12 +207,15 @@ async function submit() {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border: 1px solid rgb(255 255 255 / 24%);
|
||||
border: 1px solid rgb(255 255 255 / 20%);
|
||||
border-radius: 6px;
|
||||
background: #168aad;
|
||||
background: #15998e;
|
||||
color: #fff;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
|
||||
:deep(.lucide) {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&__copy {
|
||||
@@ -198,7 +230,7 @@ async function submit() {
|
||||
|
||||
span {
|
||||
margin-top: 3px;
|
||||
color: #aebdca;
|
||||
color: #a9c0c2;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
@@ -206,14 +238,12 @@ async function submit() {
|
||||
}
|
||||
|
||||
.overview-copy {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: 76px;
|
||||
margin-top: 72px;
|
||||
|
||||
&__eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #55c2b6;
|
||||
color: #5dd2c5;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -221,310 +251,74 @@ async function submit() {
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 30px;
|
||||
line-height: 1.25;
|
||||
font-size: 28px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 320px;
|
||||
margin: 14px 0 0;
|
||||
color: #c5d0d9;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #c4d4d5;
|
||||
font-size: 13px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
}
|
||||
|
||||
.route-visual {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 150px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.route-line {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
border-top: 1px dashed #6f8290;
|
||||
transform-origin: left center;
|
||||
|
||||
&--first {
|
||||
top: 75px;
|
||||
left: 24px;
|
||||
width: 45%;
|
||||
transform: rotate(-11deg);
|
||||
}
|
||||
|
||||
&--second {
|
||||
top: 62px;
|
||||
left: 45%;
|
||||
width: 44%;
|
||||
transform: rotate(12deg);
|
||||
}
|
||||
}
|
||||
|
||||
.route-node {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
place-items: center;
|
||||
border: 1px solid #667987;
|
||||
border-radius: 50%;
|
||||
color: #d7e1e8;
|
||||
background: #2c3e4c;
|
||||
|
||||
:deep(.lucide) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&--origin {
|
||||
top: 56px;
|
||||
left: 5px;
|
||||
border-color: #45ad9f;
|
||||
color: #66d0c2;
|
||||
}
|
||||
|
||||
&--truck {
|
||||
top: 26px;
|
||||
left: 37%;
|
||||
}
|
||||
|
||||
&--ship {
|
||||
top: 58px;
|
||||
right: 25%;
|
||||
border-color: #d3a34c;
|
||||
color: #f0bd5f;
|
||||
}
|
||||
|
||||
&--destination {
|
||||
top: 86px;
|
||||
right: 3px;
|
||||
border-color: #3f9bb7;
|
||||
color: #62bad5;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-footer {
|
||||
.overview-scope {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
color: #91a4b2;
|
||||
font-size: 11px;
|
||||
margin-top: auto;
|
||||
flex-direction: column;
|
||||
gap: 13px;
|
||||
color: #b8cbcc;
|
||||
font-size: 12px;
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
span + span::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -10px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
border-radius: 50%;
|
||||
background: #667987;
|
||||
content: "";
|
||||
:deep(.lucide) {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: #62c8be;
|
||||
}
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
padding: 54px 48px 30px;
|
||||
padding: 58px 60px 30px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mobile-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-heading {
|
||||
margin-bottom: 30px;
|
||||
|
||||
span {
|
||||
color: #168aad;
|
||||
font-size: 10px;
|
||||
color: #168f86;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 8px 0 0;
|
||||
color: #1f2d3a;
|
||||
font-size: 24px;
|
||||
color: #19323a;
|
||||
font-size: 25px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0 0;
|
||||
color: #7a8793;
|
||||
color: #738287;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label > label) {
|
||||
color: #354452;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
min-height: 44px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.ant-input-prefix) {
|
||||
margin-inline-end: 10px;
|
||||
color: #83909b;
|
||||
}
|
||||
|
||||
:deep(.ant-input-prefix .lucide) {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 44px;
|
||||
margin-top: 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
|
||||
:deep(.lucide) {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: auto;
|
||||
padding-top: 28px;
|
||||
color: #9aa4ad;
|
||||
padding-top: 26px;
|
||||
color: #8c999d;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.login-page {
|
||||
align-items: start;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
width: min(100%, 500px);
|
||||
min-height: calc(100vh - 40px);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.login-overview {
|
||||
min-height: 170px;
|
||||
padding: 25px 28px;
|
||||
}
|
||||
|
||||
.overview-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overview-copy {
|
||||
margin-top: 0;
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 7px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.route-visual,
|
||||
.overview-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
padding: 30px 34px 24px;
|
||||
}
|
||||
|
||||
.mobile-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&__mark {
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
place-items: center;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
background: #168aad;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #263746;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
min-height: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-overview {
|
||||
min-height: 140px;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.overview-copy {
|
||||
&__eyebrow {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
padding: 26px 24px 20px;
|
||||
}
|
||||
|
||||
.mobile-brand {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.login-heading {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user