大屏页面初始化

This commit is contained in:
2026-03-05 18:33:45 +08:00
parent d3d713f131
commit cf65afb47f
21 changed files with 2027 additions and 1 deletions

View File

@@ -0,0 +1,12 @@
import request from '@/utils/request'
/**
* 获取指标信息列表
*/
export function getHomeRoleList(params) {
return request({
url: '/biz/homeRole/list',
method: 'get',
params: params
})
}

View File

@@ -0,0 +1,310 @@
<template>
<div class="el-card el-card--border" ref="wrapperRef" style="height: 100%; border: none; box-shadow: none;">
<el-input
v-if="showSearch"
v-model="filterText"
:placeholder="searchPlaceholder"
clearable
class="filter-search-input"
@clear="handleClear"
size="default"
/>
<el-divider v-if="showSearch" direction="horizontal" class="filter-tree-divider" />
<div
class="list-container"
:style="{ height: listHeight, width: '100%', overflow: 'hidden' }"
>
<el-scrollbar height="100%" :native="true">
<div
class="list-item"
v-for="item in filteredList"
:key="item[nodeKey]"
@mouseenter="() => hoveredItemId = item[nodeKey]"
@mouseleave="() => handleItemLeave(item[nodeKey])"
@click="() => handleItemClick(item)"
:class="{ 'list-item--selected': selectedItemId === item[nodeKey] }"
>
<span class="item-text">{{ item[labelKey] }}</span>
<div class="action-wrapper" v-show="hoveredItemId === item[nodeKey]">
<el-dropdown
trigger="click"
placement="bottom"
:teleported="false"
@click.stop
>
<el-button size="mini" icon="MoreFilled" circle class="action-btn" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="() => handleEdit(item)">
<el-icon class="menu-icon"><Edit /></el-icon>
</el-dropdown-item>
<el-dropdown-item @click="() => handleDelete(item)" divided>
<el-icon class="menu-icon"><Delete /></el-icon>
</el-dropdown-item>
<el-dropdown-item @click="() => handleView(item)">
<el-icon class="menu-icon"><View /></el-icon>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, defineProps, defineEmits, withDefaults, onMounted, onUnmounted, nextTick, computed } from 'vue'
import { ElInput, ElDivider, ElScrollbar, ElDropdown, ElButton, ElDropdownMenu, ElDropdownItem, ElIcon } from 'element-plus'
import { Edit, Delete, View, MoreFilled, Search } from '@element-plus/icons-vue'
export interface ListItem {
[key: string]: any
}
const emit = defineEmits([
'edit',
'delete',
'view',
'item-click',
'search-change',
'search-clear'
])
const props = withDefaults(
defineProps<{
listData: ListItem[]
showSearch?: boolean
searchPlaceholder?: string
nodeKey?: string
labelKey?: string
customFilter?: (value: string, data: ListItem) => boolean
autoHeight?: boolean
minHeight?: string | number
defaultValue?: string | number
}>(),
{
showSearch: true,
searchPlaceholder: '请输入关键词过滤',
nodeKey: 'id',
labelKey: 'label',
autoHeight: true,
minHeight: '100px',
defaultValue: ''
}
)
const filterText = ref('')
const wrapperRef = ref<HTMLDivElement>(null)
const hoveredItemId = ref<string | number | null>(null)
const selectedItemId = ref<string | number | null>(props.defaultValue)
const listHeight = computed(() => {
if (!props.autoHeight || !wrapperRef.value) return props.minHeight as string
const wrapperHeight = wrapperRef.value.clientHeight
const searchHeight = props.showSearch ? 40 : 0
const dividerHeight = props.showSearch ? (16 + 2) : 0
const baseMargin = 8
return `${wrapperHeight - searchHeight - dividerHeight - baseMargin}px`
})
const filteredList = computed(() => {
if (!filterText.value) return props.listData
if (props.customFilter) {
return props.listData.filter(item => props.customFilter!(filterText.value, item))
}
return props.listData.filter(item => {
const label = item[props.labelKey] || ''
return label.toString().includes(filterText.value)
})
})
watch(
() => filterText.value,
(val) => {
emit('search-change', val)
},
{ immediate: false }
)
watch(() => props.defaultValue, (val) => {
selectedItemId.value = val
}, { immediate: true })
const handleItemLeave = (id: string | number) => {
hoveredItemId.value = null
}
const handleItemClick = (item: ListItem) => {
selectedItemId.value = item[props.nodeKey]
emit('item-click', item)
}
const handleEdit = (item: ListItem) => {
emit('edit', item)
hoveredItemId.value = null
}
const handleDelete = (item: ListItem) => {
emit('delete', item)
hoveredItemId.value = null
}
const handleView = (item: ListItem) => {
emit('view', item)
hoveredItemId.value = null
}
const handleClear = () => {
filterText.value = ''
emit('search-clear')
}
onMounted(async () => {
await nextTick()
const resizeHandler = () => void listHeight.value
window.addEventListener('resize', resizeHandler)
if (window.ResizeObserver && wrapperRef.value) {
const observer = new ResizeObserver(resizeHandler)
observer.observe(wrapperRef.value)
onUnmounted(() => {
observer.disconnect()
window.removeEventListener('resize', resizeHandler)
})
}
})
defineExpose({
doFilter: (value: string) => {
filterText.value = value
},
clearSearch: handleClear,
getFilteredList: () => filteredList.value,
setSelected: (id: string | number) => {
selectedItemId.value = id
},
getSelected: () => {
return filteredList.value.find(item => item[props.nodeKey] === selectedItemId.value)
}
})
</script>
<style scoped>
:deep(.el-card) {
height: 100%;
border: none;
box-shadow: none;
padding: 0;
}
.filter-search-input {
margin: 0 2px !important;
width: calc(100% - 4px) !important;
}
.filter-tree-divider {
margin: 2px 2px 0 2px !important;
height: 1px;
}
.list-container {
position: relative;
}
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
cursor: pointer;
transition: background-color 0.2s ease;
border-radius: 4px;
margin-bottom: 2px;
position: relative;
overflow: visible;
user-select: none;
--el-tree-node-content-hover-bg-color: #f0f9ff;
--el-tree-node-selected-bg-color: #e6f7ff;
--el-tree-node-selected-color: #1890ff;
}
.list-item:hover {
background-color: var(--el-tree-node-content-hover-bg-color);
}
.list-item--selected {
background-color: var(--el-tree-node-selected-bg-color) !important;
color: var(--el-tree-node-selected-color) !important;
}
.list-item--selected .item-text {
color: var(--el-tree-node-selected-color) !important;
}
.item-text {
flex: 1;
font-size: 14px;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 4px;
}
.action-wrapper {
position: relative;
z-index: 100;
flex-shrink: 0;
}
.action-btn {
width: 24px;
height: 24px;
padding: 0;
background-color: transparent !important;
border: none !important;
color: #909399 !important;
transition: all 0.2s ease;
}
.action-btn:hover {
color: #1890ff !important;
background-color: #f5f7fa !important;
}
.menu-icon {
width: 18px;
height: 18px;
color: #606266;
display: block;
margin: 0 auto;
}
:deep(.el-dropdown-menu) {
min-width: 40px !important;
padding: 4px 0;
z-index: 9999;
}
:deep(.el-dropdown-item) {
text-align: center;
padding: 6px 8px !important;
}
:deep(.el-dropdown__popper) {
position: absolute !important;
top: 100% !important;
left: 50% !important;
margin-top: 4px !important;
transform: translateX(-50%) !important;
z-index: 9999 !important;
}
:deep(.el-scrollbar__wrap) {
overflow-x: hidden;
}
</style>

