20260716215550
This commit is contained in:
1 parent
5cd4e1e1d2
commit
49dafe524c
21 files changed
+1218
-272
No files matched your search
@@ -2,11 +2,13 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Ellipsis, ListFilter, Plus, Search } from "@lucide/vue";
|
||||
import { mockRecords } from "../data/mockRecords";
|
||||
import { loadDataApi } from "../services/api";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const loadError = ref("");
|
||||
const sourceRows = ref([]);
|
||||
const rows = ref([]);
|
||||
const total = ref(0);
|
||||
const filters = reactive({ field: "name", keyword: "" });
|
||||
@@ -16,6 +18,7 @@ const tableScrollY = ref(240);
|
||||
const advancedOpen = ref(false);
|
||||
const appliedFilterCount = ref(0);
|
||||
let resizeObserver;
|
||||
let loadSequence = 0;
|
||||
|
||||
const columns = [
|
||||
{ title: "编号", dataIndex: "id", key: "id", width: 160 },
|
||||
@@ -74,20 +77,130 @@ function isFilterValueFilled(value) {
|
||||
if (Array.isArray(value)) return value.some(isFilterValueFilled);
|
||||
return value !== undefined && value !== null && value !== "";
|
||||
}
|
||||
function loadData() {
|
||||
loading.value = true;
|
||||
|
||||
function getRowValue(row, candidates) {
|
||||
const keys = Object.keys(row);
|
||||
for (const candidate of candidates) {
|
||||
const matchedKey = keys.find((key) => key.toLowerCase() === candidate.toLowerCase());
|
||||
if (matchedKey !== undefined && row[matchedKey] != null) return row[matchedKey];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeRow(row, index) {
|
||||
const id = getRowValue(row, ["id", "b_id", "user_id", "userid"]);
|
||||
const name = getRowValue(row, ["name", "b_name", "customer_name", "title"]);
|
||||
const state = getRowValue(row, ["state", "b_state", "status"]);
|
||||
const updatedAt = getRowValue(row, ["updated_at", "update_time", "updated_time", "b_updated_at"]);
|
||||
|
||||
return {
|
||||
...row,
|
||||
id: id == null ? "" : String(id),
|
||||
name: name == null ? "" : String(name),
|
||||
state: state == null ? "" : String(state),
|
||||
updated_at: updatedAt == null ? "" : String(updatedAt),
|
||||
__rowKey: `${String(route.meta.source)}-${String(id ?? index)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function includesValue(row, candidates, expected) {
|
||||
if (!isFilterValueFilled(expected)) return true;
|
||||
const actual = getRowValue(row, candidates);
|
||||
return String(actual ?? "")
|
||||
.toLowerCase()
|
||||
.includes(String(expected).trim().toLowerCase());
|
||||
}
|
||||
|
||||
function isInDateRange(row, candidates, range) {
|
||||
if (!Array.isArray(range) || !range.some(Boolean)) return true;
|
||||
const actual = String(getRowValue(row, candidates) ?? "").slice(0, 10);
|
||||
if (!actual) return false;
|
||||
const start = range[0]?.format?.("YYYY-MM-DD");
|
||||
const end = range[1]?.format?.("YYYY-MM-DD");
|
||||
return (!start || actual >= start) && (!end || actual <= end);
|
||||
}
|
||||
|
||||
function matchesAdvancedFilters(row) {
|
||||
const amount = Number(
|
||||
String(getRowValue(row, ["amount", "total_amount", "b_amount"]) ?? "").replaceAll(",", ""),
|
||||
);
|
||||
|
||||
return (
|
||||
includesValue(row, ["id", "b_id", "document_no"], advancedFilters.documentNo) &&
|
||||
includesValue(
|
||||
row,
|
||||
["customer", "customer_name", "name", "b_name"],
|
||||
advancedFilters.customerName,
|
||||
) &&
|
||||
includesValue(row, ["business_type", "businessType", "b_type"], advancedFilters.businessType) &&
|
||||
includesValue(row, ["state", "b_state", "status"], advancedFilters.status) &&
|
||||
isInDateRange(row, ["business_date", "b_date"], advancedFilters.businessDate) &&
|
||||
includesValue(row, ["owner", "owner_name", "sales_name"], advancedFilters.owner) &&
|
||||
includesValue(row, ["department", "department_name"], advancedFilters.department) &&
|
||||
includesValue(
|
||||
row,
|
||||
["settlement_status", "settlementStatus"],
|
||||
advancedFilters.settlementStatus,
|
||||
) &&
|
||||
includesValue(row, ["currency", "currency_code"], advancedFilters.currency) &&
|
||||
(!isFilterValueFilled(advancedFilters.amountMin) ||
|
||||
(Number.isFinite(amount) && amount >= advancedFilters.amountMin)) &&
|
||||
(!isFilterValueFilled(advancedFilters.amountMax) ||
|
||||
(Number.isFinite(amount) && amount <= advancedFilters.amountMax)) &&
|
||||
includesValue(row, ["creator", "created_by", "creator_name"], advancedFilters.creator) &&
|
||||
isInDateRange(
|
||||
row,
|
||||
["created_at", "create_time", "created_time"],
|
||||
advancedFilters.createdDate,
|
||||
) &&
|
||||
includesValue(row, ["remark", "remarks", "memo"], advancedFilters.remark)
|
||||
);
|
||||
}
|
||||
|
||||
function applyLocalFilters() {
|
||||
const keyword = filters.keyword.trim().toLowerCase();
|
||||
const filteredRows = keyword
|
||||
? mockRecords.filter((row) =>
|
||||
const filteredRows = sourceRows.value.filter(
|
||||
(row) =>
|
||||
(!keyword ||
|
||||
String(row[filters.field] ?? "")
|
||||
.toLowerCase()
|
||||
.includes(keyword),
|
||||
)
|
||||
: mockRecords;
|
||||
.includes(keyword)) &&
|
||||
matchesAdvancedFilters(row),
|
||||
);
|
||||
const pageCount = Math.max(Math.ceil(filteredRows.length / pagination.pageSize), 1);
|
||||
if (pagination.current > pageCount) pagination.current = pageCount;
|
||||
const start = (pagination.current - 1) * pagination.pageSize;
|
||||
rows.value = filteredRows.slice(start, start + pagination.pageSize);
|
||||
total.value = filteredRows.length;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function searchData() {
|
||||
pagination.current = 1;
|
||||
applyLocalFilters();
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const currentSequence = ++loadSequence;
|
||||
const viewName = String(route.meta.source || "");
|
||||
if (!viewName) return;
|
||||
|
||||
loading.value = true;
|
||||
loadError.value = "";
|
||||
|
||||
try {
|
||||
const result = await loadDataApi(viewName);
|
||||
if (currentSequence !== loadSequence) return;
|
||||
sourceRows.value = (Array.isArray(result) ? result : []).map(normalizeRow);
|
||||
applyLocalFilters();
|
||||
} catch (error) {
|
||||
if (currentSequence !== loadSequence) return;
|
||||
sourceRows.value = [];
|
||||
rows.value = [];
|
||||
total.value = 0;
|
||||
loadError.value = error.message || "数据加载失败";
|
||||
} finally {
|
||||
if (currentSequence === loadSequence) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openDetail(record) {
|
||||
@@ -97,7 +210,7 @@ function openDetail(record) {
|
||||
function changePage(page, pageSize) {
|
||||
pagination.current = page;
|
||||
pagination.pageSize = pageSize;
|
||||
loadData();
|
||||
applyLocalFilters();
|
||||
}
|
||||
|
||||
function resetAdvancedFilters() {
|
||||
@@ -108,7 +221,7 @@ function applyAdvancedFilters() {
|
||||
pagination.current = 1;
|
||||
appliedFilterCount.value = draftFilterCount.value;
|
||||
advancedOpen.value = false;
|
||||
loadData();
|
||||
applyLocalFilters();
|
||||
}
|
||||
|
||||
function updateTableHeight() {
|
||||
@@ -128,7 +241,17 @@ onMounted(() => {
|
||||
|
||||
onBeforeUnmount(() => resizeObserver?.disconnect());
|
||||
|
||||
watch(() => route.meta.source, loadData, { immediate: true });
|
||||
watch(
|
||||
() => route.meta.source,
|
||||
() => {
|
||||
pagination.current = 1;
|
||||
filters.keyword = "";
|
||||
resetAdvancedFilters();
|
||||
appliedFilterCount.value = 0;
|
||||
loadData();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -153,7 +276,7 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
||||
class="keyword-search"
|
||||
allow-clear
|
||||
enter-button="搜索"
|
||||
@search="loadData"
|
||||
@search="searchData"
|
||||
/>
|
||||
</a-space-compact>
|
||||
|
||||
@@ -179,13 +302,16 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
||||
</section>
|
||||
|
||||
<section class="table-panel">
|
||||
<a-alert v-if="loadError" class="load-alert" type="error" show-icon :message="loadError">
|
||||
<template #action><a-button size="small" @click="loadData">重试</a-button></template>
|
||||
</a-alert>
|
||||
<div
|
||||
ref="tableAreaRef"
|
||||
class="table-area"
|
||||
:style="{ '--table-body-height': `${tableScrollY}px` }"
|
||||
>
|
||||
<a-table
|
||||
row-key="id"
|
||||
row-key="__rowKey"
|
||||
:columns="columns"
|
||||
:data-source="rows"
|
||||
:loading="loading"
|
||||
@@ -196,7 +322,7 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
||||
>
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<a-button
|
||||
v-if="column.key === 'id'"
|
||||
v-if="column.key === 'id' && record.id"
|
||||
type="link"
|
||||
class="table-id-link"
|
||||
@click="openDetail(record)"
|
||||
@@ -232,89 +358,69 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
||||
}"
|
||||
>
|
||||
<a-form layout="vertical" class="advanced-filter-form">
|
||||
<a-form-item label="单据编号">
|
||||
<a-input v-model:value="advancedFilters.documentNo" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="客户名称">
|
||||
<a-input
|
||||
v-model:value="advancedFilters.customerName"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="业务类型">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.businessType"
|
||||
allow-clear
|
||||
:options="businessTypeOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="业务状态">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.status"
|
||||
allow-clear
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="业务日期" class="filter-span-full">
|
||||
<a-range-picker
|
||||
v-model:value="advancedFilters.businessDate"
|
||||
:allow-empty="[true, true]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="单据编号">
|
||||
<a-input v-model:value="advancedFilters.documentNo" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="客户名称">
|
||||
<a-input v-model:value="advancedFilters.customerName" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="业务类型">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.businessType"
|
||||
allow-clear
|
||||
:options="businessTypeOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="业务状态">
|
||||
<a-select v-model:value="advancedFilters.status" allow-clear :options="statusOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="业务日期" class="filter-span-full">
|
||||
<a-range-picker
|
||||
v-model:value="advancedFilters.businessDate"
|
||||
:allow-empty="[true, true]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="负责人">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.owner"
|
||||
allow-clear
|
||||
:options="ownerOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属部门">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.department"
|
||||
allow-clear
|
||||
:options="departmentOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="结算状态">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.settlementStatus"
|
||||
allow-clear
|
||||
:options="settlementOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="币种">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.currency"
|
||||
allow-clear
|
||||
:options="currencyOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="金额下限">
|
||||
<a-input-number
|
||||
v-model:value="advancedFilters.amountMin"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="金额上限">
|
||||
<a-input-number
|
||||
v-model:value="advancedFilters.amountMax"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="负责人">
|
||||
<a-select v-model:value="advancedFilters.owner" allow-clear :options="ownerOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所属部门">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.department"
|
||||
allow-clear
|
||||
:options="departmentOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="结算状态">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.settlementStatus"
|
||||
allow-clear
|
||||
:options="settlementOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="币种">
|
||||
<a-select
|
||||
v-model:value="advancedFilters.currency"
|
||||
allow-clear
|
||||
:options="currencyOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="金额下限">
|
||||
<a-input-number v-model:value="advancedFilters.amountMin" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="金额上限">
|
||||
<a-input-number v-model:value="advancedFilters.amountMax" :min="0" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="创建人">
|
||||
<a-input v-model:value="advancedFilters.creator" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="创建日期">
|
||||
<a-range-picker
|
||||
v-model:value="advancedFilters.createdDate"
|
||||
:allow-empty="[true, true]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注关键词" class="filter-span-full">
|
||||
<a-input v-model:value="advancedFilters.remark" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="创建人">
|
||||
<a-input v-model:value="advancedFilters.creator" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="创建日期">
|
||||
<a-range-picker v-model:value="advancedFilters.createdDate" :allow-empty="[true, true]" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注关键词" class="filter-span-full">
|
||||
<a-input v-model:value="advancedFilters.remark" allow-clear />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -396,6 +502,11 @@ watch(() => route.meta.source, loadData, { immediate: true });
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.load-alert {
|
||||
flex: none;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.table-area {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
<script setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { LockKeyhole, UserRound } from "@lucide/vue";
|
||||
import { authApi } from "../services/api";
|
||||
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 route = useRoute();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const { message } = App.useApp();
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
const form = reactive({
|
||||
orgid: "",
|
||||
userid: "",
|
||||
orgId: "",
|
||||
userId: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
function getRedirectPath() {
|
||||
const redirect = route.query.redirect;
|
||||
return typeof redirect === "string" && redirect.startsWith("/") && !redirect.startsWith("//")
|
||||
? redirect
|
||||
: "/dashboard";
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
errorMessage.value = "";
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const session = await authApi.login(form);
|
||||
authStore.setSession(session, form.orgid);
|
||||
await router.replace(route.query.redirect || "/dashboard");
|
||||
const session = await loginApi(form.orgId, form.userId, form.password);
|
||||
authStore.setSession(session, form.orgId);
|
||||
await router.replace(getRedirectPath());
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || "登录失败";
|
||||
message.error(error.message || "登录失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -34,128 +41,490 @@ async function submit() {
|
||||
|
||||
<template>
|
||||
<main class="login-page">
|
||||
<section class="login-shell">
|
||||
<div class="login-brand">
|
||||
<div class="login-brand__mark">F</div>
|
||||
<div>
|
||||
<h1>FMS 管理系统</h1>
|
||||
<p>机构业务管理平台</p>
|
||||
<section class="login-shell" aria-label="FMS 登录">
|
||||
<aside class="login-overview">
|
||||
<div class="overview-brand">
|
||||
<div class="overview-brand__mark">F</div>
|
||||
<div class="overview-brand__copy">
|
||||
<strong>FMS</strong>
|
||||
<span>Freight Management System</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-form layout="vertical" :model="form" @finish="submit">
|
||||
<a-alert
|
||||
v-if="errorMessage"
|
||||
class="login-alert"
|
||||
type="error"
|
||||
show-icon
|
||||
:message="errorMessage"
|
||||
/>
|
||||
<a-form-item
|
||||
label="机构码"
|
||||
name="orgid"
|
||||
:rules="[{ required: true, message: '请输入机构码' }]"
|
||||
>
|
||||
<a-input v-model:value="form.orgid" size="large" autocomplete="organization" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="用户ID"
|
||||
name="userid"
|
||||
:rules="[{ required: true, message: '请输入用户ID' }]"
|
||||
>
|
||||
<a-input v-model:value="form.userid" size="large" autocomplete="username">
|
||||
<template #prefix><UserRound /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="密码"
|
||||
name="password"
|
||||
:rules="[{ required: true, message: '请输入密码' }]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="form.password"
|
||||
size="large"
|
||||
autocomplete="current-password"
|
||||
<div class="overview-copy">
|
||||
<span class="overview-copy__eyebrow">一体化业务工作台</span>
|
||||
<h1>FMS 管理系统</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>
|
||||
</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>
|
||||
<h2>账号登录</h2>
|
||||
<p>欢迎回来,请登录所属机构</p>
|
||||
</header>
|
||||
|
||||
<a-form layout="vertical" :model="form" class="login-form" @finish="submit">
|
||||
<a-form-item
|
||||
label="机构码"
|
||||
name="orgId"
|
||||
:rules="[{ required: true, message: '请输入机构码' }]"
|
||||
>
|
||||
<template #prefix><LockKeyhole /></template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-button type="primary" html-type="submit" size="large" block :loading="loading">
|
||||
登录
|
||||
</a-button>
|
||||
</a-form>
|
||||
<a-input v-model:value="form.orgId" size="large" autocomplete="organization">
|
||||
<template #prefix><Building2 /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="账号"
|
||||
name="userId"
|
||||
:rules="[{ required: true, message: '请输入账号' }]"
|
||||
>
|
||||
<a-input v-model:value="form.userId" size="large" autocomplete="username">
|
||||
<template #prefix><UserRound /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="密码" name="password">
|
||||
<a-input-password
|
||||
v-model:value="form.password"
|
||||
size="large"
|
||||
autocomplete="current-password"
|
||||
>
|
||||
<template #prefix><LockKeyhole /></template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
block
|
||||
class="login-button"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #icon><LogIn /></template>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form>
|
||||
|
||||
<footer class="login-footer">FMS · 机构业务管理平台</footer>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
linear-gradient(90deg, var(--fms-border) 1px, transparent 1px),
|
||||
linear-gradient(var(--fms-border) 1px, transparent 1px), var(--fms-bg);
|
||||
background-size: 32px 32px;
|
||||
padding: 32px;
|
||||
background: #e8edf2;
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
width: min(100%, 400px);
|
||||
padding: 30px;
|
||||
border: 1px solid var(--fms-border);
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: min(100%, 920px);
|
||||
min-height: 560px;
|
||||
grid-template-columns: minmax(0, 1.08fr) minmax(380px, 0.92fr);
|
||||
overflow: hidden;
|
||||
border: 1px solid #d8e0e8;
|
||||
border-radius: 8px;
|
||||
background: var(--fms-surface);
|
||||
box-shadow: var(--fms-shadow);
|
||||
background: #fff;
|
||||
box-shadow: 0 22px 60px rgb(31 45 61 / 14%);
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
.login-overview {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
padding: 38px 42px 34px;
|
||||
overflow: hidden;
|
||||
color: #f7fafc;
|
||||
background: #22313f;
|
||||
}
|
||||
|
||||
.overview-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 28px;
|
||||
gap: 11px;
|
||||
|
||||
&__mark {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border-radius: 7px;
|
||||
background: #1677ff;
|
||||
border: 1px solid rgb(255 255 255 / 24%);
|
||||
border-radius: 6px;
|
||||
background: #168aad;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
|
||||
strong {
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-top: 3px;
|
||||
color: #aebdca;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-copy {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: 76px;
|
||||
|
||||
&__eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #55c2b6;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: var(--fms-text);
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
font-size: 30px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 3px 0 0;
|
||||
color: var(--fms-text-secondary);
|
||||
max-width: 320px;
|
||||
margin: 14px 0 0;
|
||||
color: #c5d0d9;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
color: #91a4b2;
|
||||
font-size: 11px;
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
span + span::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -10px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
border-radius: 50%;
|
||||
background: #667987;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
padding: 54px 48px 30px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mobile-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-heading {
|
||||
margin-bottom: 30px;
|
||||
|
||||
span {
|
||||
color: #168aad;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 8px 0 0;
|
||||
color: #1f2d3a;
|
||||
font-size: 24px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0 0;
|
||||
color: #7a8793;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-alert {
|
||||
margin-bottom: 18px;
|
||||
.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;
|
||||
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;
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.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