merge code pull

This commit is contained in:
2026-01-26 11:07:39 +08:00
18 changed files with 1781 additions and 1337 deletions

View File

@@ -2,7 +2,8 @@
<el-tag
:type="type"
:size="size"
:class="['clickable-tag', { 'has-action': actualRightIcon !== null }]"
:class="['clickable-tag', { 'has-action': actualRightIcon !== null}]"
:style="{ width: width ? `${width}px` : 'auto' }"
@click="handleClick">
<!-- 左侧图标 -->
<el-icon
@@ -43,6 +44,7 @@ interface Props {
leftIcon?: any // 左侧图标组件
middleIcon?: any // 中间图标组件(如警告图标)
rightIcon?: any // 右侧图标组件(默认为 Right null 则不显示)
width?: string | number // 自定义宽度
}
const props = withDefaults(defineProps<Props>(), {
@@ -50,7 +52,8 @@ const props = withDefaults(defineProps<Props>(), {
size: 'default',
leftIcon: undefined,
middleIcon: undefined,
rightIcon: undefined
rightIcon: undefined,
width: undefined
})
// 获取实际的右侧图标:未传值时使用默认图标,传 null 则不显示
@@ -97,7 +100,6 @@ export default {
}
}
}
.middle-icon {
animation: pulse 1.5s ease-in-out infinite;
}

View File

@@ -0,0 +1,162 @@
# DetailPopover 详情弹窗组件
一个通用的详情弹窗组件,用于展示结构化的详情信息。
## 功能特性
- ✅ 自定义标题和标题图标
- ✅ 支持多个详情项label + content
- ✅ 支持横向/纵向布局
- ✅ 支持自定义内容(插槽或组件)
- ✅ 支持标签图标
- ✅ 支持内容区域自定义样式类
## Props
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|-----|------|------|-------|--------|
| title | 标题 | string | - | '' |
| titleIcon | 标题图标组件 | Component | - | undefined |
| items | 详情项列表 | DetailItem[] | - | [] |
| placement | 弹出位置 | string | top/top-start/top-end/bottom/bottom-start/bottom-end/left/left-start/left-end/right/right-start/right-end | right |
| width | Popover 宽度 | string \| number | - | 300 |
| trigger | 触发方式 | string | click/focus/hover/contextmenu | click |
| popperClass | Popover 自定义类名 | string | - | '' |
## DetailItem 接口
```typescript
interface DetailItem {
label?: string // 标签文本
content?: string | number // 内容文本
labelIcon?: Component // 标签图标
layout?: 'horizontal' | 'vertical' // 布局方向,默认 vertical
contentClass?: string // 内容区域的自定义类名
component?: Component // 自定义组件
componentProps?: Record<string, any> // 自定义组件的 props
}
```
## Slots
| 插槽名 | 说明 | 参数 |
|--------|------|------|
| reference | 触发元素 | - |
| content-{index} | 自定义第 index 项的内容 | { item: DetailItem } |
| custom-content | 自定义内容(显示在所有详情项之后) | - |
## 使用示例
### 基础用法
```vue
<template>
<DetailPopover
title="专业变更详情"
:title-icon="InfoFilled"
:width="320"
:items="[
{ label: '旧专业', content: '计算机应用技术' },
{ label: '新专业', content: '软件技术', contentClass: 'new-major' }
]">
<template #reference>
<span class="link">查看详情</span>
</template>
</DetailPopover>
</template>
<script setup>
import DetailPopover from '/@/components/DetailPopover/index.vue'
import { InfoFilled } from '@element-plus/icons-vue'
</script>
```
### 横向布局
```vue
<DetailPopover
title="异动审核详情"
:items="[
{
label: '审核状态',
layout: 'horizontal',
content: '待审核'
}
]">
<template #reference>
<el-button>查看</el-button>
</template>
</DetailPopover>
```
### 使用插槽自定义内容
```vue
<DetailPopover
title="异动审核详情"
:items="[
{ label: '审核状态', layout: 'horizontal' },
{ label: '备注信息', contentClass: 'reason-content' }
]">
<template #reference>
<ClickableTag>待审核</ClickableTag>
</template>
<!-- 自定义第一项内容 -->
<template #content-0>
<ClickableTag type="warning">待审核</ClickableTag>
</template>
<!-- 自定义第二项内容 -->
<template #content-1>
<div class="reason-content">
<el-icon><Warning /></el-icon>
<span>需要补充材料</span>
</div>
</template>
</DetailPopover>
```
### 使用标签图标
```vue
<DetailPopover
title="详情"
:items="[
{
label: '状态',
labelIcon: CircleCheck,
content: '已完成'
}
]">
<template #reference>
<el-button>查看</el-button>
</template>
</DetailPopover>
```
## 样式自定义
可以通过 `contentClass` 为内容区域添加自定义样式类:
```vue
<DetailPopover
:items="[
{
label: '新专业',
content: '软件技术',
contentClass: 'new-major' // 添加自定义样式类
}
]">
<template #reference>
<span>查看</span>
</template>
</DetailPopover>
<style scoped>
.new-major {
color: var(--el-color-primary);
font-weight: 500;
}
</style>
```