View File

@@ -0,0 +1,222 @@
<template>
<div class="table-container">
<div class="main-card">
<div class="title-section">
<h1 class="title">预警信息</h1>
</div>
<div class="divider"></div>
<div class="content-section">
<div class="table-wrapper">
<el-table
:data="tableData"
border
:header-cell-style="{ background: '#f5f7fa' }"
style="width: 100%;"
height="100%"
v-loading="loading"
>
<el-table-column
prop="name"
label="姓名"
width="120"
fixed="left"
/>
<el-table-column
prop="age"
label="年龄"
width="80"
/>
<el-table-column
prop="gender"
label="性别"
width="80"
/>
<el-table-column
prop="phone"
label="手机号"
width="150"
/>
<el-table-column
prop="email"
label="邮箱"
width="200"
/>
<el-table-column
prop="address"
label="地址"
width="250"
/>
<el-table-column
prop="company"
label="公司"
width="180"
/>
<el-table-column
prop="position"
label="职位"
width="150"
/>
<el-table-column
prop="salary"
label="薪资"
width="120"
/>
<el-table-column
prop="entryDate"
label="入职日期"
width="150"
fixed="right"
/>
</el-table>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElTable, ElTableColumn, ElLoading } from 'element-plus'
// 生成模拟数据
const generateTableData = () => {
const data = []
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
const genders = ['男', '女']
const positions = ['前端开发', '后端开发', '产品经理', 'UI设计', '测试工程师', '运维工程师']
const companies = ['科技有限公司', '网络科技公司', '信息技术公司', '数据服务公司']
for (let i = 1; i <= 50; i++) {
data.push({
id: i,
name: names[Math.floor(Math.random() * names.length)] + i,
age: Math.floor(Math.random() * 30) + 20,
gender: genders[Math.floor(Math.random() * genders.length)],
phone: `13${Math.floor(Math.random() * 900000000) + 100000000}`,
email: `user${i}@example.com`,
address: `北京市朝阳区某某街道${Math.floor(Math.random() * 100)}`,
company: Math.floor(Math.random() * 10) + '号' + companies[Math.floor(Math.random() * companies.length)],
position: positions[Math.floor(Math.random() * positions.length)],
salary: `${Math.floor(Math.random() * 30) + 10}k`,
entryDate: `202${Math.floor(Math.random() * 5) + 1}-${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 28) + 1}`
})
}
return data
}
const tableData = ref([])
const loading = ref(true)
onMounted(() => {
// 模拟接口请求延迟
setTimeout(() => {
tableData.value = generateTableData()
loading.value = false
}, 500)
})
</script>
<style scoped>
.table-container {
width: 100%;
height: 100%;
padding: 2px;
margin: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
.main-card {
width: 100%;
height: 100%;
border: 1px solid #e5e7eb;
border-radius: 4px;
background-color: #fff;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.title-section {
width: 100%;
height: 36px;
padding: 0 12px;
box-sizing: border-box;
background-color: #f9fafb;
display: flex;
align-items: center;
margin: 0;
border-radius: 4px 4px 0 0;
}
.title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0;
}
.divider {
width: 100%;
height: 1px;
background-color: #e5e7eb;
}
.content-section {
flex: 1;
width: 100%;
padding: 8px;
box-sizing: border-box;
overflow: hidden;
margin: 0;
display: flex;
align-items: center;
}
.table-wrapper {
width: 100%;
height: 100%;
box-sizing: border-box;
overflow: auto;
}
:deep(.el-table__body-wrapper) {
overflow: auto;
}
:deep(.el-table__body-wrapper::-webkit-scrollbar) {
display: block;
height: 6px;
width: 6px;
}
:deep(.el-table__body-wrapper::-webkit-scrollbar-track) {
background: #f1f1f1;
border-radius: 3px;
}
:deep(.el-table__body-wrapper::-webkit-scrollbar-thumb) {
background: #dcdfe6;
border-radius: 3px;
}
:deep(.el-table__body-wrapper::-webkit-scrollbar-thumb:hover) {
background: #c0c4cc;
}
:deep(.el-table__fixed-left) {
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.05);
}
:deep(.el-table__fixed-right) {
box-shadow: -2px 0 6px rgba(0, 0, 0, 0.05);
}
:deep(.el-table th) {
font-weight: 600;
color: #303133;
}
</style>

