cleaning - 前端基础样式调整v1.0 (#2)
Some checks failed
Web UI CI/CD / build-and-deploy (push) Failing after 46m41s

## 描述

将 `cleaning` 开发分支合并到 `master`。

主要改动包括:
- 基础前端页面调整为初版设计

`cleaning` 分支中包含了多个开发过程的提交,本次合并计划 **使用 Squash 合并为一次提交**,以保持 `master` 分支历史清晰。

## 类型

- [ ] Bug 修复(非破坏性修改)
- [x] 新功能(非破坏性新增功能)
- [ ] 破坏性修改(修改会影响现有功能)
- [ ] 需要更新文档
- [x] 除非引入新的测试示例,否则不修改 `pnpm-lock.yaml`

Reviewed-on: http://124.221.55.225:3000/XW-AIOT/aiot-platform-ui/pulls/2
This commit is contained in:
lzh
2025-12-23 10:51:03 +08:00
parent 3c6a92b1d4
commit 58630196f7
54 changed files with 3235 additions and 396 deletions

View File

@@ -0,0 +1,66 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
interface Props {
/** 图表配置选项 */
options: any;
/** 图表透明度 */
opacity?: number;
/** 图表容器底部内边距 */
paddingBottom?: string;
}
const props = withDefaults(defineProps<Props>(), {
opacity: 0.6,
paddingBottom: '1.5rem',
});
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
// 初始化图表
function initChart() {
if (props.options) {
renderEcharts(props.options);
}
}
// 监听配置变化
watch(
() => props.options,
() => {
initChart();
},
{ deep: true },
);
// 窗口大小变化时调整图表
const handleResize = () => {
if (chartRef.value) {
chartRef.value.resize?.();
}
};
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
</script>
<template>
<div
class="absolute inset-x-0 bottom-0 top-0 z-0 overflow-visible rounded-lg"
:style="{ paddingBottom: paddingBottom }"
>
<EchartsUI ref="chartRef" :style="{ opacity: opacity }" class="h-full w-full" />
</div>
</template>

View File

@@ -0,0 +1,2 @@
export { default as BackgroundChart } from './BackgroundChart.vue';

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
const props = defineProps<{
class?: Record<string, boolean> | string | string[];
}>();
</script>
<template>
<div
class="glass-card glass-border glass-shadow glass-highlight hover:glass-shadow-hover rounded-[2rem] text-card-foreground transition-all duration-300 ease-out hover:-translate-y-1"
:class="[props.class]"
>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as GlassCard } from './GlassCard.vue';

View File

@@ -0,0 +1,25 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
</script>
<template>
<AuthPageLayout
:app-name="appName"
:logo="logo"
:logo-dark="logoDark"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
>
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>
</template>

View File

