fix
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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` 则不显示
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 的 type):success、danger、warning、info、primary
|
||||
type?: 'success' | 'danger' | 'warning' | 'info' | 'primary'
|
||||
// 尺寸(类似 el-tag 的 size):small、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 的 type):success、danger、warning、info、primary
|
||||
type?: 'success' | 'danger' | 'warning' | 'info' | 'primary';
|
||||
// 尺寸(类似 el-tag 的 size):small、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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ const linkList = ref([
|
||||
path: '/pages/home/biz/user/user-manage',
|
||||
name: '用户管理',
|
||||
type: LinkTypeEnum.SHOP_PAGES,
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
const handleSelect = (value: Link) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,6 @@ export default {
|
||||
dept: '部门',
|
||||
role: '角色',
|
||||
select: '选择',
|
||||
search: '搜索'
|
||||
search: '搜索',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,6 @@ export default {
|
||||
displayTheSearch: '显示搜索',
|
||||
refresh: '刷新',
|
||||
print: '打印',
|
||||
view: '视图'
|
||||
view: '视图',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 等)。
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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`,列将始终显示
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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` 的完整实现。
|
||||
|
||||
|
||||
@@ -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` 了解完整的使用示例。
|
||||
|
||||
|
||||
@@ -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 | 自定义触发按钮内容 |
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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-viewer(teleported 到 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-dialog(append-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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export default {
|
||||
verify: {
|
||||
complete: '请完成安全验证',
|
||||
slide: {
|
||||
explain: '向右滑动完成验证',
|
||||
success: '验证成功',
|
||||
fail: '验证失败',
|
||||
time: '{time}s验证成功'
|
||||
}
|
||||
}
|
||||
}
|
||||
verify: {
|
||||
complete: '请完成安全验证',
|
||||
slide: {
|
||||
explain: '向右滑动完成验证',
|
||||
success: '验证成功',
|
||||
fail: '验证失败',
|
||||
time: '{time}s验证成功',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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 约 50px,padding 约 40px,留一些余量
|
||||
return `${windowHeight.value - 120}px`;
|
||||
// 如果外部传入了 imgHeight,优先使用
|
||||
if (props.imgHeight) {
|
||||
return props.imgHeight;
|
||||
}
|
||||
// 否则根据窗口高度动态计算:dialog header 约 50px,padding 约 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user