View File

@@ -0,0 +1,224 @@
<template>
<div class="table-container">
<div class="main-card">
<div class="title-section">
<h1 class="title">便签信息</h1>
</div>
<div class="divider"></div>
<div class="content-section">
<div class="table-wrapper">
<el-table
:data="tableData"
border
:header-cell-style="{ background: '#f5f7fa' }"
style="width: 100%;"
height="100%"
v-loading="loading"
>
<el-table-column
prop="name"
label="姓名"
width="120"
fixed="left"
/>
<el-table-column
prop="age"
label="年龄"
width="80"
/>
<el-table-column
prop="gender"
label="性别"
width="80"
/>
<el-table-column
prop="phone"
label="手机号"
width="150"
/>
<el-table-column
prop="email"
label="邮箱"
width="200"
/>
<el-table-column
prop="address"
label="地址"
width="250"
/>
<el-table-column
prop="company"
label="公司"
width="180"
/>
<el-table-column
prop="position"
label="职位"
width="150"
/>
<el-table-column
prop="salary"
label="薪资"
width="120"
/>
<el-table-column
prop="entryDate"
label="入职日期"
width="150"
fixed="right"
/>
</el-table>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElTable, ElTableColumn, ElLoading } from 'element-plus'
// 生成模拟数据
const generateTableData = () => {
const data = []
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
const genders = ['男', '女']
const positions = ['前端开发', '后端开发', '产品经理', 'UI设计', '测试工程师', '运维工程师']
const companies = ['科技有限公司', '网络科技公司', '信息技术公司', '数据服务公司']
for (let i = 1; i <= 50; i++) {
data.push({
id: i,
name: names[Math.floor(Math.random() * names.length)] + i,
age: Math.floor(Math.random() * 30) + 20,
gender: genders[Math.floor(Math.random() * genders.length)],
phone: `13${Math.floor(Math.random() * 900000000) + 100000000}`,
email: `user${i}@example.com`,
address: `北京市朝阳区某某街道${Math.floor(Math.random() * 100)}`,
company: Math.floor(Math.random() * 10) + '号' + companies[Math.floor(Math.random() * companies.length)],
position: positions[Math.floor(Math.random() * positions.length)],
salary: `${Math.floor(Math.random() * 30) + 10}k`,
entryDate: `202${Math.floor(Math.random() * 5) + 1}-${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 28) + 1}`
})
}
return data
}
const tableData = ref([])
const loading = ref(true)
onMounted(() => {
// 模拟接口请求延迟
setTimeout(() => {
tableData.value = generateTableData()
loading.value = false
}, 500)
})
</script>
<style scoped>
.table-container {
width: 100%;
height: 100%;
padding: 2px;
margin: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
.main-card {
width: 100%;
height: 100%;
border: 1px solid #e5e7eb;
border-radius: 4px;
background-color: #fff;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.title-section {
width: 100%;
height: 36px;
padding: 0 12px;
box-sizing: border-box;
background-color: #f9fafb;
display: flex;
align-items: center;
margin: 0;
border-radius: 4px 4px 0 0;
}
.title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0;
}
.divider {
width: 100%;
height: 1px;
background-color: #e5e7eb;
}
.content-section {
flex: 1;
width: 100%;
padding: 8px;
box-sizing: border-box;
overflow: hidden;
margin: 0;
display: flex;
align-items: center;
}
.table-wrapper {
width: 100%;
height: 100%;
box-sizing: border-box;
overflow: auto;
}
/* 表格滚动条样式优化 */
:deep(.el-table__body-wrapper) {
overflow: auto;
}
:deep(.el-table__body-wrapper::-webkit-scrollbar) {
display: block;
height: 6px;
width: 6px;
}
:deep(.el-table__body-wrapper::-webkit-scrollbar-track) {
background: #f1f1f1;
border-radius: 3px;
}
:deep(.el-table__body-wrapper::-webkit-scrollbar-thumb) {
background: #dcdfe6;
border-radius: 3px;
}
:deep(.el-table__body-wrapper::-webkit-scrollbar-thumb:hover) {
background: #c0c4cc;
}
/* 固定表头样式优化 */
:deep(.el-table__fixed-left) {
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.05);
}
:deep(.el-table__fixed-right) {
box-shadow: -2px 0 6px rgba(0, 0, 0, 0.05);
}
:deep(.el-table th) {
font-weight: 600;
color: #303133;
}
</style>

