虽然Element Plus一直处于领先地位,但本次我们选择图森未来的Naive UI作为设计方案。
我们后端使用springboot,前端使用Naive UI来为人事部门做一个简单的员工信息查询系统。
项目结构
├── frontend/ # 前端项目
│ ├── package.json
│ ├── vite.config.js
│ ├── index.html
│ └── src/
│ ├── main.js
│ ├── App.vue
│ └── components/EmployeeSearch.vue
└── README.md
components/EmployeeSearch.vue
#components/EmployeeSearch.vue
<template>
<n-card class="search-container">
<!-- 简洁搜索区域 -->
<div class="search-header">
<n-input
v-model:value="searchKeyword"
placeholder="输入员工姓名进行搜索..."
clearable
size="large"
round
@update:value="handleSearch"
class="search-input"
>
<template #prefix>
<n-icon :component="SearchIcon" color="#3b82f6" />
</template>
</n-input>
<n-text class="search-tip">
支持中文姓名搜索,实时返回结果
</n-text>
</div>
<!-- 加载状态 -->
<n-spin v-if="loading" size="large" class="loading-spin">
<template #description>
<n-text style="color: #64748b;">搜索中...</n-text>
</template>
</n-spin>
<!-- 搜索结果 -->
<div v-if="!loading && employees.length > 0" class="results-container">
<n-card
v-for="employee in employees"
:key="employee.id"
class="employee-card"
hoverable
@click="selectEmployee(employee)"
:class="{ selected: selectedEmployee?.id === employee.id }"
>
<div class="card-content">
<n-avatar
round
size="medium"
:style="{ backgroundColor: getAvatarColor(employee.name) }"
class="avatar"
>
{{ employee.name.charAt(0) }}
</n-avatar>
<div class="employee-info">
<n-text strong class="name">
{{ employee.name }}
</n-text>
<n-text depth="3" class="department">
{{ employee.department }}
</n-text>
</div>
<n-icon
:component="selectedEmployee?.id === employee.id ? ChevronUpIcon : ChevronDownIcon"
size="18"
class="expand-icon"
/>
</div>
<!-- 详细信息 -->
<div v-if="selectedEmployee?.id === employee.id" class="details-panel">
<n-divider />
<n-space vertical :size="12">
<n-space align="center">
<n-icon :component="IdCardIcon" color="#64748b" />
<n-text strong>ID:</n-text>
<n-tag size="small" type="info">{{ employee.id }}</n-tag>
</n-space>
<n-space align="center">
<n-icon :component="DepartmentIcon" color="#64748b" />
<n-text strong>部门:</n-text>
<n-text>{{ employee.department }}</n-text>
</n-space>
<n-space align="center">
<n-icon :component="AgeIcon" color="#64748b" />
<n-text strong>年龄:</n-text>
<n-tag size="small" type="warning">{{ employee.age }}岁</n-tag>
</n-space>
<n-space align="center">
<n-icon :component="PhoneIcon" color="#64748b" />
<n-text strong>手机:</n-text>
<n-text copyable>{{ employee.phone }}</n-text>
</n-space>
<n-space align="center">
<n-icon :component="CompanyIcon" color="#64748b" />
<n-text strong>公司:</n-text>
<n-text>{{ employee.company }}</n-text>
</n-space>
</n-space>
</div>
</n-card>
</div>
<!-- 初始状态提示 -->
<div v-if="!loading && employees.length === 0 && !searchKeyword" class="welcome-state">
<n-empty description="🔍 开始搜索员工信息" class="empty-state">
<template #extra>
</template>
</n-empty>
</div>
<!-- 搜索结果为空 -->
<div v-if="!loading && employees.length === 0 && searchKeyword" class="empty-results">
<n-empty description="未找到相关员工" class="empty-state">
<template #extra>
<n-text style="color: #64748b; font-size: 14px;">
请检查姓名拼写或尝试其他关键词
</n-text>
</template>
</n-empty>
</div>
</n-card>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
import {
SearchOutline as SearchIcon,
ChevronDownOutline as ChevronDownIcon,
ChevronUpOutline as ChevronUpIcon,
IdCardOutline as IdCardIcon,
BusinessOutline as DepartmentIcon,
CalendarOutline as AgeIcon,
CallOutline as PhoneIcon,
Business as CompanyIcon
} from '@vicons/ionicons5'
const searchKeyword = ref('')
const employees = ref([])
const loading = ref(false)
const selectedEmployee = ref(null)
// 搜索员工
const handleSearch = async (keyword) => {
const searchTerm = keyword.trim()
if (!searchTerm) {
employees.value = []
return
}
loading.value = true
selectedEmployee.value = null
try {
const response = await axios.get('/api/employees/search', {
params: { keyword: searchTerm }
})
employees.value = response.data
} catch (error) {
console.error('搜索失败:', error)
employees.value = []
} finally {
loading.value = false
}
}
// 选择员工显示详情
const selectEmployee = (employee) => {
if (selectedEmployee.value?.id === employee.id) {
selectedEmployee.value = null
} else {
selectedEmployee.value = employee
}
}
// 生成头像背景色
const getAvatarColor = (name) => {
const colors = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#06b6d4']
const index = name.charCodeAt(0) % colors.length
return colors[index]
}
</script>
<style scoped>
.search-container {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
border: 1px solid #e2e8f0;
}
.search-header {
text-align: center;
margin-bottom: 24px;
}
.search-input {
max-width: 400px;
margin: 0 auto;
}
.search-tip {
display: block;
margin-top: 8px;
font-size: 13px;
color: #64748b;
}
.loading-spin {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.results-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.employee-card {
border-radius: 8px;
transition: all 0.2s ease;
cursor: pointer;
}
.employee-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.employee-card.selected {
border-color: #3b82f6;
background: #f8fafc;
}
.card-content {
display: flex;
align-items: center;
gap: 12px;
}
.avatar {
flex-shrink: 0;
font-weight: 600;
color: white;
}
.employee-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.name {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.department {
font-size: 13px;
color: #64748b;
}
.expand-icon {
color: #94a3b8;
transition: transform 0.2s ease;
}
.employee-card.selected .expand-icon {
transform: rotate(180deg);
}
.details-panel {
padding: 16px 0;
}
.welcome-state,
.empty-results {
margin: 40px 0;
text-align: center;
}
.empty-state {
color: #64748b;
}
/* 响应式设计 */
@media (max-width: 768px) {
.search-input {
max-width: 100%;
}
.card-content {
flex-direction: column;
text-align: center;
gap: 8px;
}
.employee-info {
align-items: center;
}
}
</style>
App.vue
#App.vue
<template>
<n-config-provider>
<div class="app-container">
<!-- 简洁背景 -->
<div class="background"></div>
<!-- 主内容 -->
<div class="main-content">
<!-- 简洁标题 -->
<div class="hero-section">
<n-gradient-text type="warning" :size="36" class="main-title">
🛡️ 员工信息查询系统
</n-gradient-text>
<n-text class="subtitle">
信息安全有限公司 - 高效人事管理
</n-text>
</div>
<!-- 搜索组件 -->
<div class="search-wrapper">
<employee-search />
</div>
</div>
</div>
</n-config-provider>
</template>
<script setup>
import EmployeeSearch from './components/EmployeeSearch.vue'
</script>
<style scoped>
.app-container {
min-height: 100vh;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
position: relative;
}
.background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%);
}
.main-content {
position: relative;
z-index: 2;
padding: 40px 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
.hero-section {
text-align: center;
margin-bottom: 40px;
padding: 0 20px;
}
.main-title {
font-weight: 700;
margin-bottom: 12px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.subtitle {
font-size: 18px;
color: #cbd5e1;
font-weight: 400;
}
.search-wrapper {
width: 100%;
max-width: 800px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
padding: 20px 16px;
}
.main-title {
font-size: 28px;
}
.subtitle {
font-size: 16px;
}
}
</style>
<style>
body {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: transparent;
}
#app {
background: transparent;
}
</style>
最终实现效果
image.png
本次Vue3非教程系列,仅作Vue3基础学习和AI开发笔记随笔……