PPPoE列表添加字段、多选下拉可增删改查、服务器管理列表列宽调整
This commit is contained in:
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user