<template>
<el-form-item label-width="0">
<div class="flex-col gap-10 w h option-group">
<div class="new_title flex-row align-c jc-sb">选项
<div class="flex-row align-c gap-5 right-title">彩色<el-switch v-model="multicolour" active-value="1" inactive-value="0"></el-switch></div>
</div>
<template v-if="multiple">
<el-checkbox-group v-model="checkValue">
<drag class="w" :data="drag_list.filter(item => item.is_other !== '1')" :space-col="20" @remove="remove" @on-sort="on_sort">
<template #default="{ row, index }">
<el-checkbox :value="row.value" class="option-width">
<div class="flex-row gap-2">
<el-tooltip effect="dark" :show-after="200" :hide-after="200" content="不允许重复选项标识" popper-class="custom-error-tooltip" :disabled="!is_value_error(row.value)" :show-arrow="false" raw-content placement="top-start">
<el-input v-model="row.value" :class="['option-width', {'is-error': is_value_error(row.value)}]" placeholder="标识" @input="validateInput($event, index)" @blur="value_change(row.value, index)"></el-input>
</el-tooltip>
<el-tooltip effect="dark" :show-after="200" :hide-after="200" content="不允许重复选项名称" popper-class="custom-error-tooltip" :disabled="!is_error(row.name)" :show-arrow="false" raw-content placement="top-start">
<el-input v-model="row.name" :class="['option-width', {'is-error': is_error(row.name)}]" placeholder="名称" @change="input_change(row.name, index)"></el-input>
</el-tooltip>
</div>
<template v-if="multicolour == '1'">
<el-color-picker v-model="row.color" :predefine="predefine_colors" @click.prevent />
</template>
</el-checkbox>
</template>
</drag>
<div v-if="is_drag_other" class="flex-row align-c gap-y-10 sort-target w mt-10">
<el-checkbox :value="last_drag_item.value" class="option-width">
<div class="flex-row gap-2">
<el-tooltip effect="dark" :show-after="200" :hide-after="200" content="不允许重复选项标识" popper-class="custom-error-tooltip" :disabled="!is_value_error(last_drag_item.value)" :show-arrow="false" raw-content placement="top-start">
<el-input v-model="last_drag_item.value" :class="['option-width', {'is-error': is_value_error(last_drag_item.value)}]" placeholder="标识" @input="validateInput($event, drag_list.length - 1)" @blur="value_change(last_drag_item.value, drag_list.length - 1)"></el-input>
</el-tooltip>
<el-tooltip effect="dark" :show-after="200" :hide-after="200" content="不允许重复选项名称" popper-class="custom-error-tooltip" :disabled="!is_error(last_drag_item.name)" :show-arrow="false" raw-content placement="top-start">
<el-input v-model="last_drag_item.name" :class="['option-width', {'is-error': is_error(last_drag_item.name)}]" placeholder="名称" @change="input_change(last_drag_item.name, drag_list.length - 1)"></el-input>
</el-tooltip>
</div>
</el-checkbox>
<icon name="delete-o" size="18" color="6" @click="remove(drag_list.length - 1)"/>
</div>
</el-checkbox-group>
</template>
<template v-else>
<el-radio-group v-model="radioValue">
<drag class="w" :data="drag_list.filter(item => item.is_other !== '1')" :space-col="20" @remove="remove" @on-sort="on_sort">
<template #default="{ row, index }">
<el-radio :value="row.value" class="option-width">
<div class="flex-row gap-2">
<el-tooltip effect="dark" :show-after="200" :hide-after="200" content="不允许重复选项标识" popper-class="custom-error-tooltip" :disabled="!is_value_error(row.value)" :show-arrow="false" raw-content placement="top-start">
<el-input v-model="row.value" :class="['option-width', {'is-error': is_value_error(row.value)}]" placeholder="标识" @input="validateInput($event, index)" @blur="value_change(row.value, index)"></el-input>
</el-tooltip>
<el-tooltip effect="dark" :show-after="200" :hide-after="200" content="不允许重复选项名称" popper-class="custom-error-tooltip" :disabled="!is_error(row.name)" :show-arrow="false" raw-content placement="top-start">
<el-input v-model="row.name" :class="['option-width', {'is-error': is_error(row.name)}]" placeholder="名称" @change="input_change(row.name, index)"></el-input>
</el-tooltip>
</div>
<template v-if="multicolour == '1'">
<el-color-picker v-model="row.color" :predefine="predefine_colors" @click.prevent />
</template>
</el-radio>
</template>
</drag>
<div v-if="is_drag_other" class="flex-row align-c gap-y-10 sort-target w mt-10">
<el-radio :value="last_drag_item.value" class="option-width">
<div class="flex-row gap-2">
<el-tooltip effect="dark" :show-after="200" :hide-after="200" content="不允许重复选项标识" popper-class="custom-error-tooltip" :disabled="!is_value_error(last_drag_item.value)" :show-arrow="false" raw-content placement="top-start">
<el-input v-model="last_drag_item.value" :class="['option-width', {'is-error': is_value_error(last_drag_item.name)}]" placeholder="标识" @input="validateInput($event, drag_list.length - 1)" @blur="value_change(last_drag_item.value, drag_list.length - 1)"></el-input>
</el-tooltip>
<el-tooltip effect="dark" :show-after="200" :hide-after="200" content="不允许重复选项名称" popper-class="custom-error-tooltip" :disabled="!is_error(last_drag_item.name)" :show-arrow="false" raw-content placement="top-start">
<el-input v-model="last_drag_item.name" :class="['option-width', {'is-error': is_error(last_drag_item.name)}]" placeholder="名称" @change="input_change(last_drag_item.name, drag_list.length - 1)"></el-input>
</el-tooltip>
</div>
</el-radio>
<icon name="delete-o" size="18" color="6" @click="remove(drag_list.length - 1)"/>
</div>
</el-radio-group>
</template>
<div class="flex-row align-c gap-4 ml-32">
<span class="add-title" @click="add">添加选项</span>|
<template v-if="isOther">
<span :class="['add-title', { 'other-disable': is_drag_other}]" @click="add_other">添加其他选项</span>|
</template>
<span class="add-title" @click="bulk_edit">批量编辑</span>
</div>
</div>
</el-form-item>
<el-dialog v-model="dialogVisible" title="批量编辑" width="40%" align-center :close-on-click-modal="false" :close-on-press-escape="false" append-to-body>
<div class="flex-col gap-10 mtb-20">
<div class="dialog-desc">
1. 每行对应一个选项,每行的格式:标识:名称.<br/>
2. 列子:id:名称,标识字符格式(字母、数字、下划线)<br/>
3. 其他字符会自动过滤,如果不加 标识: 只写名称 则标识自动生成。<br/>
</div>
<el-input v-model="textarea" :rows="20" type="textarea" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submit">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { isEmpty, cloneDeep } from 'lodash';
import { color_change, predefine_colors } from '@/utils/common';
import { get_math } from "@/utils/index";
// 弹出框显示逻辑
const multicolour = defineModel('multicolour', { type: String, default: '0' })
// 默认值处理
const radioValue = defineModel('radioValue', { type: String, default: '' });
const checkValue = defineModel('checkValue', { type: Array as PropType<string[]>, default: [] });
const other_data = {
name: '其他',
value: 'other',
is_other: '1',
color: '#051e500a',
}
interface Props {
list: any[];
multiple: boolean;
isOther?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
imgParams: 'cover',
isOther: true,
multiple: false,
});
const drag_list = ref(props.list);
watchEffect(() => {
drag_list.value = props.list;
});
/**
* 计算属性,用于判断拖拽的项目是否是外部项目
*
* 此处无需参数说明,因为该计算属性依赖于外部的 drag_list.value
*
* @returns {boolean} 如果最后一个项目是外部项目,则返回 true;否则返回 false
*/
const is_drag_other = computed(() => {
// 获取拖拽列表
const list = drag_list.value;
// 如果列表为空或不存在,则认为没有外部项目
if (!list || list.length === 0) return false;
// 获取列表中的最后一个项目
const last = list[list.length - 1];
// 判断并返回最后一个项目是否为外部项目
return last.is_other === '1';
});
const emits = defineEmits(['onsort', 'option-change']);
// #region 添加选项
const add = () => {
let length = drag_list.value.length;
// 判断最后一个是否是其他
if (is_drag_other.value) {
length = length - 1;
}
const data = {
name: '选项' + (length + 1),
value: get_math(),
color: color_change(length),
}
// 判断最后一个是否是其他
if (is_drag_other.value) {
drag_list.value.splice(length, 0, data);
} else {
drag_list.value.push(data);
}
old_drag_list_handle(drag_list.value);
};
const add_other = () => {
if (!is_drag_other.value) {
drag_list.value.push(other_data);
}
old_drag_list_handle(drag_list.value);
}
// #endregion
// #region 输入框操作逻辑和校验
const last_drag_item = computed(() => drag_list.value[drag_list.value.length - 1]);
// name为空处理
const input_change = (val: string, index: number) => {
if (!isEmpty(val)) {
drag_list.value[index].name = val;
} else {
drag_list.value[index].name = '选项';
}
}
// value为空处理
const value_change = (val: string, index: number) => {
if (!isEmpty(val)) {
drag_list.value[index].value = val;
} else {
drag_list.value[index].value = old_drag_list.value[index].value;
}
}
// 选项名称是否重复
const is_error = computed(() => {
return (val: string) => {
return drag_list.value.filter(item => item.name === val).length > 1;
}
});
// 选项value是否重复
const is_value_error = computed(() => {
return (val: string) => {
return drag_list.value.filter(item => item.value === val).length > 1;
}
});
//#endregion
//#region 批量编辑
const dialogVisible = ref(false);
const textarea = ref('');
// 点击编辑的时候将数组转变成字符串
const bulk_edit = () => {
let old_data = cloneDeep(Array.isArray(drag_list.value) ? drag_list.value : []);
// 判断是否有其他内容,如果有的话删除最后一个
if (is_drag_other.value) {
old_data.splice(drag_list.value.length - 1, 1);
}
const names = old_data.map(item => (item.value || '') + ':' + (item.name ?? '')).join('\n');
textarea.value = names;
dialogVisible.value = true;
}
const cancel = () => {
dialogVisible.value = false;
}
// 点击确定之后将数据转变成数组
const submit = () => {
// 去掉首尾空格之后做数据处理
const textarea_value = (textarea.value || '').trim().split(/[\r\n]+/g);
let data: any[] = []
if (textarea_value.length > 0) {
data = textarea_value.map((item: any, index: number) => {
if (item.indexOf(':') === -1) {
return {
name: item,
value: get_math(),
color: color_change(index),
}
} else {
const [value, name] = item.split(':');
// 过滤掉不需要的字符
let new_value = value.replace(/[^a-zA-Z0-9_]/g, '');
if (isEmpty(new_value)) {
new_value = get_math();
}
return {
name,
value: new_value,
color: color_change(index),
}
}
});
}
if (is_drag_other.value) {
const new_data = cloneDeep([...data, other_data]);
old_drag_list_handle(new_data);
emits('onsort', new_data);
} else {
old_drag_list_handle(data);
emits('onsort', data);
}
// 批量编辑之后清除数据
if (props.multiple) {
checkValue.value = [];
} else {
radioValue.value = '';
}
// 关闭弹出框
dialogVisible.value = false;
}
//#endregion
//#region 操作逻辑
// 拖拽之后的顺序
const on_sort = (item: any) => {
if (is_drag_other.value) {
const new_data = cloneDeep([...item, other_data]);
old_drag_list_handle(new_data);
emits('onsort', new_data);
} else {
old_drag_list_handle(item);
emits('onsort', item);
}
};
// 删除选项时的操作
const remove = (index: number) => {
// 防止 index 越界
if (index < 0 || index >= drag_list.value.length) {
return;
}
// 先删除数据,然后判断是否存在
drag_list.value.splice(index, 1);
old_drag_list_handle(drag_list.value);
const new_data = cloneDeep(drag_list.value.map(item => item.value ?? ''));
if (props.multiple) {
if (!isEmpty(checkValue.value)) {
checkValue.value = checkValue.value.filter((item: any) => new_data.includes(item));
return;
}
} else {
if (!isEmpty(radioValue.value)) {
if (!new_data.includes(radioValue.value)) {
radioValue.value = '';
}
return;
}
}
};
//#endregion
//#region 存储历史数据的内容
// 存储历史数据的显示
const old_drag_list = ref(cloneDeep(props.list));
const old_drag_list_handle = (new_data: string[]) => {
old_drag_list.value = cloneDeep(new_data);
};
//#endregion
// 输入校验函数
const validateInput = (val: string, index: number) => {
const regex = /^[a-zA-Z0-9_]*$/;
if (!regex.test(val)) {
// 若输入不合法,移除非法字符
drag_list.value[index].value = val.replace(/[^a-zA-Z0-9_]/g, '');
}
};
watch(() => drag_list.value, (new_value) => {
// 判断去除重复之后的列表长度是否一致
const newListLength = new Set(new_value.map(item => item.name)).size;
const newListValueLength = new Set(new_value.map(item => item.value)).size;
const listLength = new_value.length;
if(listLength > newListLength || listLength > newListValueLength){
emits('option-change', false);
} else {
emits('option-change', true);
}
}, { immediate: true, deep: true });
</script>
<style lang="scss" scoped>
:deep(.sort-target) {
.el-radio {
margin-right: 0 !important;
}
}
.option-width {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
:deep(.el-radio__label), :deep(.el-checkbox__label) {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
}
}
.right-title {
font-size: 1.2rem;
font-weight: 400;
}
.add-title {
cursor: pointer;
font-size: 1.4rem;
color: $cr-main;
}
.other-disable {
cursor: no-drop;
color: #B5B8BE;
}
.dialog-desc {
font-size: 1.2rem;
line-height: 1.8rem;
color: #838892;
}
.is-error {
:deep(.el-input__wrapper) {
box-shadow: #D8423A 0px 0px 0px 1px inset;
}
}
.option-group {
:deep(.gap-y-16) {
column-gap: 1rem;
}
}
:deep(.el-color-picker__trigger) {
width: 3.2rem;
.el-color-picker__icon {
display: none;
}
}
</style>