PPPoE列表添加字段、多选下拉可增删改查、服务器管理列表列宽调整

This commit is contained in:
康冉冉
2026-01-12 18:30:37 +08:00
parent 19e5bb3d31
commit 7af2e86404
4 changed files with 741 additions and 42 deletions
@@ -0,0 +1,687 @@
<template>
<div class="editable-multi-select">
<!-- 多选下拉框 -->
<el-select
v-model="selectedValues"
multiple
placeholder="请选择或搜索"
@visible-change="handleVisibleChange"
class="multi-select w100"
ref="selectRef"
@remove-tag="handleRemoveTag"
>
<!-- 自定义下拉面板 -->
<div class="custom-dropdown" v-if="dropdownVisible">
<!-- 搜索和新增区域 -->
<div class="custom-search-area">
<el-input
v-model="searchQuery"
placeholder="搜索或创建新选项"
size="mini"
@keyup.enter.native="addNewOption"
@input="handleSearch"
class="search-input"
ref="searchInputRef"
clearable
>
<!-- <el-button-->
<!-- slot="append"-->
<!-- icon="el-icon-plus"-->
<!-- @click="addNewOption"-->
<!-- size="mini"-->
<!-- :disabled="!searchQuery.trim() || isExactMatch"-->
<!-- ></el-button>-->
</el-input>
<!-- 搜索提示 -->
<div v-if="searchQuery && showCreateHint" class="search-hint">
<span>未找到"{{ searchQuery }}"按回车添加</span>
</div>
</div>
<!-- 选项列表 -->
<div class="options-list" v-if="filteredOptions.length > 0">
<div
v-for="item in filteredOptions"
:key="item.value"
class="editable-option"
:class="{
'selected': selectedValues.includes(item.value),
'editing': editingItem === item.value
}"
@click.stop="handleOptionClick(item.value)"
@mouseenter="handleMouseEnter(item.value)"
@mouseleave="handleMouseLeave(item.value)"
>
<!-- 编辑模式 -->
<div v-if="editingItem === item.value" class="edit-mode">
<el-input
v-model="tempEditValue"
size="mini"
@keyup.enter.native="saveEdit(item.value)"
@keyup.esc.native="cancelEdit"
@blur="saveEdit(item.value)"
class="edit-input"
ref="editInputRef"
autofocus
>
<el-button
slot="append"
icon="el-icon-delete"
@click.stop="deleteOption(item.value)"
size="mini"
class="delete-btn"
></el-button>
</el-input>
<!-- <i class="el-icon-delete" @click="deleteOption(item.value)"></i>-->
</div>
<!-- 查看模式 -->
<div v-else class="view-mode">
<!-- 多选复选框 -->
<!-- <el-checkbox-->
<!-- :value="selectedValues.includes(item.value)"-->
<!-- @click.native.stop="toggleOption(item.value)"-->
<!-- class="option-checkbox"-->
<!-- ></el-checkbox>-->
<!-- 选项标签高亮搜索关键词 -->
<span class="option-label" :title="item.label">
<template v-if="searchQuery && highlightSearch">
<span v-html="highlightText(item.label, searchQuery)"></span>
</template>
<template v-else>
{{ item.label }}
</template>
</span>
<!-- 编辑按钮鼠标移入时显示 -->
<el-button
v-show="hoveredItem === item.value"
icon="el-icon-more"
circle
size="mini"
@click.stop="startEdit(item)"
class="edit-btn"
></el-button>
</div>
</div>
</div>
<!-- 无数据提示 -->
<div v-else class="no-data">
<span v-if="searchQuery && !showCreateHint">未找到匹配的选项</span>
<span v-else>暂无数据</span>
</div>
</div>
<!-- 原有的el-option用于保持Select组件正常工作 -->
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
style="display: none;"
/>
</el-select>
</div>
</template>
<script>
export default {
name: 'EditableMultiSelect',
props: {
// 初始选项列表
initialOptions: {
type: Array,
default: () => []
},
// 初始选中的值
value: {
type: Array,
default: () => []
},
// 是否允许创建新选项
allowCreate: {
type: Boolean,
default: true
},
// 是否显示搜索高亮
highlightSearch: {
type: Boolean,
default: true
},
// 下拉框宽度
width: {
type: String,
default: '100%'
}
},
data() {
return {
// 所有选项
options: [],
// 选中值
selectedValues: this.value,
// 搜索关键词
searchQuery: '',
// 当前编辑的选项值
editingItem: null,
// 临时编辑值
tempEditValue: '',
// 鼠标悬停的选项值
hoveredItem: null,
// 下拉框是否可见
dropdownVisible: false,
// 过滤后的选项
filteredOptions: [],
// 精确匹配标记
isExactMatch: false,
// 防抖计时器
searchTimer: null
};
},
watch: {
value: {
immediate: true,
handler(newVal) {
this.selectedValues = Array.isArray(newVal) ? newVal : [];
}
},
selectedValues(newVal) {
this.$emit('input', newVal);
this.$emit('change', newVal);
},
initialOptions: {
immediate: true,
deep: true,
handler(newVal) {
this.options = this.processOptions(newVal || []);
this.filterOptions();
}
}
},
computed: {
// 计算是否显示创建提示
showCreateHint() {
return this.searchQuery &&
this.allowCreate &&
!this.isExactMatch;
}
},
mounted() {
// 监听点击事件,防止下拉框关闭
document.addEventListener('click', this.handleDocumentClick);
},
beforeDestroy() {
document.removeEventListener('click', this.handleDocumentClick);
},
methods: {
// 处理选项数据格式
processOptions(options) {
return options.map(option => {
if (typeof option === 'string') {
return {
label: option,
value: option
};
}
return option;
});
},
// 处理搜索(带防抖)
handleSearch() {
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
this.searchTimer = setTimeout(() => {
this.filterOptions();
this.isExactMatch = this.checkExactMatch();
}, 300);
},
// 过滤选项
filterOptions() {
if (!this.searchQuery.trim()) {
this.filteredOptions = [...this.options];
} else {
const query = this.searchQuery.toLowerCase().trim();
this.filteredOptions = this.options.filter(item =>
item.label.toLowerCase().includes(query)
);
}
},
// 检查是否完全匹配
checkExactMatch() {
if (!this.searchQuery.trim()) return false;
const query = this.searchQuery.trim().toLowerCase();
return this.options.some(item =>
item.label.toLowerCase() === query
);
},
// 高亮搜索文本
highlightText(text, query) {
if (!query.trim()) return text;
const regex = new RegExp(`(${this.escapeRegExp(query)})`, 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
},
// 转义正则表达式特殊字符
escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
},
// 添加新选项
addNewOption() {
if (!this.searchQuery.trim()) {
this.$message.warning('请输入选项内容');
return;
}
if (!this.allowCreate) {
this.$message.warning('不允许创建新选项');
return;
}
const newValue = this.searchQuery.trim();
// 检查是否已存在
if (this.isExactMatch) {
this.$message.warning('该选项已存在');
return;
}
const newOption = {
label: newValue,
value: newValue
};
this.options.push(newOption);
this.selectedValues.push(newValue);
this.searchQuery = '';
this.filterOptions();
// 触发添加事件
this.$emit('option-added', newOption);
this.$emit('options-updated', this.options);
this.$message.success('添加成功');
},
// 切换选项选中状态
toggleOption(value) {
const index = this.selectedValues.indexOf(value);
if (index === -1) {
this.selectedValues.push(value);
} else {
this.selectedValues.splice(index, 1);
}
},
// 选项点击事件
handleOptionClick(value) {
this.toggleOption(value);
},
// 开始编辑
startEdit(item) {
this.editingItem = item.value;
this.tempEditValue = item.label;
// 下一个tick聚焦到输入框
this.$nextTick(() => {
if (this.$refs.editInputRef) {
this.$refs.editInputRef.focus();
}
});
},
// 取消编辑
cancelEdit() {
this.editingItem = null;
},
// 保存编辑
saveEdit(oldValue) {
if (!this.tempEditValue.trim()) {
this.deleteOption(oldValue);
return;
}
const newValue = this.tempEditValue.trim();
// 检查是否重复(排除自身)
const duplicate = this.options.find(
opt => opt.value !== oldValue && opt.label.toLowerCase() === newValue.toLowerCase()
);
if (duplicate) {
this.$message.warning('该选项已存在');
this.editingItem = null;
return;
}
// 更新选项
const index = this.options.findIndex(opt => opt.value === oldValue);
if (index !== -1) {
this.options[index] = {
...this.options[index],
label: newValue,
value: newValue
};
// 如果原值被选中,更新选中值
const selectedIndex = this.selectedValues.indexOf(oldValue);
if (selectedIndex !== -1) {
this.selectedValues.splice(selectedIndex, 1, newValue);
}
}
this.editingItem = null;
this.filterOptions();
// 触发更新事件
this.$emit('option-updated', { oldValue, newValue: newValue });
this.$emit('options-updated', this.options);
this.$message.success('更新成功');
},
// 删除选项
deleteOption(value) {
this.$confirm('确定删除这个选项吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.options.findIndex(opt => opt.value === value);
if (index !== -1) {
this.options.splice(index, 1);
// 从选中值中移除
const selectedIndex = this.selectedValues.indexOf(value);
if (selectedIndex !== -1) {
this.selectedValues.splice(selectedIndex, 1);
}
}
this.editingItem = null;
this.filterOptions();
// 触发删除事件
this.$emit('option-deleted', value);
this.$emit('options-updated', this.options);
this.$message.success('删除成功');
}).catch(() => {});
},
// 移除标签
handleRemoveTag(tag) {
const index = this.selectedValues.indexOf(tag);
if (index > -1) {
this.selectedValues.splice(index, 1);
}
},
// 鼠标移入
handleMouseEnter(value) {
this.hoveredItem = value;
},
// 鼠标移出
handleMouseLeave() {
this.hoveredItem = null;
},
// 下拉框显示状态变化
handleVisibleChange(visible) {
this.dropdownVisible = visible;
if (visible) {
// 下拉框打开时,如果有搜索框,聚焦
this.$nextTick(() => {
if (this.$refs.searchInputRef) {
this.$refs.searchInputRef.focus();
}
});
} else {
// 下拉框关闭时,重置状态
this.searchQuery = '';
this.editingItem = null;
this.hoveredItem = null;
this.filterOptions();
}
},
// 处理文档点击事件,防止自定义区域点击导致下拉框关闭
handleDocumentClick(event) {
const selectEl = this.$refs.selectRef?.$el;
const dropdownEl = document.querySelector('.custom-dropdown');
if (selectEl &&
dropdownEl &&
!selectEl.contains(event.target) &&
!dropdownEl.contains(event.target)) {
this.dropdownVisible = false;
}
},
// 获取当前所有选项
getOptions() {
return this.options;
},
// 添加选项(外部调用)
addOption(option) {
const processedOption = typeof option === 'string'
? { label: option, value: option }
: option;
this.options.push(processedOption);
this.filterOptions();
this.$emit('options-updated', this.options);
},
// 删除选项(外部调用)
removeOption(value) {
this.deleteOption(value);
},
// 清空选中
clearSelection() {
this.selectedValues = [];
}
}
}
</script>
<style scoped>
.editable-multi-select {
width: 200px;
}
.multi-select {
width: 100%;
}
/* 自定义下拉面板 */
.custom-dropdown {
position: relative;
top: 100%;
left: 0;
right: 0;
background: white;
/*border: 1px solid #e4e7ed;*/
/*border-radius: 4px;*/
/*box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);*/
z-index: 2001;
/*margin-top: 4px;*/
margin-top: -4px;
margin-bottom: -4px;
}
.custom-search-area {
/*padding: 8px;*/
border-bottom: 1px solid #e4e7ed;
/*background-color: #f5f7fa;*/
}
.search-input {
margin-bottom: 4px;
}
.search-hint {
padding: 4px 0;
font-size: 12px;
color: #909399;
text-align: center;
}
/*.options-list {*/
/* max-height: 300px;*/
/* overflow-y: auto;*/
/*}*/
.editable-option {
padding: 8px 16px;
cursor: pointer;
display: flex;
align-items: center;
min-height: 40px;
transition: background-color 0.2s;
/*border-bottom: 1px solid #f0f0f0;*/
}
.editable-option:last-child {
border-bottom: none;
}
.editable-option:hover {
background-color: #f5f7fa;
}
/* 选中状态 */
.editable-option.selected {
/*background-color: #f0f9ff;*/
color: #1890ff;
}
.editable-option.editing {
background-color: #f0f9ff;
}
.edit-mode {
width: 100%;
display: flex;
align-items: center;
}
.edit-input {
width: 100%;
}
::v-deep .edit-input .el-input-group__append {
padding: 0 5px!important;
right: -20px;
border: none;
background: transparent;
}
.delete-btn {
color: #f56c6c;
border-color: #f56c6c;
padding: 7px 0;
font-size: 16px;
}
.delete-btn:hover {
/*background-color: #f56c6c;*/
/*color: white;*/
color: #f56c6c;
}
.view-mode {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
}
.option-checkbox {
flex-shrink: 0;
margin-right: 8px;
}
.option-checkbox:deep(.el-checkbox__inner) {
border-radius: 3px;
}
.option-label {
flex: 1;
word-break: break-all;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.option-label .highlight {
color: #409eff;
background-color: #ecf5ff;
padding: 0 2px;
border-radius: 2px;
font-weight: bold;
}
.edit-btn {
flex-shrink: 0;
padding: 4px;
font-size: 12px;
opacity: 0.8;
transition: opacity 0.2s;
}
.edit-btn:hover {
opacity: 1;
}
.no-data {
padding: 20px;
text-align: center;
color: #909399;
font-size: 14px;
}
/* 覆盖Element默认样式 */
::v-deep (.el-select-dropdown) {
display: none !important;
}
::v-deep (.el-select .el-input__inner) {
cursor: pointer;
}
::v-deep (.el-select__tags) {
flex-wrap: wrap;
gap: 4px;
}
::v-deep .el-scrollbar__wrap {
overflow: auto!important;
}
::v-deep .el-scrollbar__bar.is-vertical > div {
display: none!important;
}
::v-deep .el-scrollbar__thumb {
display: none!important;
}
::v-deep .el-input--mini .el-input__inner {
height: 35px!important;
line-height: 35px!important;
border: none!important;
}
</style>
+5 -1
View File
@@ -13,7 +13,7 @@
<!-- 表格数据 -->
<el-table v-loading="loading" :data="tableList" :ref="config && config.tableKey ? `tableRef_${config.tableKey}` : `selChangeList`" :key="tableKey" :highlight-current-row="config && config.currentSel || false" @current-change="(val) => handleClick({fnCode: 'currentData'},val)" highlight-selection-row @select-all="handleSelectAll" @select="handleSelectionChange" @sort-change="handleSortChange" :row-class-name="tableRowClassName">
<!-- :selectable="() => config && config.selectable ? false : true" -->
<el-table-column v-if="!(config && config.colHiddenCheck)" fixed="left" type="selection" width="55" :selectable="() => config && config.selectable ? false : true" align="center" />
<el-table-column v-if="!(config && config.colHiddenCheck)" fixed="left" type="selection" width="40" :selectable="() => config && config.selectable ? false : true" align="center" />
<!-- 展开列内容 -->
<el-table-column v-if="config && config.expand" type="expand">
<template #default="props">
@@ -541,6 +541,10 @@
.el-table__body .tabColBgc.hover-row.el-table__row--striped.selection-row > td.el-table__cell {
background-color: #ffb98c!important;
}
::v-deep .el-table-column--selection .cell {
padding-left: 0px!important;
padding-right: 0px!important;
}
</style>
<style>
.my-custom-tooltip {