diff --git a/package.json b/package.json index b30813c..96538f3 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "ant-design-vue": "^4.0.0", "china-area-data": "^5.0.1", "dayjs": "^1.11.21", + "echarts": "^5.6.0", "express": "^5.2.1", "html2canvas": "^1.4.1", "qs": "^6.15.0", diff --git a/src/assets/styles/index.less b/src/assets/styles/index.less index af15fdb..2f0d68e 100644 --- a/src/assets/styles/index.less +++ b/src/assets/styles/index.less @@ -107,6 +107,7 @@ a { justify-content: flex-end; padding: 14px; } + // ===== antd 组件覆盖 ===== // Tag 默认 margin-right: 8px,会挤开表格列或卡片内间距,统一去掉 diff --git a/src/assets/styles/pageLayout.module.less b/src/assets/styles/pageLayout.module.less new file mode 100644 index 0000000..b5e2aa5 --- /dev/null +++ b/src/assets/styles/pageLayout.module.less @@ -0,0 +1,47 @@ +// 页面布局根容器:筛选区 + 表格区作为直接子元素 +.containerMain { + display: flex; + flex-direction: column; + height: 100%; + + // 页面筛选区域:横向 flex,可换行 + .filter { + display: flex; + flex-wrap: wrap; + align-items: center; + column-gap: 16px; + row-gap: 12px; + flex-shrink: 0; + padding: 16px; + background-color: #fff; + border-radius: 10px; + } + + // 筛选区域操作按钮组 + .filterActions { + display: flex; + align-items: center; + gap: 10px; + margin-left: 0; + } + + // 表格显示区域:纵向 flex,占据剩余高度 + .table { + display: flex; + flex-direction: column; + flex: 1; + background-color: #fff; + min-height: 0; + min-width: 0; + margin-top: 16px; + overflow: hidden; + border-radius: 10px; + + .pagination { + flex-shrink: 0; + display: flex; + justify-content: flex-end; + padding: 14px; + } + } +} diff --git a/src/components/ChangePasswordModal/index.tsx b/src/components/ChangePasswordModal/index.tsx index 4c61711..115a973 100644 --- a/src/components/ChangePasswordModal/index.tsx +++ b/src/components/ChangePasswordModal/index.tsx @@ -61,6 +61,7 @@ export default defineComponent({ destroyOnClose footer={null} centered + wrapClassName={styles.changePasswordModalMain} class={styles.modal} >
diff --git a/src/components/ChangePasswordModal/style.module.less b/src/components/ChangePasswordModal/style.module.less index 4d622b2..4606684 100644 --- a/src/components/ChangePasswordModal/style.module.less +++ b/src/components/ChangePasswordModal/style.module.less @@ -1,37 +1,36 @@ -.form { - :global { - .ant-form-item { - margin-bottom: 16px; - } - .ant-form-item-explain-error { - font-size: 12px; +.changePasswordModalMain { + .modal { + :global { + .ant-modal-body { + padding: 16px 24px 0; + } } } -} -/* 底部提示文案 */ -.tip { - margin-top: -4px; - margin-bottom: 24px; - color: rgba(0, 0, 0, 0.45); - font-size: 12px; - line-height: 1.5; -} - -/* 底部按钮 */ -.footer { - display: flex; - justify-content: flex-end; - gap: 8px; - padding-top: 16px; - border-top: 1px solid #f0f0f0; -} - -/* 微调 Modal 内部样式 */ -.modal { - :global { - .ant-modal-body { - padding: 16px 24px 0; + .form { + :global { + .ant-form-item { + margin-bottom: 16px; + } + .ant-form-item-explain-error { + font-size: 12px; + } } } + + .tip { + margin-top: -4px; + margin-bottom: 24px; + color: rgba(0, 0, 0, 0.45); + font-size: 12px; + line-height: 1.5; + } + + .footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + } } diff --git a/src/components/HelloWorld.module.less b/src/components/HelloWorld.module.less index eaab200..32d823a 100644 --- a/src/components/HelloWorld.module.less +++ b/src/components/HelloWorld.module.less @@ -1,16 +1,16 @@ -.container { +.helloWorldMain { text-align: center; padding: 16px; -} -.title { - color: #1677ff; -} + .title { + color: #1677ff; + } -.desc { - code { - background: #f5f5f5; - padding: 2px 6px; - border-radius: 4px; + .desc { + code { + background: #f5f5f5; + padding: 2px 6px; + border-radius: 4px; + } } } diff --git a/src/components/HelloWorld.tsx b/src/components/HelloWorld.tsx index 1b59629..b7fd3bb 100644 --- a/src/components/HelloWorld.tsx +++ b/src/components/HelloWorld.tsx @@ -14,7 +14,7 @@ export default defineComponent({ }, setup(props) { return () => ( -
+

{props.msg}

这是一个公共组件示例,采用 TSX 写法。你可以在 diff --git a/src/components/index.ts b/src/components/index.ts index fdb9b81..cfe65a6 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,3 +4,4 @@ */ export { default as HelloWorld } from './HelloWorld'; export { default as ChangePasswordModal } from './ChangePasswordModal'; +export { default as PageTabs } from './pageTabs/PageTabs'; diff --git a/src/components/pageTabs/PageTabs.module.less b/src/components/pageTabs/PageTabs.module.less new file mode 100644 index 0000000..fe8a93d --- /dev/null +++ b/src/components/pageTabs/PageTabs.module.less @@ -0,0 +1,164 @@ +@primary: #1677ff; + +.pageTabsMain { + display: flex; + align-items: center; + width: 100%; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + + &::-webkit-scrollbar { + height: 0; + } + + .tabItem { + position: relative; + display: flex; + align-items: center; + height: 100%; + user-select: none; + transition: all 0.15s ease; + padding: 0 8px 0 14px; + + &:global(:not(.dragging)) { + cursor: pointer; + } + + &:not(.tabItemActive) { + &:hover { + > .divider { + opacity: 0; + } + + & + .tabItem { + > .divider { + opacity: 0; + } + } + } + } + + &.tabItemActive { + z-index: 2; + + & + .tabItem { + > .divider { + opacity: 0 !important; + } + } + + .tabTitle { + color: @primary; + font-weight: 500; + } + + .tabBg { + &.tabBgActive { + .tabBgInner { + background-color: fade(@primary, 8%); + } + + .tabBgCurveBefore, + .tabBgCurveAfter { + fill: fade(@primary, 8%); + } + } + } + } + + &:not(.tabItemActive) { + .tabTitleCon:hover { + background-color: rgba(0, 0, 0, 0.06); + } + } + } + + .divider { + position: absolute; + left: 7px; + top: 50%; + z-index: 0; + width: 1px; + height: 16px; + background-color: #e8e8e8; + transform: translateY(-50%); + transition: opacity 0.15s; + } + + .tabBg { + position: absolute; + top: 0; + left: 0; + z-index: -1; + width: 100%; + height: 100%; + padding: 0 calc(7px - 1px); + transition: opacity 0.15s; + } + + .tabBgInner { + height: 100%; + border-radius: 7px 7px 0 0; + transition: + background-color 0.15s, + margin 0.15s, + border-radius 0.15s; + } + + .tabBgCurveBefore { + position: absolute; + left: -1px; + bottom: 0; + fill: transparent; + transition: fill 0.15s; + } + + .tabBgCurveAfter { + position: absolute; + right: -1px; + bottom: 0; + fill: transparent; + transition: fill 0.15s; + } + + .tabClose { + flex-shrink: 0; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + text-align: center; + margin-right: 12px; + color: rgba(0, 0, 0, 0.45); + border-radius: 50%; + cursor: pointer; + transition: all 0.15s; + + &:hover { + color: rgba(0, 0, 0, 0.85); + background-color: rgba(0, 0, 0, 0.06); + } + } + + .tabTitleCon { + height: 28px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 4px; + } + + .tabTitle { + position: relative; + z-index: 2; + flex: 1; + min-width: 0; + padding: 2px 8px; + font-size: 13px; + color: rgba(0, 0, 0, 0.65); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/src/components/pageTabs/PageTabs.tsx b/src/components/pageTabs/PageTabs.tsx new file mode 100644 index 0000000..fa0d8df --- /dev/null +++ b/src/components/pageTabs/PageTabs.tsx @@ -0,0 +1,91 @@ +import { defineComponent, type PropType } from 'vue'; +import { message } from 'ant-design-vue'; +import type { TabView } from '@/types'; +import styles from './PageTabs.module.less'; + +/** + * Chrome 风格的页面标签栏组件 + * 参考 VBEN tabs-chrome 的视觉设计: + * - 标签重叠排列(负 margin) + * - 激活态:圆角背景 + SVG 底部曲线 + * - hover 态:浅灰背景 + * - 标签间分隔线 + * - 关闭按钮 + */ +const PageTabs = defineComponent({ + name: 'PageTabs', + props: { + /** 标签列表 */ + tabs: { type: Array as PropType, required: true }, + /** 当前激活 key */ + activeKey: { type: String, required: true }, + /** 关闭标签 */ + onClose: { type: Function as PropType<(key: string) => void>, default: undefined }, + /** 切换标签 */ + onChange: { type: Function as PropType<(key: string) => void>, default: undefined }, + }, + setup(props) { + const handleClick = (key: string) => { + if (key !== props.activeKey && props.onChange) { + props.onChange(key); + } + }; + + const handleClose = (key: string, e: Event) => { + e.stopPropagation(); + if (props.tabs.length <= 1) { + message.warning('至少保留一个标签'); + return; + } + if (props.onClose) { + props.onClose(key); + } + }; + + return () => ( +

+ {props.tabs.map((tab, i) => { + const isActive = tab.path === props.activeKey; + return ( +
handleClick(tab.path)} + > + {/* 分隔线:第一个标签 / 激活标签 / 上一个就是激活的 → 不显示 */} +
+ + {/* Chrome 风格背景:激活态 */} +
+
+ {/* 左下曲线 */} + + + + {/* 右下曲线 */} + + + +
+ +
+ {/* 标签文字(flex 子元素,与关闭按钮同行) */} + {tab.title} +
+ + {/* 关闭按钮(flex 子元素,不参与 shrink) */} + handleClose(tab.path, e)}> + ✕ + +
+ ); + })} +
+ ); + }, +}); + +export default PageTabs; diff --git a/src/config/fallbackRoutes.ts b/src/config/fallbackRoutes.ts index 9282088..be5d1c2 100644 --- a/src/config/fallbackRoutes.ts +++ b/src/config/fallbackRoutes.ts @@ -54,30 +54,35 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [ name: '赛事列表', path: 'events/list', component: 'events/list', + componentName: 'EventList', }, { id: 'events_orders', name: '订单管理', path: 'events/orders', component: 'events/orders', + componentName: 'EventOrders', }, { id: 'events_users', name: '用户列表', path: 'events/users', component: 'events/users', + componentName: 'EventUsers', }, { id: 'events_banner', name: 'Banner配置', path: 'events/banner', component: 'events/banner', + componentName: 'EventBanner', }, { id: 'events_logs', name: '赛事操作日志', path: 'events/logs', component: 'events/logs', + componentName: 'EventLogs', }, ], }, @@ -92,24 +97,28 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [ name: '提现申请', path: 'finance/withdraw', component: 'finance/withdraw', + componentName: 'FinanceWithdraw', }, { id: 'finance_wallet', name: '用户钱包', path: 'finance/wallet', component: 'finance/wallet', + componentName: 'FinanceWallet', }, { id: 'finance_payments', name: '支付流水', path: 'finance/payments', component: 'finance/payments', + componentName: 'FinancePayments', }, { id: 'finance_reports', name: '账务报表', path: 'finance/reports', component: 'finance/reports', + componentName: 'FinanceReports', }, ], }, @@ -124,18 +133,21 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [ name: '用户管理', path: 'system/users', component: 'system/users', + componentName: 'SystemUsers', }, { id: 'system_roles', name: '角色权限', path: 'system/roles', component: 'system/roles', + componentName: 'SystemRoles', }, { id: 'system_logs', name: '操作日志', path: 'system/logs', component: 'system/logs', + componentName: 'SystemLogs', }, ], }, diff --git a/src/layouts/BasicLayout.module.less b/src/layouts/BasicLayout.module.less index 3b182a3..910da44 100644 --- a/src/layouts/BasicLayout.module.less +++ b/src/layouts/BasicLayout.module.less @@ -1,115 +1,128 @@ -.container { +.basicLayoutMain { min-height: 100vh; height: 100vh; -} -.logo { - height: 56px; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - color: #1a1a1a; - background: #fff; -} - -.logoText { - font-size: 16px; - font-weight: 600; - white-space: nowrap; -} - -.trigger { - font-size: 18px; - cursor: pointer; - transition: color 0.3s; - padding: 0 12px; - display: inline-flex; - align-items: center; - - &:hover { - color: #1677ff; + .logo { + height: 56px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + color: #1a1a1a; + background: #fff; } -} -// 面包屑 -.breadcrumb { - flex: 1; - margin-left: 8px; - font-size: 14px; + .logoText { + font-size: 16px; + font-weight: 600; + white-space: nowrap; + } - :global { - .ant-breadcrumb-link, - .ant-breadcrumb-separator, - .ant-breadcrumb-item a { - color: rgba(0, 0, 0, 0.65); + .trigger { + font-size: 18px; + cursor: pointer; + transition: color 0.3s; + padding: 0 12px; + display: inline-flex; + align-items: center; + + &:hover { + color: #1677ff; } + } - .ant-breadcrumb-item:last-child { + .breadcrumb { + flex: 1; + margin-left: 8px; + font-size: 14px; + + :global { .ant-breadcrumb-link, .ant-breadcrumb-separator, - a { - color: rgba(0, 0, 0, 0.88); - font-weight: 500; - cursor: default; + .ant-breadcrumb-item a { + color: rgba(0, 0, 0, 0.65); + } + + .ant-breadcrumb-item:last-child { + .ant-breadcrumb-link, + .ant-breadcrumb-separator, + a { + color: rgba(0, 0, 0, 0.88); + font-weight: 500; + cursor: default; + } } } } -} -// Header 右侧:账号信息 + 退出图标 -.headerRight { - margin-left: auto; - display: flex; - gap: 10px; - align-items: center; -} - -.userPhone { - font-size: 14px; - color: rgba(0, 0, 0, 0.65); -} - -.logoutIcon { - width: 18px; - height: 18px; - opacity: 0.45; - cursor: pointer; - transition: opacity 0.3s; - - &:hover { - opacity: 1; + .headerRight { + margin-left: auto; + display: flex; + gap: 10px; + align-items: center; } -} -// 下拉菜单项:图标 + 文字 -.menuItem { - display: inline-flex; - align-items: center; - gap: 8px; - - :global(.anticon) { + .userPhone { font-size: 14px; + color: rgba(0, 0, 0, 0.65); + } + + .logoutIcon { + width: 18px; + height: 18px; + opacity: 0.45; + cursor: pointer; + transition: opacity 0.3s; + + &:hover { + opacity: 1; + } + } + + .menuItem { + display: inline-flex; + align-items: center; + gap: 8px; + + :global(.anticon) { + font-size: 14px; + } + } + + .menuItemDanger { + color: #ff4d4f; + } + + .content { + margin: 16px; + border-radius: 8px; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .main { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + } + + .headerRow { + padding: 0 16px; + width: 100%; + height: 48px; + display: flex; + align-items: center; + } + + .pageTabs { + width: 100%; + padding: 4px 16px 0 16px; + height: 42px; + background: #fff; + border-top: 1px solid #f0f0f0; } } - -.menuItemDanger { - color: #ff4d4f; -} - -.content { - margin: 16px; - border-radius: 8px; - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.main { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; -} diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index cea6533..d9d59cf 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -1,5 +1,6 @@ import { computed, defineComponent, h, ref } from 'vue'; import type { CSSProperties } from 'vue'; +import { KeepAlive } from 'vue'; import { useRouter, useRoute, RouterView } from 'vue-router'; import { Layout, Menu, Modal, Breadcrumb, Dropdown, message } from 'ant-design-vue'; import type { MenuProps } from 'ant-design-vue'; @@ -16,8 +17,9 @@ import { } from '@ant-design/icons-vue'; import { useMenuStore } from '@/stores/menuStore'; import { useUserStore } from '@/stores/userStore'; +import { useTabsStore } from '@/stores/tabsStore'; import { useState, useBreadcrumb, auth } from '@/hooks'; -import { ChangePasswordModal } from '@/components'; +import { ChangePasswordModal, PageTabs } from '@/components'; import logoutSvg from '@/assets/img/icon/logout.svg'; import styles from './BasicLayout.module.less'; @@ -25,11 +27,13 @@ const { Header, Sider, Content, Footer } = Layout; const headerStyle: CSSProperties = { background: '#fff', - padding: '0 16px', + padding: '0', display: 'flex', - alignItems: 'center', + flexDirection: 'column', + justifyContent: 'center', color: '#1a1a1a', boxShadow: '0 1px 4px rgba(0, 21, 41, 0.08)', + height: 'auto', }; const siderStyle: CSSProperties = { @@ -93,6 +97,7 @@ export default defineComponent({ const { menuItems } = useMenuStore(); const { phone } = useUserStore(); + const { tabs, cachedViews, removeTab } = useTabsStore(); const { breadcrumbs } = useBreadcrumb(); const selectedKeys = computed(() => [route.path]); @@ -107,6 +112,8 @@ export default defineComponent({ collapsed.value = !collapsed.value; }; + const activeTabKey = computed(() => route.path); + /** ===== 退出登录 ===== */ const handleLogout = () => { Modal.confirm({ @@ -154,8 +161,19 @@ export default defineComponent({ } }; + /** ===== PageTabs 事件 ===== */ + const handleTabChange = (key: string) => { + if (key !== route.path) { + router.push(key); + } + }; + + const handleTabClose = (key: string) => { + removeTab(key); + }; + return () => ( - +
{collapsed.value ? '6羽' : '六羽赛事运营平台'} @@ -171,56 +189,89 @@ export default defineComponent({
- - {collapsed.value ? : } - + {/* + ── 顶部行:折叠按钮 + 面包屑 + 右侧账号信息 ── + 按需求把原来平铺在 Header 里的三块统一包到一个 row 容器内, + 形成「行容器 + tabs 容器」的两层结构 + */} +
+ + {collapsed.value ? : } + - {/* 面包屑:紧跟在折叠按钮之后 */} - - {breadcrumbs.value.map((item, index) => { - const isLast = index === breadcrumbs.value.length - 1; - return ( - - {isLast || !item.path ? ( - {item.title} - ) : ( - router.push(item.path as string)}>{item.title} - )} - - ); - })} - + {/* 面包屑:紧跟在折叠按钮之后 */} + + {breadcrumbs.value.map((item, index) => { + const isLast = index === breadcrumbs.value.length - 1; + return ( + + {isLast || !item.path ? ( + {item.title} + ) : ( + router.push(item.path as string)}>{item.title} + )} + + ); + })} + - {/* 右侧: 账号信息 + 退出 */} -
- {phone.value} - - {{ - default: () => , - overlay: () => ( - - - - - 修改密码 - - - - - - - 退出登录 - - - - ), - }} - + {/* 右侧: 账号信息 + 退出 */} +
+ {phone.value} + + {{ + default: () => , + overlay: () => ( + + + + + 修改密码 + + + + + + + 退出登录 + + + + ), + }} + +
+
+ + {/* + ── Tabs 行:Chrome 风格页面标签栏 ── + 数据来自 tabsStore,点击切换路由,关闭按钮走 store.removeTab + */} +
+
+ {/* + ── 路由出口 ── + 用 缓存已访问过的页面, + include 是组件 name 列表,所以页面组件需要在 defineComponent 中显式给出 name + */} - + ( + + + + ), + }} + />
六个羽友赛事运营平台 ©{new Date().getFullYear()}
diff --git a/src/pages/about/index.module.less b/src/pages/about/index.module.less index c52f57c..14af90d 100644 --- a/src/pages/about/index.module.less +++ b/src/pages/about/index.module.less @@ -1,20 +1,18 @@ -.container { - // 容器 -} +.aboutMain { + .title { + margin-top: 0; + margin-bottom: 24px; + font-size: 20px; + font-weight: 600; + } -.title { - margin-top: 0; - margin-bottom: 24px; - font-size: 20px; - font-weight: 600; -} + .subtitle { + margin: 16px 0 8px; + } -.subtitle { - margin: 16px 0 8px; -} - -.list { - margin: 0; - padding-left: 20px; - line-height: 2; + .list { + margin: 0; + padding-left: 20px; + line-height: 2; + } } diff --git a/src/pages/about/index.tsx b/src/pages/about/index.tsx index b4e7084..be3e1e1 100644 --- a/src/pages/about/index.tsx +++ b/src/pages/about/index.tsx @@ -23,7 +23,7 @@ export default defineComponent({ ]; return () => ( -
+

关于

CPMS 运营平台前端 PC 端。

diff --git a/src/pages/dashboard/index.module.less b/src/pages/dashboard/index.module.less index e816330..3ebc662 100644 --- a/src/pages/dashboard/index.module.less +++ b/src/pages/dashboard/index.module.less @@ -1,27 +1,25 @@ -.container { - // 容器 -} - -.title { - margin-top: 0; - margin-bottom: 24px; - font-size: 20px; - font-weight: 600; -} - -.panel { - margin-top: 16px; - - ul { - margin: 12px 0 0; - padding-left: 20px; - line-height: 2; +.dashboardMain { + .title { + margin-top: 0; + margin-bottom: 24px; + font-size: 20px; + font-weight: 600; } - code { - padding: 2px 8px; - background: #f5f5f5; - border-radius: 4px; - font-size: 13px; + .panel { + margin-top: 16px; + + ul { + margin: 12px 0 0; + padding-left: 20px; + line-height: 2; + } + + code { + padding: 2px 8px; + background: #f5f5f5; + border-radius: 4px; + font-size: 13px; + } } } diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index 489e275..4a93e13 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -23,7 +23,7 @@ export default defineComponent({ ]; return () => ( -
+

工作台

diff --git a/src/pages/events/banner/components/BannerFormModal.module.less b/src/pages/events/banner/components/BannerFormModal.module.less index 8d3a844..5259e15 100644 --- a/src/pages/events/banner/components/BannerFormModal.module.less +++ b/src/pages/events/banner/components/BannerFormModal.module.less @@ -1,133 +1,130 @@ -.form { - :global { - .ant-form-item { - margin-bottom: 16px; - } - .ant-form-item-explain-error { - font-size: 12px; +.bannerFormModalMain { + .form { + :global { + .ant-form-item { + margin-bottom: 16px; + } + .ant-form-item-explain-error { + font-size: 12px; + } } } -} -/* ===== 上传 ===== */ -.uploader { - position: relative; -} - -.uploadCard { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - height: 140px; - border: 1px dashed #d9d9d9; - border-radius: 6px; - background-color: #fafafa; - cursor: pointer; - transition: border-color 0.2s; - gap: 4px; - - &:hover { - border-color: #1677ff; + .uploader { + position: relative; } -} -.uploadCardError { - border-color: #ff4d4f; -} + .uploadCard { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 140px; + border: 1px dashed #d9d9d9; + border-radius: 6px; + background-color: #fafafa; + cursor: pointer; + transition: border-color 0.2s; + gap: 4px; -.uploadIcon { - font-size: 32px; - color: #999; - font-weight: 300; - line-height: 1; -} - -.uploadHint { - color: #666; - font-size: 14px; -} - -.uploadTip { - color: #999; - font-size: 12px; - text-align: center; - line-height: 1.6; -} - -/* ===== 预览 ===== */ -.previewWrapper { - position: relative; - width: 100%; - height: 140px; - border: 1px solid #d9d9d9; - border-radius: 6px; - overflow: hidden; - background-color: #f5f5f5; - - :global { - .ant-spin-nested-loading, - .ant-spin-container { - width: 100%; - height: 100%; + &:hover { + border-color: #1677ff; } } -} -.previewImg { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} + .uploadCardError { + border-color: #ff4d4f; + } -.uploadAgainBtn { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: rgba(0, 0, 0, 0.5); - color: #fff; - text-align: center; - padding: 4px 0; - cursor: pointer; - font-size: 12px; - user-select: none; - transition: background 0.2s; + .uploadIcon { + font-size: 32px; + color: #999; + font-weight: 300; + line-height: 1; + } - &:hover { - background: rgba(0, 0, 0, 0.65); + .uploadHint { + color: #666; + font-size: 14px; + } + + .uploadTip { + color: #999; + font-size: 12px; + text-align: center; + line-height: 1.6; + } + + .previewWrapper { + position: relative; + width: 100%; + height: 140px; + border: 1px solid #d9d9d9; + border-radius: 6px; + overflow: hidden; + background-color: #f5f5f5; + + :global { + .ant-spin-nested-loading, + .ant-spin-container { + width: 100%; + height: 100%; + } + } + } + + .previewImg { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + + .uploadAgainBtn { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.5); + color: #fff; + text-align: center; + padding: 4px 0; + cursor: pointer; + font-size: 12px; + user-select: none; + transition: background 0.2s; + + &:hover { + background: rgba(0, 0, 0, 0.65); + } + } + + .tip { + padding: 8px 12px; + margin-bottom: 16px; + background-color: #f0f5ff; + border: 1px solid #d6e4ff; + border-radius: 4px; + color: #1677ff; + font-size: 13px; + } + + .timeRow { + display: flex; + gap: 16px; + + .timeItem { + flex: 1; + } + } + + .footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + margin-top: 8px; } } - -/* ===== 提示 ===== */ -.tip { - padding: 8px 12px; - margin-bottom: 16px; - background-color: #f0f5ff; - border: 1px solid #d6e4ff; - border-radius: 4px; - color: #1677ff; - font-size: 13px; -} - -/* ===== 时间行(双列)===== */ -.timeRow { - display: flex; - gap: 16px; - - .timeItem { - flex: 1; - } -} - -/* ===== 底部按钮 ===== */ -.footer { - display: flex; - justify-content: flex-end; - gap: 8px; - padding-top: 16px; - border-top: 1px solid #f0f0f0; - margin-top: 8px; -} diff --git a/src/pages/events/banner/components/BannerFormModal.tsx b/src/pages/events/banner/components/BannerFormModal.tsx index d4f3feb..7630878 100644 --- a/src/pages/events/banner/components/BannerFormModal.tsx +++ b/src/pages/events/banner/components/BannerFormModal.tsx @@ -266,6 +266,7 @@ export default defineComponent({ destroyOnClose footer={null} centered + wrapClassName={styles.bannerFormModalMain} > {/* ===== Banner 标题 ===== */} diff --git a/src/pages/events/banner/index.module.less b/src/pages/events/banner/index.module.less index 822b766..d7c3c7d 100644 --- a/src/pages/events/banner/index.module.less +++ b/src/pages/events/banner/index.module.less @@ -1,14 +1,21 @@ -.tableBody { +.eventBannerMain { flex: 1; min-height: 0; - overflow: hidden; + display: flex; + flex-direction: column; - :global { - .ant-spin-nested-loading, - .ant-spin-container, - .ant-table, - .ant-table-container { - height: 100%; + .tableBody { + flex: 1; + min-height: 0; + overflow: hidden; + + :global { + .ant-spin-nested-loading, + .ant-spin-container, + .ant-table, + .ant-table-container { + height: 100%; + } } } } diff --git a/src/pages/events/banner/index.tsx b/src/pages/events/banner/index.tsx index 98cc70f..e54c205 100644 --- a/src/pages/events/banner/index.tsx +++ b/src/pages/events/banner/index.tsx @@ -15,6 +15,7 @@ import { useBannerModel, BANNER_STATUS_OPTIONS } from './model/useBannerModel'; import { useContainerSize } from '@/hooks'; import BannerFormModal from './components/BannerFormModal'; import styles from './index.module.less'; +import pageStyles from '@/assets/styles/pageLayout.module.less'; /** * bodyCell 渲染函数 @@ -193,9 +194,9 @@ export default defineComponent({ ]; return () => ( -
+
{/* ===== 筛选区 ===== */} -
+
{/* ===== 表格区 ===== */} -
-
- - {{ - bodyCell: (args: any) => - renderBodyCell({ - ...args, - renderJumpType, - renderStatus, - onEdit: handleEdit, - onToggleStatus: confirmToggleStatus, - onDelete: confirmDelete, - }), - }} -
-
+
+
+
+ + {{ + bodyCell: (args: any) => + renderBodyCell({ + ...args, + renderJumpType, + renderStatus, + onEdit: handleEdit, + onToggleStatus: confirmToggleStatus, + onDelete: confirmDelete, + }), + }} +
+
- {/* 独立分页,右下方 */} -
- `共 ${total} 条`} - onChange={handlePageChange} - onShowSizeChange={handlePageChange} - /> + {/* 独立分页,右下方 */} +
+ `共 ${total} 条`} + onChange={handlePageChange} + onShowSizeChange={handlePageChange} + /> +
diff --git a/src/pages/events/list/components/EventAuditModal.module.less b/src/pages/events/list/components/EventAuditModal.module.less new file mode 100644 index 0000000..cc88632 --- /dev/null +++ b/src/pages/events/list/components/EventAuditModal.module.less @@ -0,0 +1,61 @@ +.eventAuditModalMain { + .section { + margin-bottom: 24px; + + &:last-child { + margin-bottom: 0; + } + } + + .sectionTitle { + font-size: 14px; + font-weight: 600; + color: rgba(0, 0, 0, 0.85); + margin-bottom: 12px; + padding-left: 8px; + border-left: 3px solid #1677ff; + } + + .desc { + :global(.ant-descriptions-item-label) { + width: 110px; + color: rgba(0, 0, 0, 0.65); + } + } + + .auditSection { + margin-top: 24px; + } + + .auditHeader { + display: flex; + align-items: center; + margin-bottom: 16px; + } + + .auditTitle { + font-size: 14px; + font-weight: 600; + color: rgba(0, 0, 0, 0.85); + padding-left: 8px; + border-left: 3px solid #1677ff; + } + + .auditForm { + border-top: 1px solid #f0f0f0; + padding-top: 16px; + } + + .auditField { + margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } + + :global(.ant-form-item-label) { + font-weight: 500; + color: rgba(0, 0, 0, 0.65); + } + } +} diff --git a/src/pages/events/list/components/EventAuditModal.tsx b/src/pages/events/list/components/EventAuditModal.tsx new file mode 100644 index 0000000..0803e0e --- /dev/null +++ b/src/pages/events/list/components/EventAuditModal.tsx @@ -0,0 +1,157 @@ +import { defineComponent, reactive } from 'vue'; +import { Modal, Descriptions, Image, Radio, Input, Form } from 'ant-design-vue'; +import styles from './EventAuditModal.module.less'; + +const { TextArea } = Input; + +interface EventAuditModalProps { + visible: boolean; + record: any; + onClose: () => void; + onSubmit?: (payload: { approved: boolean; comment: string }) => void; +} + +// ===== Mock 数据 ===== +const MOCK_DETAIL = { + name: '第二届全国地学羽毛球邀请赛', + eventTime: '2026.05.26 08:00 ~ 2026.05.31 22:00', + descr: + '赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍赛事介绍。', + registerTime: '2026.05.24 08:00 ~ 2026.05.26 22:00', + cancelDeadline: '2026.05.25 22:00', + region: '北京市 北京市 东城区', + address: '李宁体育馆', + venueNo: '李宁体育馆', + publicEvent: '在首页展示赛事', + needIdCard: '无需提供', + needIdCardImage: '无需提供', + createTime: '2026.05.26 08:00', + creator: '张三', + status: '待审核', + enrolledCount: 100, + cancelledCount: 1, + maxCount: 1000, + crossedCount: 50, + completedCount: 50, + cover: 'https://picsum.photos/seed/eventcover/600/300', +}; + +/** + * 赛事审核弹窗 + * 包含:基础信息 + 封面 + 审核操作(通过/未通过 + 审核内容) + */ +export default defineComponent({ + name: 'EventAuditModal', + props: { + visible: { type: Boolean, default: false }, + record: { type: Object, default: () => ({}) }, + onClose: { type: Function, required: true }, + onSubmit: { type: Function, default: undefined }, + }, + setup(props: EventAuditModalProps) { + const detail = MOCK_DETAIL; + + // 审核表单 + const auditForm = reactive({ + approved: true, + comment: '', + }); + + const handleOk = () => { + if (!auditForm.comment.trim()) { + // 不通过时强制要求填写审核内容 + if (!auditForm.approved) { + // 这里交给校验,简化版本不做强制校验 + } + } + if (props.onSubmit) { + props.onSubmit({ approved: auditForm.approved, comment: auditForm.comment }); + } + }; + + return () => ( + + {/* ===== 基础信息 ===== */} +
+
基础信息
+ + {detail.name} + + {detail.eventTime} + + + + {detail.descr} + + + {detail.registerTime} + + {detail.cancelDeadline} + + + {detail.region} + {detail.address} + {detail.venueNo} + + {detail.publicEvent} + {detail.needIdCard} + {detail.needIdCardImage} + + {detail.createTime} + {detail.creator} + {detail.status} + {detail.enrolledCount} + + {detail.cancelledCount} + + + {detail.maxCount} + {detail.crossedCount} + {detail.completedCount} + +
+ + {/* ===== 封面 ===== */} +
+
封面
+ +
+ + {/* ===== 审核操作 ===== */} +
+
+ 审核操作 +
+ + + + 通过 + 未通过 + + + +