View File

@@ -0,0 +1,313 @@
<template>
<div class="quick-login-container">
<div class="main-card">
<div class="title-section">
<h1 class="title">快捷登录</h1>
</div>
<div class="divider"></div>
<div class="content-section">
<div class="card-list-wrapper" ref="cardWrapperRef">
<div class="card-list-inner" ref="cardListRef">
<div
v-for="(item, index) in loginList"
:key="index"
class="login-card"
@click="handleCardClick(item)"
>
<div class="icon-wrapper">
<img :src="item.icon" :alt="item.name" class="login-icon" />
</div>
<div class="card-name">{{ item.name }}</div>
</div>
</div>
</div>
<el-button
class="control-btn left-btn"
@click="scrollCards('left')"
:disabled="isLeftDisabled"
circle
size="mini"
icon="ArrowLeft"
></el-button>
<el-button
class="control-btn right-btn"
@click="scrollCards('right')"
:disabled="isRightDisabled"
circle
size="mini"
icon="ArrowRight"
></el-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ElButton, ElMessage } from 'element-plus'
const loginList = ref([
{ icon: 'https://picsum.photos/120/80?1', name: '账号密码' },
{ icon: 'https://picsum.photos/120/80?2', name: '手机验证码' },
{ icon: 'https://picsum.photos/120/80?3', name: '微信登录' },
{ icon: 'https://picsum.photos/120/80?4', name: '支付宝登录' },
{ icon: 'https://picsum.photos/120/80?5', name: 'QQ登录' },
{ icon: 'https://picsum.photos/120/80?6', name: '微博登录' },
{ icon: 'https://picsum.photos/120/80?7', name: '邮箱登录' },
{ icon: 'https://picsum.photos/120/80?8', name: '抖音登录' },
{ icon: 'https://picsum.photos/120/80?9', name: '快手登录' }
])
const cardWrapperRef = ref(null)
const cardListRef = ref(null)
const scrollLeft = ref(0)
const isLeftDisabled = computed(() => {
return scrollLeft.value <= 0
})
const isRightDisabled = computed(() => {
if (!cardWrapperRef.value || !cardListRef.value) return true
const maxScroll = cardListRef.value.scrollWidth - cardWrapperRef.value.clientWidth
return Math.round(scrollLeft.value) >= Math.round(maxScroll)
})
const scrollCards = (direction) => {
if (!cardListRef.value || !cardWrapperRef.value) return
const step = 128
const maxScroll = cardListRef.value.scrollWidth - cardWrapperRef.value.clientWidth
let newScroll = scrollLeft.value
if (direction === 'left') {
newScroll = Math.max(0, newScroll - step)
} else {
newScroll = Math.min(maxScroll, newScroll + step)
}
if (newScroll !== scrollLeft.value) {
cardListRef.value.scrollTo({
left: newScroll,
behavior: 'smooth'
})
scrollLeft.value = newScroll
}
}
const handleCardClick = (item) => {
ElMessage.success(`选择了${item.name}登录`)
}
onMounted(() => {
const list = cardListRef.value
if (list) {
scrollLeft.value = list.scrollLeft
list.addEventListener('scroll', () => {
scrollLeft.value = list.scrollLeft
})
}
window.addEventListener('resize', () => {
if (cardListRef.value) {
scrollLeft.value = cardListRef.value.scrollLeft
}
})
})
onUnmounted(() => {
const list = cardListRef.value
if (list) {
list.removeEventListener('scroll', () => {})
}
window.removeEventListener('resize', () => {})
})
watch([isLeftDisabled, isRightDisabled], () => {}, { immediate: true })
</script>
<style scoped>
.quick-login-container {
width: 100%;
height: 100%;
padding: 2px;
margin: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
.main-card {
width: 100%;
height: 100%;
border: 1px solid #e5e7eb;
border-radius: 4px;
background-color: #fff;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.title-section {
width: 100%;
height: 36px;
padding: 0 12px;
box-sizing: border-box;
background-color: #f9fafb;
display: flex;
align-items: center;
margin: 0;
border-radius: 4px 4px 0 0;
}
.title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0;
}
.divider {
width: 100%;
height: 1px;
background-color: #e5e7eb;
}
.content-section {
flex: 1;
width: 100%;
padding: 0;
box-sizing: border-box;
overflow: hidden;
margin: 0;
display: flex;
align-items: center;
position: relative;
}
.control-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 100;
width: 16px !important;
height: 16px !important;
padding: 0 !important;
background-color: #e6f7ff !important;
border-color: #91d5ff !important;
color: #1890ff !important;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
transition: all 0.1s ease;
}
.control-btn:disabled {
background-color: #f0f8fb !important;
border-color: #b3d8ea !important;
color: #8cbfe8 !important;
opacity: 0.6;
}
.control-btn:not(:disabled):active {
background-color: #bae7ff !important;
border-color: #69c0ff !important;
color: #096dd9 !important;
}
.left-btn {
left: 4px;
}
.right-btn {
right: 4px;
}
.card-list-wrapper {
width: 100%;
height: calc(100% - 16px);
padding: 8px 20px;
box-sizing: border-box;
overflow: hidden;
}
.card-list-inner {
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
}
.card-list-inner::-webkit-scrollbar {
display: block;
height: 6px;
}
.card-list-inner::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.card-list-inner::-webkit-scrollbar-thumb {
background: #dcdfe6;
border-radius: 3px;
}
.card-list-inner::-webkit-scrollbar-thumb:hover {
background: #c0c4cc;
}
.card-list-inner {
display: flex;
gap: 8px;
align-items: center;
height: 100%;
}
.login-card {
height: 100%;
width: 120px;
padding: 8px;
border-radius: 8px;
background-color: #f5f7fa;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
flex-shrink: 0;
}
.login-card:hover {
transform: translateY(-2px);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12);
background-color: #eff6ff;
}
.icon-wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.login-icon {
width: 100%;
height: 80px;
object-fit: cover;
border-radius: 6px;
}
.card-name {
font-size: 13px;
color: #303133;
font-weight: 500;
text-align: center;
line-height: 1.4;
flex-shrink: 0;
width: 100%;
margin: 0;
padding-top: 8px;
}
</style>

