This commit is contained in:
吴红兵
2026-03-07 01:34:48 +08:00
parent adc511cfdc
commit 94c3473958
1211 changed files with 599405 additions and 322105 deletions

View File

@@ -1,60 +1,59 @@
<template>
<el-tag
:effect="currentOption.effect"
v-if="currentOption"
:type="currentOption.type"
:class="{ 'audit-state-tag': showIcon && currentOption.icon }"
>
<i v-if="showIcon && currentOption.icon" :class="currentOption.icon" style="margin-right: 4px;"></i>
{{ currentOption.label }}
</el-tag>
<span v-else>{{ emptyText }}</span>
<el-tag
:effect="currentOption.effect"
v-if="currentOption"
:type="currentOption.type"
:class="{ 'audit-state-tag': showIcon && currentOption.icon }"
>
<i v-if="showIcon && currentOption.icon" :class="currentOption.icon" style="margin-right: 4px"></i>
{{ currentOption.label }}
</el-tag>
<span v-else>{{ emptyText }}</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed } from 'vue';
export interface StateOption {
value: string | number;
label: string;
type: 'success' | 'danger' | 'warning' | 'info' | '';
icon?: string;
effect?: string;
value: string | number;
label: string;
type: 'success' | 'danger' | 'warning' | 'info' | '';
icon?: string;
effect?: string;
}
interface Props {
state?: string | number;
options?: StateOption[];
showIcon?: boolean;
emptyText?: string;
state?: string | number;
options?: StateOption[];
showIcon?: boolean;
emptyText?: string;
}
const props = withDefaults(defineProps<Props>(), {
state: '',
options: () => [
{ value: '1', label: '已通过', type: 'success', icon: 'fa-solid fa-circle-check' , effect:"dark" },
{ value: '-2', label: '已驳回', type: 'danger', icon: 'fa-solid fa-circle-xmark', effect:"dark" },
{ value: '0', label: '待审核', type: 'warning', icon: 'fa-regular fa-clock' ,effect:"light" },
{ value: '10', label: '部门通过', type: 'warning', icon: 'fa-regular fa-clock' ,effect:"dark" }
],
showIcon: true,
emptyText: '-'
})
state: '',
options: () => [
{ value: '1', label: '已通过', type: 'success', icon: 'fa-solid fa-circle-check', effect: 'dark' },
{ value: '-2', label: '已驳回', type: 'danger', icon: 'fa-solid fa-circle-xmark', effect: 'dark' },
{ value: '0', label: '待审核', type: 'warning', icon: 'fa-regular fa-clock', effect: 'light' },
{ value: '10', label: '部门通过', type: 'warning', icon: 'fa-regular fa-clock', effect: 'dark' },
],
showIcon: true,
emptyText: '-',
});
// 根据 state 值查找对应的配置(统一转字符串比较,兼容数字 10 与字符串 '10'
const currentOption = computed(() => {
if (props.state == null || props.state === '') {
return null
}
const stateStr = String(props.state)
return props.options.find(option => String(option.value) === stateStr) || null
})
if (props.state == null || props.state === '') {
return null;
}
const stateStr = String(props.state);
return props.options.find((option) => String(option.value) === stateStr) || null;
});
</script>
<style scoped>
.audit-state-tag {
display: inline-flex;
align-items: center;
display: inline-flex;
align-items: center;
}
</style>

View File

@@ -13,19 +13,19 @@
## Props
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|-----|------|------|-------|--------|
| type | 标签类型 | string | success/info/warning/danger/primary | primary |
| size | 标签大小 | string | large/default/small | default |
| leftIcon | 左侧图标组件 | Component | - | undefined |
| middleIcon | 中间图标组件(如警告图标) | Component | - | undefined |
| rightIcon | 右侧图标组件 | Component | - | DArrowRight默认双箭头传 null 不显示) |
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| ---------- | -------------------------- | --------- | ----------------------------------- | ----------------------------------------- |
| type | 标签类型 | string | success/info/warning/danger/primary | primary |
| size | 标签大小 | string | large/default/small | default |
| leftIcon | 左侧图标组件 | Component | - | undefined |
| middleIcon | 中间图标组件(如警告图标) | Component | - | undefined |
| rightIcon | 右侧图标组件 | Component | - | DArrowRight默认双箭头传 null 不显示) |
## Events
| 事件名 | 说明 | 回调参数 |
|--------|------|---------|
| click | 点击标签时触发 | - |
| 事件名 | 说明 | 回调参数 |
| ------ | -------------- | -------- |
| click | 点击标签时触发 | - |
## 使用示例
@@ -33,13 +33,11 @@
```vue
<template>
<ClickableTag type="success">
成功状态
</ClickableTag>
<ClickableTag type="success"> 成功状态 </ClickableTag>
</template>
<script setup>
import ClickableTag from '/@/components/ClickableTag/index.vue'
import ClickableTag from '/@/components/ClickableTag/index.vue';
</script>
```
@@ -47,13 +45,11 @@ import ClickableTag from '/@/components/ClickableTag/index.vue'
```vue
<template>
<ClickableTag type="success" :right-icon="null">
成功状态
</ClickableTag>
<ClickableTag type="success" :right-icon="null"> 成功状态 </ClickableTag>
</template>
<script setup>
import ClickableTag from '/@/components/ClickableTag/index.vue'
import ClickableTag from '/@/components/ClickableTag/index.vue';
</script>
```
@@ -61,16 +57,12 @@ import ClickableTag from '/@/components/ClickableTag/index.vue'
```vue
<template>
<ClickableTag
type="warning"
:left-icon="Clock">
待审核
</ClickableTag>
<ClickableTag type="warning" :left-icon="Clock"> 待审核 </ClickableTag>
</template>
<script setup>
import ClickableTag from '/@/components/ClickableTag/index.vue'
import { Clock } from '@element-plus/icons-vue'
import ClickableTag from '/@/components/ClickableTag/index.vue';
import { Clock } from '@element-plus/icons-vue';
</script>
```
@@ -78,25 +70,16 @@ import { Clock } from '@element-plus/icons-vue'
```vue
<template>
<!-- 使用默认双箭头 -->
<ClickableTag
type="info"
:left-icon="Document">
未填写
</ClickableTag>
<!-- 自定义右侧图标 -->
<ClickableTag
type="info"
:left-icon="Document"
:right-icon="More">
查看更多
</ClickableTag>
<!-- 使用默认双箭头 -->
<ClickableTag type="info" :left-icon="Document"> 未填写 </ClickableTag>
<!-- 自定义右侧图标 -->
<ClickableTag type="info" :left-icon="Document" :right-icon="More"> 查看更多 </ClickableTag>
</template>
<script setup>
import ClickableTag from '/@/components/ClickableTag/index.vue'
import { Document, More } from '@element-plus/icons-vue'
import ClickableTag from '/@/components/ClickableTag/index.vue';
import { Document, More } from '@element-plus/icons-vue';
</script>
```
@@ -104,21 +87,17 @@ import { Document, More } from '@element-plus/icons-vue'
```vue
<template>
<ClickableTag
type="warning"
:left-icon="Clock"
:middle-icon="hasProblem ? WarningFilled : undefined"
:right-icon="ArrowRight">
待审核
</ClickableTag>
<ClickableTag type="warning" :left-icon="Clock" :middle-icon="hasProblem ? WarningFilled : undefined" :right-icon="ArrowRight">
待审核
</ClickableTag>
</template>
<script setup>
import { ref } from 'vue'
import ClickableTag from '/@/components/ClickableTag/index.vue'
import { Clock, WarningFilled, ArrowRight } from '@element-plus/icons-vue'
import { ref } from 'vue';
import ClickableTag from '/@/components/ClickableTag/index.vue';
import { Clock, WarningFilled, ArrowRight } from '@element-plus/icons-vue';
const hasProblem = ref(true)
const hasProblem = ref(true);
</script>
```
@@ -126,18 +105,12 @@ const hasProblem = ref(true)
```vue
<template>
<ClickableTag
type="primary"
:left-icon="User"
:middle-icon="Star"
:right-icon="More">
自定义内容
</ClickableTag>
<ClickableTag type="primary" :left-icon="User" :middle-icon="Star" :right-icon="More"> 自定义内容 </ClickableTag>
</template>
<script setup>
import ClickableTag from '/@/components/ClickableTag/index.vue'
import { User, Star, More } from '@element-plus/icons-vue'
import ClickableTag from '/@/components/ClickableTag/index.vue';
import { User, Star, More } from '@element-plus/icons-vue';
</script>
```
@@ -145,21 +118,16 @@ import { User, Star, More } from '@element-plus/icons-vue'
```vue
<template>
<ClickableTag
type="success"
:left-icon="CircleCheck"
@click="handleClick">
审核通过
</ClickableTag>
<ClickableTag type="success" :left-icon="CircleCheck" @click="handleClick"> 审核通过 </ClickableTag>
</template>
<script setup>
import ClickableTag from '/@/components/ClickableTag/index.vue'
import { CircleCheck } from '@element-plus/icons-vue'
import ClickableTag from '/@/components/ClickableTag/index.vue';
import { CircleCheck } from '@element-plus/icons-vue';
const handleClick = () => {
console.log('标签被点击了')
}
console.log('标签被点击了');
};
</script>
```
@@ -167,22 +135,18 @@ const handleClick = () => {
```vue
<template>
<el-popover placement="right" :width="320" trigger="click">
<template #reference>
<ClickableTag
type="warning"
:left-icon="Clock">
待审核
</ClickableTag>
</template>
<div>这里是弹出的详细信息</div>
</el-popover>
<el-popover placement="right" :width="320" trigger="click">
<template #reference>
<ClickableTag type="warning" :left-icon="Clock"> 待审核 </ClickableTag>
</template>
<div>这里是弹出的详细信息</div>
</el-popover>
</template>
<script setup>
import ClickableTag from '/@/components/ClickableTag/index.vue'
import { Clock } from '@element-plus/icons-vue'
import ClickableTag from '/@/components/ClickableTag/index.vue';
import { Clock } from '@element-plus/icons-vue';
</script>
```
@@ -203,11 +167,12 @@ import { Clock } from '@element-plus/icons-vue'
```vue
<!-- 自动显示默认的右侧双箭头 -->
<ClickableTag
v-if="scope.row.zlsh=='1'"
type="warning"
:left-icon="Clock"
:middle-icon="!scope.row.graPic || hasMaterialProblem ? WarningFilled : undefined">
<ClickableTag
v-if="scope.row.zlsh == '1'"
type="warning"
:left-icon="Clock"
:middle-icon="!scope.row.graPic || hasMaterialProblem ? WarningFilled : undefined"
>
待审核
</ClickableTag>
```
@@ -222,4 +187,4 @@ leftIcon middleIcon rightIcon(默认双箭头)
- **leftIcon**: 主要图标,表示状态类型
- **middleIcon**: 辅助图标,如警告提示(带脉冲动画)
- **rightIcon**: 交互提示图标,默认为双箭头 `DArrowRight`悬停时右移透明度70%),传 `null` 则不显示
- **rightIcon**: 交互提示图标,默认为双箭头 `DArrowRight`(悬停时右移,透明度 70%),传 `null` 则不显示

View File

@@ -1,125 +1,124 @@
<template>
<el-tag
:type="type"
:size="size"
:effect="effect"
:class="['clickable-tag', { 'has-action': actualRightIcon !== null}]"
:style="{ width: width ? `${width}px` : 'auto' }"
@click="handleClick">
<!-- 左侧图标支持 Vue 组件Element 图标线条或字符串 FontAwesome class可实心 -->
<i v-if="leftIcon && isLeftIconString" :class="leftIcon" class="left-icon left-icon--fa"></i>
<el-icon v-else-if="leftIcon" :size="size" class="left-icon">
<component :is="leftIcon" />
</el-icon>
<!-- 主要内容 -->
<slot></slot>
<!-- 中间图标支持 Vue 组件或字符串 FontAwesome class -->
<i v-if="middleIcon && isMiddleIconString" :class="middleIcon" class="middle-icon middle-icon--fa"></i>
<el-icon v-else-if="middleIcon" :size="size" class="middle-icon">
<component :is="middleIcon" />
</el-icon>
<!-- 右侧图标 -->
<el-icon
v-if="actualRightIcon !== null"
:size="size"
class="right-icon">
<component :is="actualRightIcon" />
</el-icon>
</el-tag>
<el-tag
:type="type"
:size="size"
:effect="effect"
:class="['clickable-tag', { 'has-action': actualRightIcon !== null }]"
:style="{ width: width ? `${width}px` : 'auto' }"
@click="handleClick"
>
<!-- 左侧图标支持 Vue 组件Element 图标线条或字符串 FontAwesome class可实心 -->
<i v-if="leftIcon && isLeftIconString" :class="leftIcon" class="left-icon left-icon--fa"></i>
<el-icon v-else-if="leftIcon" :size="size" class="left-icon">
<component :is="leftIcon" />
</el-icon>
<!-- 主要内容 -->
<slot></slot>
<!-- 中间图标支持 Vue 组件或字符串 FontAwesome class -->
<i v-if="middleIcon && isMiddleIconString" :class="middleIcon" class="middle-icon middle-icon--fa"></i>
<el-icon v-else-if="middleIcon" :size="size" class="middle-icon">
<component :is="middleIcon" />
</el-icon>
<!-- 右侧图标 -->
<el-icon v-if="actualRightIcon !== null" :size="size" class="right-icon">
<component :is="actualRightIcon" />
</el-icon>
</el-tag>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { InfoFilled, Right } from '@element-plus/icons-vue'
import { computed } from 'vue';
import { InfoFilled, Right } from '@element-plus/icons-vue';
interface Props {
type?: 'success' | 'info' | 'warning' | 'danger' | 'primary'
size?: 'large' | 'default' | 'small'
effect?: 'dark' | 'light' | 'plain' // 主题,与 el-tag 一致
leftIcon?: any // 左侧图标Vue 组件Element 图标)或字符串(如 FontAwesome class 'fa-solid fa-circle-xmark'
middleIcon?: any // 中间图标Vue 组件或字符串(如 FontAwesome class
rightIcon?: any // 右侧图标:默认 InfoFilled表示「可查看详情」传 null 不显示;也可用 Right跳转、View查看
width?: string | number // 自定义宽度
type?: 'success' | 'info' | 'warning' | 'danger' | 'primary';
size?: 'large' | 'default' | 'small';
effect?: 'dark' | 'light' | 'plain'; // 主题,与 el-tag 一致
leftIcon?: any; // 左侧图标Vue 组件Element 图标)或字符串(如 FontAwesome class 'fa-solid fa-circle-xmark'
middleIcon?: any; // 中间图标Vue 组件或字符串(如 FontAwesome class
rightIcon?: any; // 右侧图标:默认 InfoFilled表示「可查看详情」传 null 不显示;也可用 Right跳转、View查看
width?: string | number; // 自定义宽度
}
const props = withDefaults(defineProps<Props>(), {
type: 'primary',
size: 'default',
effect: 'light',
leftIcon: undefined,
middleIcon: undefined,
rightIcon: undefined,
width: undefined
})
type: 'primary',
size: 'default',
effect: 'light',
leftIcon: undefined,
middleIcon: undefined,
rightIcon: undefined,
width: undefined,
});
// 左侧/中间图标为字符串时(如 FontAwesome class用 <i> 渲染,与 AuditState 实心一致
const isLeftIconString = computed(() => typeof props.leftIcon === 'string')
const isMiddleIconString = computed(() => typeof props.middleIcon === 'string')
const isLeftIconString = computed(() => typeof props.leftIcon === 'string');
const isMiddleIconString = computed(() => typeof props.middleIcon === 'string');
// 获取实际的右侧图标:未传值时使用默认图标,传 null 则不显示
const actualRightIcon = computed(() => {
if (props.rightIcon === null) return null
return props.rightIcon || Right
})
if (props.rightIcon === null) return null;
return props.rightIcon || Right;
});
const emit = defineEmits<{
click: []
}>()
click: [];
}>();
const handleClick = () => {
emit('click')
}
emit('click');
};
</script>
<script lang="ts">
export default {
name: 'ClickableTag'
}
name: 'ClickableTag',
};
</script>
<style scoped lang="scss">
.clickable-tag {
transition: all 0.2s;
display: inline-flex;
align-items: center;
.left-icon {
margin-right: 4px;
}
// 覆盖 el-tag 的内部结构
:deep(.el-tag__content) {
display: flex !important;
align-items: center !important;
gap: 4px;
}
// 有交互功能时才显示手型光标和悬停效果
&.has-action {
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.right-icon {
transform: translateX(2px);
}
}
}
.middle-icon {
animation: pulse 1.5s ease-in-out infinite;
}
.right-icon {
opacity: 0.7;
transition: all 0.2s;
}
transition: all 0.2s;
display: inline-flex;
align-items: center;
.left-icon {
margin-right: 4px;
}
// 覆盖 el-tag 的内部结构
:deep(.el-tag__content) {
display: flex !important;
align-items: center !important;
gap: 4px;
}
// 有交互功能时才显示手型光标和悬停效果
&.has-action {
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.right-icon {
transform: translateX(2px);
}
}
}
.middle-icon {
animation: pulse 1.5s ease-in-out infinite;
}
.right-icon {
opacity: 0.7;
transition: all 0.2s;
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
</style>

View File

@@ -1,83 +1,81 @@
<template>
<el-form-item :label="label" :required="required" :style="outerStyle">
<el-row :gutter="20">
<el-col :span="11">
<el-form-item :prop="startProp">
<slot name="startDatePicker">
</slot>
</el-form-item>
</el-col>
<el-col :span="2" class="time-separator">
<slot name="separator">
<span>{{ separator }}</span>
</slot>
</el-col>
<el-col :span="11">
<el-form-item :prop="endProp">
<slot name="endDatePicker">
</slot>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item :label="label" :required="required" :style="outerStyle">
<el-row :gutter="20">
<el-col :span="11">
<el-form-item :prop="startProp">
<slot name="startDatePicker"> </slot>
</el-form-item>
</el-col>
<el-col :span="2" class="time-separator">
<slot name="separator">
<span>{{ separator }}</span>
</slot>
</el-col>
<el-col :span="11">
<el-form-item :prop="endProp">
<slot name="endDatePicker"> </slot>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed } from 'vue';
interface Props {
label: string
startProp: string
endProp: string
startValue: string
endValue: string
required?: boolean
separator?: string
startPlaceholder?: string
endPlaceholder?: string
outerStyle?: string | Record<string, string>
label: string;
startProp: string;
endProp: string;
startValue: string;
endValue: string;
required?: boolean;
separator?: string;
startPlaceholder?: string;
endPlaceholder?: string;
outerStyle?: string | Record<string, string>;
}
const props = withDefaults(defineProps<Props>(), {
required: false,
separator: '至',
outerStyle: () => ({})
})
required: false,
separator: '至',
outerStyle: () => ({}),
});
// 将 outerStyle 字符串或对象转换为样式对象
const outerStyle = computed(() => {
if (typeof props.outerStyle === 'object' && !Array.isArray(props.outerStyle)) {
return props.outerStyle
}
if (typeof props.outerStyle === 'string' && props.outerStyle) {
const style: Record<string, string> = {}
props.outerStyle.split(';').forEach((rule) => {
const [key, value] = rule.split(':').map(s => s.trim())
if (key && value) {
// 将 kebab-case 转换为 camelCase
const camelKey = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase())
style[camelKey] = value
}
})
return style
}
return {}
})
if (typeof props.outerStyle === 'object' && !Array.isArray(props.outerStyle)) {
return props.outerStyle;
}
if (typeof props.outerStyle === 'string' && props.outerStyle) {
const style: Record<string, string> = {};
props.outerStyle.split(';').forEach((rule) => {
const [key, value] = rule.split(':').map((s) => s.trim());
if (key && value) {
// 将 kebab-case 转换为 camelCase
const camelKey = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
style[camelKey] = value;
}
});
return style;
}
return {};
});
</script>
<style lang="scss" scoped>
.time-separator {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 32px;
span {
color: #909399;
font-size: 14px;
line-height: 1;
display: inline-block;
}
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 32px;
span {
color: #909399;
font-size: 14px;
line-height: 1;
display: inline-block;
}
}
</style>

View File