View File

@@ -0,0 +1,185 @@
<template>
<el-popover
:placement="placement"
:width="width"
:trigger="trigger"
:popper-class="popperClass">
<template #reference>
<slot name="reference"></slot>
</template>
<!-- 弹出内容 -->
<div class="detail-popover">
<!-- 标题 -->
<div v-if="title" class="detail-title">
<el-icon v-if="titleIcon" class="title-icon">
<component :is="titleIcon" />
</el-icon>
<span>{{ title }}</span>
</div>
<!-- 详情项列表 -->
<div
v-for="(item, index) in items"
:key="index"
:class="['detail-section', { 'horizontal': item.layout === 'horizontal' }]">
<div v-if="item.label" class="section-label">
<el-icon v-if="item.labelIcon" class="label-icon">
<component :is="item.labelIcon" />
</el-icon>
<span>{{ item.label }}</span>
</div>
<div class="section-content" :class="item.contentClass">
<!-- 使用插槽自定义内容 -->
<slot
v-if="$slots[`content-${index}`]"
:name="`content-${index}`"
:item="item">
</slot>
<!-- 默认显示文本内容 -->
<template v-else>
<component
v-if="item.component"
:is="item.component"
v-bind="item.componentProps || {}">
</component>
<span v-else :class="item.contentClass">{{ item.content }}</span>
</template>
</div>
</div>
<!-- 自定义内容插槽 -->
<slot name="custom-content"></slot>
</div>
</el-popover>
</template>
<script setup lang="ts">
import type { Component } from 'vue'
export interface DetailItem {
label?: string // 标签文本
content?: string | number // 内容文本
labelIcon?: Component // 标签图标
layout?: 'horizontal' | 'vertical' // 布局方向,默认 vertical
contentClass?: string // 内容区域的自定义类名
component?: Component // 自定义组件
componentProps?: Record<string, any> // 自定义组件的 props
}
interface Props {
title?: string // 标题
titleIcon?: Component // 标题图标
items?: DetailItem[] // 详情项列表
placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end'
width?: string | number // Popover 宽度
trigger?: 'click' | 'focus' | 'hover' | 'contextmenu' // 触发方式
popperClass?: string // Popover 自定义类名
}
withDefaults(defineProps<Props>(), {
title: '',
titleIcon: undefined,
items: () => [],
placement: 'right',
width: 300,
trigger: 'click',
popperClass: ''
})
</script>
<script lang="ts">
export default {
name: 'DetailPopover'
}
</script>
<style scoped lang="scss">
.detail-popover {
padding: 0;
.detail-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 2px solid #EBEEF5;
.title-icon {
color: var(--el-color-primary);
font-size: 18px;
}
}
.detail-section {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
// 横向布局
&.horizontal {
display: flex;
align-items: center;
gap: 16px;
.section-label {
margin-bottom: 0;
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}
.section-content {
flex: 1;
}
}
// 纵向布局(默认)
&:not(.horizontal) {
.section-label {
margin-bottom: 10px;
}
}
.section-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #909399;
font-weight: 500;
.label-icon {
font-size: 14px;
color: var(--el-color-primary);
}
}
.section-content {
font-size: 14px;
color: #303133;
line-height: 1.6;
word-break: break-all;
// 新专业样式(蓝色高亮)
&.new-major {
color: var(--el-color-primary);
font-weight: 500;
}
// 支持嵌套的样式类
:deep(.new-major) {
color: var(--el-color-primary);
font-weight: 500;
}
}
}
}
</style>