View File

@@ -0,0 +1,152 @@
<template>
<div class="welcome-bar">
<div class="welcome-left">
<div class="avatar">
<img src="https://picsum.photos/48/48" alt="头像" />
</div>
<div class="welcome-text">
<div class="greeting">您好, 超级管理员, 开始您一天的工作吧!</div>
<div class="weather">今日小雪;夜间:多云;温度:(-4.0 2.0);东北风/1-3</div>
</div>
</div>
<div class="welcome-right">
<div class="stat-item">
<div class="stat-label">待办</div>
<div class="stat-value">0/0</div>
</div>
<div class="stat-item">
<div class="stat-label">日程</div>
<div class="stat-value">0/0</div>
</div>
<div class="stat-item">
<div class="stat-label">项目</div>
<div class="stat-value">1/7</div>
</div>
<div class="stat-item">
<div class="stat-label">团队</div>
<div class="stat-value">6</div>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
userName: {
type: String,
default: ''
},
weatherInfo: {
type: String,
default: ''
}
})
</script>
<style scoped>
.welcome-bar {
width: 100%;
height: 100%;
padding: 0 16px;
box-sizing: border-box;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
}
.welcome-left {
display: flex;
align-items: center;
gap: 12px;
height: 100%;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.welcome-text {
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
height: 100%;
}
.greeting {
font-size: 14px;
color: #303133;
font-weight: 500;
}
.weather {
font-size: 12px;
color: #909399;
}
.welcome-right {
display: flex;
align-items: center;
gap: 32px;
height: 100%;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
height: 100%;
}
.stat-label {
font-size: 12px;
color: #909399;
}
.stat-value {
font-size: 14px;
color: #303133;
font-weight: 500;
}
/* 响应式适配 - 适配主页面小屏幕布局 */
@media (max-width: 768px) {
.welcome-right {
gap: 16px;
}
.avatar {
width: 36px;
height: 36px;
}
.greeting {
font-size: 12px;
}
.weather {
font-size: 10px;
}
.stat-label {
font-size: 10px;
}
.stat-value {
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,313 @@
<template>
<div class="search-list-page">
<div class="search-section">
<div class="search-wrapper">
<el-input
v-model="searchForm.websiteName"
placeholder="请输入搜索网站名称"
class="search-input"
clearable
@keyup.enter="handleSearch"
/>
<div class="search-btn-group">
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</div>
</div>
</div>
<div class="list-wrapper">
<div class="list-section">
<div v-if="listData.length === 0" class="empty-tip">
<el-empty description="暂无数据" />
</div>
<div v-else class="card-list">
<el-card
v-for="item in listData"
:key="item.websiteId"
class="list-card"
shadow="hover"
>
<div class="card-top">
<div class="site-main">
<el-tooltip :content="item.websiteName" placement="top">
<div class="site-name">{{ item.websiteName }}</div>
</el-tooltip>
<el-tooltip :content="item.websiteUrl" placement="top">
<div class="site-url">{{ item.websiteUrl }}</div>
</el-tooltip>
</div>
</div>
<div class="card-divider"></div>
<div class="card-bottom">
<span class="card-date">{{ item.createTime }}</span>
<div class="card-actions">
<el-button size="small" type="primary" link @click="handleVisit(item)">
<el-icon><Link /></el-icon>
访问
</el-button>
</div>
</div>
</el-card>
</div>
</div>
</div>
<div class="pagination-footer">
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[20, 50, 99]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
/>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Link } from '@element-plus/icons-vue'
import { getWebsiteStorageList } from '@/api/bizWebsiteStorage'
const searchForm = reactive({
websiteName: ''
})
const pagination = reactive({
pageNum: 1,
pageSize: 20,
total: 0
})
const listData = ref([])
const getDataList = async () => {
try {
const reqParmas = {
...searchForm,
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
}
const res = await getWebsiteStorageList(reqParmas)
listData.value = res.list || []
pagination.total = res.total
} catch (error) {
console.log(error)
}
}
const handleVisit = (item) => {
window.open(item.websiteUrl, '_blank')
}
const handleSearch = () => {
pagination.pageNum = 1
getDataList()
}
const handleReset = () => {
Object.assign(searchForm, {
websiteName: ''
})
pagination.pageNum = 1
getDataList()
}
const handleSizeChange = (val) => {
pagination.pageSize = val
getDataList()
}
const handleCurrentChange = (val) => {
pagination.pageNum = val
getDataList()
}
onMounted(() => {
getDataList()
})
</script>
<style scoped>
.search-list-page {
width: 100%;
height: 100%;
padding: 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden !important;
}
.search-section {
width: 100%;
margin-bottom: 16px;
flex-shrink: 0;
padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb;
}
.search-wrapper {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.search-input {
flex: 1;
}
.search-btn-group {
display: flex;
gap: 2px;
flex-shrink: 0;
}
.list-wrapper {
flex: 1;
width: 100%;
overflow: hidden;
min-height: 0;
border: 1px solid #e5e7eb;
border-radius: 6px;
margin-bottom: 12px;
}
.list-section {
width: 100%;
height: 100%;
overflow-y: auto !important;
overflow-x: hidden !important;
padding: 12px;
box-sizing: border-box;
}
.empty-tip {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.card-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
padding: 8px;
}
.list-card {
transition: all 0.3s ease;
cursor: pointer;
border-radius: 12px;
overflow: hidden;
border: 1px solid #f0f0f0 !important;
}
.list-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08) !important;
border-color: #409eff !important;
}
.card-top {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 6px 12px 4px;
}
.site-main {
flex: 1;
min-width: 0;
}
.site-name {
font-weight: 600;
font-size: 16px;
color: #1f2937;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-url {
font-size: 13px;
color: #9ca3af;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.site-url:hover {
color: #409eff;
text-decoration: underline;
}
.card-divider {
height: 1px;
background: #f0f0f0;
margin: 0 12px;
}
.card-bottom {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px 8px;
}
.card-date {
font-size: 12px;
color: #9ca3af;
}
.card-actions {
display: flex;
gap: 8px;
}
.pagination-footer {
width: 100%;
display: flex;
justify-content: center;
padding: 8px 0;
border: 1px solid #e5e7eb;
border-radius: 6px;
flex-shrink: 0;
background: #fff;
z-index: 10;
}
.list-section::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.list-section::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
.list-section::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.list-section::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
@media (max-width: 768px) {
.card-list {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div class="icon-demo-container">
<div class="header">
<div class="search-wrapper">
<el-input
v-model="searchKey"
placeholder="输入图标名称搜索"
class="search-input"
>
<template #suffix>
<el-icon class="search-icon"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<div class="icon-list-wrapper">
<div class="icon-list">
<div v-if="Object.keys(filteredIcons).length === 0" class="empty-tip">
未找到匹配的图标请更换关键词重试
</div>
<div
v-for="(icon, key) in filteredIcons"
:key="key"
class="icon-item"
@click="copyIconName(key)"
>
<div class="icon-wrapper">
<el-icon :size="32">
<component :is="key" />
</el-icon>
</div>
<div class="icon-name">{{ key }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { Search } from '@element-plus/icons-vue'
const searchKey = ref('')
const filteredIcons = computed(() => {
if (!searchKey.value) {
return ElementPlusIconsVue
}
const lowerKey = searchKey.value.toLowerCase()
return Object.fromEntries(
Object.entries(ElementPlusIconsVue).filter(([key]) =>
key.toLowerCase().includes(lowerKey)
)
)
})
const copyIconName = (key) => {
navigator.clipboard.writeText(key)
.then(() => {
ElMessage.success(`已复制图标名称:${key}`)
})
.catch(() => {
ElMessage.error('复制失败,请手动复制')
})
}
</script>
<style scoped>
.icon-demo-container {
width: 100%;
height: 100%;
padding: 16px;
font-family: sans-serif;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e6e6e6;
flex-shrink: 0;
width: 100%;
}
.header h2 {
color: #333;
margin: 0 0 12px 0;
font-size: 18px;
}
.search-wrapper {
width: 100%;
}
.search-input {
width: 100% !important;
}
.search-icon {
color: #999;
cursor: pointer;
transition: color 0.2s;
}
.search-icon:hover {
color: #409eff;
}
.icon-list-wrapper {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
}
.icon-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 12px;
width: 100%;
box-sizing: border-box;
}
.empty-tip {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #999;
font-size: 14px;
}
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 8px;
border-radius: 6px;
background: #f5f7fa;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
}
.icon-item:hover {
background: #e8eaed;
transform: translateY(-1px);
}
.icon-wrapper {
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
color: #409eff;
margin-bottom: 6px;
}
.icon-name {
font-size: 12px;
color: #333;
font-weight: 500;
text-align: center;
word-break: keep-all;
line-height: 1.2;
}
.icon-list-wrapper::-webkit-scrollbar {
width: 6px;
}
.icon-list-wrapper::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.icon-list-wrapper::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.icon-list-wrapper::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
@media (max-width: 768px) {
.icon-list {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
}
.header h2 {
font-size: 16px;
}
}
</style>