feat(ui): 新增“天天系统”ERP管理平台主页布局与控制台页面优化
- 为控制台首页添加页面标题动态设置 - 为应用中心页面添加页面标题动态设置 - 修改控制台布局,实现动态浏览器标签页标题更新 - 新增“天天系统”ERP管理平台主页,包含侧边栏导航、顶部栏及数据概览模块 - 实现主页搜索框、通知、语言和用户信息区域交互 - 添加欢迎区、快捷入口、最近使用应用列表及应用详情抽屉功能 - 支持小程序扫码弹窗展示和应用类型图标及颜色区分 - 优化页面样式,支持响应式布局及交互效果 - 更新Nuxt国际化重定向路径片段标识符以兼容新版本
This commit is contained in:
@@ -199,6 +199,11 @@ const currentPageTitle = computed(() => {
|
||||
return '控制台'
|
||||
})
|
||||
|
||||
// 动态设置浏览器标签页标题
|
||||
useHead(() => ({
|
||||
title: currentPageTitle.value
|
||||
}))
|
||||
|
||||
// 选中 key
|
||||
const selectedKeys = computed(() => {
|
||||
const path = route.path
|
||||
|
||||
@@ -221,6 +221,11 @@ import type { ShopOrder } from '@/api/shop/shopOrder/model'
|
||||
|
||||
definePageMeta({ layout: 'console' })
|
||||
|
||||
// 设置页面标题
|
||||
useHead({
|
||||
title: '应用中心'
|
||||
})
|
||||
|
||||
// ─── 用户信息 ────────────────────────────────────────────────
|
||||
const userId = ref<string | null>(import.meta.client ? localStorage.getItem('UserId') : null)
|
||||
const userIdNum = computed(() => (userId.value ? Number(userId.value) : 0))
|
||||
|
||||
@@ -154,6 +154,11 @@ const router = useRouter()
|
||||
|
||||
definePageMeta({ layout: 'console' })
|
||||
|
||||
// 设置页面标题
|
||||
useHead({
|
||||
title: '控制台'
|
||||
})
|
||||
|
||||
const userId = import.meta.client ? localStorage.getItem('UserId') : null
|
||||
|
||||
// 快捷入口配置
|
||||
|
||||
@@ -1,326 +1,496 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="relative overflow-hidden bg-gradient-to-b from-gray-950 to-gray-900 text-white">
|
||||
<div class="pointer-events-none absolute left-1/2 top-[-140px] h-[420px] w-[900px] -translate-x-1/2 rounded-full bg-green-500/20 blur-3xl" />
|
||||
<div class="mx-auto max-w-screen-xl px-4 py-16 sm:py-24 relative">
|
||||
<a-tag color="orange" class="mb-6">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-lg">🦞</span>
|
||||
{{ $t('home.heroTag') }}
|
||||
</span>
|
||||
</a-tag>
|
||||
|
||||
<a-typography-title :level="1" class="!text-white !mb-4">
|
||||
{{ $t('home.heroTitle') }}
|
||||
</a-typography-title>
|
||||
<a-typography-paragraph class="!text-gray-300 !text-lg !mb-8">
|
||||
{{ $t('home.heroDesc') }}
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-space size="middle" wrap>
|
||||
<a-button type="primary" size="large" @click="navigateTo('/contact')">{{ $t('home.bookDemo') }}</a-button>
|
||||
<a-button size="large" @click="navigateTo('/flow')">{{ $t('home.viewFlow') }}</a-button>
|
||||
<a-button size="large" @click="navigateTo('/deploy')">{{ $t('home.deploy') }}</a-button>
|
||||
</a-space>
|
||||
|
||||
<a-row class="mt-10" :gutter="[16, 16]">
|
||||
<a-col :xs="12" :md="6">
|
||||
<!-- <a-statistic title="平台定位" value="SaaS" />-->
|
||||
</a-col>
|
||||
<a-col :xs="12" :md="6">
|
||||
<!-- <a-statistic title="交付模式" value="私有化" />-->
|
||||
</a-col>
|
||||
<a-col :xs="12" :md="6">
|
||||
<!-- <a-statistic title="能力" value="模板/插件" />-->
|
||||
</a-col>
|
||||
<a-col :xs="12" :md="6">
|
||||
<!-- <a-statistic title="开通" value="自动化" />-->
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 平台介绍视频 -->
|
||||
<div class="mt-12 rounded-2xl overflow-hidden shadow-2xl border border-gray-700/50 bg-gray-900/50">
|
||||
<ClientOnly>
|
||||
<video
|
||||
class="w-full aspect-video"
|
||||
:src="videoSrc"
|
||||
poster="/images/video-poster.jpg"
|
||||
controls
|
||||
preload="metadata"
|
||||
playsinline
|
||||
@error="handleVideoError"
|
||||
>
|
||||
<p class="text-white text-center py-8">{{ $t('home.videoNotSupported') }}</p>
|
||||
</video>
|
||||
<template #fallback>
|
||||
<div class="w-full aspect-video bg-gray-800 flex items-center justify-center">
|
||||
<span class="text-gray-400">{{ $t('common.loading') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- OpenClaw AI 智能体生态 -->
|
||||
<section class="relative overflow-hidden bg-gradient-to-r from-orange-50 via-orange-100/50 to-orange-50">
|
||||
<div class="pointer-events-none absolute left-1/4 top-0 h-[300px] w-[300px] rounded-full bg-orange-200/50 blur-3xl" />
|
||||
<div class="pointer-events-none absolute right-1/4 bottom-0 h-[200px] w-[250px] rounded-full bg-red-200/50 blur-3xl" />
|
||||
<div class="mx-auto max-w-screen-xl px-4 py-16 relative">
|
||||
<div class="text-center mb-10">
|
||||
<div class="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 rounded-full text-white text-sm font-medium mb-4">
|
||||
<span class="text-xl">🦞</span>
|
||||
{{ $t('home.openclawTitle') }}
|
||||
<div class="flex min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<!-- 左侧导航 -->
|
||||
<aside class="sidebar w-64 fixed h-full text-white flex flex-col">
|
||||
<!-- Logo -->
|
||||
<div class="p-6 border-b border-white/10">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<BlockOutlined class="text-xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-bold text-lg">天天系统</h1>
|
||||
<p class="text-xs text-white/70">ERP 管理平台</p>
|
||||
</div>
|
||||
<a-typography-title :level="2" class="!text-gray-900 !mb-2">
|
||||
{{ $t('home.openclawSubtitle') }}
|
||||
</a-typography-title>
|
||||
<a-typography-paragraph class="!text-gray-600 !mb-0 !text-base">
|
||||
{{ $t('home.openclawDesc') }}
|
||||
</a-typography-paragraph>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[24, 24]" class="px-4">
|
||||
<a-col :xs="24" :md="8">
|
||||
<div class="text-center p-6 rounded-xl bg-white border border-orange-200 shadow-sm hover:shadow-md hover:border-orange-300 transition-all">
|
||||
<div class="text-4xl mb-4">🔗</div>
|
||||
<div class="text-gray-900 font-semibold mb-2">{{ $t('home.multiPlatform') }}</div>
|
||||
<div class="text-gray-600 text-sm">{{ $t('home.multiPlatformDesc') }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<div class="text-center p-6 rounded-xl bg-white border border-orange-200 shadow-sm hover:shadow-md hover:border-orange-300 transition-all">
|
||||
<div class="text-4xl mb-4">🧠</div>
|
||||
<div class="text-gray-900 font-semibold mb-2">{{ $t('home.multiModel') }}</div>
|
||||
<div class="text-gray-600 text-sm">{{ $t('home.multiModelDesc') }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<div class="text-center p-6 rounded-xl bg-white border border-orange-200 shadow-sm hover:shadow-md hover:border-orange-300 transition-all">
|
||||
<div class="text-4xl mb-4">⚡</div>
|
||||
<div class="text-gray-900 font-semibold mb-2">{{ $t('home.autoWorkflow') }}</div>
|
||||
<div class="text-gray-600 text-sm">{{ $t('home.autoWorkflowDesc') }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<div class="text-center mt-10">
|
||||
<a-space size="middle">
|
||||
<a-button type="primary" @click="navigateTo('/ai-agent')">
|
||||
<span class="flex items-center gap-2">
|
||||
<span>🦞</span> {{ $t('home.experienceAI') }}
|
||||
</span>
|
||||
</a-button>
|
||||
<a-button @click="navigateTo('/openclaw')">{{ $t('common.learnMore') }}</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mx-auto max-w-screen-xl px-4 py-14">
|
||||
<a-typography-title :level="2" class="!mb-2">{{ $t('home.productMatrix') }}</a-typography-title>
|
||||
<a-typography-paragraph class="!text-gray-500 !mb-8">
|
||||
{{ $t('home.productMatrixDesc') }}
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col v-for="p in products" :key="p.title" :xs="24" :md="12" :lg="8">
|
||||
<a-card :title="p.title">
|
||||
<template #extra>
|
||||
<a-tag v-if="p.recommend" color="green">{{ $t('home.recommend') }}</a-tag>
|
||||
</template>
|
||||
<a-typography-paragraph class="!text-gray-600">
|
||||
{{ p.desc }}
|
||||
</a-typography-paragraph>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a-tag v-for="t in p.tags" :key="t">{{ t }}</a-tag>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-between items-center">
|
||||
<a-tag color="green">{{ p.domain }}</a-tag>
|
||||
<a-button type="primary" @click="go(p.adminUrl)">{{ $t('common.goNow') }}</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<div class="mt-6">
|
||||
<a-button @click="navigateTo('/products')">{{ $t('home.viewAll') || $t('common.viewAll') }}</a-button>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="flex-1 py-6 px-3">
|
||||
<div class="space-y-1">
|
||||
<a href="#" class="sidebar-item active flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer" @click.prevent="activeMenu = 'home'">
|
||||
<HomeOutlined class="text-base" />
|
||||
<span>工作台</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer" @click.prevent="activeMenu = 'device'">
|
||||
<SettingOutlined class="text-base" />
|
||||
<span>设备管理</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer" @click.prevent="activeMenu = 'purchase'">
|
||||
<ShoppingCartOutlined class="text-base" />
|
||||
<span>采购管理</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer" @click.prevent="activeMenu = 'warehouse'">
|
||||
<InboxOutlined class="text-base" />
|
||||
<span>仓储物流</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer" @click.prevent="activeMenu = 'finance'">
|
||||
<WalletOutlined class="text-base" />
|
||||
<span>财务管理</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer" @click.prevent="activeMenu = 'hr'">
|
||||
<TeamOutlined class="text-base" />
|
||||
<span>人力资源</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer" @click.prevent="activeMenu = 'office'">
|
||||
<ProjectOutlined class="text-base" />
|
||||
<span>协同办公</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-6 border-t border-white/10">
|
||||
<p class="px-4 text-xs text-white/50 mb-3">个人</p>
|
||||
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
|
||||
<UserOutlined class="text-base" />
|
||||
<span>个人信息</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
|
||||
<SettingOutlined class="text-base" />
|
||||
<span>系统设置</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="p-4 border-t border-white/10">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<UserOutlined />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">管理员</p>
|
||||
<p class="text-xs text-white/70">超级管理员</p>
|
||||
</div>
|
||||
<button class="text-white/70 hover:text-white">
|
||||
<LogoutOutlined />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-gray-50">
|
||||
<div class="mx-auto max-w-screen-xl px-4 py-14">
|
||||
<a-typography-title :level="2" class="!mb-2">{{ $t('home.coreCapabilities') }}</a-typography-title>
|
||||
<a-typography-paragraph class="!text-gray-500 !mb-8">
|
||||
{{ $t('home.coreCapabilitiesDesc') }}
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col v-for="item in capabilities" :key="item.title" :xs="24" :md="12" :lg="8">
|
||||
<a-card :title="item.title">
|
||||
<template #extra>
|
||||
<a-tag color="green">{{ item.badge }}</a-tag>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="flex-1 ml-64">
|
||||
<!-- 顶部栏 -->
|
||||
<header class="bg-white/85 backdrop-blur-xl border-b border-white/20 sticky top-0 z-50 px-8 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- 搜索 -->
|
||||
<div class="relative w-96">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="🔍 搜索设备、订单、员工、文档..."
|
||||
class="w-full px-5 py-3 bg-gray-100 rounded-xl border-0 focus:ring-2 focus:ring-purple-500 focus:bg-white transition-all"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 右侧 -->
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- 全屏 -->
|
||||
<button class="text-gray-500 hover:text-purple-600 transition-colors" @click="toggleFullscreen">
|
||||
<ExpandOutlined class="text-lg" />
|
||||
</button>
|
||||
|
||||
<!-- 消息 -->
|
||||
<a-popover trigger="click">
|
||||
<template #content>
|
||||
<div class="w-64">
|
||||
<div class="font-medium mb-2">通知中心</div>
|
||||
<div class="text-sm text-gray-500">暂无新通知</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-typography-paragraph class="!mb-0 !text-gray-600">
|
||||
{{ item.desc }}
|
||||
</a-typography-paragraph>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="mx-auto max-w-screen-xl px-4 py-14">
|
||||
<a-row :gutter="[24, 24]">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-typography-title :level="2" class="!mb-2">{{ $t('home.payToActivate') }}</a-typography-title>
|
||||
<a-typography-paragraph class="!text-gray-600 !mb-6">
|
||||
{{ $t('home.payToActivateDesc') }}
|
||||
</a-typography-paragraph>
|
||||
<a-button type="primary" @click="navigateTo('/flow')">{{ $t('home.viewFullFlow') }}</a-button>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card>
|
||||
<a-steps direction="vertical" :current="-1" size="small">
|
||||
<a-step :title="$t('home.selectProduct')" :description="$t('home.selectProductDesc')" />
|
||||
<a-step :title="$t('home.placeOrder')" :description="$t('home.placeOrderDesc')" />
|
||||
<a-step :title="$t('home.createTenant')" :description="$t('home.createTenantDesc')" />
|
||||
<a-step :title="$t('home.initModules')" :description="$t('home.initModulesDesc')" />
|
||||
<a-step :title="$t('home.deliverOnline')" :description="$t('home.deliverOnlineDesc')" />
|
||||
</a-steps>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-gray-50">
|
||||
<div class="mx-auto max-w-screen-xl px-4 py-14">
|
||||
<a-typography-title :level="2" class="!mb-2">{{ $t('home.templatePluginEcosystem') }}</a-typography-title>
|
||||
<a-typography-paragraph class="!text-gray-500 !mb-8">
|
||||
{{ $t('home.templatePluginDesc') }}
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-tabs>
|
||||
<a-tab-pane key="template" :tab="$t('home.template')">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-card :title="$t('home.industryTemplate')">
|
||||
<a-typography-paragraph class="!mb-0 !text-gray-600">
|
||||
{{ $t('home.industryTemplateDesc') }}
|
||||
</a-typography-paragraph>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-card :title="$t('home.deliveryStandard')">
|
||||
<a-typography-paragraph class="!mb-0 !text-gray-600">
|
||||
{{ $t('home.deliveryStandardDesc') }}
|
||||
</a-typography-paragraph>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="plugin" :tab="$t('home.plugin')">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-card :title="$t('home.capabilityExtend')">
|
||||
<a-typography-paragraph class="!mb-0 !text-gray-600">
|
||||
{{ $t('home.capabilityExtendDesc') }}
|
||||
</a-typography-paragraph>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-card :title="$t('home.upgradeAuth')">
|
||||
<a-typography-paragraph class="!mb-0 !text-gray-600">
|
||||
{{ $t('home.upgradeAuthDesc') }}
|
||||
</a-typography-paragraph>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<div class="mt-6">
|
||||
<a-button @click="navigateTo('/market')">{{ $t('home.exploreMarket') }}</a-button>
|
||||
<button class="relative text-gray-500 hover:text-purple-600 transition-colors">
|
||||
<Badge :count="3" :offset="[-2, 2]">
|
||||
<BellOutlined class="text-lg" />
|
||||
</Badge>
|
||||
</button>
|
||||
</a-popover>
|
||||
|
||||
<!-- 语言 -->
|
||||
<button class="text-gray-500 hover:text-purple-600 transition-colors">
|
||||
<GlobalOutlined class="text-lg" />
|
||||
</button>
|
||||
|
||||
<!-- 用户 -->
|
||||
<div class="flex items-center gap-3 pl-6 border-l border-gray-200">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center text-white font-bold">
|
||||
A
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">Admin</p>
|
||||
<p class="text-xs text-gray-500">超级管理员</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="p-8">
|
||||
<!-- 欢迎区 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-3xl font-bold text-gray-800 mb-2">
|
||||
早上好,管理员!
|
||||
</h2>
|
||||
<p class="text-gray-500">今天是 {{ currentDate }},本周还有 <span class="text-purple-600 font-semibold">{{ remainingDays }}</span> 天工作日</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据概览 -->
|
||||
<div class="grid grid-cols-4 gap-6 mb-8">
|
||||
<div class="stat-card blue bg-white rounded-2xl p-6 card-hover shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
|
||||
<SettingOutlined class="text-blue-600 text-xl" />
|
||||
</div>
|
||||
<span class="text-green-500 text-sm font-medium flex items-center gap-1"><ArrowUpOutlined /> 12%</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">128</h3>
|
||||
<p class="text-gray-500 text-sm">设备总数</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card green bg-white rounded-2xl p-6 card-hover shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center">
|
||||
<AppstoreOutlined class="text-green-600 text-xl" />
|
||||
</div>
|
||||
<span class="text-green-500 text-sm font-medium flex items-center gap-1"><ArrowUpOutlined /> 8%</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">5,230</h3>
|
||||
<p class="text-gray-500 text-sm">库存物料</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card orange bg-white rounded-2xl p-6 card-hover shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center">
|
||||
<ClockCircleOutlined class="text-orange-600 text-xl" />
|
||||
</div>
|
||||
<span class="text-red-500 text-sm font-medium flex items-center gap-1"><ArrowUpOutlined /> 5</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">12</h3>
|
||||
<p class="text-gray-500 text-sm">待审批</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card purple bg-white rounded-2xl p-6 card-hover shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
|
||||
<LineChartOutlined class="text-purple-600 text-xl" />
|
||||
</div>
|
||||
<span class="text-green-500 text-sm font-medium flex items-center gap-1"><ArrowUpOutlined /> 15%</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">¥89.5<span class="text-lg">万</span></h3>
|
||||
<p class="text-gray-500 text-sm">本月支出</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间区域 -->
|
||||
<div class="grid grid-cols-3 gap-6 mb-8">
|
||||
<!-- 快捷入口 -->
|
||||
<div class="bg-white rounded-2xl p-6 card-hover shadow-sm">
|
||||
<h3 class="font-bold text-lg text-gray-800 mb-4 flex items-center gap-2">
|
||||
<BoltOutlined class="text-yellow-500" />
|
||||
快捷入口
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
v-for="shortcut in shortcuts"
|
||||
:key="shortcut.label"
|
||||
class="quick-btn flex items-center gap-2 px-4 py-3 bg-gray-50 rounded-xl text-gray-700 text-sm font-medium hover:bg-purple-500 hover:text-white transition-all"
|
||||
>
|
||||
<component :is="shortcut.icon" :style="{ color: shortcut.color }" />
|
||||
{{ shortcut.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待办事项 -->
|
||||
<div class="bg-white rounded-2xl p-6 card-hover shadow-sm col-span-2">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800 flex items-center gap-2">
|
||||
<CheckSquareOutlined class="text-red-500" />
|
||||
待办事项
|
||||
<span class="bg-red-100 text-red-600 px-2 py-0.5 rounded-full text-xs font-medium">5</span>
|
||||
</h3>
|
||||
<button class="text-purple-600 text-sm font-medium hover:underline">查看全部</button>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="todo in todos"
|
||||
:key="todo.id"
|
||||
class="todo-item flex items-center gap-4 p-3 rounded-xl cursor-pointer hover:bg-purple-50 transition-all"
|
||||
>
|
||||
<div :class="['w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0', todo.bgColor]">
|
||||
<component :is="todo.icon" :class="[todo.textColor]" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-medium text-gray-800">{{ todo.title }}</p>
|
||||
<p class="text-xs text-gray-500">{{ todo.subtitle }}</p>
|
||||
</div>
|
||||
<span :class="['text-xs font-medium', todo.timeColor]">{{ todo.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用模块 -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl text-gray-800">
|
||||
<AppstoreOutlined class="text-purple-500 mr-2" />
|
||||
应用模块
|
||||
</h3>
|
||||
<button class="text-purple-600 text-sm font-medium hover:underline">自定义模块</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-6 gap-4">
|
||||
<div
|
||||
v-for="app in apps"
|
||||
:key="app.name"
|
||||
class="app-card bg-white rounded-2xl p-6 flex flex-col items-center justify-center card-hover cursor-pointer shadow-sm"
|
||||
@click="navigateToApp(app.route)"
|
||||
>
|
||||
<div
|
||||
class="app-icon w-16 h-16 rounded-2xl flex items-center justify-center mb-4 shadow-lg"
|
||||
:style="{ background: app.gradient }"
|
||||
>
|
||||
<component :is="app.icon" class="text-white text-2xl" />
|
||||
</div>
|
||||
<h4 class="font-semibold text-gray-800 mb-1">{{ app.name }}</h4>
|
||||
<p class="text-xs text-gray-500">{{ app.stats }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部区域 -->
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<!-- 经营概览图表 -->
|
||||
<div class="bg-white rounded-2xl p-6 card-hover shadow-sm">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-lg text-gray-800 flex items-center gap-2">
|
||||
<BarChartOutlined class="text-blue-500" />
|
||||
经营概览
|
||||
</h3>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="period in ['本周', '本月', '本季']"
|
||||
:key="period"
|
||||
:class="['px-3 py-1 text-xs rounded-lg font-medium', activePeriod === period ? 'bg-purple-100 text-purple-600' : 'bg-gray-100 text-gray-600']"
|
||||
@click="activePeriod = period"
|
||||
>
|
||||
{{ period }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-48 flex items-end gap-4">
|
||||
<div
|
||||
v-for="(bar, index) in chartData"
|
||||
:key="index"
|
||||
class="flex-1 flex flex-col items-center"
|
||||
>
|
||||
<div
|
||||
class="w-full bg-gradient-to-t from-purple-500 to-purple-300 rounded-t-lg transition-all hover:opacity-80 cursor-pointer"
|
||||
:style="{ height: bar + '%' }"
|
||||
></div>
|
||||
<span class="text-xs text-gray-500 mt-2">{{ weekDays[index] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最新公告 -->
|
||||
<div class="bg-white rounded-2xl p-6 card-hover shadow-sm">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-lg text-gray-800 flex items-center gap-2">
|
||||
<SoundOutlined class="text-red-500" />
|
||||
最新公告
|
||||
</h3>
|
||||
<button class="text-purple-600 text-sm font-medium hover:underline">全部公告</button>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="notice in notices"
|
||||
:key="notice.id"
|
||||
:class="['p-4 rounded-xl border-l-4 cursor-pointer hover:scale-[1.02] transition-transform', notice.bgColor, notice.borderColor]"
|
||||
>
|
||||
<h4 class="font-medium text-gray-800 mb-1">{{ notice.title }}</h4>
|
||||
<div class="flex items-center gap-4 text-xs text-gray-500">
|
||||
<span><UserOutlined class="mr-1" />{{ notice.author }}</span>
|
||||
<span><ClockCircleOutlined class="mr-1" />{{ notice.date }}</span>
|
||||
<span><EyeOutlined class="mr-1" />{{ notice.views }} 阅读</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-white">
|
||||
<div class="mx-auto max-w-screen-xl px-4 py-12 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div class="text-xl font-semibold">{{ $t('home.ctaTitle') }}</div>
|
||||
<div class="mt-1 text-gray-500">{{ $t('home.ctaDesc') }}</div>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button size="large" @click="navigateTo('/products')">{{ $t('home.viewProducts') }}</a-button>
|
||||
<a-button type="primary" size="large" @click="navigateTo('/contact')">{{ $t('home.contactNow') }}</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePageSeo } from '@/composables/usePageSeo'
|
||||
import { productMatrix } from '@/config/products'
|
||||
const { t } = useI18n()
|
||||
import { ref, computed, h } from 'vue'
|
||||
import {
|
||||
HomeOutlined,
|
||||
SettingOutlined,
|
||||
ShoppingCartOutlined,
|
||||
InboxOutlined,
|
||||
WalletOutlined,
|
||||
TeamOutlined,
|
||||
ProjectOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
ExpandOutlined,
|
||||
BellOutlined,
|
||||
GlobalOutlined,
|
||||
ArrowUpOutlined,
|
||||
CheckSquareOutlined,
|
||||
BoltOutlined,
|
||||
AppstoreOutlined,
|
||||
BarChartOutlined,
|
||||
SoundOutlined,
|
||||
EyeOutlined,
|
||||
ClockCircleOutlined,
|
||||
LineChartOutlined,
|
||||
SearchOutlined,
|
||||
ShoppingOutlined,
|
||||
TruckOutlined,
|
||||
CalendarOutlined,
|
||||
DollarOutlined,
|
||||
BoxPlotOutlined,
|
||||
ToolOutlined,
|
||||
AuditOutlined,
|
||||
ExperimentOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
// 视频路径(使用 public 目录,可直接通过 URL 访问)
|
||||
const videoSrc = '/videos/websopy-intro.mp4'
|
||||
// 响应式数据
|
||||
const searchQuery = ref('')
|
||||
const activeMenu = ref('home')
|
||||
const activePeriod = ref('本周')
|
||||
|
||||
usePageSeo({
|
||||
title: t('pageTitle.homeTitle'),
|
||||
description: t('pageTitle.homeDescription')
|
||||
// 当前日期
|
||||
const currentDate = computed(() => {
|
||||
const now = new Date()
|
||||
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 星期${['日', '一', '二', '三', '四', '五', '六'][now.getDay()]}`
|
||||
})
|
||||
|
||||
const go = (url?: string) => {
|
||||
if (!url) return
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
if (import.meta.client) window.open(url, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
return navigateTo(url)
|
||||
}
|
||||
// 剩余工作日
|
||||
const remainingDays = computed(() => {
|
||||
const now = new Date()
|
||||
const day = now.getDay()
|
||||
if (day === 0 || day === 6) return 0
|
||||
return 5 - day
|
||||
})
|
||||
|
||||
// 视频加载错误处理
|
||||
const handleVideoError = (e: Event) => {
|
||||
console.error('视频加载失败:', e)
|
||||
}
|
||||
|
||||
const products = productMatrix
|
||||
|
||||
const capabilities = [
|
||||
{
|
||||
title: t('home.openclawAI'),
|
||||
badge: 'NEW',
|
||||
desc: t('home.openclawAIDesc')
|
||||
},
|
||||
{
|
||||
title: t('home.ragKnowledge'),
|
||||
badge: 'AI',
|
||||
desc: t('home.ragKnowledgeDesc')
|
||||
},
|
||||
{
|
||||
title: t('home.saasMultiTenant'),
|
||||
badge: '核心',
|
||||
desc: t('home.saasMultiTenantDesc')
|
||||
},
|
||||
{
|
||||
title: t('home.privateDeploy'),
|
||||
badge: '可选',
|
||||
desc: t('home.privateDeployDesc')
|
||||
},
|
||||
{
|
||||
title: t('home.templateMarket'),
|
||||
badge: '生态',
|
||||
desc: t('home.templateMarketDesc')
|
||||
},
|
||||
{
|
||||
title: t('home.autoActivate'),
|
||||
badge: '交付',
|
||||
desc: t('home.autoActivateDesc')
|
||||
}
|
||||
// 快捷入口
|
||||
const shortcuts = [
|
||||
{ label: '设备巡检', icon: SearchOutlined, color: '#667eea' },
|
||||
{ label: '采购申请', icon: ShoppingOutlined, color: '#11998e' },
|
||||
{ label: '入库登记', icon: TruckOutlined, color: '#f5576c' },
|
||||
{ label: '请假申请', icon: CalendarOutlined, color: '#fc8181' },
|
||||
{ label: '费用报销', icon: DollarOutlined, color: '#f6ad55' },
|
||||
{ label: '库存查询', icon: BoxPlotOutlined, color: '#63b3ed' },
|
||||
]
|
||||
|
||||
// 待办事项
|
||||
const todos = [
|
||||
{ id: 1, title: '审批 - 张三的报销单', subtitle: '差旅费 ¥2,580.00', time: '2小时前', icon: CheckSquareOutlined, bgColor: 'bg-red-100', textColor: 'text-red-600', timeColor: 'text-red-500' },
|
||||
{ id: 2, title: '巡检 - 3号车间设备待检', subtitle: '包含12台设备', time: '4小时前', icon: SearchOutlined, bgColor: 'bg-yellow-100', textColor: 'text-yellow-600', timeColor: 'text-yellow-600' },
|
||||
{ id: 3, title: '采购 - 办公用品采购单', subtitle: '待审批', time: '1天前', icon: ShoppingCartOutlined, bgColor: 'bg-blue-100', textColor: 'text-blue-600', timeColor: 'text-blue-600' },
|
||||
{ id: 4, title: '保养 - 空压机定期保养', subtitle: '例行保养', time: '2天前', icon: ToolOutlined, bgColor: 'bg-green-100', textColor: 'text-green-600', timeColor: 'text-green-600' },
|
||||
]
|
||||
|
||||
// 应用模块
|
||||
const apps = [
|
||||
{ name: '设备管理', icon: SettingOutlined, gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', stats: '128 设备', route: '/admin/device' },
|
||||
{ name: '采购管理', icon: ShoppingCartOutlined, gradient: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)', stats: '23 订单', route: '/admin/purchase' },
|
||||
{ name: '仓储物流', icon: InboxOutlined, gradient: 'linear-gradient(135deg, #f5576c 0%, #f093fb 100%)', stats: '5,230 物料', route: '/admin/warehouse' },
|
||||
{ name: '财务管理', icon: WalletOutlined, gradient: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)', stats: '¥89.5万', route: '/admin/finance' },
|
||||
{ name: '人力资源', icon: TeamOutlined, gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', stats: '56 员工', route: '/admin/hr' },
|
||||
{ name: '协同办公', icon: ProjectOutlined, gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)', stats: '8 流程', route: '/admin/office' },
|
||||
]
|
||||
|
||||
// 图表数据
|
||||
const chartData = [60, 75, 45, 90, 85, 40, 30]
|
||||
const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
|
||||
// 公告
|
||||
const notices = [
|
||||
{ id: 1, title: '关于清明节放假安排的通知', author: '人事行政部', date: '2026-04-01', views: '1,258', bgColor: 'bg-red-50', borderColor: 'border-red-500' },
|
||||
{ id: 2, title: '2026年第一季度财报公告', author: '财务部', date: '2026-03-28', views: '986', bgColor: 'bg-blue-50', borderColor: 'border-blue-500' },
|
||||
{ id: 3, title: '新版本系统功能更新说明', author: '技术部', date: '2026-03-25', views: '756', bgColor: 'bg-green-50', borderColor: 'border-green-500' },
|
||||
]
|
||||
|
||||
// 方法
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen()
|
||||
} else {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
const navigateToApp = (route: string) => {
|
||||
navigateTo(route)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.sidebar-item:hover,
|
||||
.sidebar-item.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.app-card {
|
||||
aspect-ratio: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.app-card:hover .app-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.quick-btn:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
</style>
|
||||
|
||||
597
app/pages/tiantian-system/index.vue
Normal file
597
app/pages/tiantian-system/index.vue
Normal file
@@ -0,0 +1,597 @@
|
||||
<template>
|
||||
<div class="console-home">
|
||||
<!-- 顶部欢迎区 -->
|
||||
<div class="welcome-section">
|
||||
<div class="welcome-content">
|
||||
<h1 class="welcome-title">欢迎回来 👋</h1>
|
||||
<p class="welcome-subtitle">管理您的应用、订单与账号,一站式控制台</p>
|
||||
</div>
|
||||
<div class="welcome-actions">
|
||||
<a-button type="primary" size="large" @click="navigateTo('/console/apps')">
|
||||
<template #icon><AppstoreOutlined /></template>
|
||||
进入应用中心
|
||||
</a-button>
|
||||
<a-button size="large" @click="navigateTo('/market')">
|
||||
<template #icon><ShopOutlined /></template>
|
||||
浏览应用商店
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">快捷入口</h3>
|
||||
</div>
|
||||
<div class="quick-cards-grid">
|
||||
<div
|
||||
v-for="card in quickCards"
|
||||
:key="card.to"
|
||||
class="quick-card"
|
||||
@click="navigateTo(card.to)"
|
||||
>
|
||||
<div class="quick-card-icon" :style="{ background: card.bg }">
|
||||
<component :is="card.icon" :style="{ fontSize: '22px', color: card.color }" />
|
||||
</div>
|
||||
<div class="quick-card-info">
|
||||
<div class="quick-card-label">{{ card.label }}</div>
|
||||
<div class="quick-card-desc">{{ card.desc }}</div>
|
||||
</div>
|
||||
<RightOutlined class="quick-card-arrow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近使用的应用 -->
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">最近使用</h3>
|
||||
<NuxtLink to="/console/apps">
|
||||
<a-button type="link">
|
||||
查看全部 <RightOutlined />
|
||||
</a-button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="recentAppsLoading" class="recent-apps-loading">
|
||||
<a-skeleton active :paragraph="{ rows: 1 }" />
|
||||
</div>
|
||||
|
||||
<!-- 应用列表 -->
|
||||
<div v-else-if="recentApps.length > 0" class="recent-apps-grid">
|
||||
<div
|
||||
v-for="app in recentApps.slice(0, 6)"
|
||||
:key="app.productId"
|
||||
class="recent-app-card"
|
||||
@click="handleAppClick(app)"
|
||||
>
|
||||
<div class="recent-app-icon" :style="{ background: iconBgColor(app.productName) }">
|
||||
<img
|
||||
v-if="app.icon"
|
||||
:src="app.icon"
|
||||
:alt="app.productName"
|
||||
class="recent-app-icon-img"
|
||||
/>
|
||||
<span v-else class="recent-app-icon-text">{{ appTypeIcon(app.appType) }}</span>
|
||||
</div>
|
||||
<div class="recent-app-info">
|
||||
<div class="recent-app-name">{{ app.productName }}</div>
|
||||
<div class="recent-app-meta">
|
||||
<span class="recent-app-type">{{ appTypeName(app.type, app.appType) }}</span>
|
||||
<span class="recent-app-time">{{ formatTime(app.updateTime || app.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recent-app-entries">
|
||||
<template v-for="entry in getAppEntries(app)" :key="entry.type">
|
||||
<a-button
|
||||
v-if="entry.available"
|
||||
:type="entry.isPrimary ? 'primary' : 'default'"
|
||||
size="small"
|
||||
class="recent-app-enter-btn"
|
||||
@click.stop="handleEntryClick(entry, app)"
|
||||
>
|
||||
<component :is="entry.icon" />
|
||||
{{ entry.label }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="recent-apps-empty">
|
||||
<a-empty description="暂无最近使用的应用">
|
||||
<template #image>
|
||||
<div class="empty-icon">📦</div>
|
||||
</template>
|
||||
<a-button type="primary" @click="navigateTo('/console/apps')">去创建应用</a-button>
|
||||
</a-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用详情抽屉 -->
|
||||
<AppDetail v-model:open="detailOpen" :app="selectedApp" @deleted="handleDeletedFromDetail" @updated="handleUpdatedFromDetail" />
|
||||
|
||||
<!-- 小程序扫码弹窗 -->
|
||||
<QrCodeModal
|
||||
v-model:open="qrOpen"
|
||||
:qrcode-url="qrApp?.qrcode"
|
||||
:app-name="qrApp?.productName"
|
||||
:title="qrApp ? (APP_TYPE_NAME[qrApp.appType ?? 10] || '小程序') + '二维码' : ''"
|
||||
:tip="qrApp ? getScanTip(qrApp.appType ?? 20) : ''"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
GiftOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ShoppingCartOutlined,
|
||||
ShoppingOutlined,
|
||||
UserOutlined,
|
||||
ShopOutlined,
|
||||
RightOutlined,
|
||||
TeamOutlined,
|
||||
CustomerServiceOutlined,
|
||||
GlobalOutlined,
|
||||
QrcodeOutlined,
|
||||
DownloadOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getJoinedApps, recordVisit } from '@/api/app/appProduct'
|
||||
import type { AppProduct } from '@/api/app/appProduct/model'
|
||||
import { APP_TYPE, APP_TYPE_NAME } from '@/api/app/appProduct/model'
|
||||
import AppDetail from '@/components/developer/AppDetail.vue'
|
||||
import QrCodeModal from '@/components/QrCodeModal.vue'
|
||||
import { getAppEntries, executeEntry, getScanTip } from '@/utils/appEntry'
|
||||
import type { AppEntry } from '@/utils/appEntry'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
definePageMeta({ layout: 'console' })
|
||||
|
||||
const userId = import.meta.client ? localStorage.getItem('UserId') : null
|
||||
|
||||
// 快捷入口配置
|
||||
const quickCards = [
|
||||
{
|
||||
label: '应用中心',
|
||||
desc: '管理我的应用',
|
||||
to: '/console/apps',
|
||||
icon: AppstoreOutlined,
|
||||
bg: '#eff6ff',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
{
|
||||
label: '已购产品',
|
||||
desc: '查看授权与订阅',
|
||||
to: '/console/products',
|
||||
icon: ShoppingOutlined,
|
||||
bg: '#f0fdf4',
|
||||
color: '#22c55e',
|
||||
},
|
||||
{
|
||||
label: '订单记录',
|
||||
desc: '历史订单与账单',
|
||||
to: '/console/orders',
|
||||
icon: ShoppingCartOutlined,
|
||||
bg: '#fff7ed',
|
||||
color: '#f97316',
|
||||
},
|
||||
{
|
||||
label: '优惠券',
|
||||
desc: '查看可用优惠',
|
||||
to: '/console/coupons',
|
||||
icon: GiftOutlined,
|
||||
bg: '#fdf4ff',
|
||||
color: '#a855f7',
|
||||
},
|
||||
{
|
||||
label: '成员管理',
|
||||
desc: '团队与权限管理',
|
||||
to: '/console/account/members',
|
||||
icon: TeamOutlined,
|
||||
bg: '#fefce8',
|
||||
color: '#eab308',
|
||||
},
|
||||
{
|
||||
label: '工单管理',
|
||||
desc: '技术支持与反馈',
|
||||
to: '/console/tickets',
|
||||
icon: CustomerServiceOutlined,
|
||||
bg: '#f0f9ff',
|
||||
color: '#0ea5e9',
|
||||
},
|
||||
{
|
||||
label: '账号安全',
|
||||
desc: '密码与安全设置',
|
||||
to: '/console/account/security',
|
||||
icon: SafetyCertificateOutlined,
|
||||
bg: '#fff1f2',
|
||||
color: '#f43f5e',
|
||||
},
|
||||
]
|
||||
|
||||
// 最近使用的应用
|
||||
const recentApps = ref<AppProduct[]>([])
|
||||
const recentAppsLoading = ref(false)
|
||||
const detailOpen = ref(false)
|
||||
const selectedApp = ref<AppProduct | null>(null)
|
||||
|
||||
// 跳转方法(模板中不能直接调用 navigateTo)
|
||||
function navigateTo(path: string) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
// 入口处理
|
||||
function handleEntryClick(entry: AppEntry, app: AppProduct) {
|
||||
if (entry.type === 'scan-qr') {
|
||||
qrApp.value = app
|
||||
qrOpen.value = true
|
||||
return
|
||||
}
|
||||
executeEntry(entry)
|
||||
}
|
||||
|
||||
// 扫码弹窗
|
||||
const qrOpen = ref(false)
|
||||
const qrApp = ref<AppProduct | null>(null)
|
||||
|
||||
// 应用类型名称(使用统一枚举)
|
||||
function appTypeName(type?: number, appType?: number): string {
|
||||
return APP_TYPE_NAME[type ?? 10] ?? 'Web 应用'
|
||||
}
|
||||
|
||||
function appTypeIcon(appType?: number): string {
|
||||
const iconMap: Record<number, string> = {
|
||||
[APP_TYPE.WEBSITE]: '🌐',
|
||||
[APP_TYPE.WECHAT_MP]: '📱',
|
||||
[APP_TYPE.DOUYIN_MP]: '🎵',
|
||||
[APP_TYPE.BAIDU_MP]: '🔍',
|
||||
[APP_TYPE.ALIPAY_MP]: '💎',
|
||||
[APP_TYPE.ANDROID]: '🤖',
|
||||
[APP_TYPE.IOS]: '🍎',
|
||||
[APP_TYPE.MACOS]: '💻',
|
||||
[APP_TYPE.WINDOWS]: '🪟',
|
||||
[APP_TYPE.PLUGIN]: '🔌',
|
||||
}
|
||||
return iconMap[appType ?? 10] ?? '🌐'
|
||||
}
|
||||
|
||||
// 图标背景色
|
||||
const PALETTE = ['#f56a00', '#7265e6', '#ffbf00', '#00a2ae', '#87d068', '#108ee9']
|
||||
|
||||
function iconBgColor(name?: string) {
|
||||
if (!name) return PALETTE[0]
|
||||
let h = 0
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffffffff
|
||||
return PALETTE[Math.abs(h) % PALETTE.length]
|
||||
}
|
||||
|
||||
function formatTime(timestamp?: string | number | Date) {
|
||||
if (!timestamp) return '-'
|
||||
const date = typeof timestamp === 'string' ? new Date(timestamp) : new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (days === 0) {
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
if (hours === 0) {
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
return minutes <= 1 ? '刚刚' : `${minutes}分钟前`
|
||||
}
|
||||
return `${hours}小时前`
|
||||
} else if (days === 1) {
|
||||
return '昨天'
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最近使用的应用(包括我创建的和被邀请参与的应用)
|
||||
async function loadRecentApps() {
|
||||
if (!userId) return
|
||||
|
||||
recentAppsLoading.value = true
|
||||
try {
|
||||
const uid = Number(userId)
|
||||
|
||||
// 获取我参与的所有应用(后端按 app_user.update_time 排序,实现"最近使用")
|
||||
const result = await getJoinedApps({ page: 1, limit: 10 })
|
||||
recentApps.value = result?.list || []
|
||||
} catch (e) {
|
||||
console.error('加载最近应用失败', e)
|
||||
} finally {
|
||||
recentAppsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppClick(app: AppProduct) {
|
||||
// 记录访问(异步,不阻塞)
|
||||
if (app.productId) {
|
||||
recordVisit(app.productId)
|
||||
}
|
||||
selectedApp.value = app
|
||||
detailOpen.value = true
|
||||
}
|
||||
|
||||
function handleDeletedFromDetail() {
|
||||
selectedApp.value = null
|
||||
detailOpen.value = false
|
||||
loadRecentApps()
|
||||
}
|
||||
|
||||
function handleUpdatedFromDetail(updatedApp: AppProduct) {
|
||||
const index = recentApps.value.findIndex((app) => app.productId === updatedApp.productId)
|
||||
if (index !== -1) {
|
||||
recentApps.value[index] = { ...recentApps.value[index], ...updatedApp }
|
||||
}
|
||||
if (selectedApp.value?.productId === updatedApp.productId) {
|
||||
selectedApp.value = { ...selectedApp.value, ...updatedApp }
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRecentApps()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ===== 页面整体 ===== */
|
||||
.console-home {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
/* ===== 欢迎区 ===== */
|
||||
.welcome-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.welcome-section {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.welcome-actions {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 区块样式 ===== */
|
||||
.section-block {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ===== 快捷入口 ===== */
|
||||
.quick-cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.quick-card:hover {
|
||||
background: #fff;
|
||||
border-color: #d6e4ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.quick-card-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quick-card-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.quick-card-desc {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.quick-card-arrow {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-card:hover .quick-card-arrow {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* ===== 最近使用 ===== */
|
||||
.recent-apps-loading {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.recent-apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.recent-app-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.recent-app-card:hover {
|
||||
background: #fff;
|
||||
border-color: #d6e4ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.recent-app-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.recent-app-icon-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.recent-app-icon-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.recent-app-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recent-app-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.recent-app-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.recent-app-type {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
background: #f0f0f0;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.recent-app-time {
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.recent-app-entries {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.recent-app-card:hover .recent-app-entries {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.recent-app-enter-btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 移动端始终显示进入按钮 */
|
||||
@media (max-width: 768px) {
|
||||
.recent-app-entries {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.recent-apps-empty {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user