@@ -1,25 +1,170 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
import { LanguageToggle, LoginIllustration } from '@vben/layouts';
import { preferences, usePreferences } from '@vben/preferences';
const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
const { isDark } = usePreferences();
</script>
<template>
<AuthPageLayout
:app-name="appName"
:logo="logo"
:logo-dark="logoDark"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
<div
:class="[isDark ? 'dark' : '']"
class="relative flex h-screen w-full overflow-hidden font-sans"
>
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>
<!-- 橙色渐变背景 + SVG 贴图预留 -->
<div class="absolute inset-0 z-0 size-full">
<!-- 橙色渐变背景 - 从左到右依次变淡 -->
<div
class="to-[#FFA00A]/8 dark:to-[#FFA00A]/12 absolute inset-0 size-full bg-gradient-to-r from-[rgb(218,125,68)] via-[#FFA00A]/30 dark:from-[rgb(218,125,68)] dark:via-[#FFA00A]/40"
>
<!-- 浮动插图 - 左侧区域 -->
<div
class="absolute -left-[5%] top-1/2 hidden h-[48rem] w-[65%] -translate-y-1/2 lg:block"
>
<LoginIllustration :alt="appName" />
</div>
</div>
</div>
<!-- 左上角品牌标识 -->
<div
class="absolute left-4 top-4 z-20 flex items-center gap-3 lg:left-6 lg:top-6"
@click.prevent
>
<div class="flex items-center justify-center rounded-lg bg-white/90 backdrop-blur-md p-0.5 shadow-[0_2px_8px_rgba(0,0,0,0.12)] transition-all hover:bg-white/95 hover:shadow-[0_4px_12px_rgba(0,0,0,0.18)] lg:p-1">
<img v-if="logo" :src="logo" :alt="appName" class="size-10 lg:size-12" />
<span v-else class="text-xl text-[#FFA00A] lg:text-2xl">💡</span>
</div>
<span
class="relative top-[1px] text-xl font-semibold tracking-tight text-white drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)] lg:top-[2px] lg:text-2xl"
>
{{ appName }}
</span>
</div>
<!-- 右上角 Toolbar -->
<div
class="absolute right-2 top-4 z-20 flex items-center gap-1 rounded-3xl bg-accent px-3 py-1 lg:right-6 lg:top-6"
>
<LanguageToggle v-if="preferences.widget.languageToggle" />
<!-- <ThemeToggle v-if="preferences.widget.themeToggle" /> -->
</div>
<!-- 内容区域 -->
<div
class="relative z-10 flex w-full flex-1 items-center justify-center px-6 py-5 lg:justify-end lg:px-12"
>
<!-- 登录卡片 - 响应式居中/靠右 -->
<div class="w-full md:w-[480px] lg:mr-[10%]">
<div
class="relative overflow-hidden rounded-[2.5rem] bg-background p-8 shadow-[0_30px_60px_-15px_rgba(255,160,10,0.2)] dark:shadow-[0_30px_60px_-15px_rgba(255,160,10,0.3)]"
>
<!-- 登录表单 - RouterView 渲染实际的登录组件 -->
<div class="login-form-container">
<RouterView v-slot="{ Component, route }">
<Transition appear mode="out-in" name="fade">
<KeepAlive :include="['Login']">
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</Transition>
</RouterView>
</div>
<!-- 遇到问题 -->
<div class="mt-6 text-center">
<span class="text-xs text-muted-foreground">
{{ $t('authentication.contactSupport') }}
<a
href="#"
class="ml-1 font-bold text-[#FFA00A] hover:underline"
@click.prevent
>
{{ $t('authentication.support') }}
</a>
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 登录表单容器样式覆盖 */
:deep(.login-form-container) {
/* 标题靠左对齐 */
.mb-7,
.mb-7 h2,
.mb-7 p {
text-align: left !important;
}
/* 输入框样式 */
.vben-input,
.vben-input-password input {
@apply rounded-2xl border-slate-100 bg-slate-50 px-6 py-4 text-sm transition-all;
@apply focus:border-[#FFA00A]/30 focus:bg-white focus:ring-4 focus:ring-[#FFA00A]/10;
}
/* 深色模式输入框 */
.dark .vben-input,
.dark .vben-input-password input {
@apply border-slate-700 bg-slate-800;
@apply focus:border-[#FFA00A]/50 focus:bg-slate-900 focus:ring-[#FFA00A]/20;
}
/* 选择框样式 */
.vben-select .vben-input {
@apply rounded-2xl border-slate-100 bg-slate-50 px-6 py-4 text-sm;
}
.dark .vben-select .vben-input {
@apply border-slate-700 bg-slate-800;
}
/* 登录按钮样式 */
.vben-button[aria-label='login'] {
@apply w-full rounded-2xl bg-[#FFA00A] py-4 text-base font-bold tracking-wide text-white shadow-lg shadow-[#FFA00A]/30;
@apply transition-all hover:-translate-y-1 hover:bg-[#ff8c00] active:translate-y-0;
}
.dark .vben-button[aria-label='login'] {
@apply shadow-[#FFA00A]/40;
}
/* 记住我和忘记密码 */
.vben-checkbox label {
@apply text-xs text-slate-400 dark:text-slate-500;
}
.vben-link {
@apply text-xs text-slate-400 transition-colors hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-400;
}
}
/* 过渡动画 - 快速响应 */
.fade-enter-active {
transition:
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-leave-active {
transition:
opacity 0.15s cubic-bezier(0.4, 0, 1, 1),
transform 0.15s cubic-bezier(0.4, 0, 1, 1);
}
.fade-enter-from {
opacity: 0;
transform: translateY(8px);
}
.fade-leave-to {
opacity: 0;
transform: translateY(-8px);
}
</style>

View File

@@ -64,24 +64,24 @@ const menus = computed(() => [
icon: AntdProfileOutlined,
text: $t('ui.widgets.profile'),
},
{
handler: () => {
openWindow(VBEN_DOC_URL, {
target: '_blank',
});
},
icon: BookOpenText,
text: $t('ui.widgets.document'),
},
{
handler: () => {
openWindow(VBEN_GITHUB_URL, {
target: '_blank',
});
},
icon: SvgGithubIcon,
text: 'GitHub',
},
// {
// handler: () => {
// openWindow(VBEN_DOC_URL, {
// target: '_blank',
// });
// },
// icon: BookOpenText,
// text: $t('ui.widgets.document'),
// },
// {
// handler: () => {
// openWindow(VBEN_GITHUB_URL, {
// target: '_blank',
// });
// },
// icon: SvgGithubIcon,
// text: 'GitHub',
// },
{
handler: () => {
helpModalApi.open();

View File

@@ -20,6 +20,16 @@ export const overridesPreferences = defineOverridesPreferences({
},
copyright: {
companyName: import.meta.env.VITE_APP_TITLE,
companySiteLink: 'https://gitee.com/yudaocode/yudao-ui-admin-vben',
companySiteLink: 'https://www.vs-cushwake.com/',
},
logo: {
/** Logo 图片地址 */
source: '/logo.svg',
/** Logo 图片适应方式 */
fit: 'contain',
},
theme: {
/** 主题模式:'light' 为浅色模式,'dark' 为深色模式,'auto' 为跟随系统 */
mode: 'light',
},
});

View File

@@ -21,16 +21,16 @@ const routes: RouteRecordRaw[] = [
title: $t('page.dashboard.workspace'),
},
},
{
name: 'Analytics',
path: '/analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'),
},
},
// {
// name: 'Analytics',
// path: '/analytics',
// component: () => import('#/views/dashboard/analytics/index.vue'),
// meta: {
// affixTab: true,
// icon: 'lucide:area-chart',
// title: $t('page.dashboard.analytics'),
// },
// },
],
},
{

View File

@@ -24,7 +24,7 @@ const accessStore = useAccessStore();
const tenantEnable = isTenantEnable();
const loading = ref(false);
const CODE_LENGTH = 4;
const CODE_LENGTH = 6;
const loginRef = ref();

View File

@@ -24,7 +24,7 @@ const router = useRouter();
const tenantEnable = isTenantEnable();
const loading = ref(false);
const CODE_LENGTH = 4;
const CODE_LENGTH = 6;
const forgetPasswordRef = ref();
/** 获取租户列表,并默认选中 */

View File

@@ -170,17 +170,99 @@ const formSchema = computed((): VbenFormSchema[] => {
</script>
<template>
<div>
<div class="custom-login-wrapper">
<AuthenticationLogin
ref="loginRef"
:form-schema="formSchema"
:loading="authStore.loginLoading"
:show-code-login="false"
:show-qrcode-login="false"
@submit="handleLogin"
@third-login="handleThirdLogin"
/>
<!-- 自定义其他登录方式 -->
<div class="mt-8">
<div class="relative mb-6 flex justify-center text-xs text-slate-400">
<span
class="relative z-10 bg-background px-3 font-medium dark:bg-slate-900"
>
{{ $t('authentication.otherLoginMethods') }}
</span>
<div class="absolute inset-0 flex items-center">
<div
class="w-full border-t border-slate-100 dark:border-slate-700"
></div>
</div>
</div>
<div class="flex justify-center gap-8">
<!-- 手机登录 -->
<button
class="flex size-12 items-center justify-center rounded-2xl border border-slate-100 bg-slate-50 text-xl text-slate-400 transition-all hover:scale-110 hover:bg-white hover:text-slate-600 hover:shadow-md dark:border-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700"
title="手机登录"
type="button"
@click="$router.push('/auth/code-login')"
>
<svg
class="size-5"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<rect height="18" rx="2" width="11" x="6.5" y="3" />
<path d="M12 18h.01" />
</svg>
</button>
<!-- 二维码登录 -->
<button
class="flex size-12 items-center justify-center rounded-2xl border border-slate-100 bg-slate-50 text-xl text-slate-400 transition-all hover:scale-110 hover:bg-white hover:text-slate-600 hover:shadow-md dark:border-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700"
title="二维码登录"
type="button"
@click="$router.push('/auth/qrcode-login')"
>
<svg
class="size-6"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<path d="M14 17h7" />
<path d="M17 14v7" />
<circle cx="6.5" cy="6.5" r="1" fill="currentColor" />
<circle cx="17.5" cy="6.5" r="1" fill="currentColor" />
<circle cx="6.5" cy="17.5" r="1" fill="currentColor" />
</svg>
</button>
<!-- 微信扫码登录 -->
<!-- <button
class="flex size-12 items-center justify-center rounded-2xl border border-slate-100 bg-slate-50 text-xl text-slate-400 transition-all hover:scale-110 hover:bg-white hover:text-[#46AF35] hover:shadow-md dark:border-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700"
title="微信扫码登录"
type="button"
@click="handleThirdLogin(30)"
>
<svg
class="size-6"
viewBox="0 0 1024 1024"
fill="currentColor"
>
<path d="M712.149333 352.234667c5.184 0 10.282667 0.064 15.381334 0.341333-26.944-146.837333-178.602667-259.2-361.642667-259.2-202.090667 0-365.888 137.002667-365.888 306.005333 0 99.093333 56.298667 187.178667 143.637333 243.093334l3.349334 2.133333-35.349334 110.72 132.266667-67.370667 6.229333 1.792a431.296 431.296 0 0 0 140.330667 14.848 237.141333 237.141333 0 0 1-11.626667-73.002666c0.021333-154.282667 149.290667-279.36 333.312-279.36z m-218.901333-107.968c28.373333 0 51.349333 22.250667 51.349333 49.728 0 27.456-22.976 49.770667-51.349333 49.770666-28.416 0-51.370667-22.293333-51.370667-49.770666-0.021333-27.498667 22.954667-49.728 51.370667-49.728z m-254.677333 99.477333c-28.394667 0-51.370667-22.293333-51.370667-49.770667 0-27.477333 22.997333-49.728 51.370667-49.728 28.394667 0 51.434667 22.250667 51.434666 49.728s-23.04 49.770667-51.434666 49.770667z" />
<path d="M405.76 633.408c0 142.805333 138.453333 258.56 309.162667 258.56a363.392 363.392 0 0 0 103.04-14.762667l111.701333 56.96-29.866667-93.589333 2.816-1.792c73.770667-47.232 121.344-121.621333 121.344-205.397333 0-142.741333-138.389333-258.496-309.056-258.496-170.688 0.042667-309.141333 115.776-309.141333 258.517333z m373.312-89.045333c0-23.168 19.413333-41.962667 43.370667-41.962667 24.021333 0 43.413333 18.816 43.413333 41.962667 0 23.253333-19.413333 42.090667-43.413333 42.090666-23.957333 0-43.370667-18.858667-43.370667-42.090666z m-215.146667 0c0-23.168 19.456-41.962667 43.413334-41.962667 23.978667 0 43.413333 18.816 43.413333 41.962667 0 23.253333-19.434667 42.090667-43.413333 42.090666-23.957333 0-43.413333-18.858667-43.413334-42.090666z" />
</svg>
</button> -->
</div>
</div>
<Verification
ref="verifyRef"
v-if="captchaEnable"
ref="verifyRef"
:captcha-type="captchaType"
:check-captcha-api="checkCaptcha"
:get-captcha-api="getCaptcha"
@@ -190,3 +272,10 @@ const formSchema = computed((): VbenFormSchema[] => {
/>
</div>
</template>
<style scoped>
/* 确保表单容器宽度 */
.custom-login-wrapper {
width: 100%;
}
</style>

View File

@@ -1,260 +1,608 @@
<script lang="ts" setup>
import type {
WorkbenchProjectItem,
WorkbenchQuickNavItem,
WorkbenchTodoItem,
WorkbenchTrendItem,
} from '@vben/common-ui';
import { computed, ref } from 'vue';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { GlassCard } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Button, Dropdown, message, Modal, Tag } from 'ant-design-vue';
import { BackgroundChart } from '../../../components/background-chart';
import { createBackgroundChartOptions } from './utils/chart-options';
import {
AnalysisChartCard,
WorkbenchHeader,
WorkbenchProject,
WorkbenchQuickNav,
WorkbenchTodo,
WorkbenchTrends,
} from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
aiCardClasses,
buttonClasses,
cardContentClasses,
modalClasses,
statCardClasses,
taskListClasses,
textShadowClasses,
} from './utils/styles';
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
// 机器人图片路径
const robotImage = '/images/Image_robot.png';
const userStore = useUserStore();
// --- 类型定义 ---
interface Task {
id: number;
title: string;
location: string;
priority: 'P0' | 'P1' | 'P2';
status?: 'completed' | 'pending';
createTime?: string;
assignee?: string;
}
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
// 例如url: /dashboard/workspace
const projectItems: WorkbenchProjectItem[] = [
{
color: '#6DB33F',
content: 'github.com/YunaiV/ruoyi-vue-pro',
date: '2025-01-02',
group: 'Spring Boot 单体架构',
icon: 'simple-icons:springboot',
title: 'ruoyi-vue-pro',
url: 'https://github.com/YunaiV/ruoyi-vue-pro',
},
{
color: '#409EFF',
content: 'github.com/yudaocode/yudao-ui-admin-vue3',
date: '2025-02-03',
group: 'Vue3 + element-plus 管理后台',
icon: 'ep:element-plus',
title: 'yudao-ui-admin-vue3',
url: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
},
{
color: '#ff4d4f',
content: 'github.com/yudaocode/yudao-mall-uniapp',
date: '2025-03-04',
group: 'Vue3 + uniapp 商城手机端',
icon: 'icon-park-outline:mall-bag',
title: 'yudao-mall-uniapp',
url: 'https://github.com/yudaocode/yudao-mall-uniapp',
},
{
color: '#1890ff',
content: 'github.com/YunaiV/yudao-cloud',
date: '2025-04-05',
group: 'Spring Cloud 微服务架构',
icon: 'material-symbols:cloud-outline',
title: 'yudao-cloud',
url: 'https://github.com/YunaiV/yudao-cloud',
},
{
color: '#e18525',
content: 'github.com/yudaocode/yudao-ui-admin-vben',
date: '2025-05-06',
group: 'Vue3 + vben5(antd) 管理后台',
icon: 'devicon:antdesign',
title: 'yudao-ui-admin-vben',
url: 'https://github.com/yudaocode/yudao-ui-admin-vben',
},
{
color: '#2979ff',
content: 'github.com/yudaocode/yudao-ui-admin-uniapp',
date: '2025-06-01',
group: 'Vue3 + uniapp 管理手机端',
icon: 'ant-design:mobile',
title: 'yudao-ui-admin-uniapp',
url: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
},
];
interface StatItem {
label: string;
value: string;
icon: string;
color: string;
}
// 同样,这里的 url 也可以使用以 http 开头的外部链接
const quickNavItems: WorkbenchQuickNavItem[] = [
// --- 响应式数据 ---
const urgentTasks = ref<Task[]>([
{
color: '#1fdaca',
icon: 'ion:home-outline',
title: '首页',
url: '/',
id: 1,
title: '大堂地面湿滑',
location: '1F 大堂',
priority: 'P0',
status: 'pending',
createTime: '09:15',
assignee: '张工',
},
{
color: '#ff6b6b',
icon: 'lucide:shopping-bag',
title: '商城中心',
url: '/mall',
id: 2,
title: '人员超限 (>200)',
location: '3F 洗手间',
priority: 'P0',
status: 'pending',
createTime: '09:30',
assignee: '李工',
},
{
color: '#7c3aed',
icon: 'tabler:ai',
title: 'AI 大模型',
url: '/ai',
id: 3,
title: '补充纸巾',
location: '2F 贵宾室',
priority: 'P1',
status: 'pending',
createTime: '10:05',
assignee: '王工',
},
{
color: '#3fb27f',
icon: 'simple-icons:erpnext',
title: 'ERP 系统',
url: '/erp',
id: 4,
title: '传感器离线',
location: 'B1 停车场',
priority: 'P1',
status: 'pending',
createTime: '10:20',
assignee: '赵工',
},
{
color: '#4daf1bc9',
icon: 'simple-icons:civicrm',
title: 'CRM 系统',
url: '/crm',
id: 5,
title: '垃圾桶满溢',
location: '1F 出口闸机',
priority: 'P2',
status: 'pending',
createTime: '10:45',
assignee: '钱工',
},
{
color: '#1a73e8',
icon: 'fa-solid:hdd',
title: 'IoT 物联网',
url: '/iot',
},
];
const todoItems = ref<WorkbenchTodoItem[]>([
{
completed: false,
content: `系统支持 JDK 8/17/21Vue 2/3`,
date: '2024-07-15 09:30:00',
title: '技术兼容性',
id: 6,
title: '通道受阻',
location: '3F 楼梯间',
priority: 'P1',
status: 'pending',
createTime: '11:00',
assignee: '孙工',
},
{
completed: false,
content: `后端提供 Spring Boot 2.7/3.2 + Cloud 双架构`,
date: '2024-08-30 14:20:00',
title: '架构灵活性',
},
{
completed: false,
content: `全部开源,个人与企业可 100% 直接使用,无需授权`,
date: '2024-07-25 16:45:00',
title: '开源免授权',
},
{
completed: false,
content: `国内使用最广泛的快速开发平台,远超 10w+ 企业使用`,
date: '2024-07-10 11:15:00',
title: '广泛企业认可',
id: 7,
title: '灯光闪烁',
location: '2F 走廊',
priority: 'P2',
status: 'pending',
createTime: '11:15',
assignee: '周工',
},
]);
const trendItems: WorkbenchTrendItem[] = [
const flowData = [
{ time: '09:00', value: 120 },
{ time: '10:00', value: 180 },
{ time: '11:00', value: 250 },
{ time: '12:00', value: 190 },
{ time: '13:00', value: 150 },
{ time: '14:00', value: 210 },
{ time: '15:00', value: 310 },
{ time: '16:00', value: 280 },
{ time: '17:00', value: 350 },
{ time: '18:00', value: 200 },
];
const workOrderTrend = [
{ time: '09:00', value: 5 },
{ time: '10:00', value: 12 },
{ time: '11:00', value: 8 },
{ time: '12:00', value: 15 },
{ time: '13:00', value: 24 },
{ time: '14:00', value: 18 },
{ time: '15:00', value: 28 },
{ time: '16:00', value: 22 },
{ time: '17:00', value: 14 },
];
const stats: StatItem[] = [
{
avatar: 'svg:avatar-1',
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
date: '刚刚',
title: '威廉',
label: '在岗人员',
value: '12/15',
icon: 'ant-design:user-outlined',
color: 'indigo',
},
{
avatar: 'svg:avatar-2',
content: `关注了 <a>威廉</a> `,
date: '1个小时前',
title: '艾文',
label: '待处理',
value: '5',
icon: 'ant-design:file-text-outlined',
color: 'orange',
},
{
avatar: 'svg:avatar-3',
content: `发布了 <a>个人动态</a> `,
date: '1天前',
title: '克里斯',
label: '平均响应',
value: '3.2m',
icon: 'ant-design:thunderbolt-outlined',
color: 'teal',
},
{
avatar: 'svg:avatar-4',
content: `发表文章 <a>如何编写一个Vite插件</a> `,
date: '2天前',
title: 'Vben',
},
{
avatar: 'svg:avatar-1',
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
date: '3天前',
title: '皮特',
},
{
avatar: 'svg:avatar-2',
content: `关闭了问题 <a>如何运行项目</a> `,
date: '1周前',
title: '杰克',
},
{
avatar: 'svg:avatar-3',
content: `发布了 <a>个人动态</a> `,
date: '1周前',
title: '威廉',
},
{
avatar: 'svg:avatar-4',
content: `推送了代码到 <a>Github</a>`,
date: '2021-04-01 20:00',
title: '威廉',
},
{
avatar: 'svg:avatar-4',
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
date: '2021-03-01 20:00',
title: 'Vben',
label: '满意度',
value: '85%',
icon: 'ant-design:bar-chart-outlined',
color: 'pink',
},
];
const router = useRouter();
// --- 筛选和交互状态 ---
const priorityFilter = ref<'all' | 'P0' | 'P1' | 'P2'>('all');
const showCompleted = ref(false);
const selectedTask = ref<null | Task>(null);
const taskDetailVisible = ref(false);
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
// This is a sample method, adjust according to the actual project requirements
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
if (nav.url?.startsWith('http')) {
openWindow(nav.url);
return;
// --- 计算属性 ---
const filteredTasks = computed(() => {
let tasks = urgentTasks.value;
// 按优先级筛选
if (priorityFilter.value !== 'all') {
tasks = tasks.filter((task) => task.priority === priorityFilter.value);
}
if (nav.url?.startsWith('/')) {
router.push(nav.url).catch((error) => {
console.error('Navigation failed:', error);
});
} else {
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
// 按状态筛选
if (!showCompleted.value) {
tasks = tasks.filter((task) => task.status !== 'completed');
}
return tasks;
});
// --- 图表配置 ---
const flowChartOptions = computed(() =>
createBackgroundChartOptions({
xAxisData: flowData.map((item) => item.time),
yAxisData: flowData.map((item) => item.value),
seriesName: '客流',
lineColor: '#FFA00A',
areaColor: [
'rgba(255, 160, 10, 0.25)',
'rgba(255, 160, 10, 0.15)',
'rgba(255, 160, 10, 0)',
],
yAxisFormatter: (value: number) => {
if (value >= 1000) {
return `${(value / 1000).toFixed(1)}k`;
}
return value.toString();
},
}),
);
const workOrderChartOptions = computed(() =>
createBackgroundChartOptions({
xAxisData: workOrderTrend.map((item) => item.time),
yAxisData: workOrderTrend.map((item) => item.value),
seriesName: '工单',
lineColor: '#3B82F6',
areaColor: [
'rgba(59, 130, 246, 0.25)',
'rgba(59, 130, 246, 0.15)',
'rgba(59, 130, 246, 0)',
],
}),
);
// --- 交互方法 ---
function handleTaskComplete(taskId: number) {
const task = urgentTasks.value.find((t) => t.id === taskId);
if (task) {
task.status = 'completed';
message.success(`任务"${task.title}"已完成`);
}
}
function handleTaskDetail(task: Task) {
selectedTask.value = task;
taskDetailVisible.value = true;
}
function handleTaskDelete(taskId: number) {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个任务吗?',
onOk() {
const index = urgentTasks.value.findIndex((t) => t.id === taskId);
if (index !== -1) {
urgentTasks.value.splice(index, 1);
message.success('任务已删除');
}
},
});
}
function handleRefreshStats() {
message.loading('刷新中...', 0.5);
// 这里可以添加实际的刷新逻辑
setTimeout(() => {
message.success('刷新成功');
}, 500);
}
function handleStatCardClick(stat: StatItem) {
message.info(`查看${stat.label}详情`);
// 这里可以添加跳转到详情页面的逻辑
}
function handleFilterChange(value: 'all' | 'P0' | 'P1' | 'P2') {
priorityFilter.value = value;
}
function getPriorityConfig(priority: string) {
const configs = {
P0: {
color: 'red',
bg: 'bg-red-50',
border: 'border-red-100',
text: 'text-red-600',
},
P1: {
color: 'orange',
bg: 'bg-orange-50',
border: 'border-orange-100',
text: 'text-orange-600',
},
P2: {
color: 'slate',
bg: 'bg-slate-50',
border: 'border-slate-100',
text: 'text-slate-600',
},
};
return configs[priority as keyof typeof configs] || configs.P2;
}
function getColorClass(color: string, type: 'bg' | 'text') {
const colors: Record<string, Record<string, string>> = {
indigo: { bg: 'bg-indigo-50', text: 'text-indigo-500' },
orange: { bg: 'bg-orange-50', text: 'text-orange-500' },
teal: { bg: 'bg-teal-50', text: 'text-teal-500' },
pink: { bg: 'bg-pink-50', text: 'text-pink-500' },
};
return colors[color]?.[type] || '';
}
</script>
<template>
<div class="p-5">
<WorkbenchHeader
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
>
<template #title>
早安, {{ userStore.userInfo?.nickname }}, 开始您一天的工作吧
</template>
<template #description> 今日晴20 - 32 </template>
</WorkbenchHeader>
<div class="animate-fade-in flex h-full w-full gap-5 px-5 py-5">
<!-- --- 左侧列 (60%) --- -->
<div class="flex h-full w-[60%] flex-none flex-col gap-5">
<!-- 卡片1: 实时客流监测 -->
<GlassCard class="relative flex h-[35%] flex-col overflow-hidden p-6">
<div class="relative z-10 flex items-start justify-between">
<div :class="cardContentClasses.container">
<h3 :class="cardContentClasses.title">实时客流监测</h3>
<div :class="cardContentClasses.numberContainer">
<span :class="cardContentClasses.largeNumber">2,450</span>
<Tag color="success" :class="`text-xs font-bold ${textShadowClasses.tag}`">+12%</Tag>
</div>
<p :class="cardContentClasses.description">预计高峰时间: 14:00</p>
</div>
<Button type="text" shape="circle" :class="buttonClasses.refresh" @click="handleRefreshStats">
<template #icon>
<IconifyIcon icon="ant-design:more-outlined" class="text-base" />
</template>
</Button>
</div>
<div class="mt-5 flex flex-col lg:flex-row">
<div class="mr-4 w-full lg:w-3/5">
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
</div>
<div class="w-full lg:w-2/5">
<WorkbenchQuickNav
:items="quickNavItems"
class="mt-5 lg:mt-0"
title="快捷导航"
@click="navTo"
/>
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
<AnalysisChartCard class="mt-5" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
<!-- 背景图表 -->
<BackgroundChart :options="flowChartOptions" :opacity="0.6" />
</GlassCard>
<!-- 卡片2: 紧急待办事项 -->
<GlassCard :class="taskListClasses.card">
<div :class="taskListClasses.header">
<h3 :class="taskListClasses.title">
<span :class="taskListClasses.titleDot"></span>
紧急待办事项
</h3>
<Dropdown>
<Button type="text" size="small" :class="taskListClasses.filterButton">
筛选
<IconifyIcon icon="ant-design:filter-outlined" class="ml-1 text-xs" />
</Button>
<template #overlay>
<div :class="taskListClasses.filterMenu">
<div
v-for="option in [
{ label: '全部', value: 'all' },
{ label: 'P0 紧急', value: 'P0' },
{ label: 'P1 重要', value: 'P1' },
{ label: 'P2 普通', value: 'P2' },
]"
:key="option.value"
:class="[
taskListClasses.filterMenuItem,
priorityFilter === option.value && taskListClasses.filterMenuItemActive,
]"
@click="handleFilterChange(option.value as any)"
>
{{ option.label }}
</div>
</div>
</template>
</Dropdown>
</div>
<div :class="taskListClasses.listContainer">
<div v-if="filteredTasks.length === 0" :class="taskListClasses.emptyState">
<div :class="taskListClasses.emptyContent">
<IconifyIcon icon="ant-design:check-circle-outlined" :class="taskListClasses.emptyIcon" />
<p>暂无待办任务</p>
</div>
</div>
<div v-else class="space-y-2">
<div
v-for="task in filteredTasks"
:key="task.id"
:class="taskListClasses.taskItem"
@click="handleTaskDetail(task)"
>
<div :class="taskListClasses.taskContent">
<div :class="taskListClasses.taskLeft">
<div
:class="[taskListClasses.taskIcon, getPriorityConfig(task.priority).bg]"
>
<IconifyIcon
icon="ant-design:clock-circle-outlined"
class="text-xs"
:class="[getPriorityConfig(task.priority).text]"
/>
</div>
<div :class="taskListClasses.taskInfo">
<div :class="taskListClasses.taskTitle">{{ task.title }}</div>
<div :class="taskListClasses.taskMeta">
<span>{{ task.location }}</span>
<span v-if="task.createTime">• {{ task.createTime }}</span>
<span v-if="task.assignee">• {{ task.assignee }}</span>
</div>
</div>
<Tag
:color="getPriorityConfig(task.priority).color"
:class="[taskListClasses.taskPriorityTag, getPriorityConfig(task.priority).border]"
>
{{ task.priority }}
</Tag>
</div>
<div :class="taskListClasses.taskActions">
<Button
type="text"
size="small"
shape="circle"
:class="[buttonClasses.circleIcon, buttonClasses.complete]"
@click.stop="handleTaskComplete(task.id)"
>
<template #icon>
<IconifyIcon icon="ant-design:check-circle-outlined" class="text-xs" />
</template>
</Button>
<Button
type="text"
size="small"
shape="circle"
:class="[buttonClasses.circleIcon, buttonClasses.view]"
@click.stop="handleTaskDetail(task)"
>
<template #icon>
<IconifyIcon icon="ant-design:eye-outlined" class="text-xs" />
</template>
</Button>
<Button
type="text"
size="small"
shape="circle"
:class="[buttonClasses.circleIcon, buttonClasses.delete]"
@click.stop="handleTaskDelete(task.id)"
>
<template #icon>
<IconifyIcon icon="ant-design:delete-outlined" class="text-xs" />
</template>
</Button>
</div>
</div>
</div>
</div>
</div>
</GlassCard>
</div>
<!-- --- 右侧列 (40%) --- -->
<div class="flex h-full w-[40%] flex-none flex-col gap-5" style="padding-right: 1.25rem;">
<!-- 卡片3: 工单趋势分析 -->
<GlassCard class="relative flex h-[35%] flex-col overflow-hidden p-6">
<div class="relative z-10 mb-2 flex items-start justify-between">
<div :class="cardContentClasses.container">
<h3 :class="cardContentClasses.title">工单趋势分析</h3>
<div :class="cardContentClasses.numberContainer">
<span :class="cardContentClasses.largeNumber">89</span>
<Tag color="processing" :class="`text-xs font-bold ${textShadowClasses.tag}`">+5 新增</Tag>
</div>
</div>
<Button type="text" size="small" :class="buttonClasses.viewAll" @click="message.info('查看全部工单')">
查看全部
</Button>
</div>
<!-- 背景图表 -->
<BackgroundChart :options="workOrderChartOptions" :opacity="0.6" />
</GlassCard>
<!-- 卡片4: 统计网格 + AI卡片 -->
<div class="flex min-h-0 flex-1 flex-col gap-4">
<!-- 2x2 统计网格 -->
<div :class="statCardClasses.container">
<GlassCard
v-for="(stat, idx) in stats"
:key="idx"
:class="statCardClasses.card"
@click="handleStatCardClick(stat)"
>
<div
:class="[
statCardClasses.iconContainer,
getColorClass(stat.color, 'bg'),
getColorClass(stat.color, 'text'),
]"
>
<IconifyIcon :icon="stat.icon" class="text-lg" />
</div>
<div :class="statCardClasses.content">
<p :class="statCardClasses.label">{{ stat.label }}</p>
<p :class="statCardClasses.value">{{ stat.value }}</p>
</div>
</GlassCard>
</div>
<!-- AI助手卡片 -->
<GlassCard :class="aiCardClasses.container">
<!-- 神经网络背景图案 -->
<div :class="aiCardClasses.background">
<svg width="100%" height="100%">
<pattern id="grid" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="1" fill="white" />
</pattern>
<rect width="100%" height="100%" fill="url(#grid)" />
<path
d="M0,50 Q100,20 200,80 T400,50"
fill="none"
stroke="white"
stroke-width="1"
stroke-opacity="0.3"
/>
</svg>
</div>
<!-- 左侧内容 -->
<div :class="aiCardClasses.leftContent">
<div class="mb-2 flex items-center gap-2">
<span :class="aiCardClasses.iconContainer">
<IconifyIcon icon="ant-design:robot-outlined" class="text-xs text-white" />
</span>
<span :class="aiCardClasses.label">AI Agent</span>
</div>
<h3 :class="aiCardClasses.title">需要我帮你做什么吗?</h3>
<Button type="primary" :class="aiCardClasses.button" @click="message.info('打开AI助手')">
使用AI助手
<IconifyIcon icon="ant-design:robot-outlined" :class="aiCardClasses.buttonIcon" />
</Button>
</div>
<!-- 右侧图片 -->
<div :class="aiCardClasses.imageContainer">
<img :src="robotImage" alt="AI Robot" :class="aiCardClasses.image" />
</div>
</GlassCard>
</div>
</div>
<!-- 任务详情模态框 -->
<Modal
v-model:open="taskDetailVisible"
title="任务详情"
:footer="null"
width="600px"
>
<div v-if="selectedTask" :class="modalClasses.field">
<div>
<div :class="modalClasses.label">任务标题</div>
<div :class="modalClasses.titleValue">{{ selectedTask.title }}</div>
</div>
<div>
<div :class="modalClasses.label">位置</div>
<div :class="modalClasses.value">{{ selectedTask.location }}</div>
</div>
<div>
<div :class="modalClasses.label">优先级</div>
<Tag :color="getPriorityConfig(selectedTask.priority).color" class="px-2 py-1 text-xs font-bold uppercase">
{{ selectedTask.priority }}
</Tag>
</div>
<div v-if="selectedTask.assignee">
<div :class="modalClasses.label">负责人</div>
<div :class="modalClasses.value">{{ selectedTask.assignee }}</div>
</div>
<div v-if="selectedTask.createTime">
<div :class="modalClasses.label">创建时间</div>
<div :class="modalClasses.value">{{ selectedTask.createTime }}</div>
</div>
<div :class="modalClasses.actions">
<Button
type="primary"
@click="
handleTaskComplete(selectedTask.id);
taskDetailVisible = false;
"
>
<template #icon>
<IconifyIcon icon="ant-design:check-circle-outlined" />
</template>
标记完成
</Button>
<Button
danger
@click="
handleTaskDelete(selectedTask.id);
taskDetailVisible = false;
"
>
<template #icon>
<IconifyIcon icon="ant-design:delete-outlined" />
</template>
删除任务
</Button>
</div>
</div>
</Modal>
</div>
</template>
<style scoped>
.animate-fade-in {
animation: fade-in 0.5s ease-in;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -0,0 +1,138 @@
/**
* 创建背景图表的通用配置
*/
export function createBackgroundChartOptions(config: {
xAxisData: string[];
yAxisData: number[];
seriesName: string;
lineColor: string;
areaColor: string[];
yAxisFormatter?: (value: number) => string;
}) {
const {
xAxisData,
yAxisData,
seriesName,
lineColor,
areaColor,
yAxisFormatter,
} = config;
return {
grid: {
left: '4%',
right: '2%',
top: '5%',
bottom: '15%',
containLabel: true,
},
xAxis: {
type: 'category' as const,
data: xAxisData,
show: true,
boundaryGap: false,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
show: true,
color: 'rgba(148, 163, 184, 0.6)',
fontSize: 10,
fontWeight: 400,
margin: 6,
interval: 0,
rotate: 0,
},
splitLine: {
show: false,
},
},
yAxis: {
type: 'value' as const,
show: true,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
show: true,
color: 'rgba(148, 163, 184, 0.6)',
fontSize: 10,
fontWeight: 400,
margin: 6,
formatter: yAxisFormatter,
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(148, 163, 184, 0.1)',
type: 'dashed',
width: 1,
},
},
},
series: [
{
name: seriesName,
type: 'line' as const,
data: yAxisData,
smooth: true,
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: areaColor[0] },
{ offset: 0.5, color: areaColor[1] },
{ offset: 1, color: areaColor[2] },
],
},
},
lineStyle: {
color: lineColor,
width: 4,
},
symbol: 'none',
symbolSize: 0,
},
],
tooltip: {
show: true,
trigger: 'axis',
axisPointer: {
type: 'line',
lineStyle: {
color: 'rgba(148, 163, 184, 0.3)',
width: 1,
},
},
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: 'rgba(148, 163, 184, 0.2)',
borderWidth: 1,
textStyle: {
color: '#475569',
fontSize: 12,
},
padding: [8, 12],
formatter: (params: any) => {
const param = params[0];
return `
<div style="margin-bottom: 4px; font-weight: 600;">${param.name}</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 10px; height: 10px; background-color: ${param.color}; border-radius: 50%;"></span>
<span>${param.seriesName || '数值'}: <strong>${param.value}</strong></span>
</div>
`;
},
},
};
}