@@ -13,37 +13,37 @@
## 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 | - | '' |
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| ----------- | ------------------ | ---------------- | --------------------------------------------------------------------------------------------------------- | --------- |
| 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
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 | 自定义内容(显示在所有详情项之后) | - |
| 插槽名 | 说明 | 参数 |
| --------------- | ---------------------------------- | -------------------- |
| reference | 触发元素 | - |
| content-{index} | 自定义第 index 项的内容 | { item: DetailItem } |
| custom-content | 自定义内容(显示在所有详情项之后) | - |
## 使用示例
@@ -51,23 +51,24 @@ interface DetailItem {
```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>
<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'
import DetailPopover from '/@/components/DetailPopover/index.vue';
import { InfoFilled } from '@element-plus/icons-vue';
</script>
```
@@ -75,14 +76,15 @@ import { InfoFilled } from '@element-plus/icons-vue'
```vue
<DetailPopover
title="异动审核详情"
:items="[
{
label: '审核状态',
layout: 'horizontal',
content: '待审核'
}
]">
title="异动审核详情"
:items="[
{
label: '审核状态',
layout: 'horizontal',
content: '待审核',
},
]"
>
<template #reference>
<el-button>查看</el-button>
</template>
@@ -93,11 +95,12 @@ import { InfoFilled } from '@element-plus/icons-vue'
```vue
<DetailPopover
title="异动审核详情"
:items="[
{ label: '审核状态', layout: 'horizontal' },
{ label: '备注信息', contentClass: 'reason-content' }
]">
title="异动审核详情"
:items="[
{ label: '审核状态', layout: 'horizontal' },
{ label: '备注信息', contentClass: 'reason-content' },
]"
>
<template #reference>
<ClickableTag>待审核</ClickableTag>
</template>
@@ -121,14 +124,15 @@ import { InfoFilled } from '@element-plus/icons-vue'
```vue
<DetailPopover
title="详情"
:items="[
{
label: '状态',
labelIcon: CircleCheck,
content: '已完成'
}
]">
title="详情"
:items="[
{
label: '状态',
labelIcon: CircleCheck,
content: '已完成',
},
]"
>
<template #reference>
<el-button>查看</el-button>
</template>
@@ -141,13 +145,14 @@ import { InfoFilled } from '@element-plus/icons-vue'
```vue
<DetailPopover
:items="[
{
label: '新专业',
content: '软件技术',
contentClass: 'new-major' // 添加自定义样式类
}
]">
:items="[
{
label: '新专业',
content: '软件技术',
contentClass: 'new-major', // 添加自定义样式类
},
]"
>
<template #reference>
<span>查看</span>
</template>
@@ -155,8 +160,8 @@ import { InfoFilled } from '@element-plus/icons-vue'
<style scoped>
.new-major {
color: var(--el-color-primary);
font-weight: 500;
color: var(--el-color-primary);
font-weight: 500;
}
</style>
```

View File

@@ -1,185 +1,182 @@
<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>
<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'
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
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 自定义类名
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: ''
})
title: '',
titleIcon: undefined,
items: () => [],
placement: 'right',
width: 300,
trigger: 'click',
popperClass: '',
});
</script>
<script lang="ts">
export default {
name: 'DetailPopover'
}
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;
}
}
}
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>

View File

@@ -52,11 +52,11 @@ const dictList = ref<DictOption[]>([]);
async function loadDictData() {
if (props.dictType) {
const res = await getDicts(props.dictType);
dictList.value = res.data.map((p: any) => ({
label: p.label,
value: p.value
}));
const res = await getDicts(props.dictType);
dictList.value = res.data.map((p: any) => ({
label: p.label,
value: p.value,
}));
}
}

View File

@@ -22,15 +22,7 @@
<template #default="scope">
<div :class="['form-table-handle', { 'form-table-handle-delete': !hideDelete }]">
<span>{{ scope.$index + 1 }}</span>
<el-button
v-if="!hideDelete"
type="danger"
icon="Delete"
size="small"
plain
circle
@click="rowDel(scope.row, scope.$index)"
></el-button>
<el-button v-if="!hideDelete" type="danger" icon="Delete" size="small" plain circle @click="rowDel(scope.row, scope.$index)"></el-button>
</div>
</template>
</el-table-column>

View File

@@ -1,114 +1,109 @@
<template>
<el-tag
v-if="showTag"
:type="type"
:effect="effect"
>
<span class="gender-tag">
<el-icon>
<Male v-if="isMale" />
<Female v-else-if="isFemale" />
</el-icon>
<span class="gender-label">{{ label }}</span>
</span>
</el-tag>
<span v-else class="gender-tag" :class="genderClass">
<el-icon>
<Male v-if="isMale" />
<Female v-else-if="isFemale" />
</el-icon>
<span class="gender-label">{{ label }}</span>
</span>
<el-tag v-if="showTag" :type="type" :effect="effect">
<span class="gender-tag">
<el-icon>
<Male v-if="isMale" />
<Female v-else-if="isFemale" />
</el-icon>
<span class="gender-label">{{ label }}</span>
</span>
</el-tag>
<span v-else class="gender-tag" :class="genderClass">
<el-icon>
<Male v-if="isMale" />
<Female v-else-if="isFemale" />
</el-icon>
<span class="gender-label">{{ label }}</span>
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Male, Female } from '@element-plus/icons-vue'
import { computed } from 'vue';
import { Male, Female } from '@element-plus/icons-vue';
interface Props {
sex?: string | number; // 性别值1=男0=女
showTag?: boolean; // 是否显示标签样式(有边框和背景),默认为 false
sex?: string | number; // 性别值1=男0=女
showTag?: boolean; // 是否显示标签样式(有边框和背景),默认为 false
}
const props = withDefaults(defineProps<Props>(), {
sex: '',
showTag: false
})
sex: '',
showTag: false,
});
// 判断是否为男性1=男)
const isMale = computed(() => {
const sex = String(props.sex)
const sexNum = Number(props.sex)
return sexNum === 1 || sex === '1'
})
const sex = String(props.sex);
const sexNum = Number(props.sex);
return sexNum === 1 || sex === '1';
});
// 判断是否为女性0=女)
const isFemale = computed(() => {
const sex = String(props.sex)
const sexNum = Number(props.sex)
return sexNum === 0 || sex === '0'
})
const sex = String(props.sex);
const sexNum = Number(props.sex);
return sexNum === 0 || sex === '0';
});
// 根据性别计算显示内容
const label = computed(() => {
if (isMale.value) {
return '男'
} else if (isFemale.value) {
return '女'
}
return '-'
})
if (isMale.value) {
return '男';
} else if (isFemale.value) {
return '女';
}
return '-';
});
const type = computed(() => {
if (isMale.value) {
return 'primary' // 蓝色
} else if (isFemale.value) {
return 'danger' // 红色/粉色
}
return 'info' // 灰色
})
if (isMale.value) {
return 'primary'; // 蓝色
} else if (isFemale.value) {
return 'danger'; // 红色/粉色
}
return 'info'; // 灰色
});
const effect = computed(() => {
if (isMale.value || isFemale.value) {
return 'light'
}
return 'plain'
})
if (isMale.value || isFemale.value) {
return 'light';
}
return 'plain';
});
const genderClass = computed(() => {
if (isMale.value) {
return 'gender-male'
} else if (isFemale.value) {
return 'gender-female'
}
return 'gender-unknown'
})
if (isMale.value) {
return 'gender-male';
} else if (isFemale.value) {
return 'gender-female';
}
return 'gender-unknown';
});
</script>
<style scoped>
.gender-tag {
display: inline-flex;
align-items: center;
gap: 2px;
border: none;
background: transparent;
padding: 0;
display: inline-flex;
align-items: center;
gap: 2px;
border: none;
background: transparent;
padding: 0;
}
.gender-tag .el-icon {
font-size: 12px;
font-size: 12px;
}
.gender-male {
color: var(--el-color-primary);
color: var(--el-color-primary);
}
.gender-female {
color: var(--el-color-danger);
color: var(--el-color-danger);
}
.gender-unknown {
color: var(--el-text-color-secondary);
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -236,13 +236,13 @@ const initResize = () => {
// 页面加载时
onMounted(() => {
initFontIconData(initFontIconName());
window.addEventListener('resize', getInputWidth);
window.addEventListener('resize', getInputWidth);
getInputWidth();
});
onUnmounted(() => {
// 移除监听
window.removeEventListener("resize", getInputWidth);
})
// 移除监听
window.removeEventListener('resize', getInputWidth);
});
// 监听双向绑定 modelValue 的变化
watch(
() => props.modelValue,

View File

@@ -1,168 +1,162 @@
<template>
<div class="icon-text" :style="containerStyle">
<el-icon
v-if="icon"
class="icon-text-icon"
:style="iconStyle">
<component :is="icon" />
</el-icon>
<span
v-if="text"
class="icon-text-text"
:style="textStyle">
{{ text }}
</span>
<slot />
</div>
<div class="icon-text" :style="containerStyle">
<el-icon v-if="icon" class="icon-text-icon" :style="iconStyle">
<component :is="icon" />
</el-icon>
<span v-if="text" class="icon-text-text" :style="textStyle">
{{ text }}
</span>
<slot />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { Component } from 'vue'
import { computed } from 'vue';
import type { Component } from 'vue';
// 尺寸配置映射(参考 el-tag 的尺寸)
const sizeConfig = {
small: {
iconSize: 14,
textSize: 12,
gap: 4
},
medium: {
iconSize: 16,
textSize: 14,
gap: 4
},
large: {
iconSize: 20,
textSize: 16,
gap: 5
}
}
small: {
iconSize: 14,
textSize: 12,
gap: 4,
},
medium: {
iconSize: 16,
textSize: 14,
gap: 4,
},
large: {
iconSize: 20,
textSize: 16,
gap: 5,
},
};
// 类型颜色映射(与 el-tag 保持一致,提高可读性)
const typeColorMap: Record<string, string> = {
success: '#67c23a', // 成功 - 绿色
danger: '#f56c6c', // 危险 - 红色
warning: '#e6a23c', // 警告 - 橙色
info: '#909399', // 信息 - 灰色
primary: '#409eff' // 主要 - 蓝色
}
success: '#67c23a', // 成功 - 绿色
danger: '#f56c6c', // 危险 - 红色
warning: '#e6a23c', // 警告 - 橙色
info: '#909399', // 信息 - 灰色
primary: '#409eff', // 主要 - 蓝色
};
interface Props {
// 图标组件Element Plus 图标)
icon?: Component | string
// 文字内容
text?: string
// 类型(类似 el-tag 的 typesuccess、danger、warning、info、primary
type?: 'success' | 'danger' | 'warning' | 'info' | 'primary'
// 尺寸(类似 el-tag 的 sizesmall、medium、large
size?: 'small' | 'medium' | 'large'
// 图标颜色(优先级高于 type
iconColor?: string
// 文字颜色(优先级高于 type
textColor?: string
// 图标尺寸单位px优先级高于 size
iconSize?: number | string
// 文字尺寸单位px优先级高于 size
textSize?: number | string
// 图标和文字之间的间距单位px优先级高于 size
gap?: number | string
// 对齐方式
align?: 'left' | 'center' | 'right'
// 图标组件Element Plus 图标)
icon?: Component | string;
// 文字内容
text?: string;
// 类型(类似 el-tag 的 typesuccess、danger、warning、info、primary
type?: 'success' | 'danger' | 'warning' | 'info' | 'primary';
// 尺寸(类似 el-tag 的 sizesmall、medium、large
size?: 'small' | 'medium' | 'large';
// 图标颜色(优先级高于 type
iconColor?: string;
// 文字颜色(优先级高于 type
textColor?: string;
// 图标尺寸单位px优先级高于 size
iconSize?: number | string;
// 文字尺寸单位px优先级高于 size
textSize?: number | string;
// 图标和文字之间的间距单位px优先级高于 size
gap?: number | string;
// 对齐方式
align?: 'left' | 'center' | 'right';
}
const props = withDefaults(defineProps<Props>(), {
icon: undefined,
text: '',
type: undefined,
size: 'medium',
iconColor: '',
textColor: '',
iconSize: undefined,
textSize: undefined,
gap: undefined,
align: 'left'
})
icon: undefined,
text: '',
type: undefined,
size: 'medium',
iconColor: '',
textColor: '',
iconSize: undefined,
textSize: undefined,
gap: undefined,
align: 'left',
});
// 获取实际尺寸配置
const actualSizeConfig = computed(() => {
const config = sizeConfig[props.size]
return {
iconSize: props.iconSize ?? config.iconSize,
textSize: props.textSize ?? config.textSize,
gap: props.gap ?? config.gap
}
})
const config = sizeConfig[props.size];
return {
iconSize: props.iconSize ?? config.iconSize,
textSize: props.textSize ?? config.textSize,
gap: props.gap ?? config.gap,
};
});
// 获取实际颜色
const actualIconColor = computed(() => {
if (props.iconColor) return props.iconColor
if (props.type && typeColorMap[props.type]) return typeColorMap[props.type]
return ''
})
if (props.iconColor) return props.iconColor;
if (props.type && typeColorMap[props.type]) return typeColorMap[props.type];
return '';
});
const actualTextColor = computed(() => {
if (props.textColor) return props.textColor
if (props.type && typeColorMap[props.type]) return typeColorMap[props.type]
return ''
})
if (props.textColor) return props.textColor;
if (props.type && typeColorMap[props.type]) return typeColorMap[props.type];
return '';
});
// 容器样式
const containerStyle = computed(() => {
const styles: Record<string, string> = {
display: 'inline-flex',
alignItems: 'center',
gap: `${actualSizeConfig.value.gap}px`
}
if (props.align === 'center') {
styles.justifyContent = 'center'
} else if (props.align === 'right') {
styles.justifyContent = 'flex-end'
}
return styles
})
const styles: Record<string, string> = {
display: 'inline-flex',
alignItems: 'center',
gap: `${actualSizeConfig.value.gap}px`,
};
if (props.align === 'center') {
styles.justifyContent = 'center';
} else if (props.align === 'right') {
styles.justifyContent = 'flex-end';
}
return styles;
});
// 图标样式
const iconStyle = computed(() => {
const styles: Record<string, string> = {
fontSize: `${actualSizeConfig.value.iconSize}px`,
verticalAlign: 'middle'
}
if (actualIconColor.value) {
styles.color = actualIconColor.value
}
return styles
})
const styles: Record<string, string> = {
fontSize: `${actualSizeConfig.value.iconSize}px`,
verticalAlign: 'middle',
};
if (actualIconColor.value) {
styles.color = actualIconColor.value;
}
return styles;
});
// 文字样式
const textStyle = computed(() => {
const styles: Record<string, string> = {
fontSize: `${actualSizeConfig.value.textSize}px`,
verticalAlign: 'middle'
}
if (actualTextColor.value) {
styles.color = actualTextColor.value
}
return styles
})
const styles: Record<string, string> = {
fontSize: `${actualSizeConfig.value.textSize}px`,
verticalAlign: 'middle',
};
if (actualTextColor.value) {
styles.color = actualTextColor.value;
}
return styles;
});
</script>
<style scoped lang="scss">
.icon-text {
line-height: 1;
.icon-text-icon {
flex-shrink: 0;
}
.icon-text-text {
white-space: nowrap;
}
line-height: 1;
.icon-text-icon {
flex-shrink: 0;
}
.icon-text-text {
white-space: nowrap;
}
}
</style>

View File

@@ -8,7 +8,7 @@
</template>
<script lang="ts" setup>
import {type Link, LinkTypeEnum} from '.';
import { type Link, LinkTypeEnum } from '.';
import LinkContent from './index.vue';
import Popup from '/@/components/Popup/index.vue';

View File

@@ -155,7 +155,7 @@ const linkList = ref([
path: '/pages/home/biz/user/user-manage',
name: '用户管理',
type: LinkTypeEnum.SHOP_PAGES,
}
},
]);
const handleSelect = (value: Link) => {

View File

@@ -1,364 +1,347 @@
<template>
<div class="material">
<div class="material__left">
<div class="flex-1 min-h-0">
<el-scrollbar>
<div class="pt-4 material-left__content p-b-4">
<el-tree
ref="treeRef"
node-key="id"
:data="cateLists"
empty-text="''"
:highlight-current="true"
:expand-on-click-node="false"
:current-node-key="cateId"
@node-click="handleCatSelect"
>
<template v-slot="{ data }">
<div class="flex flex-1 items-center pr-4 min-w-0">
<img class="w-[20px] h-[16px] mr-3" src="/@/assets/icon_folder.png"/>
<span class="flex-1 mr-2 truncate">
<div class="material">
<div class="material__left">
<div class="flex-1 min-h-0">
<el-scrollbar>
<div class="pt-4 material-left__content p-b-4">
<el-tree
ref="treeRef"
node-key="id"
:data="cateLists"
empty-text="''"
:highlight-current="true"
:expand-on-click-node="false"
:current-node-key="cateId"
@node-click="handleCatSelect"
>
<template v-slot="{ data }">
<div class="flex flex-1 items-center pr-4 min-w-0">
<img class="w-[20px] h-[16px] mr-3" src="/@/assets/icon_folder.png" />
<span class="flex-1 mr-2 truncate">
{{ data.name }}
</span>
<el-dropdown v-if="data.id > 0" :hide-on-click="false">
<span class="muted m-r-10">···</span>
<template #dropdown>
<el-dropdown-menu>
<popover-input
@confirm="handleEditCate($event, data.id)"
size="default"
:value="data.name"
width="400px"
:limit="20"
show-limit
teleported
>
<div>
<el-dropdown-item> {{ $t('material.editGroup') }}</el-dropdown-item>
</div>
</popover-input>
<div @click="handleDeleteCate(data.id)">
<el-dropdown-item>{{ $t('material.delGroup') }}</el-dropdown-item>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-tree>
</div>
</el-scrollbar>
</div>
<el-dropdown v-if="data.id > 0" :hide-on-click="false">
<span class="muted m-r-10">···</span>
<template #dropdown>
<el-dropdown-menu>
<popover-input
@confirm="handleEditCate($event, data.id)"
size="default"
:value="data.name"
width="400px"
:limit="20"
show-limit
teleported
>
<div>
<el-dropdown-item> {{ $t('material.editGroup') }}</el-dropdown-item>
</div>
</popover-input>
<div @click="handleDeleteCate(data.id)">
<el-dropdown-item>{{ $t('material.delGroup') }}</el-dropdown-item>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-tree>
</div>
</el-scrollbar>
</div>
<div class="flex justify-center p-2 border-t border-br">
<popover-input @confirm="handleAddCate" size="default" width="400px" :limit="20" show-limit teleported>
<el-button> {{ $t('material.addGroup') }}</el-button>
</popover-input>
</div>
</div>
<div class="flex flex-col material__center">
<div class="flex operate-btn">
<div class="flex flex-1">
<el-button icon="folder-add" type="primary" class="ml10" v-auth="'sys_file_del'" @click="visibleUpload = true"
>{{ $t('material.uploadFileTip') }}
</el-button>
<div class="flex justify-center p-2 border-t border-br">
<popover-input @confirm="handleAddCate" size="default" width="400px" :limit="20" show-limit teleported>
<el-button> {{ $t('material.addGroup') }}</el-button>
</popover-input>
</div>
</div>
<div class="flex flex-col material__center">
<div class="flex operate-btn">
<div class="flex flex-1">
<el-button icon="folder-add" type="primary" class="ml10" v-auth="'sys_file_del'" @click="visibleUpload = true"
>{{ $t('material.uploadFileTip') }}
</el-button>
<el-button v-if="mode == 'page'" :disabled="!select.length" @click.stop="batchFileDelete()">
{{ $t('common.delBtn') }}
</el-button>
<el-button v-if="mode == 'page'" :disabled="!select.length" @click.stop="batchFileDelete()">
{{ $t('common.delBtn') }}
</el-button>
<popup v-if="mode == 'page'" class="ml-3" @confirm="batchFileMove" :disabled="!select.length"
:title="$t('material.moveBtn')">
<template #trigger>
<el-button :disabled="!select.length">{{ $t('material.moveBtn') }}</el-button>
</template>
<popup v-if="mode == 'page'" class="ml-3" @confirm="batchFileMove" :disabled="!select.length" :title="$t('material.moveBtn')">
<template #trigger>
<el-button :disabled="!select.length">{{ $t('material.moveBtn') }}</el-button>
</template>
<div>
<span class="mr-5">移动文件至</span>
<el-select v-model="moveId" placeholder="请选择">
<template v-for="item in cateLists" :key="item.id">
<el-option v-if="item.id !== ''" :label="item.name" :value="item.id"></el-option>
</template>
</el-select>
</div>
</popup>
</div>
<el-input class="mr-16 ml-80" :placeholder="$t('file.inputfileNameTip')" v-model="fileParams.original"
@keyup.enter="refresh">
<template #append>
<el-button @click="refresh">
<template #icon>
<el-icon>
<Search/>
</el-icon>
</template>
</el-button>
</template>
</el-input>
<div class="flex gap-2 items-center">
<el-tooltip :content="$t('material.list')" placement="top">
<div
class="flex justify-center items-center w-8 h-8 list-icon"
:class="{
'bg-primary-light-8 text-primary': listShowType === 'table'
}"
@click="listShowType = 'table'"
>
<el-icon>
<Expand/>
</el-icon>
</div>
</el-tooltip>
<el-tooltip :content="$t('material.grid')" placement="top">
<div
class="flex justify-center items-center w-8 h-8 list-icon"
:class="{
'bg-primary-light-8 text-primary': listShowType === 'normal'
}"
@click="listShowType = 'normal'"
>
<el-icon>
<Menu/>
</el-icon>
</div>
</el-tooltip>
</div>
</div>
<div class="mt-3" v-if="mode == 'page'">
<el-checkbox :disabled="!pager.lists.length" v-model="isCheckAll" @change="selectAll"
:indeterminate="isIndeterminate">
{{ $t('material.allCheck') }}
</el-checkbox>
</div>
<div class="flex flex-col flex-1 mb-1 min-h-0 material-center__content">
<el-scrollbar v-if="pager.lists.length" v-show="listShowType == 'normal'">
<ul class="flex flex-wrap mt-4 file-list">
<li class="file-item-wrap" v-for="item in pager.lists" :key="item.id" :style="{ width: fileSize }">
<del-wrap @close="batchFileDelete([item.id])">
<file-item :uri="getFileUri(item)" :file-size="fileSize" :type="type" @click="selectFile(item)">
<div class="item-selected" v-if="isSelect(item.id)">
<el-icon class="el-input__icon">
<Check/>
</el-icon>
</div>
</file-item>
</del-wrap>
<div class="flex justify-center items-center mt-2">
{{ item.original }}
</div>
<div class="flex justify-center items-center operation-btns">
<popover-input
@confirm="handleFileRename($event, item.id)"
size="default"
:value="item.name"
width="400px"
:limit="50"
show-limit
teleported
>
<el-button type="primary" link> {{ $t('material.rename') }}</el-button>
</popover-input>
<el-button type="primary" link @click="handleDownFile(item)"> {{ $t('material.download') }}</el-button>
<el-button type="primary" link @click="handlePreview(item)"> {{ $t('material.view') }}</el-button>
</div>
</li>
</ul>
</el-scrollbar>
<div>
<span class="mr-5">移动文件至</span>
<el-select v-model="moveId" placeholder="请选择">
<template v-for="item in cateLists" :key="item.id">
<el-option v-if="item.id !== ''" :label="item.name" :value="item.id"></el-option>
</template>
</el-select>
</div>
</popup>
</div>
<el-input class="mr-16 ml-80" :placeholder="$t('file.inputfileNameTip')" v-model="fileParams.original" @keyup.enter="refresh">
<template #append>
<el-button @click="refresh">
<template #icon>
<el-icon>
<Search />
</el-icon>
</template>
</el-button>
</template>
</el-input>
<div class="flex gap-2 items-center">
<el-tooltip :content="$t('material.list')" placement="top">
<div
class="flex justify-center items-center w-8 h-8 list-icon"
:class="{
'bg-primary-light-8 text-primary': listShowType === 'table',
}"
@click="listShowType = 'table'"
>
<el-icon>
<Expand />
</el-icon>
</div>
</el-tooltip>
<el-tooltip :content="$t('material.grid')" placement="top">
<div
class="flex justify-center items-center w-8 h-8 list-icon"
:class="{
'bg-primary-light-8 text-primary': listShowType === 'normal',
}"
@click="listShowType = 'normal'"
>
<el-icon>
<Menu />
</el-icon>
</div>
</el-tooltip>
</div>
</div>
<div class="mt-3" v-if="mode == 'page'">
<el-checkbox :disabled="!pager.lists.length" v-model="isCheckAll" @change="selectAll" :indeterminate="isIndeterminate">
{{ $t('material.allCheck') }}
</el-checkbox>
</div>
<div class="flex flex-col flex-1 mb-1 min-h-0 material-center__content">
<el-scrollbar v-if="pager.lists.length" v-show="listShowType == 'normal'">
<ul class="flex flex-wrap mt-4 file-list">
<li class="file-item-wrap" v-for="item in pager.lists" :key="item.id" :style="{ width: fileSize }">
<del-wrap @close="batchFileDelete([item.id])">
<file-item :uri="getFileUri(item)" :file-size="fileSize" :type="type" @click="selectFile(item)">
<div class="item-selected" v-if="isSelect(item.id)">
<el-icon class="el-input__icon">
<Check />
</el-icon>
</div>
</file-item>
</del-wrap>
<div class="flex justify-center items-center mt-2">
{{ item.original }}
</div>
<div class="flex justify-center items-center operation-btns">
<popover-input
@confirm="handleFileRename($event, item.id)"
size="default"
:value="item.name"
width="400px"
:limit="50"
show-limit
teleported
>
<el-button type="primary" link> {{ $t('material.rename') }}</el-button>
</popover-input>
<el-button type="primary" link @click="handleDownFile(item)"> {{ $t('material.download') }}</el-button>
<el-button type="primary" link @click="handlePreview(item)"> {{ $t('material.view') }}</el-button>
</div>
</li>
</ul>
</el-scrollbar>
<el-table
ref="tableRef"
class="mt-4"
v-show="listShowType == 'table'"
:data="pager.lists"
width="100%"
height="100%"
size="large"
@row-click="selectFile"
>
<el-table-column width="55">
<template #default="{ row }">
<el-checkbox :modelValue="isSelect(row.id)" @change="selectFile(row)"/>
</template>
</el-table-column>
<el-table-column label="图片" width="100">
<template #default="{ row }">
<file-item :uri="getFileUri(row)" file-size="50px" :type="type"></file-item>
</template>
</el-table-column>
<el-table-column label="名称" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
<el-link @click.stop="handlePreview(getFileUri(row))" :underline="false">
{{ row.original }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="createTime" label="上传时间" min-width="100"/>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<div class="inline-block">
<popover-input
@confirm="handleFileRename($event, row.id)"
size="default"
:value="row.name"
width="400px"
:limit="50"
show-limit
teleported
>
<el-button type="primary" link> 重命名</el-button>
</popover-input>
</div>
<div class="inline-block">
<el-button type="primary" link @click.stop="handlePreview(getFileUri(row))"> 查看</el-button>
</div>
<div class="inline-block">
<el-button type="primary" link @click.stop="batchFileDelete([row.id])"> 删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-table
ref="tableRef"
class="mt-4"
v-show="listShowType == 'table'"
:data="pager.lists"
width="100%"
height="100%"
size="large"
@row-click="selectFile"
>
<el-table-column width="55">
<template #default="{ row }">
<el-checkbox :modelValue="isSelect(row.id)" @change="selectFile(row)" />
</template>
</el-table-column>
<el-table-column label="图片" width="100">
<template #default="{ row }">
<file-item :uri="getFileUri(row)" file-size="50px" :type="type"></file-item>
</template>
</el-table-column>
<el-table-column label="名称" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
<el-link @click.stop="handlePreview(getFileUri(row))" :underline="false">
{{ row.original }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="createTime" label="上传时间" min-width="100" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<div class="inline-block">
<popover-input
@confirm="handleFileRename($event, row.id)"
size="default"
:value="row.name"
width="400px"
:limit="50"
show-limit
teleported
>
<el-button type="primary" link> 重命名</el-button>
</popover-input>
</div>
<div class="inline-block">
<el-button type="primary" link @click.stop="handlePreview(getFileUri(row))"> 查看</el-button>
</div>
<div class="inline-block">
<el-button type="primary" link @click.stop="batchFileDelete([row.id])"> 删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="flex flex-1 justify-center items-center" v-if="!pager.lists.length">{{
$t('el.transfer.noData')
}}~
</div>
</div>
<div>
<pagination v-bind="pager" @current-change="currentChangeHandle" layout="total, prev, pager, next, jumper"/>
</div>
</div>
<div class="material__right" v-if="mode == 'picker'">
<div class="flex flex-wrap justify-between p-2">
<div class="flex items-center sm">
已选择 {{ select.length }}
<span v-if="limit">/{{ limit }}</span>
</div>
<el-button type="primary" link @click="clearSelect">清空</el-button>
</div>
<div class="flex-1 min-h-0">
<el-scrollbar class="ls-scrollbar">
<ul class="flex flex-col select-lists p-t-3">
<li class="mb-4" v-for="item in select" :key="item.id">
<div class="select-item">
<del-wrap @close="cancelSelete(item.id)">
<file-item :uri="item.uri" file-size="100px" :type="type"></file-item>
</del-wrap>
</div>
</li>
</ul>
</el-scrollbar>
</div>
</div>
<preview v-model="showPreview" :url="previewUrl" :type="type" :fileName="fileName"/>
</div>
<div class="flex flex-1 justify-center items-center" v-if="!pager.lists.length">{{ $t('el.transfer.noData') }}~</div>
</div>
<div>
<pagination v-bind="pager" @current-change="currentChangeHandle" layout="total, prev, pager, next, jumper" />
</div>
</div>
<div class="material__right" v-if="mode == 'picker'">
<div class="flex flex-wrap justify-between p-2">
<div class="flex items-center sm">
已选择 {{ select.length }}
<span v-if="limit">/{{ limit }}</span>
</div>
<el-button type="primary" link @click="clearSelect">清空</el-button>
</div>
<div class="flex-1 min-h-0">
<el-scrollbar class="ls-scrollbar">
<ul class="flex flex-col select-lists p-t-3">
<li class="mb-4" v-for="item in select" :key="item.id">
<div class="select-item">
<del-wrap @close="cancelSelete(item.id)">
<file-item :uri="item.uri" file-size="100px" :type="type"></file-item>
</del-wrap>
</div>
</li>
</ul>
</el-scrollbar>
</div>
</div>
<preview v-model="showPreview" :url="previewUrl" :type="type" :fileName="fileName" />
</div>
<el-dialog :title="$t('material.uploadFileTip')" v-model="visibleUpload" :destroy-on-close="true" draggable>
<upload-file @change="refresh" v-if="props.type === 'image'" :data="{ groupId: cateId, type: typeValue }"
:fileType="['png', 'jpg', 'jpeg']"/>
<el-dialog :title="$t('material.uploadFileTip')" v-model="visibleUpload" :destroy-on-close="true" draggable>
<upload-file @change="refresh" v-if="props.type === 'image'" :data="{ groupId: cateId, type: typeValue }" :fileType="['png', 'jpg', 'jpeg']" />
<upload-file @change="refresh" v-if="props.type === 'video'" :data="{ groupId: cateId, type: typeValue }"
:fileType="['mp4']"/>
<upload-file @change="refresh" v-if="props.type === 'video'" :data="{ groupId: cateId, type: typeValue }" :fileType="['mp4']" />
<upload-file
@change="refresh"
v-if="props.type === 'file'"
:data="{ cid: cateId, type: typeValue }"
:fileType="['doc', 'xls', 'ppt', 'txt', 'pdf', 'docx', 'xlsx', 'pptx']"
/>
</el-dialog>
<upload-file
@change="refresh"
v-if="props.type === 'file'"
:data="{ cid: cateId, type: typeValue }"
:fileType="['doc', 'xls', 'ppt', 'txt', 'pdf', 'docx', 'xlsx', 'pptx']"
/>
</el-dialog>
</template>
<script lang="ts" setup>
const Popup = defineAsyncComponent(() => import('/@/components/Popup/index.vue'));
const PopoverInput = defineAsyncComponent(() => import('/@/components/PopoverInput/index.vue'));
import {useCate, useFile} from './hook';
import { useCate, useFile } from './hook';
import FileItem from './file.vue';
import Preview from './preview.vue';
import type {Ref} from 'vue';
import type { Ref } from 'vue';
import other from '/@/utils/other';
const {proxy} = getCurrentInstance();
const kkServerURL = import.meta.env.VITE_KK_SERVER_URL
const { proxy } = getCurrentInstance();
const kkServerURL = import.meta.env.VITE_KK_SERVER_URL;
const props = defineProps({
fileSize: {
type: String,
default: '100px',
},
limit: {
type: Number,
default: 1,
},
type: {
type: String,
default: 'image',
},
mode: {
type: String,
default: 'picker',
},
pageSize: {
type: Number,
default: 15,
},
fileSize: {
type: String,
default: '100px',
},
limit: {
type: Number,
default: 1,
},
type: {
type: String,
default: 'image',
},
mode: {
type: String,
default: 'picker',
},
pageSize: {
type: Number,
default: 15,
},
});
const emit = defineEmits(['change']);
const {limit} = toRefs(props);
const { limit } = toRefs(props);
const typeValue = computed<number>(() => {
switch (props.type) {
case 'image':
return 10;
case 'video':
return 20;
case 'file':
return 30;
default:
return 0;
}
switch (props.type) {
case 'image':
return 10;
case 'video':
return 20;
case 'file':
return 30;
default:
return 0;
}
});
const visible: Ref<boolean> = ref(false);
const visibleUpload: Ref<boolean> = ref(false);
const previewUrl = ref('');
const fileName = ref('');
const showPreview = ref(false);
const {
treeRef,
cateId,
cateLists,
handleAddCate,
handleEditCate,
handleDeleteCate,
getCateLists,
handleCatSelect
} = useCate(typeValue.value);
const { treeRef, cateId, cateLists, handleAddCate, handleEditCate, handleDeleteCate, getCateLists, handleCatSelect } = useCate(typeValue.value);
const {
tableRef,
listShowType,
moveId,
pager,
fileParams,
select,
isCheckAll,
isIndeterminate,
getFileList,
refresh,
batchFileDelete,
batchFileMove,
selectFile,
isSelect,
clearSelect,
cancelSelete,
selectAll,
handleFileRename,
tableRef,
listShowType,
moveId,
pager,
fileParams,
select,
isCheckAll,
isIndeterminate,
getFileList,
refresh,
batchFileDelete,
batchFileMove,
selectFile,
isSelect,
clearSelect,
cancelSelete,
selectAll,
handleFileRename,
} = useFile(cateId, typeValue, limit, props.pageSize);
/**
* 获取数据
*/
const getData = async () => {
await getCateLists();
treeRef.value?.setCurrentKey(cateId.value);
getFileList();
await getCateLists();
treeRef.value?.setCurrentKey(cateId.value);
getFileList();
};
/**
@@ -366,10 +349,10 @@ const getData = async () => {
* @param val 新的页码
*/
const currentChangeHandle = (val: number) => {
// 修改state.pagination中的current属性
pager.current = val;
// 再次发起查询操作
getFileList();
// 修改state.pagination中的current属性
pager.current = val;
// 再次发起查询操作
getFileList();
};
/**
@@ -378,9 +361,9 @@ const currentChangeHandle = (val: number) => {
* @param {string} item - 资源
*/
const handlePreview = (item: { fileName: string }) => {
previewUrl.value = getFileUri(item);
showPreview.value = true;
fileName.value = item.fileName;
previewUrl.value = getFileUri(item);
showPreview.value = true;
fileName.value = item.fileName;
};
/**
@@ -389,123 +372,123 @@ const handlePreview = (item: { fileName: string }) => {
* @param {any} item - 文件项对象
*/
const handleDownFile = (item: any) => {
other.downBlobFile(`/admin/sys-file/oss/file?fileName=${item.fileName}`, {}, item.original);
other.downBlobFile(`/admin/sys-file/oss/file?fileName=${item.fileName}`, {}, item.original);
};
watch(
visible,
async (val: boolean) => {
if (val) {
getData();
}
},
{
immediate: true,
}
visible,
async (val: boolean) => {
if (val) {
getData();
}
},
{
immediate: true,
}
);
watch(cateId, () => {
fileParams.name = '';
refresh();
fileParams.name = '';
refresh();
});
watch(
select,
(val: any[]) => {
emit('change', val);
if (val.length == pager.lists.length && val.length !== 0) {
isIndeterminate.value = false;
isCheckAll.value = true;
return;
}
if (val.length > 0) {
isIndeterminate.value = true;
} else {
isCheckAll.value = false;
isIndeterminate.value = false;
}
},
{
deep: true,
}
select,
(val: any[]) => {
emit('change', val);
if (val.length == pager.lists.length && val.length !== 0) {
isIndeterminate.value = false;
isCheckAll.value = true;
return;
}
if (val.length > 0) {
isIndeterminate.value = true;
} else {
isCheckAll.value = false;
isIndeterminate.value = false;
}
},
{
deep: true,
}
);
const getFileUri = (item: any) => {
return `${proxy.baseURL}/admin/sys-file/oss/file?fileName=${item.fileName}`;
return `${proxy.baseURL}/admin/sys-file/oss/file?fileName=${item.fileName}`;
};
onMounted(() => {
props.mode == 'page' && getData();
props.mode == 'page' && getData();
});
defineExpose({
clearSelect,
clearSelect,
});
</script>
<style scoped lang="scss">
.material {
@apply h-full min-h-0 flex flex-1;
&__left {
@apply border-r border-br flex flex-col w-[200px];
:deep(.el-tree-node__content) {
height: 36px;
}
}
@apply h-full min-h-0 flex flex-1;
&__left {
@apply border-r border-br flex flex-col w-[200px];
:deep(.el-tree-node__content) {
height: 36px;
}
}
&__center {
flex: 1;
min-width: 0;
min-height: 0;
padding: 16px 16px 0;
&__center {
flex: 1;
min-width: 0;
min-height: 0;
padding: 16px 16px 0;
.list-icon {
@apply rounded transition-colors duration-200 cursor-pointer hover:bg-gray-100;
}
.list-icon {
@apply rounded transition-colors duration-200 cursor-pointer hover:bg-gray-100;
}
.file-list {
.file-item-wrap {
margin-right: 16px;
line-height: 1.3;
cursor: pointer;
.file-list {
.file-item-wrap {
margin-right: 16px;
line-height: 1.3;
cursor: pointer;
.item-selected {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
box-sizing: border-box;
}
.item-selected {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
box-sizing: border-box;
}
.operation-btns {
height: 28px;
visibility: hidden;
}
.operation-btns {
height: 28px;
visibility: hidden;
}
&:hover .operation-btns {
visibility: visible;
}
}
}
}
&:hover .operation-btns {
visibility: visible;
}
}
}
}
&__right {
@apply border-l border-br flex flex-col;
width: 130px;
&__right {
@apply border-l border-br flex flex-col;
width: 130px;
.select-lists {
padding: 10px;
.select-lists {
padding: 10px;
.select-item {
width: 100px;
height: 100px;
}
}
}
.select-item {
width: 100px;
height: 100px;
}
}
}
}
</style>

View File

@@ -1,93 +1,91 @@
<template>
<div v-show="modelValue">
<div v-if="type == 'image'">
<el-image-viewer v-if="previewLists.length" :url-list="previewLists" hide-on-click-modal @close="handleClose"/>
</div>
<div v-if="type == 'video'">
<el-dialog v-model="visible" width="740px" :title="$t('material.preview')" :before-close="handleClose">
<video-player ref="playerRef" :src="url" width="100%" height="450px"/>
</el-dialog>
</div>
<div v-if="type == 'file'">
<el-drawer v-model="visible" size="100%">
<iframe
:src="src"
width="100%" height="100%" frameborder="0" class="h-screen" v-if="src"></iframe>
<span v-else>未配置预览服务器请参考文档配置</span>
</el-drawer>
</div>
</div>
<div v-show="modelValue">
<div v-if="type == 'image'">
<el-image-viewer v-if="previewLists.length" :url-list="previewLists" hide-on-click-modal @close="handleClose" />
</div>
<div v-if="type == 'video'">
<el-dialog v-model="visible" width="740px" :title="$t('material.preview')" :before-close="handleClose">
<video-player ref="playerRef" :src="url" width="100%" height="450px" />
</el-dialog>
</div>
<div v-if="type == 'file'">
<el-drawer v-model="visible" size="100%">
<iframe :src="src" width="100%" height="100%" frameborder="0" class="h-screen" v-if="src"></iframe>
<span v-else>未配置预览服务器请参考文档配置</span>
</el-drawer>
</div>
</div>
</template>
<script lang="ts" setup>
import {Base64} from 'js-base64';
import {validateNull} from "/@/utils/validate";
import { Base64 } from 'js-base64';
import { validateNull } from '/@/utils/validate';
const VideoPlayer = defineAsyncComponent(() => import('/@/components/VideoPlayer/index.vue'));
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
url: {
type: String,
default: '',
},
fileName: {
type: String,
default: '',
},
type: {
type: String,
default: 'image',
},
modelValue: {
type: Boolean,
default: false,
},
url: {
type: String,
default: '',
},
fileName: {
type: String,
default: '',
},
type: {
type: String,
default: 'image',
},
});
const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void;
(event: 'update:modelValue', value: boolean): void;
}>();
const playerRef = shallowRef();
const visible = computed({
get() {
return props.modelValue;
},
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
set(value) {
emit('update:modelValue', value);
},
});
const handleClose = () => {
emit('update:modelValue', false);
emit('update:modelValue', false);
};
const previewLists = ref<any[]>([]);
watch(
() => props.modelValue,
(value) => {
if (value) {
nextTick(() => {
previewLists.value = [props.url];
playerRef.value?.play();
});
} else {
nextTick(() => {
previewLists.value = [];
playerRef.value?.pause();
});
}
}
() => props.modelValue,
(value) => {
if (value) {
nextTick(() => {
previewLists.value = [props.url];
playerRef.value?.play();
});
} else {
nextTick(() => {
previewLists.value = [];
playerRef.value?.pause();
});
}
}
);
const kkServerURL = import.meta.env.VITE_KK_SERVER_URL
const localURL = import.meta.env.VITE_KK_LOCAL_URL
const kkServerURL = import.meta.env.VITE_KK_SERVER_URL;
const localURL = import.meta.env.VITE_KK_LOCAL_URL;
const src = computed(() => {
if (validateNull(kkServerURL)) {
return undefined;
}
return `${kkServerURL}?url=` + encodeURIComponent(Base64.encode(`${localURL}${props.url}&fullfilename=${props.fileName}`));
if (validateNull(kkServerURL)) {
return undefined;
}
return `${kkServerURL}?url=` + encodeURIComponent(Base64.encode(`${localURL}${props.url}&fullfilename=${props.fileName}`));
});
</script>

View File

@@ -5,38 +5,43 @@
* @FilePath: /Workflow-Vue3/src/components/dialog/common.js
*/
import {deptRoleList} from '/@/api/admin/role';
import {orgTree, orgTreeSearcheUser} from '/@/api/admin/dept';
import { deptRoleList } from '/@/api/admin/role';
import { orgTree, orgTreeSearcheUser } from '/@/api/admin/dept';
export const searchVal = ref('');
export const departments = ref({
titleDepartments: [], childDepartments: [], roleList: [], employees: [],
titleDepartments: [],
childDepartments: [],
roleList: [],
employees: [],
});
export const roles = ref({});
export const getRoleList = async () => {
let {
data: {list},
} = await deptRoleList();
roles.value = list;
let {
data: { list },
} = await deptRoleList();
roles.value = list;
};
export const getDepartmentList = async (parentId = 0, type = 'org') => {
// let { data } = await getDepartments({ parentId })
// let { data } = await getDepartments({ parentId })
let {data} = await orgTree(type, parentId);
let { data } = await orgTree(type, parentId);
departments.value = data;
departments.value = data;
};
export const getDebounceData = async (event: any, type = 1) => {
if (event) {
let data = {
username: event, pageNum: 1, pageSize: 30,
};
if (type === 1) {
departments.value.childDepartments = [];
let res = await orgTreeSearcheUser(data);
departments.value.employees = res.data;
}
} else {
type === 1 ? await getDepartmentList() : await getRoleList();
}
if (event) {
let data = {
username: event,
pageNum: 1,
pageSize: 30,
};
if (type === 1) {
departments.value.childDepartments = [];
let res = await orgTreeSearcheUser(data);
departments.value.employees = res.data;
}
} else {
type === 1 ? await getDepartmentList() : await getRoleList();
}
};

View File

@@ -1,50 +1,55 @@
<template>
<el-dialog :title="$t('orgSelecotr.select') + $t(`orgSelecotr.${props.type}`)" v-model="visibleDialog" :width="600" append-to-body class="promoter_person">
<div class="person_body clear">
<div class="person_tree l">
<selectBox ref="selectBoxRef" :selectSelf="selectSelf" :list="list" :multiple="multiple"
v-model:selectedList="selectedList" :type="type"/>
</div>
<selectResult :total="total" @del="delList" :list="resList"/>
</div>
<template #footer>
<el-button @click="$emit('update:visible', false)">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="saveDialog">{{ $t('common.confirmButtonText') }}</el-button>
</template>
</el-dialog>
<el-dialog
:title="$t('orgSelecotr.select') + $t(`orgSelecotr.${props.type}`)"
v-model="visibleDialog"
:width="600"
append-to-body
class="promoter_person"
>
<div class="person_body clear">
<div class="person_tree l">
<selectBox ref="selectBoxRef" :selectSelf="selectSelf" :list="list" :multiple="multiple" v-model:selectedList="selectedList" :type="type" />
</div>
<selectResult :total="total" @del="delList" :list="resList" />
</div>
<template #footer>
<el-button @click="$emit('update:visible', false)">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="saveDialog">{{ $t('common.confirmButtonText') }}</el-button>
</template>
</el-dialog>
</template>
<script setup>
import selectBox from './selectBox.vue';
import selectResult from './selectResult.vue';
import {computed, watch, ref, onMounted} from 'vue';
import {departments, searchVal} from './common';
import { computed, watch, ref, onMounted } from 'vue';
import { departments, searchVal } from './common';
import other from '/@/utils/other';
import {useI18n} from "vue-i18n";
import { useI18n } from 'vue-i18n';
const selectBoxRef = ref();
let props = defineProps({
visible: {
type: Boolean,
default: false,
},
data: {
type: Array,
default: () => [],
},
type: {
type: String,
default: 'user',
},
multiple: {
type: Boolean,
default: true,
},
selectSelf: {
type: Boolean,
default: true,
},
visible: {
type: Boolean,
default: false,
},
data: {
type: Array,
default: () => [],
},
type: {
type: String,
default: 'user',
},
multiple: {
type: Boolean,
default: true,
},
selectSelf: {
type: Boolean,
default: true,
},
});
//已选择的集合
@@ -52,128 +57,128 @@ let selectedList = ref([]);
let emits = defineEmits(['update:visible', 'change']);
let visibleDialog = computed({
get() {
return props.visible;
},
set() {
closeDialog();
},
get() {
return props.visible;
},
set() {
closeDialog();
},
});
const isChecked = (id, type) => {
return selectedList.value.filter((res) => res.id === id && res.type === type).length > 0;
return selectedList.value.filter((res) => res.id === id && res.type === type).length > 0;
};
let list = computed(() => {
let value = departments.value;
return [
{
type: 'dept',
data: value === undefined ? [] : value.childDepartments,
},
{
type: 'role',
data: value === undefined ? [] : value.roleList,
},
{
type: 'user',
data: value === undefined ? [] : value.employees,
change: (item) => {
if (!isChecked(item.id, item.type)) {
if (!props.multiple) {
//单选
selectedList.value = [];
}
let value = departments.value;
return [
{
type: 'dept',
data: value === undefined ? [] : value.childDepartments,
},
{
type: 'role',
data: value === undefined ? [] : value.roleList,
},
{
type: 'user',
data: value === undefined ? [] : value.employees,
change: (item) => {
if (!isChecked(item.id, item.type)) {
if (!props.multiple) {
//单选
selectedList.value = [];
}
selectedList.value.push(item);
} else {
selectedList.value = selectedList.value.filter((res) => !(res.id === item.id && res.type === item.type));
}
},
},
];
selectedList.value.push(item);
} else {
selectedList.value = selectedList.value.filter((res) => !(res.id === item.id && res.type === item.type));
}
},
},
];
});
let resList = computed(() => {
let userData = selectedList.value.filter((res) => res.type === 'user');
let deptData = selectedList.value.filter((res) => res.type === 'dept');
let roleData = selectedList.value.filter((res) => res.type === 'role');
let userData = selectedList.value.filter((res) => res.type === 'user');
let deptData = selectedList.value.filter((res) => res.type === 'dept');
let roleData = selectedList.value.filter((res) => res.type === 'role');
let data = [
{
type: 'user',
data: userData,
cancel: (item) => {
item.selected = false;
selectBoxRef.value.changeEvent(item);
},
},
];
if (props.type === 'org' || props.type === 'dept') {
data.unshift({
type: 'dept',
data: deptData,
cancel: (item) => {
item.selected = false;
selectBoxRef.value.changeEvent(item);
},
});
}
if (props.type === 'role') {
data.unshift({
type: 'role',
data: roleData,
cancel: (item) => {
item.selected = false;
selectBoxRef.value.changeEvent(item);
},
});
}
return data;
let data = [
{
type: 'user',
data: userData,
cancel: (item) => {
item.selected = false;
selectBoxRef.value.changeEvent(item);
},
},
];
if (props.type === 'org' || props.type === 'dept') {
data.unshift({
type: 'dept',
data: deptData,
cancel: (item) => {
item.selected = false;
selectBoxRef.value.changeEvent(item);
},
});
}
if (props.type === 'role') {
data.unshift({
type: 'role',
data: roleData,
cancel: (item) => {
item.selected = false;
selectBoxRef.value.changeEvent(item);
},
});
}
return data;
});
watch(
() => props.visible,
(val) => {
if (val) {
selectedList.value = props.data;
searchVal.value = '';
}
}
() => props.visible,
(val) => {
if (val) {
selectedList.value = props.data;
searchVal.value = '';
}
}
);
const closeDialog = () => {
emits('update:visible', false);
emits('update:visible', false);
};
let total = computed(() => {
let v = departments.value;
if (!v) {
return 0;
}
return selectedList.value.length;
let v = departments.value;
if (!v) {
return 0;
}
return selectedList.value.length;
});
const {proxy} = getCurrentInstance();
const { proxy } = getCurrentInstance();
let saveDialog = () => {
const v = selectedList.value;
const v = selectedList.value;
let checkedList = other.deepClone(v).map((item) => ({
type: item.type,
id: item.id,
name: item.name,
avatar: item.avatar,
username: item.username,
userName: item.username,
}));
emits('change', checkedList);
//selectedList.value=[]
let checkedList = other.deepClone(v).map((item) => ({
type: item.type,
id: item.id,
name: item.name,
avatar: item.avatar,
username: item.username,
userName: item.username,
}));
emits('change', checkedList);
//selectedList.value=[]
};
const delList = () => {
for (const item of other.deepClone(selectedList.value)) {
item.selected = false;
selectBoxRef.value.changeEvent(item);
}
selectedList.value = [];
for (const item of other.deepClone(selectedList.value)) {
item.selected = false;
selectBoxRef.value.changeEvent(item);
}
selectedList.value = [];
};
</script>
<style scoped>

View File

@@ -1,10 +1,10 @@
export default {
orgSelecotr: {
org: 'org',
user: 'user',
dept: 'dept',
role: 'role',
select: 'select',
search: 'search'
},
orgSelecotr: {
org: 'org',
user: 'user',
dept: 'dept',
role: 'role',
select: 'select',
search: 'search',
},
};

View File

@@ -5,6 +5,6 @@ export default {
dept: '部门',
role: '角色',
select: '选择',
search: '搜索'
search: '搜索',
},
};

View File

@@ -5,114 +5,113 @@
* @FilePath: /Workflow-Vue3/src/components/dialog/roleDialog.vue
-->
<template>
<el-dialog title="选择角色" v-model="visibleDialog" :width="600" append-to-body class="promoter_person">
<div class="person_body clear">
<div class="person_tree l">
<input type="text" placeholder="搜索角色" v-model="searchVal" @input="getDebounceData($event, 2)"/>
<selectBox :list="list"/>
</div>
<selectResult :total="total" @del="delList" :list="resList"/>
</div>
<template #footer>
<el-button @click="closeDialog"> </el-button>
<el-button type="primary" @click="saveDialog"> </el-button>
</template>
</el-dialog>
<el-dialog title="选择角色" v-model="visibleDialog" :width="600" append-to-body class="promoter_person">
<div class="person_body clear">
<div class="person_tree l">
<input type="text" placeholder="搜索角色" v-model="searchVal" @input="getDebounceData($event, 2)" />
<selectBox :list="list" />
</div>
<selectResult :total="total" @del="delList" :list="resList" />
</div>
<template #footer>
<el-button @click="closeDialog"> </el-button>
<el-button type="primary" @click="saveDialog"> </el-button>
</template>
</el-dialog>
</template>
<script setup>
import selectBox from './selectBox.vue';
import selectResult from './selectResult.vue';
import {computed, watch, ref} from 'vue';
import {roles, getDebounceData, getRoleList, searchVal} from './common';
import { computed, watch, ref } from 'vue';
import { roles, getDebounceData, getRoleList, searchVal } from './common';
let props = defineProps({
visible: {
type: Boolean,
default: false,
},
data: {
type: Array,
default: () => [],
},
visible: {
type: Boolean,
default: false,
},
data: {
type: Array,
default: () => [],
},
});
let checkedRoleList = ref([]);
let emits = defineEmits(['update:visible', 'change']);
let list = computed(() => {
return [
{
type: 'role',
not: true,
data: roles.value,
isActive: (item) => toggleClass(checkedRoleList.value, item, 'roleId'),
change: (item) => {
checkedRoleList.value = [item];
},
},
];
return [
{
type: 'role',
not: true,
data: roles.value,
isActive: (item) => toggleClass(checkedRoleList.value, item, 'roleId'),
change: (item) => {
checkedRoleList.value = [item];
},
},
];
});
let resList = computed(() => {
return [
{
type: 'role',
data: checkedRoleList.value,
cancel: (item) => removeEle(checkedRoleList.value, item, 'roleId'),
},
];
return [
{
type: 'role',
data: checkedRoleList.value,
cancel: (item) => removeEle(checkedRoleList.value, item, 'roleId'),
},
];
});
let visibleDialog = computed({
get() {
return props.visible;
},
set(val) {
closeDialog();
},
get() {
return props.visible;
},
set(val) {
closeDialog();
},
});
watch(
() => props.visible,
(val) => {
if (val) {
getRoleList();
searchVal.value = '';
checkedRoleList.value = props.data.map(({name, targetId}) => ({
roleName: name,
roleId: targetId,
}));
}
}
() => props.visible,
(val) => {
if (val) {
getRoleList();
searchVal.value = '';
checkedRoleList.value = props.data.map(({ name, targetId }) => ({
roleName: name,
roleId: targetId,
}));
}
}
);
let total = computed(() => checkedRoleList.value.length);
const saveDialog = () => {
let checkedList = checkedRoleList.value.map((item) => ({
type: 2,
targetId: item.roleId,
name: item.roleName,
}));
emits('change', checkedList);
let checkedList = checkedRoleList.value.map((item) => ({
type: 2,
targetId: item.roleId,
name: item.roleName,
}));
emits('change', checkedList);
};
const delList = () => {
checkedRoleList.value = [];
checkedRoleList.value = [];
};
const closeDialog = () => {
emits('update:visible', false);
emits('update:visible', false);
};
const toggleClass = (arr, elem, key = 'id') => {
return arr.some((item) => {
return item[key] === elem[key];
});
}
return arr.some((item) => {
return item[key] === elem[key];
});
};
const removeEle = (arr, elem, key = 'id') => {
let includesIndex;
arr.map((item, index) => {
if (item[key] === elem[key]) {
includesIndex = index;
}
});
arr.splice(includesIndex, 1);
}
let includesIndex;
arr.map((item, index) => {
if (item[key] === elem[key]) {
includesIndex = index;
}
});
arr.splice(includesIndex, 1);
};
</script>
<style>

View File

@@ -1,295 +1,293 @@
<template>
<div>
<el-input
v-model="searchVal"
class="w-50 m-2"
style="width: 100%"
v-if="type === 'user'"
:placeholder="$t('orgSelecotr.search')"
@input="getDebounceData($event)"
:prefix-icon="Search"
/>
<p class="ellipsis tree_nav" v-if="!searchVal && type !== 'role'">
<span @click="queryData(0)" class="ellipsis">根节点</span>
<span v-for="(item, index) in departments.titleDepartments" class="ellipsis" :key="index + 'a'" @click="queryData(item.id)">{{
item.name
}}</span>
</p>
<div>
<el-input
v-model="searchVal"
class="w-50 m-2"
style="width: 100%"
v-if="type === 'user'"
:placeholder="$t('orgSelecotr.search')"
@input="getDebounceData($event)"
:prefix-icon="Search"
/>
<p class="ellipsis tree_nav" v-if="!searchVal && type !== 'role'">
<span @click="queryData(0)" class="ellipsis">根节点</span>
<span v-for="(item, index) in departments.titleDepartments" class="ellipsis" :key="index + 'a'" @click="queryData(item.id)">{{
item.name
}}</span>
</p>
<ul class="select-box">
<template v-for="(elem, i) in dataList" :key="i">
<template v-if="elem.type === 'role'">
<li v-for="item in elem.data" :key="item.id">
<el-checkbox v-model="item.selected" @change="changeEvent(item)" :disabled="item.status === 0">
<div style="display: flex; flex-direction: row">
<div class="f11">
<el-icon style="font-size: 20px">
<Share />
</el-icon>
</div>
<div class="f12">{{ item.name }}</div>
</div>
</el-checkbox>
</li>
</template>
<template v-if="elem.type === 'dept' && (type === 'org' || type === 'dept' || type === 'user')">
<li v-for="item in elem.data" :key="item.id">
<div style="display: flex; flex-direction: row">
<div class="d11">
<el-checkbox v-model="item.selected" @change="changeEvent(item)" :disabled="!(type === 'org' || type === 'dept') || item.status == 0">
<div style="display: flex; flex-direction: row">
<div class="f11">
<el-icon style="font-size: 20px">
<Grid />
</el-icon>
</div>
<div class="f12">{{ item.name }}</div>
</div>
</el-checkbox>
</div>
<div class="d22" @click="queryData(item.id)">下级</div>
</div>
</li>
</template>
<template v-if="elem.type === 'user' && (type === 'org' || type === 'user')">
<li v-for="item in elem.data" :key="item.id" class="check_box">
<el-checkbox
v-model="item.selected"
:disabled="item.status === 0 || (!selectSelf && currentUserId === item.id)"
@change="changeEvent(item)"
>
<div style="display: flex; flex-direction: row">
<div class="f11">
<upload-img v-model:image-url="item.avatar" disabled width="20px" height="20px"/>
</div>
<div class="f12">
<span v-if="item.commonDeptName">{{ item.commonDeptName }} - </span>
<span>{{ item.realName || item.name }}</span>
<span v-if="item.teacherNo" style="color: #999; margin-left: 4px;">({{ item.teacherNo }})</span>
</div>
</div>
</el-checkbox>
</li>
</template>
</template>
</ul>
</div>
<ul class="select-box">
<template v-for="(elem, i) in dataList" :key="i">
<template v-if="elem.type === 'role'">
<li v-for="item in elem.data" :key="item.id">
<el-checkbox v-model="item.selected" @change="changeEvent(item)" :disabled="item.status === 0">
<div style="display: flex; flex-direction: row">
<div class="f11">
<el-icon style="font-size: 20px">
<Share />
</el-icon>
</div>
<div class="f12">{{ item.name }}</div>
</div>
</el-checkbox>
</li>
</template>
<template v-if="elem.type === 'dept' && (type === 'org' || type === 'dept' || type === 'user')">
<li v-for="item in elem.data" :key="item.id">
<div style="display: flex; flex-direction: row">
<div class="d11">
<el-checkbox v-model="item.selected" @change="changeEvent(item)" :disabled="!(type === 'org' || type === 'dept') || item.status == 0">
<div style="display: flex; flex-direction: row">
<div class="f11">
<el-icon style="font-size: 20px">
<Grid />
</el-icon>
</div>
<div class="f12">{{ item.name }}</div>
</div>
</el-checkbox>
</div>
<div class="d22" @click="queryData(item.id)">下级</div>
</div>
</li>
</template>
<template v-if="elem.type === 'user' && (type === 'org' || type === 'user')">
<li v-for="item in elem.data" :key="item.id" class="check_box">
<el-checkbox
v-model="item.selected"
:disabled="item.status === 0 || (!selectSelf && currentUserId === item.id)"
@change="changeEvent(item)"
>
<div style="display: flex; flex-direction: row">
<div class="f11">
<upload-img v-model:image-url="item.avatar" disabled width="20px" height="20px" />
</div>
<div class="f12">
<span v-if="item.commonDeptName">{{ item.commonDeptName }} - </span>
<span>{{ item.realName || item.name }}</span>
<span v-if="item.teacherNo" style="color: #999; margin-left: 4px">({{ item.teacherNo }})</span>
</div>
</div>
</el-checkbox>
</li>
</template>
</template>
</ul>
</div>
</template>
<script setup>
import {useUserInfo} from '/@/stores/userInfo';
import { useUserInfo } from '/@/stores/userInfo';
import {departments, getDebounceData, getDepartmentList, searchVal} from './common';
import { departments, getDebounceData, getDepartmentList, searchVal } from './common';
import other from '/@/utils/other';
import UploadImg from "/@/components/Upload/Image.vue";
import {Grid, Search, Share} from '@element-plus/icons-vue';
import UploadImg from '/@/components/Upload/Image.vue';
import { Grid, Search, Share } from '@element-plus/icons-vue';
var props = defineProps({
selectedList: {
type: Array,
default: () => [],
},
type: {
type: String,
default: 'org',
},
multiple: {
type: Boolean,
default: true,
},
selectSelf: {
type: Boolean,
default: true,
},
selectedList: {
type: Array,
default: () => [],
},
type: {
type: String,
default: 'org',
},
multiple: {
type: Boolean,
default: true,
},
selectSelf: {
type: Boolean,
default: true,
},
});
const currentUserId = computed(() => {
return useUserInfo().userInfos.user.userId;
return useUserInfo().userInfos.user.userId;
});
const queryData = (pid) => {
getDepartmentList(pid, props.type).then((res) => {
let selectedList = props.selectedList;
getDepartmentList(pid, props.type).then((res) => {
let selectedList = props.selectedList;
for (const it of dataList.value) {
for (const item of it.data) {
item.selected = selectedList.filter((res) => res.id === item.id && res.type === item.type).length > 0;
}
}
});
for (const it of dataList.value) {
for (const item of it.data) {
item.selected = selectedList.filter((res) => res.id === item.id && res.type === item.type).length > 0;
}
}
});
};
let deptList = computed(() => {
return departments.value.childDepartments;
return departments.value.childDepartments;
});
let userList = computed(() => {
return departments.value.employees;
return departments.value.employees;
});
let roleList = computed(() => {
return departments.value.roleList;
return departments.value.roleList;
});
const dataList = computed(() => {
return [
{
type: 'dept',
data: deptList.value,
},
{
type: 'user',
data: userList.value,
},
{
type: 'role',
data: roleList.value,
}
];
return [
{
type: 'dept',
data: deptList.value,
},
{
type: 'user',
data: userList.value,
},
{
type: 'role',
data: roleList.value,
},
];
});
const { proxy } = getCurrentInstance();
let emits = defineEmits(['update:selectedList']);
onMounted(() => {
queryData(0);
queryData(0);
});
const changeEvent = (e) => {
let selectedList = other.deepClone(props.selectedList);
let selectedList = other.deepClone(props.selectedList);
if (e.selected) {
if (!props.multiple) {
userList.value.forEach((res) => (res.selected = false));
selectedList = [];
}
e.selected = true;
selectedList.push(e);
} else {
for (const it of dataList.value) {
let filter = it.data.filter((res) => res.id === e.id && res.type === e.type);
if (filter.length > 0) {
filter[0].selected = false;
}
}
selectedList = selectedList.filter((res) => !(res.id === e.id && res.type === e.type));
}
emits('update:selectedList', selectedList);
if (e.selected) {
if (!props.multiple) {
userList.value.forEach((res) => (res.selected = false));
selectedList = [];
}
e.selected = true;
selectedList.push(e);
} else {
for (const it of dataList.value) {
let filter = it.data.filter((res) => res.id === e.id && res.type === e.type);
if (filter.length > 0) {
filter[0].selected = false;
}
}
selectedList = selectedList.filter((res) => !(res.id === e.id && res.type === e.type));
}
emits('update:selectedList', selectedList);
};
const refreshData = () => {
let selectedList = props.selectedList;
let selectedList = props.selectedList;
for (var it of dataList.value) {
for (var item of it.data) {
var b = selectedList.filter((res) => res.id === item.id && res.type === item.type).length > 0;
item.selected = b;
}
}
for (var it of dataList.value) {
for (var item of it.data) {
var b = selectedList.filter((res) => res.id === item.id && res.type === item.type).length > 0;
item.selected = b;
}
}
};
defineExpose({ queryData, changeEvent, refreshData });
watch(
() => props.selectedList,
(val) => {
refreshData();
}
() => props.selectedList,
(val) => {
refreshData();
}
);
</script>
<style scoped lang="scss">
@import './dialog.css';
.select-box {
height: 420px;
overflow-y: auto;
height: 420px;
overflow-y: auto;
li {
padding: 5px 0;
}
li {
padding: 5px 0;
}
}
.radio_box a,
.check_box a {
font-size: 12px;
position: relative;
padding-left: 20px;
margin-right: 30px;
cursor: pointer;
color: #333;
white-space: pre;
font-size: 12px;
position: relative;
padding-left: 20px;
margin-right: 30px;
cursor: pointer;
color: #333;
white-space: pre;
}
.check_box.not a:hover {
color: #333;
color: #333;
}
.check_box.not a::before,
.check_box.not a:hover::before {
border: none;
border: none;
}
.check_box.not.active {
background: #f3f3f3;
background: #f3f3f3;
}
.radio_box a:hover::before,
.check_box a:hover::before {
border: 1px solid #46a6fe;
border: 1px solid #46a6fe;
}
.radio_box a::before,
.check_box a::before {
position: absolute;
width: 14px;
height: 14px;
border: 1px solid #dcdfe6;
border-radius: 2px;
left: 0;
top: 1px;
content: '';
position: absolute;
width: 14px;
height: 14px;
border: 1px solid #dcdfe6;
border-radius: 2px;
left: 0;
top: 1px;
content: '';
}
.radio_box a::before {
border-radius: 50%;
border-radius: 50%;
}
.check-dot.active::after,
.radio_box a.active::after,
.check_box a.active::after {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
top: 3px;
left: 3px;
content: '';
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
top: 3px;
left: 3px;
content: '';
}
.radio_box a.active::after {
background: #46a6fe;
background: #46a6fe;
}
.check_box a.active::after {
background: url(./assets/check_box.png) no-repeat center;
background: url(./assets/check_box.png) no-repeat center;
}
.f11 {
width: 30px;
width: 30px;
}
.f12 {
width: calc(100% - 30px);
height: 20px;
line-height: 20px;
width: calc(100% - 30px);
height: 20px;
line-height: 20px;
}
.d11 {
width: calc(100% - 30px);
width: calc(100% - 30px);
}
.d22 {
width: 30px;
line-height: 41px;
cursor: pointer;
width: 30px;
line-height: 41px;
cursor: pointer;
}
</style>

View File

@@ -51,7 +51,7 @@
<div class="f12">
<span v-if="item.commonDeptName">{{ item.commonDeptName }} - </span>
<span>{{ item.realName || item.name }}</span>
<span v-if="item.teacherNo" style="color: #999; margin-left: 4px;">({{ item.teacherNo }})</span>
<span v-if="item.teacherNo" style="color: #999; margin-left: 4px">({{ item.teacherNo }})</span>
</div>
<div class="f13">
<el-button size="small" text @click="cancel(item)" :icon="CircleClose"></el-button>

View File

@@ -73,10 +73,10 @@ const props = defineProps({
type: Number,
default: 200,
},
maxlength: {
type: Number,
default: 20,
},
maxlength: {
type: Number,
default: 20,
},
showLimit: {
type: Boolean,
default: false,

View File

@@ -1,9 +1,9 @@
export default {
queryTree: {
hideSearch: 'hideSearch',
displayTheSearch: 'displayTheSearch',
refresh: 'refresh',
print: 'print',
view: 'view'
},
queryTree: {
hideSearch: 'hideSearch',
displayTheSearch: 'displayTheSearch',
refresh: 'refresh',
print: 'print',
view: 'view',
},
};

View File

@@ -4,6 +4,6 @@ export default {
displayTheSearch: '显示搜索',
refresh: '刷新',
print: '打印',
view: '视图'
view: '视图',
},
};

View File

@@ -22,9 +22,9 @@
<el-button circle icon="Refresh" @click="handleRefresh()" />
</el-tooltip>
<!-- 插槽 -->
<slot></slot>
</el-row>
<!-- 插槽 -->
<slot></slot>
</el-row>
</div>
</template>
@@ -106,17 +106,17 @@ const isExport = () => {
.top-right-btn {
display: flex;
align-items: center;
:deep(.el-row) {
display: flex;
align-items: center;
gap: 8px;
}
:deep(.el-button) {
margin-left: 0;
}
:deep(.el-tooltip) {
margin-left: 0;
}

View File

@@ -6,21 +6,16 @@
<!-- 筛选 + 展开更多 放在同一表单项内保证垂直对齐 -->
<el-form-item v-if="(filterTitle && props.showFilterTitle) || hasCollapsibleItems" class="search-form__left-group">
<div class="search-form__left-inner">
<span v-if="filterTitle && props.showFilterTitle" class="search-form__title" :style="{ marginRight: hasCollapsibleItems ? '12px' : '0' }">
<span
v-if="filterTitle && props.showFilterTitle"
class="search-form__title"
:style="{ marginRight: hasCollapsibleItems ? '12px' : '0' }"
>
<el-icon class="search-form__title-icon"><Search /></el-icon>
{{ filterTitle }}
</span>
<el-button
v-if="hasCollapsibleItems"
type="primary"
class="toggle-btn"
@click="toggleExpand"
round
>
<el-icon
class="toggle-btn__icon"
:class="{ 'toggle-btn__icon--expanded': isExpanded }"
>
<el-button v-if="hasCollapsibleItems" type="primary" class="toggle-btn" @click="toggleExpand" round>
<el-icon class="toggle-btn__icon" :class="{ 'toggle-btn__icon--expanded': isExpanded }">
<CaretBottom />
</el-icon>
<span class="toggle-btn__text">
@@ -109,53 +104,54 @@ const detectionWrapperRef = ref<HTMLElement | null>(null);
const checkCollapsibleContent = () => {
// 如果 showCollapse 明确指定,则使用指定值
if (props.showCollapse !== undefined) {
hasCollapsibleContent.value = props.showCollapse
return
hasCollapsibleContent.value = props.showCollapse;
return;
}
// 否则,通过检查隐藏的检测元素是否有内容来判断
// 需要等待 DOM 渲染完成,可能需要多次尝试以确保数据加载完成
let retryCount = 0
const maxRetries = 5
let retryCount = 0;
const maxRetries = 5;
const check = () => {
nextTick(() => {
nextTick(() => {
setTimeout(() => {
if (detectionWrapperRef.value) {
// 检查检测元素是否有子元素(排除文本节点)
if (detectionWrapperRef.value) {
// 检查检测元素是否有子元素(排除文本节点)
// 检查是否有 el-form-item 元素(因为表单项会被渲染为 el-form-item
const hasContent = detectionWrapperRef.value.children.length > 0 ||
const hasContent =
detectionWrapperRef.value.children.length > 0 ||
detectionWrapperRef.value.querySelector('.el-form-item') !== null ||
(!!detectionWrapperRef.value.textContent && detectionWrapperRef.value.textContent.trim() !== '')
(!!detectionWrapperRef.value.textContent && detectionWrapperRef.value.textContent.trim() !== '');
if (hasContent || retryCount >= maxRetries) {
hasCollapsibleContent.value = hasContent
hasCollapsibleContent.value = hasContent;
} else {
// 如果还没检测到内容且未达到最大重试次数,继续重试
retryCount++
setTimeout(check, 100)
retryCount++;
setTimeout(check, 100);
}
} else {
if (retryCount < maxRetries) {
retryCount++
setTimeout(check, 100)
} else {
hasCollapsibleContent.value = false
}
retryCount++;
setTimeout(check, 100);
} else {
hasCollapsibleContent.value = false;
}
}
}, 50)
})
}
check()
}
}, 50);
});
};
check();
};
// 是否有需要折叠的项
const hasCollapsibleItems = computed(() => {
if (props.showCollapse !== undefined) {
return props.showCollapse
return props.showCollapse;
}
return hasCollapsibleContent.value
return hasCollapsibleContent.value;
});
// 处理回车键
@@ -181,13 +177,13 @@ watch(
watch(
() => props.showCollapse,
() => {
checkCollapsibleContent()
checkCollapsibleContent();
}
);
// 初始化时检测
onMounted(() => {
checkCollapsibleContent()
checkCollapsibleContent();
});
// 暴露方法供外部调用
@@ -314,4 +310,3 @@ defineExpose({
}
}
</style>

View File

@@ -10,13 +10,7 @@
<el-icon class="search-form__title-icon"><Search /></el-icon>
{{ filterTitle }}
</span>
<el-button
v-if="hasCollapsibleItems"
link
type="primary"
class="toggle-btn"
@click="toggleExpand"
>
<el-button v-if="hasCollapsibleItems" link type="primary" class="toggle-btn" @click="toggleExpand">
<el-icon style="margin-right: 4px">
<ArrowUp v-if="isExpanded" />
<ArrowDown v-else />
@@ -106,53 +100,54 @@ const detectionWrapperRef = ref<HTMLElement | null>(null);
const checkCollapsibleContent = () => {
// 如果 showCollapse 明确指定,则使用指定值
if (props.showCollapse !== undefined) {
hasCollapsibleContent.value = props.showCollapse
return
hasCollapsibleContent.value = props.showCollapse;
return;
}
// 否则,通过检查隐藏的检测元素是否有内容来判断
// 需要等待 DOM 渲染完成,可能需要多次尝试以确保数据加载完成
let retryCount = 0
const maxRetries = 5
let retryCount = 0;
const maxRetries = 5;
const check = () => {
nextTick(() => {
nextTick(() => {
setTimeout(() => {
if (detectionWrapperRef.value) {
// 检查检测元素是否有子元素(排除文本节点)
if (detectionWrapperRef.value) {
// 检查检测元素是否有子元素(排除文本节点)
// 检查是否有 el-form-item 元素(因为表单项会被渲染为 el-form-item
const hasContent = detectionWrapperRef.value.children.length > 0 ||
const hasContent =
detectionWrapperRef.value.children.length > 0 ||
detectionWrapperRef.value.querySelector('.el-form-item') !== null ||
(!!detectionWrapperRef.value.textContent && detectionWrapperRef.value.textContent.trim() !== '')
(!!detectionWrapperRef.value.textContent && detectionWrapperRef.value.textContent.trim() !== '');
if (hasContent || retryCount >= maxRetries) {
hasCollapsibleContent.value = hasContent
hasCollapsibleContent.value = hasContent;
} else {
// 如果还没检测到内容且未达到最大重试次数,继续重试
retryCount++
setTimeout(check, 100)
retryCount++;
setTimeout(check, 100);
}
} else {
if (retryCount < maxRetries) {
retryCount++
setTimeout(check, 100)
} else {
hasCollapsibleContent.value = false
}
retryCount++;
setTimeout(check, 100);
} else {
hasCollapsibleContent.value = false;
}
}
}, 50)
})
}
check()
}
}, 50);
});
};
check();
};
// 是否有需要折叠的项
const hasCollapsibleItems = computed(() => {
if (props.showCollapse !== undefined) {
return props.showCollapse
return props.showCollapse;
}
return hasCollapsibleContent.value
return hasCollapsibleContent.value;
});
// 处理回车键
@@ -178,13 +173,13 @@ watch(
watch(
() => props.showCollapse,
() => {
checkCollapsibleContent()
checkCollapsibleContent();
}
);
// 初始化时检测
onMounted(() => {
checkCollapsibleContent()
checkCollapsibleContent();
});
// 暴露方法供外部调用
@@ -313,9 +308,8 @@ defineExpose({
.actions-inside {
display: contents;
:deep(.el-form-item) {
margin-right: 0!important;
margin-right: 0 !important;
}
}
}
</style>

View File

@@ -11,20 +11,20 @@
## Props
| 参数 | 说明 | 类型 | 默认值 | 必填 |
|------|------|------|--------|------|
| value | 当前状态值 | `string \| number` | `''` | 是 |
| options | 选项列表,格式:`[{label: '是', value: '1'}, {label: '否', value: '0'}]` | `Option[]` | `[]` | 是 |
| showTag | 是否显示标签样式(有边框和背景),`false` 为纯文本样式 | `boolean` | `true` | 否 |
| typeMap | 自定义类型映射,用于标签模式,如:`{'1': {type: 'warning', effect: 'dark'}}` | `Record<string \| number, { type: string; effect?: string }>` | `{}` | 否 |
| colorMap | 自定义颜色映射,用于纯文本模式,如:`{'1': '#E6A23C'}` | `Record<string \| number, string>` | `{}` | 否 |
| 参数 | 说明 | 类型 | 默认值 | 必填 |
| -------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------- | ------ | ---- |
| value | 当前状态值 | `string \| number` | `''` | 是 |
| options | 选项列表,格式:`[{label: '是', value: '1'}, {label: '否', value: '0'}]` | `Option[]` | `[]` | 是 |
| showTag | 是否显示标签样式(有边框和背景),`false` 为纯文本样式 | `boolean` | `true` | 否 |
| typeMap | 自定义类型映射,用于标签模式,如:`{'1': {type: 'warning', effect: 'dark'}}` | `Record<string \| number, { type: string; effect?: string }>` | `{}` | 否 |
| colorMap | 自定义颜色映射,用于纯文本模式,如:`{'1': '#E6A23C'}` | `Record<string \| number, string>` | `{}` | 否 |
### Option 接口
```typescript
interface Option {
label: string // 显示文本
value: string | number // 选项值
label: string; // 显示文本
value: string | number; // 选项值
}
```
@@ -35,8 +35,8 @@ interface Option {
- **值 '1' 或 1**
- 标签模式:`warning` 类型 + `dark` 效果(橙色深色)
- 纯文本模式:`var(--el-color-warning)`(橙色)
- **值 '0' 或 0**
- 标签模式:`primary` 类型 + `light` 效果(蓝色浅色)
- 纯文本模式:`var(--el-color-primary)`(蓝色)
@@ -50,56 +50,49 @@ interface Option {
```vue
<template>
<StatusTag
:value="scope.row.tied"
:options="YES_OR_NO"
/>
<StatusTag :value="scope.row.tied" :options="YES_OR_NO" />
</template>
<script setup>
import StatusTag from '/@/components/StatusTag/index.vue'
import StatusTag from '/@/components/StatusTag/index.vue';
const YES_OR_NO = [
{ label: '是', value: '1' },
{ label: '否', value: '0' }
]
{ label: '是', value: '1' },
{ label: '否', value: '0' },
];
</script>
```
### 纯文本模式(无边框和背景)
```vue
<StatusTag
:value="scope.row.tied"
:options="YES_OR_NO"
:show-tag="false"
/>
<StatusTag :value="scope.row.tied" :options="YES_OR_NO" :show-tag="false" />
```
### 自定义类型映射
```vue
<StatusTag
:value="scope.row.status"
:options="statusOptions"
:type-map="{
'1': { type: 'success', effect: 'dark' },
'0': { type: 'danger', effect: 'light' }
}"
<StatusTag
:value="scope.row.status"
:options="statusOptions"
:type-map="{
'1': { type: 'success', effect: 'dark' },
'0': { type: 'danger', effect: 'light' },
}"
/>
```
### 自定义颜色映射(纯文本模式)
```vue
<StatusTag
:value="scope.row.status"
:options="statusOptions"
:show-tag="false"
:color-map="{
'1': '#67C23A',
'0': '#F56C6C'
}"
<StatusTag
:value="scope.row.status"
:options="statusOptions"
:show-tag="false"
:color-map="{
'1': '#67C23A',
'0': '#F56C6C',
}"
/>
```
@@ -107,24 +100,21 @@ const YES_OR_NO = [
```vue
<template>
<el-table :data="tableData">
<el-table-column label="是否退休" width="100" align="center">
<template #default="scope">
<StatusTag
:value="scope.row.tied"
:options="YES_OR_NO"
/>
</template>
</el-table-column>
</el-table>
<el-table :data="tableData">
<el-table-column label="是否退休" width="100" align="center">
<template #default="scope">
<StatusTag :value="scope.row.tied" :options="YES_OR_NO" />
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import global from '/@/components/tools/commondict.vue'
import { defineAsyncComponent } from 'vue';
import global from '/@/components/tools/commondict.vue';
const StatusTag = defineAsyncComponent(() => import('/@/components/StatusTag/index.vue'))
const YES_OR_NO = global.YES_OR_NO
const StatusTag = defineAsyncComponent(() => import('/@/components/StatusTag/index.vue'));
const YES_OR_NO = global.YES_OR_NO;
</script>
```
@@ -140,10 +130,10 @@ const YES_OR_NO = global.YES_OR_NO
### 标签模式showTag: true
使用 Element Plus 的 `el-tag` 组件,支持所有 `el-tag` 的类型和效果:
- `type`: `success` | `info` | `warning` | `danger` | `primary`
- `effect`: `dark` | `light` | `plain`
### 纯文本模式showTag: false
使用纯文本显示,通过 CSS 颜色控制样式支持任何颜色值CSS 变量、十六进制、RGB 等)。

View File

@@ -1,140 +1,135 @@
<template>
<el-tag
v-if="showTag"
:type="tagType"
:effect="tagEffect"
>
{{ label }}
</el-tag>
<span v-else class="status-tag" :class="statusClass" :style="statusStyle">
{{ label }}
</span>
<el-tag v-if="showTag" :type="tagType" :effect="tagEffect">
{{ label }}
</el-tag>
<span v-else class="status-tag" :class="statusClass" :style="statusStyle">
{{ label }}
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed } from 'vue';
interface Option {
label: string
value: string | number
label: string;
value: string | number;
}
interface Props {
value?: string | number // 当前值
options?: Option[] // 选项列表,格式:[{label: '是', value: '1'}, {label: '否', value: '0'}]
showTag?: boolean // 是否显示标签样式(有边框和背景),默认为 true
typeMap?: Record<string | number, { type: string; effect?: string }> // 自定义类型映射,如 {'1': {type: 'warning', effect: 'dark'}}
colorMap?: Record<string | number, string> // 纯文本模式下的颜色映射,如 {'1': '#E6A23C'}
value?: string | number; // 当前值
options?: Option[]; // 选项列表,格式:[{label: '是', value: '1'}, {label: '否', value: '0'}]
showTag?: boolean; // 是否显示标签样式(有边框和背景),默认为 true
typeMap?: Record<string | number, { type: string; effect?: string }>; // 自定义类型映射,如 {'1': {type: 'warning', effect: 'dark'}}
colorMap?: Record<string | number, string>; // 纯文本模式下的颜色映射,如 {'1': '#E6A23C'}
}
const props = withDefaults(defineProps<Props>(), {
value: '',
options: () => [],
showTag: true,
typeMap: () => ({}),
colorMap: () => ({})
})
value: '',
options: () => [],
showTag: true,
typeMap: () => ({}),
colorMap: () => ({}),
});
// 默认的类型映射(只使用字符串键)
const defaultTypeMap: Record<string, { type: string; effect: string }> = {
'1': { type: 'danger', effect: 'dark' },
'0': { type: 'primary', effect: 'light' }
}
'1': { type: 'danger', effect: 'dark' },
'0': { type: 'primary', effect: 'light' },
};
// 默认的颜色映射(只使用字符串键)
const defaultColorMap: Record<string, string> = {
'1': 'var(--el-color-danger)',
'0': 'var(--el-color-primary)'
}
'1': 'var(--el-color-danger)',
'0': 'var(--el-color-primary)',
};
// 获取值的字符串形式(用于查找映射)
const getValueKey = (value: string | number): string => {
return String(value)
}
return String(value);
};
// 合并后的类型映射(外部传入优先,否则使用默认)
const mergedTypeMap = computed(() => {
// 将外部传入的 typeMap 也转换为字符串键
const externalTypeMap: Record<string, { type: string; effect?: string }> = {}
Object.keys(props.typeMap).forEach(key => {
externalTypeMap[String(key)] = props.typeMap[key]
})
return { ...defaultTypeMap, ...externalTypeMap }
})
// 将外部传入的 typeMap 也转换为字符串键
const externalTypeMap: Record<string, { type: string; effect?: string }> = {};
Object.keys(props.typeMap).forEach((key) => {
externalTypeMap[String(key)] = props.typeMap[key];
});
return { ...defaultTypeMap, ...externalTypeMap };
});
// 合并后的颜色映射(外部传入优先,否则使用默认)
const mergedColorMap = computed(() => {
// 将外部传入的 colorMap 也转换为字符串键
const externalColorMap: Record<string, string> = {}
Object.keys(props.colorMap).forEach(key => {
externalColorMap[String(key)] = props.colorMap[key]
})
return { ...defaultColorMap, ...externalColorMap }
})
// 将外部传入的 colorMap 也转换为字符串键
const externalColorMap: Record<string, string> = {};
Object.keys(props.colorMap).forEach((key) => {
externalColorMap[String(key)] = props.colorMap[key];
});
return { ...defaultColorMap, ...externalColorMap };
});
// 合并后的选项列表(必须通过外部传入 options
const mergedOptions = computed(() => {
// 必须传入 options否则返回空数组
return props.options && props.options.length > 0 ? props.options : []
})
// 必须传入 options否则返回空数组
return props.options && props.options.length > 0 ? props.options : [];
});
// 根据值找到对应的选项
const currentOption = computed(() => {
return mergedOptions.value.find((opt: Option) => {
const optValue = String(opt.value)
const propValue = String(props.value)
return optValue === propValue || Number(opt.value) === Number(props.value)
})
})
return mergedOptions.value.find((opt: Option) => {
const optValue = String(opt.value);
const propValue = String(props.value);
return optValue === propValue || Number(opt.value) === Number(props.value);
});
});
// 显示标签
const label = computed(() => {
return currentOption.value?.label || '-'
})
return currentOption.value?.label || '-';
});
// 标签类型showTag 为 true 时使用)
const tagType = computed(() => {
const valueKey = getValueKey(props.value)
if (mergedTypeMap.value[valueKey]) {
return mergedTypeMap.value[valueKey].type
}
return 'info'
})
const valueKey = getValueKey(props.value);
if (mergedTypeMap.value[valueKey]) {
return mergedTypeMap.value[valueKey].type;
}
return 'info';
});
// 标签效果showTag 为 true 时使用)
const tagEffect = computed(() => {
const valueKey = getValueKey(props.value)
if (mergedTypeMap.value[valueKey]?.effect) {
return mergedTypeMap.value[valueKey].effect
}
return 'light'
})
const valueKey = getValueKey(props.value);
if (mergedTypeMap.value[valueKey]?.effect) {
return mergedTypeMap.value[valueKey].effect;
}
return 'light';
});
// 纯文本模式下的样式类
const statusClass = computed(() => {
if (props.colorMap[props.value]) {
return ''
}
return 'status-default'
})
if (props.colorMap[props.value]) {
return '';
}
return 'status-default';
});
// 纯文本模式下的内联样式
const statusStyle = computed(() => {
const valueKey = getValueKey(props.value)
if (mergedColorMap.value[valueKey]) {
return { color: mergedColorMap.value[valueKey] }
}
return {}
})
const valueKey = getValueKey(props.value);
if (mergedColorMap.value[valueKey]) {
return { color: mergedColorMap.value[valueKey] };
}
return {};
});
</script>
<style scoped>
.status-tag {
display: inline-block;
display: inline-block;
}
.status-default {
color: var(--el-text-color-regular);
color: var(--el-text-color-regular);
}
</style>

View File

@@ -1,17 +1,16 @@
<template>
<slot />
<slot />
</template>
<script setup lang="ts">
import { provide } from 'vue'
import { provide } from 'vue';
interface Props {
isColumnVisible: (propOrLabel: string) => boolean
isColumnVisible: (propOrLabel: string) => boolean;
}
const props = defineProps<Props>()
const props = defineProps<Props>();
// 提供 isColumnVisible 函数给子组件
provide('isColumnVisible', props.isColumnVisible)
provide('isColumnVisible', props.isColumnVisible);
</script>

View File

@@ -14,25 +14,25 @@
```vue
<template>
<el-table ref="tableRef">
<TableColumnProvider :is-column-visible="isColumnVisible">
<TableColumn prop="name" label="姓名" width="120" />
<TableColumn prop="age" label="年龄" width="80" />
</TableColumnProvider>
</el-table>
<el-table ref="tableRef">
<TableColumnProvider :is-column-visible="isColumnVisible">
<TableColumn prop="name" label="姓名" width="120" />
<TableColumn prop="age" label="年龄" width="80" />
</TableColumnProvider>
</el-table>
</template>
<script setup>
import { provide } from 'vue'
import TableColumnProvider from '/@/components/TableColumn/Provider.vue'
import TableColumn from '/@/components/TableColumn/index.vue'
import { provide } from 'vue';
import TableColumnProvider from '/@/components/TableColumn/Provider.vue';
import TableColumn from '/@/components/TableColumn/index.vue';
const isColumnVisible = (propOrLabel: string) => {
// 你的列显示逻辑
return true
}
// 你的列显示逻辑
return true;
};
provide('isColumnVisible', isColumnVisible)
provide('isColumnVisible', isColumnVisible);
</script>
```
@@ -40,20 +40,21 @@ provide('isColumnVisible', isColumnVisible)
```vue
<template>
<el-table>
<TableColumn prop="name" label="姓名" width="120" />
<TableColumn prop="age" label="年龄" width="80" />
</el-table>
<el-table>
<TableColumn prop="name" label="姓名" width="120" />
<TableColumn prop="age" label="年龄" width="80" />
</el-table>
</template>
<script setup>
import TableColumn from '/@/components/TableColumn/index.vue'
import TableColumn from '/@/components/TableColumn/index.vue';
</script>
```
## Props
继承 `el-table-column` 的所有属性,包括:
- `prop`: 列的字段名
- `label`: 列的标题
- `width`: 列宽度
@@ -64,6 +65,7 @@ import TableColumn from '/@/components/TableColumn/index.vue'
## Slots
继承 `el-table-column` 的所有插槽,包括:
- `default`: 默认插槽
- `header`: 表头插槽
- 等等...
@@ -73,4 +75,3 @@ import TableColumn from '/@/components/TableColumn/index.vue'
1. 需要在父组件中使用 `provide` 提供 `isColumnVisible` 函数,或者使用 `TableColumnProvider` 组件
2. `isColumnVisible` 函数接收 `prop``label` 作为参数
3. 如果既没有 `prop` 也没有 `label`,列将始终显示

View File

@@ -1,44 +1,38 @@
<template>
<el-table-column
v-if="shouldShow"
:prop="prop"
:label="label"
v-bind="$attrs"
>
<template v-for="(_, name) in $slots" #[name]="slotProps">
<slot :name="name" v-bind="slotProps" />
</template>
</el-table-column>
<el-table-column v-if="shouldShow" :prop="prop" :label="label" v-bind="$attrs">
<template v-for="(_, name) in $slots" #[name]="slotProps">
<slot :name="name" v-bind="slotProps" />
</template>
</el-table-column>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
import { computed, inject } from 'vue';
interface Props {
prop?: string
label?: string
// 其他 el-table-column 的所有属性都通过 $attrs 传递
prop?: string;
label?: string;
// 其他 el-table-column 的所有属性都通过 $attrs 传递
}
const props = withDefaults(defineProps<Props>(), {
prop: '',
label: ''
})
prop: '',
label: '',
});
// 从父组件注入 isColumnVisible 函数
const isColumnVisible = inject<(propOrLabel: string) => boolean>('isColumnVisible', () => true)
const isColumnVisible = inject<(propOrLabel: string) => boolean>('isColumnVisible', () => true);
// 计算是否应该显示该列
const shouldShow = computed(() => {
// 优先使用 prop如果没有 prop 则使用 label
const key = props.prop || props.label || ''
if (!key) {
// 如果没有 prop 和 label默认显示可能是序号列等特殊列
return true
}
// isColumnVisible 函数会同时匹配 prop 和 label所以直接传递即可
return isColumnVisible(key)
})
</script>
// 优先使用 prop如果没有 prop 则使用 label
const key = props.prop || props.label || '';
if (!key) {
// 如果没有 prop 和 label默认显示可能是序号列等特殊列
return true;
}
// isColumnVisible 函数会同时匹配 prop 和 label所以直接传递即可
return isColumnVisible(key);
});
</script>

View File

@@ -1,6 +1,7 @@
# TableColumnControl 集成指南
## 概述
`TableColumnControl` 是一个通用的表格列显隐控制组件,可以为任何表格页面添加列显示/隐藏和排序功能。
## 集成步骤
@@ -8,10 +9,10 @@
### 1. 导入必要的依赖
```typescript
import { ref, computed, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { Menu } from '@element-plus/icons-vue'
import { ref, computed, onMounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
import { Menu } from '@element-plus/icons-vue';
```
### 2. 定义表格列配置
@@ -19,32 +20,32 @@ import { Menu } from '@element-plus/icons-vue'
```typescript
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '学年' },
{ prop: 'schoolTerm', label: '学期' },
{ prop: 'title', label: '标题' },
{ prop: 'author', label: '作者' },
{ prop: '操作', label: '操作', alwaysShow: true, fixed: true }
]
{ prop: 'schoolYear', label: '学年' },
{ prop: 'schoolTerm', label: '学期' },
{ prop: 'title', label: '标题' },
{ prop: 'author', label: '作者' },
{ prop: '操作', label: '操作', alwaysShow: true, fixed: true },
];
// 列配置映射(用于图标,可选)
const columnConfigMap: Record<string, { icon: any }> = {
schoolYear: { icon: Calendar },
schoolTerm: { icon: Clock },
title: { icon: Document },
author: { icon: User }
}
schoolYear: { icon: Calendar },
schoolTerm: { icon: Clock },
title: { icon: Document },
author: { icon: User },
};
```
### 3. 添加状态变量
```typescript
const route = useRoute()
const columnControlRef = ref<any>()
const route = useRoute();
const columnControlRef = ref<any>();
// 当前显示的列
const visibleColumns = ref<string[]>([])
const visibleColumns = ref<string[]>([]);
// 列排序顺序
const columnOrder = ref<string[]>([])
const columnOrder = ref<string[]>([]);
```
### 4. 添加配置加载和保存逻辑
@@ -52,62 +53,50 @@ const columnOrder = ref<string[]>([])
```typescript
// 立即从 localStorage 加载配置
const loadSavedConfig = () => {
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}`
const saved = localStorage.getItem(storageKey)
if (saved) {
try {
const savedColumns = JSON.parse(saved)
const validColumns = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
const filteredSaved = savedColumns.filter((col: string) => validColumns.includes(col))
visibleColumns.value = filteredSaved.length > 0 ? filteredSaved : validColumns
} catch (e) {
console.error('解析列配置失败:', e)
visibleColumns.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
} else {
visibleColumns.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
// 加载列排序配置
const orderKey = `${storageKey}-order`
const savedOrder = localStorage.getItem(orderKey)
if (savedOrder) {
try {
const parsedOrder = JSON.parse(savedOrder)
const validColumns = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
columnOrder.value = parsedOrder.filter((key: string) => validColumns.includes(key))
validColumns.forEach(key => {
if (!columnOrder.value.includes(key)) {
columnOrder.value.push(key)
}
})
} catch (e) {
console.error('解析列排序失败:', e)
columnOrder.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
} else {
columnOrder.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
}
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-');
const storageKey = `table-columns-${routePath}`;
const saved = localStorage.getItem(storageKey);
if (saved) {
try {
const savedColumns = JSON.parse(saved);
const validColumns = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
const filteredSaved = savedColumns.filter((col: string) => validColumns.includes(col));
visibleColumns.value = filteredSaved.length > 0 ? filteredSaved : validColumns;
} catch (e) {
console.error('解析列配置失败:', e);
visibleColumns.value = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
}
} else {
visibleColumns.value = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
}
// 加载列排序配置
const orderKey = `${storageKey}-order`;
const savedOrder = localStorage.getItem(orderKey);
if (savedOrder) {
try {
const parsedOrder = JSON.parse(savedOrder);
const validColumns = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
columnOrder.value = parsedOrder.filter((key: string) => validColumns.includes(key));
validColumns.forEach((key) => {
if (!columnOrder.value.includes(key)) {
columnOrder.value.push(key);
}
});
} catch (e) {
console.error('解析列排序失败:', e);
columnOrder.value = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
}
} else {
columnOrder.value = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
}
};
// 立即加载保存的配置
loadSavedConfig()
loadSavedConfig();
```
### 5. 添加排序后的表格列计算属性
@@ -115,34 +104,34 @@ loadSavedConfig()
```typescript
// 排序后的表格列
const sortedTableColumns = computed(() => {
const columns = tableColumns.filter(col => {
const key = col.prop || col.label
return col.alwaysShow || col.fixed || visibleColumns.value.includes(key)
})
if (columnOrder.value.length > 0) {
const orderedColumns: typeof tableColumns = []
const unorderedColumns: typeof tableColumns = []
columnOrder.value.forEach(key => {
const col = columns.find(c => (c.prop || c.label) === key)
if (col) {
orderedColumns.push(col)
}
})
columns.forEach(col => {
const key = col.prop || col.label
if (!columnOrder.value.includes(key)) {
unorderedColumns.push(col)
}
})
return [...orderedColumns, ...unorderedColumns]
}
return columns
})
const columns = tableColumns.filter((col) => {
const key = col.prop || col.label;
return col.alwaysShow || col.fixed || visibleColumns.value.includes(key);
});
if (columnOrder.value.length > 0) {
const orderedColumns: typeof tableColumns = [];
const unorderedColumns: typeof tableColumns = [];
columnOrder.value.forEach((key) => {
const col = columns.find((c) => (c.prop || c.label) === key);
if (col) {
orderedColumns.push(col);
}
});
columns.forEach((col) => {
const key = col.prop || col.label;
if (!columnOrder.value.includes(key)) {
unorderedColumns.push(col);
}
});
return [...orderedColumns, ...unorderedColumns];
}
return columns;
});
```
### 6. 添加列显示控制函数
@@ -150,11 +139,11 @@ const sortedTableColumns = computed(() => {
```typescript
// 列显示控制函数
const checkColumnVisible = (prop: string): boolean => {
if (visibleColumns.value.length === 0) {
return true
}
return visibleColumns.value.includes(prop)
}
if (visibleColumns.value.length === 0) {
return true;
}
return visibleColumns.value.includes(prop);
};
```
### 7. 添加事件处理函数
@@ -162,23 +151,23 @@ const checkColumnVisible = (prop: string): boolean => {
```typescript
// 监听列变化
const handleColumnChange = (columns: string[]) => {
visibleColumns.value = columns
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}`
const selectableColumns = columns.filter(col => {
const column = tableColumns.find(c => (c.prop || c.label) === col)
return column && !column.alwaysShow && !column.fixed
})
localStorage.setItem(storageKey, JSON.stringify(selectableColumns))
}
visibleColumns.value = columns;
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-');
const storageKey = `table-columns-${routePath}`;
const selectableColumns = columns.filter((col) => {
const column = tableColumns.find((c) => (c.prop || c.label) === col);
return column && !column.alwaysShow && !column.fixed;
});
localStorage.setItem(storageKey, JSON.stringify(selectableColumns));
};
// 监听列排序变化
const handleColumnOrderChange = (order: string[]) => {
columnOrder.value = order
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}-order`
localStorage.setItem(storageKey, JSON.stringify(order))
}
columnOrder.value = order;
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-');
const storageKey = `table-columns-${routePath}-order`;
localStorage.setItem(storageKey, JSON.stringify(order));
};
```
### 8. 在模板中添加 TableColumnControl 组件
@@ -186,11 +175,7 @@ const handleColumnOrderChange = (order: string[]) => {
`right-toolbar` 组件内添加:
```vue
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
style="float: right;"
@queryTable="getDataList">
<right-toolbar v-model:showSearch="showSearch" class="ml10" style="float: right;" @queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
@@ -222,22 +207,18 @@ const handleColumnOrderChange = (order: string[]) => {
</template>
</el-table-column>
<template v-for="col in sortedTableColumns" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '')"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip>
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 特殊列的模板可以在这里添加 -->
<template v-if="col.prop === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ scope.row.schoolTerm === 1 ? '上学期' : scope.row.schoolTerm === 2 ? '下学期' : scope.row.schoolTerm }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="checkColumnVisible(col.prop || '')" :prop="col.prop" :label="col.label" show-overflow-tooltip>
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 特殊列的模板可以在这里添加 -->
<template v-if="col.prop === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ scope.row.schoolTerm === 1 ? '上学期' : scope.row.schoolTerm === 2 ? '下学期' : scope.row.schoolTerm }}
</el-tag>
</template>
</el-table-column>
</template>
<el-table-column label="操作" align="center" fixed="right">
<!-- 操作列内容 -->
@@ -248,15 +229,15 @@ const handleColumnOrderChange = (order: string[]) => {
```typescript
onMounted(() => {
// 其他初始化代码...
// 确保配置已同步
nextTick(() => {
if (visibleColumns.value.length === 0) {
loadSavedConfig()
}
})
})
// 其他初始化代码...
// 确保配置已同步
nextTick(() => {
if (visibleColumns.value.length === 0) {
loadSavedConfig();
}
});
});
```
## 注意事项
@@ -269,4 +250,3 @@ onMounted(() => {
## 示例
参考 `src/views/stuwork/weekPlan/index.vue``src/views/stuwork/classroomhygienemonthly/index.vue` 的完整实现。

View File

@@ -3,6 +3,7 @@
## 简介
`TableColumnControl` 是一个通用的表格列控制组件,支持:
- ✅ 列的显示/隐藏控制
- ✅ 列的拖拽排序
- ✅ 配置的自动保存和恢复(基于路由)
@@ -14,11 +15,11 @@
```vue
<script setup lang="ts">
import { ref } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import type { TableInstance } from 'element-plus'
import { ref } from 'vue';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
import type { TableInstance } from 'element-plus';
const tableRef = ref<TableInstance>()
const tableRef = ref<TableInstance>();
</script>
```
@@ -26,41 +27,39 @@ const tableRef = ref<TableInstance>()
```vue
<template>
<div>
<!-- 列设置按钮放在表格上方 -->
<right-toolbar>
<TableColumnControl
:table-ref="tableRef"
trigger-circle
>
<template #trigger>
<el-tooltip content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
<!-- 表格 -->
<el-table ref="tableRef" :data="tableData">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" />
<el-table-column prop="email" label="邮箱" />
<el-table-column label="操作" fixed="right">
<template #default="scope">
<el-button @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div>
<!-- 列设置按钮放在表格上方 -->
<right-toolbar>
<TableColumnControl :table-ref="tableRef" trigger-circle>
<template #trigger>
<el-tooltip content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
<!-- 表格 -->
<el-table ref="tableRef" :data="tableData">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" />
<el-table-column prop="email" label="邮箱" />
<el-table-column label="操作" fixed="right">
<template #default="scope">
<el-button @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
```
### 步骤 3: 完成!
就这么简单!组件会自动:
- 从表格中提取列配置
- 根据当前路由自动生成存储 key
- 保存和恢复用户的列设置
@@ -71,149 +70,135 @@ const tableRef = ref<TableInstance>()
```vue
<template>
<div>
<right-toolbar>
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
trigger-circle
>
<template #trigger>
<el-tooltip content="列设置" placement="top">
<el-button circle>
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
<el-table :data="tableData">
<el-table-column
v-for="col in sortedTableColumns"
v-if="checkColumnVisible(col.prop || '')"
:key="col.prop"
:prop="col.prop"
:label="col.label"
/>
</el-table>
</div>
<div>
<right-toolbar>
<TableColumnControl :columns="tableColumns" v-model="visibleColumns" trigger-circle>
<template #trigger>
<el-tooltip content="列设置" placement="top">
<el-button circle>
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
<el-table :data="tableData">
<el-table-column
v-for="col in sortedTableColumns"
v-if="checkColumnVisible(col.prop || '')"
:key="col.prop"
:prop="col.prop"
:label="col.label"
/>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
const route = useRoute()
const visibleColumns = ref<string[]>([])
const columnOrder = ref<string[]>([])
const route = useRoute();
const visibleColumns = ref<string[]>([]);
const columnOrder = ref<string[]>([]);
const tableColumns = [
{ prop: 'name', label: '姓名' },
{ prop: 'age', label: '年龄' },
{ prop: 'email', label: '邮箱' },
{ prop: 'action', label: '操作', fixed: true, alwaysShow: true }
]
{ prop: 'name', label: '姓名' },
{ prop: 'age', label: '年龄' },
{ prop: 'email', label: '邮箱' },
{ prop: 'action', label: '操作', fixed: true, alwaysShow: true },
];
// 检查列是否可见
const checkColumnVisible = (prop: string): boolean => {
if (visibleColumns.value.length === 0) return true
return visibleColumns.value.includes(prop)
}
if (visibleColumns.value.length === 0) return true;
return visibleColumns.value.includes(prop);
};
// 排序后的列
const sortedTableColumns = computed(() => {
if (columnOrder.value.length === 0) return tableColumns
const ordered: typeof tableColumns = []
const unordered: typeof tableColumns = []
columnOrder.value.forEach(key => {
const col = tableColumns.find(c => (c.prop || c.label) === key)
if (col) ordered.push(col)
})
tableColumns.forEach(col => {
const key = col.prop || col.label
if (!columnOrder.value.includes(key)) {
unordered.push(col)
}
})
return [...ordered, ...unordered]
})
if (columnOrder.value.length === 0) return tableColumns;
const ordered: typeof tableColumns = [];
const unordered: typeof tableColumns = [];
columnOrder.value.forEach((key) => {
const col = tableColumns.find((c) => (c.prop || c.label) === key);
if (col) ordered.push(col);
});
tableColumns.forEach((col) => {
const key = col.prop || col.label;
if (!columnOrder.value.includes(key)) {
unordered.push(col);
}
});
return [...ordered, ...unordered];
});
// 加载保存的配置
const loadSavedConfig = () => {
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}`
const saved = localStorage.getItem(storageKey)
if (saved) {
try {
const savedColumns = JSON.parse(saved)
const validColumns = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
const filteredSaved = savedColumns.filter((col: string) => validColumns.includes(col))
if (filteredSaved.length > 0) {
visibleColumns.value = filteredSaved
} else {
visibleColumns.value = validColumns
}
} catch (e) {
visibleColumns.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
} else {
visibleColumns.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
// 加载排序
const orderKey = `${storageKey}-order`
const savedOrder = localStorage.getItem(orderKey)
if (savedOrder) {
try {
columnOrder.value = JSON.parse(savedOrder)
} catch (e) {
columnOrder.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
} else {
columnOrder.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
}
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-');
const storageKey = `table-columns-${routePath}`;
const saved = localStorage.getItem(storageKey);
if (saved) {
try {
const savedColumns = JSON.parse(saved);
const validColumns = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
const filteredSaved = savedColumns.filter((col: string) => validColumns.includes(col));
if (filteredSaved.length > 0) {
visibleColumns.value = filteredSaved;
} else {
visibleColumns.value = validColumns;
}
} catch (e) {
visibleColumns.value = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
}
} else {
visibleColumns.value = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
}
// 加载排序
const orderKey = `${storageKey}-order`;
const savedOrder = localStorage.getItem(orderKey);
if (savedOrder) {
try {
columnOrder.value = JSON.parse(savedOrder);
} catch (e) {
columnOrder.value = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
}
} else {
columnOrder.value = tableColumns.filter((col) => !col.alwaysShow && !col.fixed).map((col) => col.prop || col.label);
}
};
// 监听列变化
const handleColumnChange = (columns: string[]) => {
visibleColumns.value = columns
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}`
const selectableColumns = columns.filter(col => {
const column = tableColumns.find(c => (c.prop || c.label) === col)
return column && !column.alwaysShow && !column.fixed
})
localStorage.setItem(storageKey, JSON.stringify(selectableColumns))
}
visibleColumns.value = columns;
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-');
const storageKey = `table-columns-${routePath}`;
const selectableColumns = columns.filter((col) => {
const column = tableColumns.find((c) => (c.prop || c.label) === col);
return column && !column.alwaysShow && !column.fixed;
});
localStorage.setItem(storageKey, JSON.stringify(selectableColumns));
};
// 监听列排序变化
const handleColumnOrderChange = (order: string[]) => {
columnOrder.value = order
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}-order`
localStorage.setItem(storageKey, JSON.stringify(order))
}
columnOrder.value = order;
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-');
const storageKey = `table-columns-${routePath}-order`;
localStorage.setItem(storageKey, JSON.stringify(order));
};
// 立即加载配置
loadSavedConfig()
loadSavedConfig();
</script>
```
@@ -223,30 +208,24 @@ loadSavedConfig()
```vue
<script setup lang="ts">
import { computed } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { useTableColumnControl } from '/@/composables/useTableColumnControl'
import { computed } from 'vue';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
import { useTableColumnControl } from '/@/composables/useTableColumnControl';
const tableColumns = [
{ prop: 'name', label: '姓名' },
{ prop: 'age', label: '年龄' },
{ prop: 'email', label: '邮箱' },
{ prop: 'action', label: '操作', fixed: true, alwaysShow: true }
]
{ prop: 'name', label: '姓名' },
{ prop: 'age', label: '年龄' },
{ prop: 'email', label: '邮箱' },
{ prop: 'action', label: '操作', fixed: true, alwaysShow: true },
];
const {
visibleColumns,
columnOrder,
checkColumnVisible,
saveColumnConfig,
saveColumnOrder
} = useTableColumnControl({
columns: tableColumns
})
const { visibleColumns, columnOrder, checkColumnVisible, saveColumnConfig, saveColumnOrder } = useTableColumnControl({
columns: tableColumns,
});
const sortedTableColumns = computed(() => {
// ... 排序逻辑
})
// ... 排序逻辑
});
</script>
```
@@ -255,20 +234,17 @@ const sortedTableColumns = computed(() => {
### Q: 如何自定义存储 key
```vue
<TableColumnControl
:table-ref="tableRef"
storage-key="my-custom-key"
/>
<TableColumnControl :table-ref="tableRef" storage-key="my-custom-key" />
```
### Q: 如何设置始终显示的列?
```vue
<TableColumnControl
:table-ref="tableRef"
:auto-extract-options="{
alwaysShow: ['name', 'action']
}"
:table-ref="tableRef"
:auto-extract-options="{
alwaysShow: ['name', 'action'],
}"
/>
```
@@ -276,10 +252,10 @@ const sortedTableColumns = computed(() => {
```vue
<TableColumnControl
:table-ref="tableRef"
:auto-extract-options="{
defaultHidden: ['remark', 'description']
}"
:table-ref="tableRef"
:auto-extract-options="{
defaultHidden: ['remark', 'description'],
}"
/>
```
@@ -290,4 +266,3 @@ const sortedTableColumns = computed(() => {
## 完整示例
查看 `src/views/stuwork/classroomhygienemonthly/index.vue` 了解完整的使用示例。

View File

@@ -17,11 +17,11 @@
```vue
<script setup lang="ts">
import { ref } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import type { TableInstance } from 'element-plus'
import { ref } from 'vue';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
import type { TableInstance } from 'element-plus';
const tableRef = ref<TableInstance>()
const tableRef = ref<TableInstance>();
</script>
```
@@ -29,32 +29,30 @@ const tableRef = ref<TableInstance>()
```vue
<template>
<right-toolbar>
<TableColumnControl
:table-ref="tableRef"
trigger-circle
>
<template #trigger>
<el-tooltip content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
<el-table ref="tableRef" :data="tableData">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" />
<el-table-column label="操作" fixed="right" />
</el-table>
<right-toolbar>
<TableColumnControl :table-ref="tableRef" trigger-circle>
<template #trigger>
<el-tooltip content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
<el-table ref="tableRef" :data="tableData">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" />
<el-table-column label="操作" fixed="right" />
</el-table>
</template>
```
### 步骤 3: 完成!
就这么简单!组件会自动:
- ✅ 从表格中提取列配置
- ✅ 根据当前路由自动生成存储 key
- ✅ 保存和恢复用户的列设置
@@ -67,145 +65,107 @@ const tableRef = ref<TableInstance>()
```vue
<template>
<div>
<!-- 表格列控制按钮 -->
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
@change="handleColumnChange"
/>
<!-- 表格 -->
<el-table :data="tableData">
<el-table-column
v-for="col in visibleTableColumns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:width="col.width"
/>
</el-table>
</div>
<div>
<!-- 表格列控制按钮 -->
<TableColumnControl :columns="tableColumns" v-model="visibleColumns" @change="handleColumnChange" />
<!-- 表格 -->
<el-table :data="tableData">
<el-table-column v-for="col in visibleTableColumns" :key="col.prop" :prop="col.prop" :label="col.label" :width="col.width" />
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { ref, computed } from 'vue';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';
const tableColumns = [
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'age', label: '年龄', width: 80 },
{ prop: 'email', label: '邮箱', width: 200 },
{ prop: 'address', label: '地址', width: 300 }
]
{ prop: 'name', label: '姓名', width: 120 },
{ prop: 'age', label: '年龄', width: 80 },
{ prop: 'email', label: '邮箱', width: 200 },
{ prop: 'address', label: '地址', width: 300 },
];
const visibleColumns = ref<string[]>(['name', 'age', 'email', 'address'])
const visibleColumns = ref<string[]>(['name', 'age', 'email', 'address']);
// 根据 visibleColumns 过滤出需要显示的列
const visibleTableColumns = computed(() => {
return tableColumns.filter(col =>
visibleColumns.value.includes(col.prop || col.label)
)
})
return tableColumns.filter((col) => visibleColumns.value.includes(col.prop || col.label));
});
const handleColumnChange = (columns: string[]) => {
console.log('显示的列:', columns)
}
console.log('显示的列:', columns);
};
</script>
```
### 使用 localStorage 持久化
```vue
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
storage-key="my-table-columns"
/>
<TableColumnControl :columns="tableColumns" v-model="visibleColumns" storage-key="my-table-columns" />
```
### 固定列(不可隐藏)
```vue
const tableColumns = [
{ prop: 'name', label: '姓名', fixed: 'left' }, // 固定侧,不可隐藏
{ prop: 'age', label: '年龄' },
{ prop: 'action', label: '操作', fixed: 'right' } // 固定右侧,不可隐藏
]
const tableColumns = [ { prop: 'name', label: '姓名', fixed: 'left' }, // 固定左侧,不可隐藏 { prop: 'age', label: '年龄' }, { prop: 'action', label:
'操作', fixed: 'right' } // 固定侧,不可隐藏 ]
```
### 始终显示的列
```vue
const tableColumns = [
{ prop: 'name', label: '姓名', alwaysShow: true }, // 始终显示,不可隐藏
{ prop: 'age', label: '年龄' }
]
const tableColumns = [ { prop: 'name', label: '姓名', alwaysShow: true }, // 始终显示,不可隐藏 { prop: 'age', label: '年龄' } ]
```
### 自定义触发按钮
```vue
<!-- 使用图标按钮 -->
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
/>
<TableColumnControl :columns="tableColumns" v-model="visibleColumns" trigger-type="default" trigger-circle />
<!-- 使用文字按钮 -->
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
trigger-text="列设置"
trigger-type="primary"
/>
<TableColumnControl :columns="tableColumns" v-model="visibleColumns" trigger-text="列设置" trigger-type="primary" />
<!-- 使用链接按钮 -->
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
trigger-link
trigger-text="自定义列"
/>
<TableColumnControl :columns="tableColumns" v-model="visibleColumns" trigger-link trigger-text="自定义列" />
```
## Props
| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| columns | 表格列配置数组 | Column[] | 必填 |
| modelValue | 当前显示的列的 prop 或 label 数组 | string[] | - |
| storageKey | localStorage 存储的 key用于持久化 | string | - |
| triggerType | 触发按钮的类型 | 'default' \| 'primary' \| 'success' \| 'warning' \| 'danger' \| 'info' \| 'text' | 'default' |
| triggerSize | 触发按钮的大小 | 'large' \| 'default' \| 'small' | 'default' |
| triggerCircle | 触发按钮是否为圆形 | boolean | false |
| triggerText | 触发按钮的文字 | string | '' |
| triggerLink | 触发按钮是否为链接样式 | boolean | false |
| 参数 | 说明 | 类型 | 默认值 |
| ------------- | ----------------------------------- | -------------------------------------------------------------------------------- | --------- |
| columns | 表格列配置数组 | Column[] | 必填 |
| modelValue | 当前显示的列的 prop 或 label 数组 | string[] | - |
| storageKey | localStorage 存储的 key用于持久化 | string | - |
| triggerType | 触发按钮的类型 | 'default' \| 'primary' \| 'success' \| 'warning' \| 'danger' \| 'info' \| 'text' | 'default' |
| triggerSize | 触发按钮的大小 | 'large' \| 'default' \| 'small' | 'default' |
| triggerCircle | 触发按钮是否为圆形 | boolean | false |
| triggerText | 触发按钮的文字 | string | '' |
| triggerLink | 触发按钮是否为链接样式 | boolean | false |
## Events
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| 事件名 | 说明 | 回调参数 |
| ----------------- | ------------------ | ------------------- |
| update:modelValue | 显示的列变化时触发 | (columns: string[]) |
| change | 显示的列变化时触发 | (columns: string[]) |
| change | 显示的列变化时触发 | (columns: string[]) |
## Column 接口
```typescript
interface Column {
prop?: string // 列的 prop用于标识列
label: string // 列的标签
fixed?: boolean | 'left' | 'right' // 固定列,不可隐藏
alwaysShow?: boolean // 始终显示的列,不可隐藏
[key: string]: any // 其他属性
prop?: string; // 列的 prop用于标识列
label: string; // 列的标签
fixed?: boolean | 'left' | 'right'; // 固定列,不可隐藏
alwaysShow?: boolean; // 始终显示的列,不可隐藏
[key: string]: any; // 其他属性
}
```
## 插槽
| 插槽名 | 说明 |
|--------|------|
| 插槽名 | 说明 |
| ------- | ------------------ |
| trigger | 自定义触发按钮内容 |

View File

@@ -8,32 +8,32 @@
```vue
<template>
<div>
<!-- 列设置按钮 -->
<TableColumnControl
:table-ref="tableRef"
v-model="visibleTableColumns"
storage-key="my-table-columns"
trigger-text="列设置"
:auto-extract-options="{
alwaysShow: ['name', 'action'], // 始终显示的列prop label
defaultHidden: ['remark'], // 默认隐藏的列
}"
/>
<!-- 表格 -->
<el-table ref="tableRef" :data="tableData">
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="age" label="年龄" width="80" />
<el-table-column prop="email" label="邮箱" width="200" />
<el-table-column prop="remark" label="备注" width="300" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div>
<!-- 列设置按钮 -->
<TableColumnControl
:table-ref="tableRef"
v-model="visibleTableColumns"
storage-key="my-table-columns"
trigger-text="列设置"
:auto-extract-options="{
alwaysShow: ['name', 'action'], // 始终显示的列prop label
defaultHidden: ['remark'], // 默认隐藏的列
}"
/>
<!-- 表格 -->
<el-table ref="tableRef" :data="tableData">
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="age" label="年龄" width="80" />
<el-table-column prop="email" label="邮箱" width="200" />
<el-table-column prop="remark" label="备注" width="300" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
@@ -48,6 +48,7 @@ const tableData = ref([...])
```
**优点:**
- ✅ 无需手动配置列信息
- ✅ 自动同步表格列的变化
- ✅ 代码更简洁
@@ -58,32 +59,17 @@ const tableData = ref([...])
```vue
<template>
<div>
<!-- 列设置按钮 -->
<TableColumnControl
:columns="tableColumnConfig"
v-model="visibleTableColumns"
storage-key="my-table-columns"
trigger-text="列设置"
/>
<!-- 表格 -->
<el-table :data="tableData">
<el-table-column
v-if="isColumnVisible('name')"
prop="name"
label="姓名"
width="120"
/>
<el-table-column
v-if="isColumnVisible('age')"
prop="age"
label="年龄"
width="80"
/>
<!-- ... 其他列 ... -->
</el-table>
</div>
<div>
<!-- 列设置按钮 -->
<TableColumnControl :columns="tableColumnConfig" v-model="visibleTableColumns" storage-key="my-table-columns" trigger-text="列设置" />
<!-- 表格 -->
<el-table :data="tableData">
<el-table-column v-if="isColumnVisible('name')" prop="name" label="姓名" width="120" />
<el-table-column v-if="isColumnVisible('age')" prop="age" label="年龄" width="80" />
<!-- ... 其他列 ... -->
</el-table>
</div>
</template>
<script setup lang="ts">
@@ -115,14 +101,14 @@ const isColumnVisible = (propOrLabel: string) => {
```typescript
interface AutoExtractOptions {
// 默认隐藏的列prop 或 label
defaultHidden?: string[]
// 始终显示的列prop 或 label
alwaysShow?: string[]
// 列配置映射(用于自定义列的显示名称等)
columnMap?: Record<string, Partial<ColumnConfig>>
// 默认隐藏的列prop 或 label
defaultHidden?: string[];
// 始终显示的列prop 或 label
alwaysShow?: string[];
// 列配置映射(用于自定义列的显示名称等)
columnMap?: Record<string, Partial<ColumnConfig>>;
}
```
@@ -130,36 +116,39 @@ interface AutoExtractOptions {
```vue
<TableColumnControl
:table-ref="tableRef"
v-model="visibleTableColumns"
storage-key="my-table-columns"
:auto-extract-options="{
// 默认隐藏备注列
defaultHidden: ['remark', 'description'],
// 始终显示姓名和操作列
alwaysShow: ['name', 'action'],
// 自定义列的显示名称
columnMap: {
'email': { label: '电子邮箱' },
'phone': { label: '联系电话' }
}
}"
:table-ref="tableRef"
v-model="visibleTableColumns"
storage-key="my-table-columns"
:auto-extract-options="{
// 默认隐藏备注列
defaultHidden: ['remark', 'description'],
// 始终显示姓名和操作列
alwaysShow: ['name', 'action'],
// 自定义列的显示名称
columnMap: {
email: { label: '电子邮箱' },
phone: { label: '联系电话' },
},
}"
/>
```
## 注意事项
1. **自动提取的限制**
- 需要在表格渲染完成后才能提取列配置
- 如果表格列是动态生成的,可能需要调用 `refreshColumns()` 方法
2. **固定列**
- 使用 `fixed="left"``fixed="right"` 的列会自动标记为不可隐藏
-`alwaysShow` 中指定的列也会不可隐藏
3. **存储键storageKey**
- 建议为每个页面使用唯一的 `storageKey`,避免列配置冲突
- 格式建议:`页面名称-table-columns`,如 `user-list-table-columns`
@@ -172,25 +161,19 @@ interface AutoExtractOptions {
从手动配置迁移到自动提取:
**之前(手动配置):**
```vue
<TableColumnControl
:columns="tableColumnConfig"
v-model="visibleTableColumns"
storage-key="my-table-columns"
/>
<TableColumnControl :columns="tableColumnConfig" v-model="visibleTableColumns" storage-key="my-table-columns" />
```
**之后(自动提取):**
```vue
<TableColumnControl
:table-ref="tableRef"
v-model="visibleTableColumns"
storage-key="my-table-columns"
/>
<TableColumnControl :table-ref="tableRef" v-model="visibleTableColumns" storage-key="my-table-columns" />
```
只需要:
1.`:columns` 改为 `:table-ref="tableRef"`
2.`el-table` 上添加 `ref="tableRef"`
3. 移除 `tableColumnConfig``isColumnVisible` 函数(如果不再需要)

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,43 @@
<template>
<div class="flex flex-wrap items-center gap-2 max-w-full">
<el-tag
v-for="tag in tags"
:key="tag"
:type="tagType"
closable
:disable-transitions="false"
@close="handleClose(tag)"
>
{{ tag }}
</el-tag>
<el-input
v-if="inputVisible"
ref="InputRef"
v-model="inputValue"
class="w-20"
size="small"
@keyup.enter="handleInputConfirm"
@blur="handleInputConfirm"
/>
<el-button v-else class="button-new-tag" size="small" @click="showInput">
{{ buttonText }}
</el-button>
</div>
<div class="flex flex-wrap items-center gap-2 max-w-full">
<el-tag v-for="tag in tags" :key="tag" :type="tagType" closable :disable-transitions="false" @close="handleClose(tag)">
{{ tag }}
</el-tag>
<el-input
v-if="inputVisible"
ref="InputRef"
v-model="inputValue"
class="w-20"
size="small"
@keyup.enter="handleInputConfirm"
@blur="handleInputConfirm"
/>
<el-button v-else class="button-new-tag" size="small" @click="showInput">
{{ buttonText }}
</el-button>
</div>
</template>
<script lang="ts" setup>
import {ref, nextTick, defineProps, defineEmits} from 'vue';
import {ElInput} from 'element-plus';
import { ref, nextTick, defineProps, defineEmits } from 'vue';
import { ElInput } from 'element-plus';
const props = defineProps({
modelValue: {
type: Array as () => string[],
default: () => []
},
buttonText: {
type: String,
default: '+ New Tag'
},
tagType: {
type: String,
default: 'primary',
validator: (value: string) => {
return ['primary', 'success', 'info', 'warning', 'danger'].includes(value);
}
}
modelValue: {
type: Array as () => string[],
default: () => [],
},
buttonText: {
type: String,
default: '+ New Tag',
},
tagType: {
type: String,
default: 'primary',
validator: (value: string) => {
return ['primary', 'success', 'info', 'warning', 'danger'].includes(value);
},
},
});
const emits = defineEmits(['update:modelValue']);
@@ -56,27 +49,30 @@ const InputRef = ref<InstanceType<typeof ElInput>>();
const tags = ref([...props.modelValue]);
const handleClose = (tag: string) => {
tags.value.splice(tags.value.indexOf(tag), 1);
emits('update:modelValue', tags.value);
tags.value.splice(tags.value.indexOf(tag), 1);
emits('update:modelValue', tags.value);
};
watch(() => props.modelValue, (val) => {
tags.value = val;
});
watch(
() => props.modelValue,
(val) => {
tags.value = val;
}
);
const showInput = () => {
inputVisible.value = true;
nextTick(() => {
InputRef.value!.input!.focus();
});
inputVisible.value = true;
nextTick(() => {
InputRef.value!.input!.focus();
});
};
const handleInputConfirm = () => {
if (inputValue.value) {
tags.value.push(inputValue.value);
emits('update:modelValue', tags.value);
}
inputVisible.value = false;
inputValue.value = '';
if (inputValue.value) {
tags.value.push(inputValue.value);
emits('update:modelValue', tags.value);
}
inputVisible.value = false;
inputValue.value = '';
};
</script>

View File

@@ -1,107 +1,106 @@
<template>
<!-- 方案1: 姓名用 tag工号普通文本 -->
<div v-if="variant === 'tag-name'" class="teacher-name-no">
<el-tag size="small" type="primary" effect="plain">{{ name || '-' }}</el-tag>
<span class="separator">/</span>
<span class="no">{{ no || '-' }}</span>
</div>
<!-- 方案2: 工号用 tag姓名普通文本推荐 -->
<div v-else-if="variant === 'tag-no'" class="teacher-name-no">
<span class="name">{{ name || '-' }}</span>
<span class="separator">/</span>
<el-tag size="small" type="primary" effect="plain">{{ no || '-' }}</el-tag>
</div>
<!-- 方案3: 姓名和工号都用 tag -->
<div v-else-if="variant === 'tag-both'" class="teacher-name-no">
<el-tag size="small" type="primary" effect="plain">{{ name || '-' }}</el-tag>
<span class="separator">/</span>
<el-tag size="small" type="info" effect="plain">{{ no || '-' }}</el-tag>
</div>
<!-- 方案4: 姓名用 tag实心工号普通文本 -->
<div v-else-if="variant === 'tag-name-solid'" class="teacher-name-no">
<el-tag size="small" type="primary">{{ name || '-' }}</el-tag>
<span class="separator">/</span>
<span class="no">{{ no || '-' }}</span>
</div>
<!-- 方案5: 姓名用彩色背景工号普通文本 -->
<div v-else-if="variant === 'badge-name'" class="teacher-name-no">
<span class="name-badge">{{ name || '-' }}</span>
<span class="separator">/</span>
<span class="no">{{ no || '-' }}</span>
</div>
<!-- 默认方案: 姓名普通文本工号轻量徽章推荐与页面协调 -->
<div v-else class="teacher-name-no">
<span class="name">{{ name || '-' }}</span>
<span class="separator">/</span>
<span class="no-badge">{{ no || '-' }}</span>
</div>
<!-- 方案1: 姓名用 tag工号普通文本 -->
<div v-if="variant === 'tag-name'" class="teacher-name-no">
<el-tag size="small" type="primary" effect="plain">{{ name || '-' }}</el-tag>
<span class="separator">/</span>
<span class="no">{{ no || '-' }}</span>
</div>
<!-- 方案2: 工号用 tag姓名普通文本推荐 -->
<div v-else-if="variant === 'tag-no'" class="teacher-name-no">
<span class="name">{{ name || '-' }}</span>
<span class="separator">/</span>
<el-tag size="small" type="primary" effect="plain">{{ no || '-' }}</el-tag>
</div>
<!-- 方案3: 姓名和工号都用 tag -->
<div v-else-if="variant === 'tag-both'" class="teacher-name-no">
<el-tag size="small" type="primary" effect="plain">{{ name || '-' }}</el-tag>
<span class="separator">/</span>
<el-tag size="small" type="info" effect="plain">{{ no || '-' }}</el-tag>
</div>
<!-- 方案4: 姓名用 tag实心工号普通文本 -->
<div v-else-if="variant === 'tag-name-solid'" class="teacher-name-no">
<el-tag size="small" type="primary">{{ name || '-' }}</el-tag>
<span class="separator">/</span>
<span class="no">{{ no || '-' }}</span>
</div>
<!-- 方案5: 姓名用彩色背景工号普通文本 -->
<div v-else-if="variant === 'badge-name'" class="teacher-name-no">
<span class="name-badge">{{ name || '-' }}</span>
<span class="separator">/</span>
<span class="no">{{ no || '-' }}</span>
</div>
<!-- 默认方案: 姓名普通文本工号轻量徽章推荐与页面协调 -->
<div v-else class="teacher-name-no">
<span class="name">{{ name || '-' }}</span>
<span class="separator">/</span>
<span class="no-badge">{{ no || '-' }}</span>
</div>
</template>
<script setup lang="ts">
interface Props {
name?: string;
no?: string;
variant?: 'default' | 'tag-name' | 'tag-no' | 'tag-both' | 'tag-name-solid' | 'badge-name';
name?: string;
no?: string;
variant?: 'default' | 'tag-name' | 'tag-no' | 'tag-both' | 'tag-name-solid' | 'badge-name';
}
withDefaults(defineProps<Props>(), {
variant: 'default'
variant: 'default',
});
</script>
<style scoped>
.teacher-name-no {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.name {
font-weight: 500;
color: #303133;
font-weight: 500;
color: #303133;
}
.separator {
font-size: 12px;
color: #909399;
font-size: 12px;
color: #909399;
}
.no {
font-size: 12px;
color: #606266;
font-size: 12px;
color: #606266;
}
.no-primary {
font-size: 12px;
color: var(--el-color-primary, #667eea);
font-weight: 500;
font-size: 12px;
color: var(--el-color-primary, #667eea);
font-weight: 500;
}
.no-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
background: var(--el-color-primary-light-9, #ecf5ff);
color: var(--el-color-primary, #667eea);
line-height: 1.2;
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
background: var(--el-color-primary-light-9, #ecf5ff);
color: var(--el-color-primary, #667eea);
line-height: 1.2;
}
.name-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;
font-size: 12px;
background: var(--el-color-primary-light-9, #ecf5ff);
color: var(--el-color-primary, #667eea);
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;
font-size: 12px;
background: var(--el-color-primary-light-9, #ecf5ff);
color: var(--el-color-primary, #667eea);
}
</style>

View File

@@ -1,135 +1,134 @@
<!-- excel 导入组件 -->
<template>
<el-dialog :title="prop.title" v-model="state.upload.open" :close-on-click-modal="false" draggable>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="headers"
:action="baseURL + other.adaptationUrl(url)"
:disabled="state.upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:on-error="handleFileError"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
{{ $t('excel.operationNotice') }}
<em>{{ $t('excel.clickUpload') }}</em>
</div>
<template #tip>
<div class="el-upload__tip text-center">
<span>{{ $t('excel.fileFormat') }}</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline"
@click="downExcelTemp" v-if="tempUrl"
>{{ $t('excel.downloadTemplate') }}
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<el-button type="primary" @click="submitFileForm">{{ $t('common.confirmButtonText') }}</el-button>
<el-button @click="state.upload.open = false">{{ $t('common.cancelButtonText') }}</el-button>
</template>
</el-dialog>
<el-dialog :title="prop.title" v-model="state.upload.open" :close-on-click-modal="false" draggable>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="headers"
:action="baseURL + other.adaptationUrl(url)"
:disabled="state.upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:on-error="handleFileError"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
{{ $t('excel.operationNotice') }}
<em>{{ $t('excel.clickUpload') }}</em>
</div>
<template #tip>
<div class="el-upload__tip text-center">
<span>{{ $t('excel.fileFormat') }}</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="downExcelTemp" v-if="tempUrl"
>{{ $t('excel.downloadTemplate') }}
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<el-button type="primary" @click="submitFileForm">{{ $t('common.confirmButtonText') }}</el-button>
<el-button @click="state.upload.open = false">{{ $t('common.cancelButtonText') }}</el-button>
</template>
</el-dialog>
<!--校验失败错误数据-->
<el-dialog :title="$t('excel.validationFailureData')" v-model="state.errorVisible">
<el-table :data="state.errorData">
<el-table-column property="lineNum" :label="$t('excel.lineNumbers')" width="100"></el-table-column>
<el-table-column property="errors" :label="$t('excel.misDescription')" show-overflow-tooltip>
<template v-slot="scope">
<el-tag type="danger" v-for="error in scope.row.errors" :key="error">{{ error }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!--校验失败错误数据-->
<el-dialog :title="$t('excel.validationFailureData')" v-model="state.errorVisible">
<el-table :data="state.errorData">
<el-table-column property="lineNum" :label="$t('excel.lineNumbers')" width="100"></el-table-column>
<el-table-column property="errors" :label="$t('excel.misDescription')" show-overflow-tooltip>
<template v-slot="scope">
<el-tag type="danger" v-for="error in scope.row.errors" :key="error">{{ error }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script setup lang="ts" name="upload-excel">
import {useMessage} from '/@/hooks/message';
import { useMessage } from '/@/hooks/message';
import other from '/@/utils/other';
import {Session} from '/@/utils/storage';
import { Session } from '/@/utils/storage';
const emit = defineEmits(['sizeChange', 'refreshDataList']);
const prop = defineProps({
url: {
type: String,
},
title: {
type: String,
},
tempUrl: {
type: String,
},
url: {
type: String,
},
title: {
type: String,
},
tempUrl: {
type: String,
},
});
const uploadRef = ref();
const state = reactive({
errorVisible: false,
errorData: [],
dialog: {
title: '',
isShowDialog: false,
},
upload: {
open: false,
isUploading: false,
},
errorVisible: false,
errorData: [],
dialog: {
title: '',
isShowDialog: false,
},
upload: {
open: false,
isUploading: false,
},
});
/**
* 下载模板文件
*/
const downExcelTemp = async () => {
try {
// 如果是 assets 文件夹下的文件,使用 import.meta.url 获取文件
if (prop.tempUrl && prop.tempUrl.includes('assets/file')) {
// 从路径中提取文件名
const fileName = prop.tempUrl.split('/').pop() || 'template.xlsx';
// 使用动态导入获取文件URL从 components/Upload 到 assets/file 的相对路径
const fileUrl = new URL(`../../assets/file/${fileName}`, import.meta.url).href;
const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error('文件下载失败');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
} else {
// 使用后端接口下载,文件名使用导入功能名称
const title = prop.title?.replace('导入', '') || '导入模板';
const fileName = `${title}模板.xlsx`;
await other.downBlobFile(other.adaptationUrl(prop.tempUrl), {}, fileName);
}
} catch (error) {
console.error('模板下载失败:', error);
useMessage().error('模板下载失败,请先维护模板文件');
}
try {
// 如果是 assets 文件夹下的文件,使用 import.meta.url 获取文件
if (prop.tempUrl && prop.tempUrl.includes('assets/file')) {
// 从路径中提取文件名
const fileName = prop.tempUrl.split('/').pop() || 'template.xlsx';
// 使用动态导入获取文件URL从 components/Upload 到 assets/file 的相对路径
const fileUrl = new URL(`../../assets/file/${fileName}`, import.meta.url).href;
const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error('文件下载失败');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
} else {
// 使用后端接口下载,文件名使用导入功能名称
const title = prop.title?.replace('导入', '') || '导入模板';
const fileName = `${title}模板.xlsx`;
await other.downBlobFile(other.adaptationUrl(prop.tempUrl), {}, fileName);
}
} catch (error) {
console.error('模板下载失败:', error);
useMessage().error('模板下载失败,请先维护模板文件');
}
};
/**
* 上传进度条变化事件
*/
const handleFileUploadProgress = () => {
state.upload.isUploading = true;
state.upload.isUploading = true;
};
/**
* 上传失败事件处理
*/
const handleFileError = () => {
useMessage().error('上传失败,数据格式不合法!');
state.upload.open = false;
useMessage().error('上传失败,数据格式不合法!');
state.upload.open = false;
};
/**
@@ -137,53 +136,53 @@ const handleFileError = () => {
* @param {any} response - 上传成功的响应结果
*/
const handleFileSuccess = (response: any) => {
state.upload.isUploading = false;
state.upload.open = false;
uploadRef.value.clearFiles();
state.upload.isUploading = false;
state.upload.open = false;
uploadRef.value.clearFiles();
// 校验失败
if (response.code === 1) {
useMessage().error('导入失败,以下数据不合法');
state.errorVisible = true;
state.errorData = response.data;
uploadRef.value.clearFiles();
// 刷新表格
emit?.('refreshDataList');
} else {
useMessage().success(response.msg ? response.msg : '导入成功');
// 刷新表格
emit?.('refreshDataList');
}
// 校验失败
if (response.code === 1) {
useMessage().error('导入失败,以下数据不合法');
state.errorVisible = true;
state.errorData = response.data;
uploadRef.value.clearFiles();
// 刷新表格
emit?.('refreshDataList');
} else {
useMessage().success(response.msg ? response.msg : '导入成功');
// 刷新表格
emit?.('refreshDataList');
}
};
/**
* 提交表单,触发上传
*/
const submitFileForm = () => {
uploadRef.value.submit();
uploadRef.value.submit();
};
/**
* 显示上传文件对话框,并清除上传信息
*/
const show = () => {
state.upload.isUploading = false;
state.upload.open = true;
state.upload.isUploading = false;
state.upload.open = true;
};
/**
* 计算请求头部信息
*/
const headers = computed(() => {
return {
Authorization: 'Bearer ' + Session.getToken(),
'TENANT-ID': Session.getTenant(),
};
return {
Authorization: 'Bearer ' + Session.getToken(),
'TENANT-ID': Session.getTenant(),
};
});
// 暴露变量
defineExpose({
show,
show,
});
</script>

View File

@@ -18,7 +18,9 @@
<span class="flex-1 text-gray-700 truncate transition-colors duration-200 group-hover:text-blue-600">
{{ getFileName(file) }}
</span>
<el-icon class="mr-2 text-gray-400 transition-colors duration-200 group-hover:text-blue-500" @click.stop="handlePreview(file)"><View /></el-icon>
<el-icon class="mr-2 text-gray-400 transition-colors duration-200 group-hover:text-blue-500" @click.stop="handlePreview(file)"
><View
/></el-icon>
<el-icon class="text-gray-400 transition-colors duration-200 group-hover:text-blue-500"><Download /></el-icon>
</div>
</div>
@@ -46,7 +48,7 @@
{{ $t('excel.clickUpload') }}
</el-button>
<template #tip>
<div class="el-upload__tip" v-if="props.isShowTip" style="margin-top: 8px; font-size: 12px; color: #909399;">
<div class="el-upload__tip" v-if="props.isShowTip" style="margin-top: 8px; font-size: 12px; color: #909399">
{{ $t('excel.pleaseUpload') }}
<template v-if="props.fileSize">
{{ $t('excel.size') }} <b style="color: #f56c6c">{{ props.fileSize }}MB</b></template
@@ -59,42 +61,24 @@
</template>
</el-upload>
<!-- 已上传文件列表 -->
<div v-if="fileList.length > 0" class="uploaded-files-list" style="margin-top: 12px;">
<div v-if="fileList.length > 0" class="uploaded-files-list" style="margin-top: 12px">
<div
v-for="(file, index) in fileList"
:key="index"
class="uploaded-file-item"
style="display: flex; align-items: center; padding: 8px 12px; margin-bottom: 8px; background: #f5f7fa; border-radius: 4px;"
style="display: flex; align-items: center; padding: 8px 12px; margin-bottom: 8px; background: #f5f7fa; border-radius: 4px"
>
<el-icon class="mr-2" style="color: #409eff;"><Document /></el-icon>
<span class="file-name" style="flex: 1; font-size: 14px; color: #606266; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<el-icon class="mr-2" style="color: #409eff"><Document /></el-icon>
<span class="file-name" style="flex: 1; font-size: 14px; color: #606266; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
{{ getFileName(file) }}
</span>
<el-button
type="primary"
link
size="small"
@click="handlePreview(file)"
style="margin-left: 4px;"
>
<el-button type="primary" link size="small" @click="handlePreview(file)" style="margin-left: 4px">
<el-icon><View /></el-icon>
</el-button>
<el-button
type="primary"
link
size="small"
@click="handleDownload(file)"
style="margin-left: 4px;"
>
<el-button type="primary" link size="small" @click="handleDownload(file)" style="margin-left: 4px">
<el-icon><Download /></el-icon>
</el-button>
<el-button
type="danger"
link
size="small"
@click="handleRemove(file)"
style="margin-left: 8px;"
>
<el-button type="danger" link size="small" @click="handleRemove(file)" style="margin-left: 8px">
<el-icon><Delete /></el-icon>
</el-button>
</div>
@@ -134,19 +118,15 @@
</template>
</el-upload>
</div>
<!-- 图片预览使用 teleport + 最高 z-index 确保全屏 -->
<teleport to="body">
<div
v-if="imagePreviewVisible"
class="image-preview-overlay"
@click="imagePreviewVisible = false"
>
<div v-if="imagePreviewVisible" class="image-preview-overlay" @click="imagePreviewVisible = false">
<img :src="imagePreviewUrl" class="image-preview-img" />
<button class="image-preview-close" @click.stop="imagePreviewVisible = false"></button>
</div>
</teleport>
<!-- PDF预览使用 teleport -->
<teleport to="body">
<el-dialog
@@ -160,10 +140,7 @@
append-to-body
class="pdf-preview-teleport"
>
<iframe
:src="pdfPreviewUrl"
style="width: 100%; height: 80vh; border: none;"
/>
<iframe :src="pdfPreviewUrl" style="width: 100%; height: 80vh; border: none" />
</el-dialog>
</teleport>
</template>
@@ -355,11 +332,11 @@ function handleUploadSuccess(res: any, file: any) {
// 调试:打印完整的响应数据
console.log('上传成功响应数据:', res);
console.log('res.data:', res.data);
// 获取文件ID - 尝试多种可能的字段名
let fileId = res.data?.id || res.data?.fileId || res.data?.file_id || null;
let fileUrl = res.data?.url || res.data?.fileUrl || '';
// 如果fileId不存在尝试从fileName中提取fileName可能是ID
if (!fileId && res.data?.fileName) {
const fileName = res.data.fileName;
@@ -368,7 +345,7 @@ function handleUploadSuccess(res: any, file: any) {
fileId = fileName;
}
}
// 如果fileId仍然不存在尝试从URL中提取
if (!fileId && fileUrl) {
try {
@@ -376,7 +353,7 @@ function handleUploadSuccess(res: any, file: any) {
fileId = urlObj.searchParams.get('id');
// 如果URL参数中没有id检查路径中是否有32位十六进制字符串
if (!fileId) {
const pathParts = urlObj.pathname.split('/').filter(p => p);
const pathParts = urlObj.pathname.split('/').filter((p) => p);
const lastPart = pathParts[pathParts.length - 1];
if (lastPart && /^[a-f0-9]{32}$/i.test(lastPart)) {
fileId = lastPart;
@@ -386,7 +363,7 @@ function handleUploadSuccess(res: any, file: any) {
// URL解析失败忽略
}
}
// 如果仍然没有fileId尝试从整个res.data中查找可能的ID字段
if (!fileId && res.data) {
// 遍历res.data的所有属性查找32位十六进制字符串
@@ -398,16 +375,16 @@ function handleUploadSuccess(res: any, file: any) {
}
}
}
console.log('提取的文件ID:', fileId);
// 构建URL如果存在id则添加到URL参数中用于兼容性
if (fileId && fileUrl) {
// 如果URL中已经有参数使用&,否则使用?
fileUrl = fileUrl.includes('?') ? `${fileUrl}&id=${fileId}` : `${fileUrl}?id=${fileId}`;
}
fileUrl = `${fileUrl}&originalFileName=${file.name}`;
uploadList.value.push({
name: file.name,
url: fileUrl,
@@ -417,9 +394,9 @@ function handleUploadSuccess(res: any, file: any) {
fileType: file.raw.type,
id: fileId, // 保存文件ID优先使用
});
console.log('保存的文件对象:', uploadList.value[uploadList.value.length - 1]);
uploadedSuccessfully();
} else {
number.value--;
@@ -441,9 +418,7 @@ const uploadedSuccessfully = () => {
};
const handleRemove = (file: { name?: string; id?: string; url?: string }) => {
fileList.value = fileList.value.filter(
(f) => !(f.id && f.id === file.id) && !(f.name && f.name === file.name)
);
fileList.value = fileList.value.filter((f) => !(f.id && f.id === file.id) && !(f.name && f.name === file.name));
emit('update:modelValue', fileList.value.length ? fileList.value : '');
emit('change', listToString(fileList.value), fileList.value);
};
@@ -451,7 +426,7 @@ const handleRemove = (file: { name?: string; id?: string; url?: string }) => {
const handleDownload = (file: any) => {
// 优先使用文件ID下载如果有自定义下载URL
if (file.id && downloadBaseUrl.value) {
const downloadUrl = downloadBaseUrl.value.includes('?')
const downloadUrl = downloadBaseUrl.value.includes('?')
? `${downloadBaseUrl.value}&fileId=${encodeURIComponent(file.id)}`
: `${downloadBaseUrl.value}?fileId=${encodeURIComponent(file.id)}`;
other.downBlobFile(downloadUrl, {}, file.name || file.fileTitle || '文件');
@@ -470,7 +445,7 @@ const handlePreview = async (file: any) => {
let fileUrl = '';
if (file.id && downloadBaseUrl.value) {
// 使用配置的下载URL
fileUrl = downloadBaseUrl.value.includes('?')
fileUrl = downloadBaseUrl.value.includes('?')
? `${downloadBaseUrl.value}&fileId=${encodeURIComponent(file.id)}`
: `${downloadBaseUrl.value}?fileId=${encodeURIComponent(file.id)}`;
} else if (file.id) {
@@ -498,26 +473,26 @@ const handlePreview = async (file: any) => {
if (!fileName) {
fileName = '文件';
}
// 获取文件扩展名
const ext = fileName.split('.').pop()?.toLowerCase() || '';
// 判断是否是图片
const imageExts = ['png', 'jpg', 'jpeg'];
// 判断是否是PDF
const isPdf = ext === 'pdf';
// 检查是否是图片
const isImage = imageExts.includes(ext);
try {
if (isImage) {
// 图片预览:使用 el-image-viewerteleported 到 body全屏显示
const response = await request({
const response = (await request({
url: fileUrl,
method: 'get',
responseType: 'blob',
}) as unknown as Response;
})) as unknown as Response;
if (!response || response.type === 'error') {
useMessage().error('获取文件失败');
return;
@@ -527,11 +502,11 @@ const handlePreview = async (file: any) => {
imagePreviewVisible.value = true;
} else if (isPdf) {
// PDF预览使用 el-dialogappend-to-body全屏显示
const response = await request({
const response = (await request({
url: fileUrl,
method: 'get',
responseType: 'blob',
}) as unknown as Response;
})) as unknown as Response;
if (!response || response.type === 'error') {
useMessage().error('获取文件失败');
return;
@@ -608,15 +583,15 @@ watch(
try {
const urlObj = new URL(item, window.location.origin);
const id = urlObj.searchParams.get('id');
item = {
name: other.getQueryString(item, 'originalFileName') || other.getQueryString(item, 'fileName'),
item = {
name: other.getQueryString(item, 'originalFileName') || other.getQueryString(item, 'fileName'),
url: item,
id: id || undefined
id: id || undefined,
};
} catch {
item = {
name: other.getQueryString(item, 'originalFileName') || other.getQueryString(item, 'fileName'),
url: item
item = {
name: other.getQueryString(item, 'originalFileName') || other.getQueryString(item, 'fileName'),
url: item,
};
}
}
@@ -642,13 +617,13 @@ defineExpose({
<style scoped>
.w-full {
width: 100%;
width: 100%;
}
.upload-file {
width: 100%;
width: 100%;
}
.mb20 {
margin-bottom: 20px;
margin-bottom: 20px;
}
</style>

View File

@@ -5,7 +5,7 @@ export default {
explain: 'Slide right to verify',
success: 'Verification successful',
fail: 'Verification failed',
time: 'Verified in {time}s'
}
}
time: 'Verified in {time}s',
},
},
};

View File

@@ -1,11 +1,11 @@
export default {
verify: {
complete: '请完成安全验证',
slide: {
explain: '向右滑动完成验证',
success: '验证成功',
fail: '验证失败',
time: '{time}s验证成功'
}
}
}
verify: {
complete: '请完成安全验证',
slide: {
explain: '向右滑动完成验证',
success: '验证成功',
fail: '验证失败',
time: '{time}s验证成功',
},
},
};

View File

@@ -1,72 +1,64 @@
<template>
<div>
<video-play
ref="playerRef"
v-bind="options"
:src="src"
@play="onPlay"
@pause="onPause"
@timeupdate="onTimeupdate"
@canplay="onCanplay"
/>
</div>
<div>
<video-play ref="playerRef" v-bind="options" :src="src" @play="onPlay" @pause="onPause" @timeupdate="onTimeupdate" @canplay="onCanplay" />
</div>
</template>
<script setup lang="ts">
import { reactive, shallowRef } from 'vue'
import 'vue3-video-play/dist/style.css'
import VideoPlay from 'vue3-video-play'
import { reactive, shallowRef } from 'vue';
import 'vue3-video-play/dist/style.css';
import VideoPlay from 'vue3-video-play';
const props = defineProps({
src: {
type: String,
required: true
},
width: String,
height: String,
poster: String
})
src: {
type: String,
required: true,
},
width: String,
height: String,
poster: String,
});
const playerRef = shallowRef()
const playerRef = shallowRef();
const options = reactive({
color: 'var(--el-color-primary)', //主题色
muted: false, //静音
webFullScreen: false,
speedRate: ['0.75', '1.0', '1.25', '1.5', '2.0'], //播放倍速
autoPlay: true, //自动播放
loop: false, //循环播放
mirror: false, //镜像画面
ligthOff: false, //关灯模式
volume: 0.3, //默认音量大小
control: true, //是否显示控制器
title: '', //视频名称
poster: '', //封面
...props
})
color: 'var(--el-color-primary)', //主题色
muted: false, //静音
webFullScreen: false,
speedRate: ['0.75', '1.0', '1.25', '1.5', '2.0'], //播放倍速
autoPlay: true, //自动播放
loop: false, //循环播放
mirror: false, //镜像画面
ligthOff: false, //关灯模式
volume: 0.3, //默认音量大小
control: true, //是否显示控制器
title: '', //视频名称
poster: '', //封面
...props,
});
const play = () => {
playerRef.value.play()
}
playerRef.value.play();
};
const pause = () => {
playerRef.value.pause()
}
playerRef.value.pause();
};
const onPlay = (event: any) => {
console.log(event, '播放')
}
console.log(event, '播放');
};
const onPause = (event: any) => {
console.log(event, '暂停')
}
console.log(event, '暂停');
};
const onTimeupdate = (event: any) => {
console.log(event, '时间更新')
}
console.log(event, '时间更新');
};
const onCanplay = (event: any) => {
console.log(event, '可以播放')
}
console.log(event, '可以播放');
};
defineExpose({
play,
pause
})
play,
pause,
});
</script>

View File

@@ -5,7 +5,7 @@
import { ElNotification } from 'element-plus';
import { Session } from '/@/utils/storage';
import other from '/@/utils/other';
import * as JsonFlowJob from "../../flow/stores";
import * as JsonFlowJob from '../../flow/stores';
const emit = defineEmits(['rollback']);
@@ -151,17 +151,16 @@ const onMessage = (msgEvent: any) => {
return;
}
if (JsonFlowJob.isJSON(text)) {
JsonFlowJob.notifyJsonFlowJob(msgEvent)
} else {
ElNotification.warning({
title: '消息提醒',
dangerouslyUseHTMLString: true,
message: text + '请及时处理',
offset: 60,
});
emit('rollback', text);
}
if (JsonFlowJob.isJSON(text)) {
JsonFlowJob.notifyJsonFlowJob(msgEvent);
} else {
ElNotification.warning({
title: '消息提醒',
dangerouslyUseHTMLString: true,
message: text + '请及时处理',
offset: 60,
});
emit('rollback', text);
}
};
</script>

View File

@@ -1,92 +1,77 @@
<template>
<el-dropdown
v-if="hasVisibleItems"
trigger="click"
@command="handleCommand"
:style="dropdownStyle"
>
<el-button
:type="buttonType"
link
:style="buttonStyle"
>
<slot name="button">
{{ buttonText }}
<el-icon v-if="buttonIcon" class="el-icon--right" :style="iconStyle">
<component :is="buttonIcon" v-if="buttonIcon" />
</el-icon>
</slot>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in visibleItems"
:key="item.command"
:command="item.command"
>
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span :style="item.icon ? { marginLeft: '8px' } : {}">{{ item.label }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown v-if="hasVisibleItems" trigger="click" @command="handleCommand" :style="dropdownStyle">
<el-button :type="buttonType" link :style="buttonStyle">
<slot name="button">
{{ buttonText }}
<el-icon v-if="buttonIcon" class="el-icon--right" :style="iconStyle">
<component :is="buttonIcon" v-if="buttonIcon" />
</el-icon>
</slot>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in visibleItems" :key="item.command" :command="item.command">
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span :style="item.icon ? { marginLeft: '8px' } : {}">{{ item.label }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Operation } from '@element-plus/icons-vue'
import { computed } from 'vue';
import { Operation } from '@element-plus/icons-vue';
interface MenuItem {
command: string
label: string
icon?: any
visible?: boolean | (() => boolean)
command: string;
label: string;
icon?: any;
visible?: boolean | (() => boolean);
}
interface Props {
items: MenuItem[]
buttonText?: string
buttonIcon?: any
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
buttonStyle?: string | Record<string, any>
dropdownStyle?: string | Record<string, any>
iconStyle?: string | Record<string, any>
items: MenuItem[];
buttonText?: string;
buttonIcon?: any;
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text';
buttonStyle?: string | Record<string, any>;
dropdownStyle?: string | Record<string, any>;
iconStyle?: string | Record<string, any>;
}
const props = withDefaults(defineProps<Props>(), {
buttonText: '更多',
buttonIcon: Operation,
buttonType: 'primary',
buttonStyle: () => ({ whiteSpace: 'nowrap' }),
dropdownStyle: () => ({ marginLeft: '12px' }),
iconStyle: () => ({ marginLeft: '4px' })
})
buttonText: '更多',
buttonIcon: Operation,
buttonType: 'primary',
buttonStyle: () => ({ whiteSpace: 'nowrap' }),
dropdownStyle: () => ({ marginLeft: '12px' }),
iconStyle: () => ({ marginLeft: '4px' }),
});
const emit = defineEmits<{
command: [command: string]
}>()
command: [command: string];
}>();
// 计算可见的菜单项
const visibleItems = computed(() => {
return props.items.filter(item => {
if (item.visible === undefined) return true
if (typeof item.visible === 'boolean') return item.visible
if (typeof item.visible === 'function') return item.visible()
return false
})
})
return props.items.filter((item) => {
if (item.visible === undefined) return true;
if (typeof item.visible === 'boolean') return item.visible;
if (typeof item.visible === 'function') return item.visible();
return false;
});
});
// 是否有可见的菜单项
const hasVisibleItems = computed(() => {
return visibleItems.value.length > 0
})
return visibleItems.value.length > 0;
});
// 处理命令
const handleCommand = (command: string) => {
emit('command', command)
}
emit('command', command);
};
</script>

View File

@@ -1,85 +1,74 @@
<template>
<div style="width: 100%; height: 100%;">
<!-- 图片直接使用原始地址展示保持原有行为 -->
<viewer :images="[authSrc]" v-if="!showIframe">
<img
v-if="!showIframe"
ref="imgRef"
:width="imgWidth ? imgWidth : '100%;'"
:height="imgHeight ? imgHeight : '100%;'"
:src="authSrc"
/>
</viewer>
<div style="width: 100%; height: 100%">
<!-- 图片直接使用原始地址展示保持原有行为 -->
<viewer :images="[authSrc]" v-if="!showIframe">
<img v-if="!showIframe" ref="imgRef" :width="imgWidth ? imgWidth : '100%;'" :height="imgHeight ? imgHeight : '100%;'" :src="authSrc" />
</viewer>
<!-- PDF通过 iframe + token 的请求展示 -->
<iframe
v-if="showIframe"
ref="authIframeRef"
style="width: 100%; height: 100%;"
/>
</div>
<!-- PDF通过 iframe + token 的请求展示 -->
<iframe v-if="showIframe" ref="authIframeRef" style="width: 100%; height: 100%" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import { Session } from '/@/utils/storage'
import { ref, onMounted, nextTick } from 'vue';
import { Session } from '/@/utils/storage';
const props = defineProps<{
authSrc: string
imgWidth?: string
imgHeight?: string
}>()
authSrc: string;
imgWidth?: string;
imgHeight?: string;
}>();
const showIframe = ref(false)
const authIframeRef = ref<HTMLIFrameElement | null>(null)
const imgRef = ref<HTMLImageElement | null>(null)
const showIframe = ref(false);
const authIframeRef = ref<HTMLIFrameElement | null>(null);
const imgRef = ref<HTMLImageElement | null>(null);
// 携带 token 请求 img/pdf 的 src
const getImgSrcByToken = (src?: string) => {
const targetSrc = src || props.authSrc
const targetSrc = src || props.authSrc;
if (targetSrc.indexOf('.pdf') >= 0) {
// PDF通过 iframe 展示
showIframe.value = true
nextTick(() => {
const tenantId = Session.getTenant()
const iframe = authIframeRef.value
if (!iframe) return
if (targetSrc.indexOf('.pdf') >= 0) {
// PDF通过 iframe 展示
showIframe.value = true;
nextTick(() => {
const tenantId = Session.getTenant();
const iframe = authIframeRef.value;
if (!iframe) return;
const request = new XMLHttpRequest()
request.responseType = 'blob'
request.open('get', targetSrc, true)
request.setRequestHeader('Authorization', 'Bearer ' + Session.getToken())
request.setRequestHeader('TENANT-ID', tenantId)
request.onreadystatechange = () => {
if (request.readyState === XMLHttpRequest.DONE && request.status === 200) {
const binaryData: BlobPart[] = []
binaryData.push(request.response)
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }))
iframe.onload = () => {
URL.revokeObjectURL(iframe.src)
}
}
}
request.send(null)
})
} else {
// 图片:保持原有行为(直接使用 authSrc不做 token 转发)
showIframe.value = false
// 如需带 token 加载图片,可参考被注释的旧逻辑在此扩展
}
}
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', targetSrc, true);
request.setRequestHeader('Authorization', 'Bearer ' + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState === XMLHttpRequest.DONE && request.status === 200) {
const binaryData: BlobPart[] = [];
binaryData.push(request.response);
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }));
iframe.onload = () => {
URL.revokeObjectURL(iframe.src);
};
}
};
request.send(null);
});
} else {
// 图片:保持原有行为(直接使用 authSrc不做 token 转发)
showIframe.value = false;
// 如需带 token 加载图片,可参考被注释的旧逻辑在此扩展
}
};
const refreshImg = (src?: string) => {
getImgSrcByToken(src)
}
getImgSrcByToken(src);
};
onMounted(() => {
getImgSrcByToken()
})
getImgSrcByToken();
});
defineExpose({
refreshImg,
})
refreshImg,
});
</script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,33 @@
<template>
<!-- 组件不占据任何布局空间所有预览组件都是 teleported -->
<div style="display: none;">
<!-- 图片直接使用 el-image-viewer 全屏预览 -->
<el-image-viewer
v-if="!showIframe && imageSrc && imagePreviewVisible"
:url-list="[imageSrc]"
:teleported="true"
hide-on-click-modal
@close="imagePreviewVisible = false"
/>
<!-- PDF dialog 中显示 -->
<el-dialog
v-if="showIframe"
v-model="pdfDialogVisible"
:title="dialogTitle || '文件预览'"
append-to-body
width="90%"
class="pdf-preview-dialog"
>
<iframe ref="authIframeRef" :style="{ width: '100%', height: pdfIframeHeight }" />
</el-dialog>
</div>
<!-- 组件不占据任何布局空间所有预览组件都是 teleported -->
<div style="display: none">
<!-- 图片直接使用 el-image-viewer 全屏预览 -->
<el-image-viewer
v-if="!showIframe && imageSrc && imagePreviewVisible"
:url-list="[imageSrc]"
:teleported="true"
hide-on-click-modal
@close="imagePreviewVisible = false"
/>
<!-- PDF dialog 中显示 -->
<el-dialog v-if="showIframe" v-model="pdfDialogVisible" :title="dialogTitle || '文件预览'" append-to-body width="90%" class="pdf-preview-dialog">
<iframe ref="authIframeRef" :style="{ width: '100%', height: pdfIframeHeight }" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, computed, onUnmounted } from 'vue';
import { ElImageViewer } from 'element-plus';
import { Session } from "/@/utils/storage";
import { Session } from '/@/utils/storage';
// 定义 props
const props = defineProps<{
authSrc: string;
imgWidth?: string;
imgHeight?: string;
dialogTitle?: string;
authSrc: string;
imgWidth?: string;
imgHeight?: string;
dialogTitle?: string;
}>();
// 定义响应式数据
@@ -47,95 +40,91 @@ const windowHeight = ref(window.innerHeight);
// 计算 PDF iframe 的合适高度(优先使用外部传入的 imgHeight否则根据窗口高度动态计算
const pdfIframeHeight = computed(() => {
// 如果外部传入了 imgHeight优先使用
if (props.imgHeight) {
return props.imgHeight;
}
// 否则根据窗口高度动态计算dialog header 约 50pxpadding 约 40px留一些余量
return `${windowHeight.value - 120}px`;
// 如果外部传入了 imgHeight优先使用
if (props.imgHeight) {
return props.imgHeight;
}
// 否则根据窗口高度动态计算dialog header 约 50pxpadding 约 40px留一些余量
return `${windowHeight.value - 120}px`;
});
// 监听窗口大小变化
const handleResize = () => {
windowHeight.value = window.innerHeight;
windowHeight.value = window.innerHeight;
};
onMounted(() => {
window.addEventListener('resize', handleResize);
getImgSrcByToken();
window.addEventListener('resize', handleResize);
getImgSrcByToken();
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('resize', handleResize);
});
// 携带token请求img的src
const getImgSrcByToken = (src?: string) => {
if (props.authSrc.indexOf(".pdf") >= 0) {
showIframe.value = true;
pdfDialogVisible.value = true;
nextTick(() => {
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const iframe = authIframeRef.value;
if (!iframe) return;
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
const binaryData: BlobPart[] = [];
binaryData.push(request.response);
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }));
iframe.onload = () => {
URL.revokeObjectURL(iframe.src);
};
}
};
request.send(null);
});
} else {
// 图片处理逻辑:加载后直接打开预览
showIframe.value = false;
pdfDialogVisible.value = false;
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', "Bearer " + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
imageSrc.value = URL.createObjectURL(request.response);
imagePreviewVisible.value = true;
}
};
request.send(null);
}
if (props.authSrc.indexOf('.pdf') >= 0) {
showIframe.value = true;
pdfDialogVisible.value = true;
nextTick(() => {
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const iframe = authIframeRef.value;
if (!iframe) return;
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', 'Bearer ' + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
const binaryData: BlobPart[] = [];
binaryData.push(request.response);
iframe.src = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }));
iframe.onload = () => {
URL.revokeObjectURL(iframe.src);
};
}
};
request.send(null);
});
} else {
// 图片处理逻辑:加载后直接打开预览
showIframe.value = false;
pdfDialogVisible.value = false;
const imgSrc = src || props.authSrc;
const tenantId = Session.getTenant();
const request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', imgSrc, true);
request.setRequestHeader('Authorization', 'Bearer ' + Session.getToken());
request.setRequestHeader('TENANT-ID', tenantId);
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
imageSrc.value = URL.createObjectURL(request.response);
imagePreviewVisible.value = true;
}
};
request.send(null);
}
};
// 刷新图片
const refreshImg = (src?: string) => {
getImgSrcByToken(src);
getImgSrcByToken(src);
};
// 暴露方法供外部调用
defineExpose({
refreshImg
refreshImg,
});
</script>
<style scoped>
.pdf-preview-dialog :deep(.el-dialog__body) {
padding: 20px !important;
overflow-y: hidden !important;
padding: 20px !important;
overflow-y: hidden !important;
}
</style>
</style>

View File

@@ -1,139 +1,134 @@
<!-- 敏感信息授权按钮组件 -->
<template>
<div>
<el-button class="grant-btn" type="text" @click="grantPrivilege">敏感信息授权</el-button>
<el-dialog append-to-body title="敏感信息短信授权"
v-model:visible="dialogVisible">
<el-form>
<el-form-item label="手机号">
<el-row>{{mobile}}</el-row>
</el-form-item>
<el-form-item label="验证码">
<el-col :span="11">
<el-input type="number" style="display: inline-block" maxlength="6" placeholder="请输入验证码" v-model="form.code"></el-input>
</el-col>
<el-col :span="5">
<el-button style="display: inline;margin-left: 5px;" plain type="text" @click.prevent="getCode()">{{title}}</el-button>
</el-col>
</el-form-item>
<el-alert :closable="false">验证通过后将去除脱敏有效期为30分钟请尽快操作</el-alert>
<el-alert type="warning" :closable="false">手机号码登记后统一维护如有修改请联系相关部门教职工(组织人事处) 学生(班主任) 驻校人员(后勤处)</el-alert>
</el-form>
<template v-slot:footer>
<span class="dialog-footer" style="text-aligin:center">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="checkCode"> </el-button>
</span>
</template>
</el-dialog>
</div>
<div>
<el-button class="grant-btn" type="text" @click="grantPrivilege">敏感信息授权</el-button>
<el-dialog append-to-body title="敏感信息短信授权" v-model:visible="dialogVisible">
<el-form>
<el-form-item label="手机号">
<el-row>{{ mobile }}</el-row>
</el-form-item>
<el-form-item label="验证码">
<el-col :span="11">
<el-input type="number" style="display: inline-block" maxlength="6" placeholder="请输入验证码" v-model="form.code"></el-input>
</el-col>
<el-col :span="5">
<el-button style="display: inline; margin-left: 5px" plain type="text" @click.prevent="getCode()">{{ title }}</el-button>
</el-col>
</el-form-item>
<el-alert :closable="false">验证通过后将去除脱敏有效期为30分钟请尽快操作</el-alert>
<el-alert type="warning" :closable="false"
>手机号码登记后统一维护如有修改请联系相关部门教职工(组织人事处) 学生(班主任) 驻校人员(后勤处)</el-alert
>
</el-form>
<template v-slot:footer>
<span class="dialog-footer" style="text-aligin: center">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="checkCode"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import request from '@/router/axios'
import request from '@/router/axios';
export default {
name: "sensitive",
data(){
return{
dialogVisible: false,
second: '', //倒计时
disabled: false, //是否禁用按钮
timer: null, //计时器
form:{
code:''
},
mobile:'**'
}
name: 'sensitive',
data() {
return {
dialogVisible: false,
second: '', //倒计时
disabled: false, //是否禁用按钮
timer: null, //计时器
form: {
code: '',
},
mobile: '**',
};
},
computed: {
title() {
return this.disabled ? `重新获取 ( ${this.second} ) s` : '获取验证码';
},
},
methods: {
grantPrivilege() {
this.dialogVisible = true;
this.form.code = '';
this.getMobile();
},
getCode() {
// console.log('点击')
},
computed: {
title() {
return this.disabled ? `重新获取 ( ${this.second} ) s` : '获取验证码';
},
},
methods:{
grantPrivilege(){
this.dialogVisible = true
this.form.code = ''
this.getMobile()
},
getCode() {
// console.log('点击')
let that = this;
let s = 60; //倒计时间
if (!that.timer) {
that.second = s;
that.disabled = true;
let that = this
let s = 60 //倒计时间
if (!that.timer) {
that.second = s
that.disabled = true
that.timer = setInterval(() => {
if (that.second > 0 && this.second <= s) {
that.second--;
} else {
that.disabled = false;
clearInterval(that.timer);
this.timer = null;
}
}, 1000);
that.timer = setInterval(() => {
if (that.second > 0 && this.second <= s) {
that.second--
} else {
that.disabled = false
clearInterval(that.timer)
this.timer = null
}
}, 1000)
that.sendCode()
}
},
getMobile(){
request({
url: '/admin/mobile/getMobile',
method: 'get'
}).then(response => {
console.log(response.data)
if (response.data.data) {
this.mobile = response.data.data
} else {
this.$message.error(response.data.msg)
}
})
},
sendCode(){
request({
url: '/admin/mobile/codeSensitive',
method: 'get'
}).then(response => {
console.log(response.data)
if (response.data.data) {
this.$message.success("短信发送成功")
} else {
this.$message.error(response.data.msg)
}
})
},
checkCode(){
request({
url: '/admin/mobile/checkSensitiveCode',
method: 'post',
data:{'code':this.form.code}
}).then(response => {
console.log(response.data)
if (response.data.data) {
this.dialogVisible = false
this.$message.success("校验通过")
} else {
this.$message.error("校验失败")
}
})
}
}
}
that.sendCode();
}
},
getMobile() {
request({
url: '/admin/mobile/getMobile',
method: 'get',
}).then((response) => {
console.log(response.data);
if (response.data.data) {
this.mobile = response.data.data;
} else {
this.$message.error(response.data.msg);
}
});
},
sendCode() {
request({
url: '/admin/mobile/codeSensitive',
method: 'get',
}).then((response) => {
console.log(response.data);
if (response.data.data) {
this.$message.success('短信发送成功');
} else {
this.$message.error(response.data.msg);
}
});
},
checkCode() {
request({
url: '/admin/mobile/checkSensitiveCode',
method: 'post',
data: { code: this.form.code },
}).then((response) => {
console.log(response.data);
if (response.data.data) {
this.dialogVisible = false;
this.$message.success('校验通过');
} else {
this.$message.error('校验失败');
}
});
},
},
};
</script>
<style scoped lang="scss">
.wrapper {
display: inline-block;
}
.grant-btn{
marigin: 0px 5px !important;
color: #f39217;
}
.wrapper {
display: inline-block;
}
.grant-btn {
marigin: 0px 5px !important;
color: #f39217;
}
</style>

View File

@@ -9,171 +9,170 @@
-->
<template>
<div class="w_editor">
<!-- 富文本编辑器 -->
<div ref="w_view"></div>
</div>
<div class="w_editor">
<!-- 富文本编辑器 -->
<div ref="w_view"></div>
</div>
</template>
<script>
//自定义字体类型
/*富文本编辑图片上传配置*/
const uploadConfig = {
action: '' // 必填参数 图片上传地址
};
// 引入富文本
import WE from "wangeditor";
// 引入elementUI Message模块用于提示信息
import { Message } from "element-ui";
//自定义字体类型
/*富文本编辑图片上传配置*/
const uploadConfig = {
action: '', // 必填参数 图片上传地址
};
// 引入富文本
import WE from 'wangeditor';
// 引入elementUI Message模块用于提示信息
import { Message } from 'element-ui';
export default {
name: "wangEditor",
model: {
prop: 'desc',
event:'change'
},
props:{
desc:{
type:String,
default:""
},
//业务中我们经常会有添加操作和编辑操作,添加操作时,我们需清除上一操作留下的缓存
isClear:{
type:Boolean,
default:false
},
needImage:'',
imageUrl:'',
data:{},
showFullScreen: {
type:Boolean,
default:true
}
},
data() {
return {
info_:null,
isChange:false,
// 编辑器实例
editor: null,
// 富文本菜单选项配置
menuItem: [
"head",
"bold",
"fontSize",
"fontName",
"italic",
"underline",
"indent",
"lineHeight",
"foreColor",
"backColor",
"link",
"list",
"justify"
]
};
},
watch: {
// 监听默认值
isClear(val){
// console.log(val)
if (val){
this.editor.txt.clear()
}
},
//接收父组件传递过来的值
desc(value){
//判断父组件传递过来的值跟当前编辑器内容是否一样
if (value != this.editor.txt.html()) {
this.editor.txt.html(this.desc)
}
}
},
mounted() {
this.initEditor();
if(this.editor){
this.editor.txt.html(this.desc)
}
},
methods: {
clearText(){
if (this.editor) {
this.editor.txt.clear()
}
},
// 初始化编辑器
initEditor() {
if(this.needImage){
this.menuItem.push("image")
}
// 获取编辑器dom节点
const editor = new WE(this.$refs.w_view);
// 配置编辑器
editor.config.showLinkImg = false; /* 隐藏插入网络图片的功能 */
editor.config.onchangeTimeout = 5000; /* 配置触发 onchange 的时间频率,默认为 200ms */
editor.config.uploadImgMaxLength = 1; /* 限制一次最多能传几张图片 */
editor.config.showFullScreen = this.showFullScreen; /* 配置全屏功能按钮是否展示 */
editor.config.menus = [...this.menuItem]; /* 自定义系统菜单 */
// editor.config.uploadImgMaxSize = 5 * 1024 * 1024 /* 限制图片大小 */;
editor.config.customAlert = err => {
Message.error(err);
};
// 监控变化,同步更新数据
editor.config.onchange = newHtml => {
// this.isChange = true;
// // 异步更新组件富文本值的变化
// // this.defaultText=newHtml
// this.$emit("update:rich-text", newHtml);
this.info_ = newHtml // 绑定当前逐渐地值
this.$emit('change', this.info_) // 将内容同步到父组件中
};
export default {
name: 'wangEditor',
model: {
prop: 'desc',
event: 'change',
},
props: {
desc: {
type: String,
default: '',
},
//业务中我们经常会有添加操作和编辑操作,添加操作时,我们需清除上一操作留下的缓存
isClear: {
type: Boolean,
default: false,
},
needImage: '',
imageUrl: '',
data: {},
showFullScreen: {
type: Boolean,
default: true,
},
},
data() {
return {
info_: null,
isChange: false,
// 编辑器实例
editor: null,
// 富文本菜单选项配置
menuItem: [
'head',
'bold',
'fontSize',
'fontName',
'italic',
'underline',
'indent',
'lineHeight',
'foreColor',
'backColor',
'link',
'list',
'justify',
],
};
},
watch: {
// 监听默认值
isClear(val) {
// console.log(val)
if (val) {
this.editor.txt.clear();
}
},
//接收父组件传递过来的值
desc(value) {
//判断父组件传递过来的值跟当前编辑器内容是否一样
if (value != this.editor.txt.html()) {
this.editor.txt.html(this.desc);
}
},
},
mounted() {
this.initEditor();
if (this.editor) {
this.editor.txt.html(this.desc);
}
},
methods: {
clearText() {
if (this.editor) {
this.editor.txt.clear();
}
},
// 初始化编辑器
initEditor() {
if (this.needImage) {
this.menuItem.push('image');
}
// 获取编辑器dom节点
const editor = new WE(this.$refs.w_view);
// 配置编辑器
editor.config.showLinkImg = false; /* 隐藏插入网络图片的功能 */
editor.config.onchangeTimeout = 5000; /* 配置触发 onchange 的时间频率,默认为 200ms */
editor.config.uploadImgMaxLength = 1; /* 限制一次最多能传几张图片 */
editor.config.showFullScreen = this.showFullScreen; /* 配置全屏功能按钮是否展示 */
editor.config.menus = [...this.menuItem]; /* 自定义系统菜单 */
// editor.config.uploadImgMaxSize = 5 * 1024 * 1024 /* 限制图片大小 */;
editor.config.customAlert = (err) => {
Message.error(err);
};
// 监控变化,同步更新数据
editor.config.onchange = (newHtml) => {
// this.isChange = true;
// // 异步更新组件富文本值的变化
// // this.defaultText=newHtml
// this.$emit("update:rich-text", newHtml);
this.info_ = newHtml; // 绑定当前逐渐地值
this.$emit('change', this.info_); // 将内容同步到父组件中
};
// 自定义上传图片
editor.config.customUploadImg = (resultFiles, insertImgFn) => {
let param = new FormData(); // 创建form对象
param.append("filename", "test");
param.append("file", resultFiles[0]);
for(let i in this.data){
param.append(this.data[i].key,this.data[i].val)
}
// 一般项目中都会封装好发送请求得方法,我这为了通用直接用axios
axios.post(this.imageUrl, param).then(res => {
// res是上传成功后返回的数据,返回的数据中需要有上传图片的路径,
// 通过insert方法将路径传入,即可将图片在富文本中插入
if (res.status === 200) {
// 我这返回的是JSON数据,需要解析
let path = res.data.data.fileUrl
// 自定义上传图片
editor.config.customUploadImg = (resultFiles, insertImgFn) => {
let param = new FormData(); // 创建form对象
param.append('filename', 'test');
param.append('file', resultFiles[0]);
for (let i in this.data) {
param.append(this.data[i].key, this.data[i].val);
}
// 一般项目中都会封装好发送请求得方法,我这为了通用直接用axios
axios.post(this.imageUrl, param).then((res) => {
// res是上传成功后返回的数据,返回的数据中需要有上传图片的路径,
// 通过insert方法将路径传入,即可将图片在富文本中插入
if (res.status === 200) {
// 我这返回的是JSON数据,需要解析
let path = res.data.data.fileUrl;
// 上传代码返回结果之后,将图片插入到编辑器中
insertImgFn(path);
}
});
// 上传代码返回结果之后,将图片插入到编辑器中
insertImgFn(path);
}
});
};
};
// 创建编辑器
editor.create();
this.editor = editor;
}
},
beforeUnmount() {
// 销毁编辑器
this.editor.destroy();
this.editor = null;
},
};
// 创建编辑器
editor.create();
this.editor = editor;
},
},
beforeUnmount() {
// 销毁编辑器
this.editor.destroy();
this.editor = null;
},
};
</script>
<style>
.w-e-toolbar{
z-index: 1 !important;
.w-e-toolbar {
z-index: 1 !important;
}
.w-e-menu {
z-index: 2 !important;
z-index: 2 !important;
}
.w-e-text-container {
z-index: 1 !important;
height: auto;
z-index: 1 !important;
height: auto;
}
</style>