feat(web): 初始化前端项目

This commit is contained in:
R524809
2026-09-03 13:11:38 +08:00
parent 195b6d25e9
commit df40ddde8c
31 changed files with 948 additions and 848 deletions
+15
View File
@@ -0,0 +1,15 @@
export default {
printWidth: 100,
tabWidth: 4,
useTabs: false,
semi: true,
singleQuote: true,
quoteProps: 'as-needed',
jsxSingleQuote: false,
trailingComma: 'es5',
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'always',
endOfLine: 'lf',
embeddedLanguageFormatting: 'auto',
};
+76 -34
View File
@@ -12,18 +12,66 @@
padding: 24px 20px; padding: 24px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
text-align: center; text-align: center;
transition: padding 0.2s ease;
}
/* 折叠态收窄水平留白,与宽度动画同步 */
.ant-layout-sider-collapsed .sidebar-header {
padding: 20px 8px;
} }
.logo { .logo {
position: relative;
height: 31px;
line-height: 31px;
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
color: white; color: white;
margin-bottom: 4px; margin-bottom: 4px;
} }
/* 全称与缩写叠加存放,折叠时交叉淡入淡出 */
.logo-full,
.logo-short {
display: block;
white-space: nowrap;
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.logo-short {
position: absolute;
inset: 0;
opacity: 0;
transform: scale(0.5);
}
.ant-layout-sider-collapsed .logo-full {
opacity: 0;
transform: scale(0.5);
}
.ant-layout-sider-collapsed .logo-short {
opacity: 1;
transform: scale(1);
}
/* 副标题折叠时渐隐并收起占位高度 */
.logo-subtitle { .logo-subtitle {
max-height: 18px;
overflow: hidden;
opacity: 1;
font-size: 12px; font-size: 12px;
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
transition:
opacity 0.2s ease,
max-height 0.2s ease;
}
.ant-layout-sider-collapsed .logo-subtitle {
max-height: 0;
opacity: 0;
} }
.main-header { .main-header {
@@ -38,19 +86,19 @@
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 100; z-index: 100;
height: 64px; height: 50px;
line-height: 1; line-height: 1;
} }
.header-left { .header-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 12px;
} }
.collapse-trigger, .collapse-trigger,
.mobile-menu-trigger { .mobile-menu-trigger {
font-size: 18px; font-size: 16px;
cursor: pointer; cursor: pointer;
color: #1f2937; color: #1f2937;
border-radius: 4px; border-radius: 4px;
@@ -58,8 +106,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 32px; width: 28px;
height: 32px; height: 28px;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -68,32 +116,34 @@
background: #f9fafb; background: #f9fafb;
} }
.page-title-group { .page-breadcrumb {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1.4;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #1f2937;
line-height: 1.2;
margin-bottom: 4px;
}
.page-subtitle {
font-size: 14px; font-size: 14px;
color: #6b7280; line-height: 1;
font-style: italic; white-space: nowrap;
line-height: 1.2; }
.page-breadcrumb .ant-breadcrumb-link {
color: #1f2937;
}
.breadcrumb-crumb {
display: inline-flex;
align-items: center;
gap: 6px;
}
.breadcrumb-crumb .anticon {
font-size: 15px;
}
.page-breadcrumb .ant-breadcrumb-separator {
color: #9ca3af;
} }
.main-content { .main-content {
padding: 24px; padding: 20px;
background: #f3f4f6; background: #f3f4f6;
min-height: calc(100vh - 64px); min-height: calc(100vh - 50px);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -104,12 +154,4 @@
.main-header { .main-header {
padding: 0 16px !important; padding: 0 16px !important;
} }
.page-title {
font-size: 18px;
}
.page-subtitle {
font-size: 12px;
}
} }
+28 -10
View File
@@ -1,9 +1,9 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Outlet, useLocation } from 'react-router-dom'; import { Outlet, useLocation } from 'react-router-dom';
import { Drawer, Layout } from 'antd'; import { Breadcrumb, Drawer, Layout } from 'antd';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import SidebarMenu from './SidebarMenu'; import SidebarMenu from './SidebarMenu';
import { getPageInfo } from './menuConfig'; import { getBreadcrumbItems } from './menuConfig';
import './MainLayout.css'; import './MainLayout.css';
const { Header, Sider, Content } = Layout; const { Header, Sider, Content } = Layout;
@@ -14,7 +14,18 @@ const MainLayout = () => {
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const location = useLocation(); const location = useLocation();
const pageInfo = useMemo(() => getPageInfo(location.pathname), [location.pathname]); const breadcrumbItems = useMemo(
() =>
getBreadcrumbItems(location.pathname).map((crumb) => ({
title: (
<span className="breadcrumb-crumb">
{crumb.icon}
<span>{crumb.label}</span>
</span>
),
})),
[location.pathname]
);
useEffect(() => { useEffect(() => {
const checkMobile = () => { const checkMobile = () => {
@@ -45,8 +56,11 @@ const MainLayout = () => {
}} }}
> >
<div className="sidebar-header"> <div className="sidebar-header">
<div className="logo">{collapsed ? 'A' : 'Atlas Kit'}</div> <div className="logo">
{!collapsed && <div className="logo-subtitle">商品工作台</div>} <span className="logo-full">Atlas Kit</span>
<span className="logo-short">A</span>
</div>
<div className="logo-subtitle">商品工作台</div>
</div> </div>
<SidebarMenu collapsed={collapsed} /> <SidebarMenu collapsed={collapsed} />
</Sider> </Sider>
@@ -81,14 +95,18 @@ const MainLayout = () => {
/> />
)} )}
{!isMobile && ( {!isMobile && (
<div className="collapse-trigger" onClick={() => setCollapsed(!collapsed)}> <div
className="collapse-trigger"
onClick={() => setCollapsed(!collapsed)}
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} {collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div> </div>
)} )}
<div className="page-title-group"> <Breadcrumb
<div className="page-title">{pageInfo.title}</div> className="page-breadcrumb"
<div className="page-subtitle">{pageInfo.subtitle}</div> separator=">"
</div> items={breadcrumbItems}
/>
</div> </div>
</Header> </Header>
+39 -11
View File
@@ -26,8 +26,20 @@
.sidebar-menu .ant-menu-item .ant-menu-title-content { .sidebar-menu .ant-menu-item .ant-menu-title-content {
font-size: 15px !important; font-size: 15px !important;
font-weight: 600 !important; font-weight: 600 !important;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* antd 默认只过渡 color,会覆盖下面的 width/opacity 过渡;必须 !important 提权 */
transition:
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
width 0.2s ease,
opacity 0.2s ease,
margin 0.3s,
color 0.3s !important;
display: inline-block; display: inline-block;
/* 显式宽度让折叠时 width→0 可插值,配合裁剪+渐隐平滑收起(auto→0 无法动画) */
width: 100%;
opacity: 1;
overflow: hidden;
white-space: nowrap;
vertical-align: middle;
} }
.sidebar-menu .ant-menu-item:hover { .sidebar-menu .ant-menu-item:hover {
@@ -58,6 +70,14 @@
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
font-weight: 500; font-weight: 500;
/* 折叠时渐隐并收起占位高度(!important 防止被 antd 默认 transition 覆盖) */
max-height: 40px;
opacity: 1;
overflow: hidden;
transition:
opacity 0.2s ease,
max-height 0.2s ease,
padding 0.2s ease !important;
} }
.sidebar-menu .ant-menu-item-icon { .sidebar-menu .ant-menu-item-icon {
@@ -67,7 +87,8 @@
align-items: center; align-items: center;
} }
.sidebar-menu .ant-menu-item:hover .ant-menu-item-icon { /* hover 放大只作用于非选中项;选中项图标固定 translateX(8px),hover 时不得变 transform 导致跳动 */
.sidebar-menu .ant-menu-item:hover:not(.ant-menu-item-selected) .ant-menu-item-icon {
transform: scale(1.05); transform: scale(1.05);
} }
@@ -88,6 +109,14 @@
text-align: center; text-align: center;
} }
/* 分组标题折叠时渐隐并完全收起,不占纵向空间 */
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item-group-title {
max-height: 0;
opacity: 0;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item { .sidebar-menu.ant-menu-inline-collapsed .ant-menu-item {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -107,14 +136,17 @@
margin: 0; margin: 0;
} }
/* 折叠时去掉左侧 3px 选中边框(会挤偏图标),选中态用高亮背景区分 */ /* 折叠时去掉左侧 3px 选中边框(会挤偏图标);只过渡宽度,避免 none 突变 */
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item-selected { .sidebar-menu.ant-menu-inline-collapsed .ant-menu-item-selected {
border-left: none !important; border-left-width: 0 !important;
} }
/* 折叠时隐藏 label 占位(antd 折叠动画保留 opacity 占位,会把图标挤离中心) */ /* 折叠时菜单文字平滑收起:width 100%→0 插值 + 裁剪 + 渐隐;
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item .ant-menu-title-content { 不用 display:none(无法过渡,文字瞬间消失) */
display: none !important; .sidebar-menu.ant-menu-inline-collapsed .ant-menu-item .ant-menu-title-content,
.sidebar-menu.ant-menu-vertical .ant-menu-item .ant-menu-title-content {
width: 0 !important;
opacity: 0 !important;
} }
/* 兼容:部分版本折叠态用 vertical 类渲染,同样居中 */ /* 兼容:部分版本折叠态用 vertical 类渲染,同样居中 */
@@ -124,10 +156,6 @@
padding-right: 0 !important; padding-right: 0 !important;
} }
.sidebar-menu.ant-menu-vertical .ant-menu-item .ant-menu-title-content {
display: none !important;
}
/* 折叠态 flex 子元素居中(display:flex 已生效,缺 justify-content) */ /* 折叠态 flex 子元素居中(display:flex 已生效,缺 justify-content) */
.sidebar-menu.ant-menu-inline-collapsed .ant-menu-item { .sidebar-menu.ant-menu-inline-collapsed .ant-menu-item {
justify-content: center !important; justify-content: center !important;
+3 -2
View File
@@ -14,6 +14,7 @@ const SidebarMenu = ({ collapsed, onMenuClick }: SidebarMenuProps) => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
// 分组标题保持常驻,折叠时的隐藏由 CSS 过渡完成,避免文字瞬间消失
const menuItems: MenuProps['items'] = useMemo(() => { const menuItems: MenuProps['items'] = useMemo(() => {
const children = routeMenuConfig.map((config) => ({ const children = routeMenuConfig.map((config) => ({
key: config.key, key: config.key,
@@ -23,12 +24,12 @@ const SidebarMenu = ({ collapsed, onMenuClick }: SidebarMenuProps) => {
return [ return [
{ {
type: 'group' as const, type: 'group' as const,
label: collapsed ? '' : '主要功能', label: '主要功能',
className: 'menu-section-title', className: 'menu-section-title',
children, children,
}, },
]; ];
}, [collapsed]); }, []);
const selectedKeys = useMemo(() => { const selectedKeys = useMemo(() => {
const path = location.pathname; const path = location.pathname;
+16 -24
View File
@@ -1,5 +1,7 @@
import { import {
CalculatorOutlined, CalculatorOutlined,
CodeSandboxOutlined,
EditOutlined,
GlobalOutlined, GlobalOutlined,
InboxOutlined, InboxOutlined,
PictureOutlined, PictureOutlined,
@@ -12,14 +14,10 @@ export interface RouteMenuConfig {
path: string; path: string;
/** 菜单键值(与 path 相同) */ /** 菜单键值(与 path 相同) */
key: string; key: string;
/** 菜单图标 */ /** 菜单图标(菜单与页头面包屑共用) */
icon: ReactNode; icon: ReactNode;
/** 菜单标签 */ /** 菜单标签 */
label: string; label: string;
/** 页面标题 */
title: string;
/** 页面副标题 */
subtitle: string;
} }
/** 左侧一级菜单,顺序与 docs/requirements/account-store-watermark.md 的导航约定一致 */ /** 左侧一级菜单,顺序与 docs/requirements/account-store-watermark.md 的导航约定一致 */
@@ -29,52 +27,46 @@ export const routeMenuConfig: RouteMenuConfig[] = [
key: '/online-products', key: '/online-products',
icon: <GlobalOutlined />, icon: <GlobalOutlined />,
label: '在线商品', label: '在线商品',
title: '在线商品',
subtitle: '按 SKU 查看平台已上架商品与同步状态',
}, },
{ {
path: '/collection', path: '/collection',
key: '/collection', key: '/collection',
icon: <InboxOutlined />, icon: <InboxOutlined />,
label: '采集箱', label: '采集箱',
title: '采集箱',
subtitle: '查看已采集的商品,进入编辑上架',
}, },
{ {
path: '/shops', path: '/shops',
key: '/shops', key: '/shops',
icon: <ShopOutlined />, icon: <ShopOutlined />,
label: '店铺管理', label: '店铺管理',
title: '店铺管理',
subtitle: '绑定平台店铺授权与水印',
}, },
{ {
path: '/watermarks', path: '/watermarks',
key: '/watermarks', key: '/watermarks',
icon: <PictureOutlined />, icon: <PictureOutlined />,
label: '水印管理', label: '水印管理',
title: '水印管理',
subtitle: '维护水印模板并绑定到店铺',
}, },
{ {
path: '/pricing', path: '/pricing',
key: '/pricing', key: '/pricing',
icon: <CalculatorOutlined />, icon: <CalculatorOutlined />,
label: '定价工具', label: '定价工具',
title: '定价工具',
subtitle: '独立试算与物流计价规则',
}, },
]; ];
/** 根据路径获取页面标题信息 */ export interface BreadcrumbCrumb {
export const getPageInfo = (path: string): { title: string; subtitle: string } => { icon: ReactNode;
if (path === '/' || path === '') { label: string;
const first = routeMenuConfig[0]; }
return { title: first.title, subtitle: first.subtitle };
} /** 页头面包屑:一级菜单页显示单级;商品 AI 编辑页不在菜单中,显示「商品 > 商品编辑」两级 */
export const getBreadcrumbItems = (path: string): BreadcrumbCrumb[] => {
if (path.startsWith('/products/')) { if (path.startsWith('/products/')) {
return { title: '商品编辑', subtitle: '商品 AI 编辑页:SPU/SKU、媒体、定价与发布' }; return [
{ icon: <CodeSandboxOutlined />, label: '商品' },
{ icon: <EditOutlined />, label: '商品编辑' },
];
} }
const config = routeMenuConfig.find((item) => path.startsWith(item.path)); const config = routeMenuConfig.find((item) => path.startsWith(item.path)) ?? routeMenuConfig[0];
return config ? { title: config.title, subtitle: config.subtitle } : getPageInfo('/'); return [{ icon: config.icon, label: config.label }];
}; };
+1 -1
View File
@@ -6,5 +6,5 @@ import App from './App';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <App />
</StrictMode>, </StrictMode>
); );
+5 -1
View File
@@ -65,7 +65,11 @@ const LoginPage = () => {
autoFocus autoFocus
/> />
</Form.Item> </Form.Item>
<Form.Item name="password" label="密码" rules={[{ required: true, message: '请输入密码' }]}> <Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password <Input.Password
prefix={<LockOutlined style={{ color: 'var(--text-secondary)' }} />} prefix={<LockOutlined style={{ color: 'var(--text-secondary)' }} />}
placeholder="请输入密码" placeholder="请输入密码"
+1 -1
View File
@@ -69,7 +69,7 @@ apiClient.interceptors.response.use(
window.location.assign('/login'); window.location.assign('/login');
} }
return Promise.reject(error); return Promise.reject(error);
}, }
); );
export const api = { export const api = {