View File

@@ -0,0 +1,156 @@
/**
* 文字阴影样式类名
*/
export const textShadowClasses = {
/** 标题阴影 */
title: 'drop-shadow-[0_2px_16px_rgba(255,255,255,0.95),0_0_8px_rgba(255,255,255,0.8)]',
/** 大数字阴影 */
largeNumber: 'drop-shadow-[0_2px_20px_rgba(255,255,255,0.98),0_0_12px_rgba(255,255,255,0.9)]',
/** 标签阴影 */
tag: 'drop-shadow-[0_1px_8px_rgba(255,255,255,0.9),0_0_4px_rgba(255,255,255,0.8)]',
/** 小文字阴影 */
smallText: 'drop-shadow-[0_1px_8px_rgba(255,255,255,0.85),0_0_4px_rgba(255,255,255,0.7)]',
};
/**
* 卡片内容样式类名
*/
export const cardContentClasses = {
/** 内容容器 */
container: 'relative',
/** 标题 */
title: `text-sm font-semibold uppercase tracking-wide text-slate-500 ${textShadowClasses.title}`,
/** 数字容器 */
numberContainer: 'mt-1 flex items-baseline gap-2',
/** 大数字 */
largeNumber: `text-4xl font-bold text-slate-800 ${textShadowClasses.largeNumber}`,
/** 描述文字 */
description: `mt-1 text-xs text-slate-400 ${textShadowClasses.smallText}`,
};
/**
* 任务列表样式类名
*/
export const taskListClasses = {
/** 任务卡片容器 */
card: 'flex min-h-0 flex-1 flex-col overflow-hidden p-0',
/** 任务列表头部 */
header: 'flex shrink-0 items-center justify-between border-b border-white/40 p-5',
/** 任务列表标题 */
title: 'flex items-center gap-2 font-bold text-slate-800',
/** 任务列表标题指示点 */
titleDot: 'h-2 w-2 animate-pulse rounded-full bg-red-500',
/** 筛选按钮 */
filterButton: 'text-xs font-medium text-slate-400 hover:text-orange-600',
/** 筛选下拉菜单 */
filterMenu: 'min-w-[120px] rounded-lg bg-white p-2 shadow-lg',
/** 筛选菜单项 */
filterMenuItem: 'cursor-pointer rounded px-3 py-2 text-sm hover:bg-gray-50',
/** 筛选菜单项激活状态 */
filterMenuItemActive: 'bg-orange-50 text-orange-600',
/** 任务列表容器 */
listContainer: 'flex-1 overflow-y-auto px-2',
/** 空状态容器 */
emptyState: 'flex h-full items-center justify-center text-slate-400',
/** 空状态内容 */
emptyContent: 'text-center',
/** 空状态图标 */
emptyIcon: 'mb-2 text-4xl',
/** 任务项容器 */
taskItem: 'group cursor-pointer rounded-lg border-b border-slate-50 p-3 transition-colors last:border-0 hover:bg-orange-50/40',
/** 任务项内容 */
taskContent: 'flex items-center justify-between',
/** 任务左侧内容 */
taskLeft: 'flex min-w-0 flex-1 items-center gap-3',
/** 任务图标容器 */
taskIcon: 'flex h-7 w-7 items-center justify-center rounded-lg shadow-sm transition-transform group-hover:scale-110',
/** 任务信息容器 */
taskInfo: 'min-w-0 flex-1',
/** 任务标题 */
taskTitle: 'truncate font-semibold text-slate-700',
/** 任务元信息 */
taskMeta: 'mt-1 flex items-center gap-3 text-xs text-slate-500',
/** 任务操作按钮组 */
taskActions: 'ml-2 flex items-center gap-1',
/** 任务优先级标签 */
taskPriorityTag: 'border px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide',
};
/**
* 按钮样式类名
*/
export const buttonClasses = {
/** 圆形图标按钮基础样式 */
circleIcon: 'h-7 w-7 bg-slate-50 text-slate-400',
/** 完成按钮 */
complete: 'hover:bg-green-500 hover:text-white',
/** 查看按钮 */
view: 'hover:bg-orange-500 hover:text-white',
/** 删除按钮 */
delete: 'hover:bg-red-500 hover:text-white',
/** 刷新按钮 */
refresh: 'text-slate-400 hover:text-orange-600',
/** 查看全部按钮 */
viewAll: 'rounded-lg bg-white/50 px-2 py-1 text-xs font-medium hover:text-blue-600',
};
/**
* 统计卡片样式类名
*/
export const statCardClasses = {
/** 统计卡片容器 */
container: 'grid shrink-0 grid-cols-2 gap-3',
/** 统计卡片 */
card: 'glass-card glass-border glass-shadow glass-highlight flex cursor-pointer items-center gap-3 rounded-[2rem] p-3 transition-colors hover:bg-white/60',
/** 图标容器 */
iconContainer: 'flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-lg',
/** 内容容器 */
content: 'min-w-0',
/** 标签文字 */
label: 'truncate text-[10px] font-bold uppercase text-slate-400',
/** 数值 */
value: 'mt-0.5 text-lg font-bold leading-none text-slate-800',
};
/**
* AI助手卡片样式类名
*/
export const aiCardClasses = {
/** AI卡片容器 */
container: 'group relative flex min-h-[140px] flex-1 items-center overflow-hidden border-0 !bg-gradient-to-br from-orange-400 to-amber-300 p-0 shadow-lg',
/** 背景图案容器 */
background: 'pointer-events-none absolute inset-0 opacity-20',
/** 左侧内容容器 */
leftContent: 'relative z-10 flex h-full w-[60%] flex-col justify-center py-4 pl-8 pr-2',
/** 图标容器 */
iconContainer: 'flex h-6 w-6 items-center justify-center rounded-lg bg-white/20 shadow-sm backdrop-blur-sm',
/** 标签文字 */
label: 'text-[10px] font-bold uppercase tracking-widest text-white/90',
/** 标题 */
title: 'mb-4 whitespace-nowrap text-lg font-bold leading-tight text-white drop-shadow-sm',
/** 按钮 */
button: 'flex w-fit items-center gap-2 rounded-full bg-white px-6 py-2.5 text-sm font-bold text-amber-600 shadow-md shadow-orange-900/10 transition-all hover:scale-105 hover:shadow-lg',
/** 按钮图标 */
buttonIcon: 'text-base text-amber-400 transition-transform group-hover:rotate-12',
/** 右侧图片容器 */
imageContainer: 'pointer-events-none absolute bottom-0 right-[-10px] top-0 z-20 flex w-[45%] items-center justify-center',
/** 图片 */
image: 'h-full w-full object-contain',
};
/**
* 模态框样式类名
*/
export const modalClasses = {
/** 字段容器 */
field: 'space-y-4',
/** 字段标签 */
label: 'mb-1 text-sm text-slate-400',
/** 字段值 */
value: 'text-base text-slate-700',
/** 标题值 */
titleValue: 'text-lg font-bold text-slate-800',
/** 操作按钮组 */
actions: 'flex gap-2 pt-4',
};