
23 changed files with 23803 additions and 7720 deletions
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,7 @@ |
|||||
|
import CustomDropdown from './src/index.vue' |
||||
|
|
||||
|
CustomDropdown.install = function(Vue) { |
||||
|
Vue.component(CustomDropdown.name || 'CustomDropdown', CustomDropdown) |
||||
|
} |
||||
|
|
||||
|
export default CustomDropdown |
@ -0,0 +1,271 @@ |
|||||
|
<template> |
||||
|
<div class="custom-select" v-clickaway="closeDropdown" ref="dropdown" :class="{ 'is-open': isOpen }" |
||||
|
:style="{ width }"> |
||||
|
<!-- 触发按钮 --> |
||||
|
<div class="select-trigger" @click="toggleDropdown"> |
||||
|
<slot name="trigger"> |
||||
|
{{ localSelected ? localSelected[displayKey] : placeholder }} |
||||
|
</slot> |
||||
|
<img class="arrow-icon" |
||||
|
:src="isOpen ? require('../../assets/dropDown_open.png') : require('../../assets/dropDown_expand.png')" |
||||
|
alt=""> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 下拉内容 --> |
||||
|
<transition name="slide-fade"> |
||||
|
<div v-if="isOpen" class="select-dropdown"> |
||||
|
<slot v-if="isOpen" name="normal"></slot> |
||||
|
<div v-if="options"> |
||||
|
<div v-for="(item, index) in options" :key="index" class="dropdown-item " |
||||
|
:class="{ 'is-selected': isSelected(item) }" @click="selectItem(item)"> |
||||
|
<slot name="item" :item="item"> |
||||
|
<div class="flex-between"> |
||||
|
<div class="left"> |
||||
|
<p class="one">{{ item[displayKey] }}</p> |
||||
|
</div> |
||||
|
<div class="right"> |
||||
|
<img v-if="localSelected[displayKey] == item[displayKey]" |
||||
|
src="../../assets/drop-selected.svg" alt=""> |
||||
|
</div> |
||||
|
</div> |
||||
|
</slot> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="flex-between dropdown-item" v-if="options_null" @click="selectNullItem"> |
||||
|
<div class="left"> |
||||
|
<p class="one">暂无收款账号</p> |
||||
|
<p>暂时没有收款账号,我想稍后配置</p> |
||||
|
</div> |
||||
|
<div class="right"> |
||||
|
<img src="../../assets/drop-selected.svg" alt=""> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</transition> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { $emit, $on, $off } from '../../utils/eventBus' |
||||
|
export default { |
||||
|
name: 'CustomDropdown', |
||||
|
props: { |
||||
|
width: { |
||||
|
type: String, |
||||
|
default: "200px", |
||||
|
}, |
||||
|
options: { |
||||
|
type: Array, |
||||
|
default: () => [], |
||||
|
}, |
||||
|
options_null: { |
||||
|
type: Object, |
||||
|
default: () => { }, |
||||
|
}, |
||||
|
placeholder: { |
||||
|
type: String, |
||||
|
default: "请选择", |
||||
|
}, |
||||
|
value: { |
||||
|
type: [String, Number, Object], |
||||
|
default: null, |
||||
|
}, |
||||
|
valueKey: { |
||||
|
type: String, |
||||
|
default: "value", |
||||
|
}, |
||||
|
displayKey: { |
||||
|
type: String, |
||||
|
default: "label", |
||||
|
}, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
isOpen: false, |
||||
|
localSelected: null, // 完全独立的内部状态 |
||||
|
isUpdating: false // 循环终止标志 |
||||
|
// selectedItem: null, |
||||
|
}; |
||||
|
}, |
||||
|
watch: { |
||||
|
value: { |
||||
|
immediate: true, |
||||
|
handler(val) { |
||||
|
// 仅当外部值确实变化时更新内部状态 |
||||
|
if (!this.localSelected || this.localSelected[this.valueKey] !== val) { |
||||
|
this.localSelected = this.options.find(item => item[this.valueKey] === val); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
// selectedItem: { |
||||
|
// get() { |
||||
|
// // 仅从props获取值,不做任何修改 |
||||
|
// return this.options.find(item => item[this.valueKey] === this.value) || null; |
||||
|
// }, |
||||
|
// set(newVal) { |
||||
|
// // 仅触发事件,不修改内部状态 |
||||
|
// this.$emit('input', newVal ? newVal[this.valueKey] : null); |
||||
|
// this.$emit('change', newVal); |
||||
|
// } |
||||
|
// } |
||||
|
// selectedItem: { |
||||
|
// get() { |
||||
|
// if (this.isUpdating) return this._selectedItem; |
||||
|
// return this.options.find(item => item[this.valueKey] === this.value) || null; |
||||
|
// }, |
||||
|
// set(newVal) { |
||||
|
// this.isUpdating = true; |
||||
|
// this._selectedItem = newVal; |
||||
|
// this.$nextTick(() => { |
||||
|
// this.$emit('input', newVal ? newVal[this.valueKey] : null); |
||||
|
// this.isUpdating = false; |
||||
|
// }); |
||||
|
// } |
||||
|
// } |
||||
|
}, |
||||
|
// 移除 data 中的 selectedItem 和 watch |
||||
|
created() { |
||||
|
// 监听关闭所有下拉框的事件 |
||||
|
$on('close-all-dropdowns', this.closeDropdown) |
||||
|
}, |
||||
|
beforeDestroy() { |
||||
|
// 组件销毁移除监听 |
||||
|
$off('close-all-dropdowns', this.closeDropdown) |
||||
|
}, |
||||
|
methods: { |
||||
|
closeDropdown() { |
||||
|
this.isOpen = false; |
||||
|
}, |
||||
|
toggleDropdown(e) { |
||||
|
if (e) { |
||||
|
e.stopPropagation(); |
||||
|
} |
||||
|
// 先通知所有下拉框关闭 |
||||
|
$emit('close-all-dropdowns') |
||||
|
this.isOpen = !this.isOpen; |
||||
|
}, |
||||
|
// selectItem(item) { |
||||
|
// this.selectedItem = item; |
||||
|
// this.$emit("input", item[this.valueKey]); // Use the specified valueKey |
||||
|
// this.$emit("change", item); |
||||
|
// this.isOpen = false; |
||||
|
// }, |
||||
|
// selectItem(item) { |
||||
|
// // 先触发外部事件 |
||||
|
// this.$emit("input", item[this.valueKey]); |
||||
|
// this.$emit("change", item); |
||||
|
|
||||
|
// // 延迟内部状态更新,让父组件先处理 |
||||
|
// this.$nextTick(() => { |
||||
|
// this.selectedItem = item; |
||||
|
// this.isOpen = false; |
||||
|
// }); |
||||
|
// }, |
||||
|
selectItem(item) { |
||||
|
this.localSelected = item; |
||||
|
this.$emit('input', item[this.valueKey]); |
||||
|
this.$emit('change', item); |
||||
|
this.isOpen = false; |
||||
|
}, |
||||
|
// selectItem(item) { |
||||
|
// this.selectedItem = item; // 这会自动触发 computed setter |
||||
|
// // this.$emit("change", item); |
||||
|
// this.isOpen = false; |
||||
|
// }, |
||||
|
isSelected(item) { |
||||
|
return this.localSelected && this.localSelected[this.valueKey] === item[this.valueKey]; |
||||
|
}, |
||||
|
selectNullItem() { |
||||
|
this.$emit("changeNormal", ''); |
||||
|
this.isOpen = false; |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Your existing styles remain the same */ |
||||
|
.custom-select { |
||||
|
display: inline-block; |
||||
|
vertical-align: middle; |
||||
|
height: 38px; |
||||
|
position: relative; |
||||
|
font-family: Arial, sans-serif; |
||||
|
} |
||||
|
|
||||
|
.select-trigger { |
||||
|
border-radius: 2px; |
||||
|
opacity: 1; |
||||
|
background: #FFFFFF; |
||||
|
border: 1px solid #DFE2E6; |
||||
|
width: 100%; |
||||
|
height: 40px; |
||||
|
box-sizing: border-box; |
||||
|
padding: 10px 12px; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.is-open .select-trigger { |
||||
|
border: 1px solid #006AFF; |
||||
|
transition: all .5s; |
||||
|
outline: 3px solid #D8E9FA; |
||||
|
} |
||||
|
|
||||
|
.select-trigger:hover { |
||||
|
border-color: #006AFF; |
||||
|
transition: all .5s; |
||||
|
} |
||||
|
|
||||
|
.arrow-icon { |
||||
|
width: 12px; |
||||
|
} |
||||
|
|
||||
|
.select-dropdown { |
||||
|
position: absolute; |
||||
|
top: 100%; |
||||
|
right: 0; |
||||
|
width: 100%; |
||||
|
border: 1px solid #ccc; |
||||
|
box-sizing: border-box; |
||||
|
border-radius: 4px; |
||||
|
background-color: #fff; |
||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
||||
|
z-index: 1000; |
||||
|
margin-top: 4px; |
||||
|
max-height: 384px; |
||||
|
overflow-y: auto; |
||||
|
padding: 0 12px 12px 12px; |
||||
|
} |
||||
|
|
||||
|
.dropdown-item { |
||||
|
padding: 12px 10px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.dropdown-item:hover { |
||||
|
background: #F6F7FA; |
||||
|
} |
||||
|
|
||||
|
.dropdown-item.is-selected { |
||||
|
background-color: #F6F7FA; |
||||
|
color: #006AFF; |
||||
|
} |
||||
|
|
||||
|
/* 展开收起动画 */ |
||||
|
.slide-fade-enter-active, |
||||
|
.slide-fade-leave-active { |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.slide-fade-enter-from, |
||||
|
.slide-fade-leave-to { |
||||
|
opacity: 0; |
||||
|
transform: translateY(-10px); |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,328 @@ |
|||||
|
<template> |
||||
|
<div class="custom-select" |
||||
|
v-clickaway="handleClickAway" |
||||
|
ref="dropdown" |
||||
|
:class="{ 'is-open': state.isOpen }" |
||||
|
:style="{ width: computedWidth }"> |
||||
|
<!-- 触发按钮 --> |
||||
|
<div class="select-trigger" @click.stop="toggleDropdown"> |
||||
|
<slot name="trigger"> |
||||
|
{{ displayText }} |
||||
|
</slot> |
||||
|
<img class="arrow-icon" |
||||
|
:src="state.isOpen ? openIcon : expandIcon" |
||||
|
alt="dropdown indicator"> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 下拉内容 --> |
||||
|
<transition name="slide-fade"> |
||||
|
<div v-show="state.isOpen" class="select-dropdown"> |
||||
|
<slot v-if="state.isOpen" name="normal"></slot> |
||||
|
|
||||
|
<template v-if="filteredOptions.length"> |
||||
|
<div v-for="(item, index) in filteredOptions" |
||||
|
:key="`option-${index}`" |
||||
|
class="dropdown-item" |
||||
|
:class="{ 'is-selected': isSelected(item) }" |
||||
|
@click.stop="selectItem(item)"> |
||||
|
<slot name="item" :item="item"> |
||||
|
<div class="flex-between"> |
||||
|
<div class="left"> |
||||
|
<p class="one">{{ item[displayKey] }}</p> |
||||
|
</div> |
||||
|
<div class="right"> |
||||
|
<img v-if="isSelected(item)" |
||||
|
:src="selectedIcon" |
||||
|
alt="selected"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</slot> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<div v-if="showNullOption" |
||||
|
class="flex-between dropdown-item" |
||||
|
@click.stop="selectNullItem"> |
||||
|
<div class="left"> |
||||
|
<p class="one">暂无收款账号</p> |
||||
|
<p>暂时没有收款账号,我想稍后配置</p> |
||||
|
</div> |
||||
|
<div class="right"> |
||||
|
<img :src="selectedIcon" alt="selected"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</transition> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
// 静态资源引入(Webpack编译时处理) |
||||
|
const STATIC_ICONS = Object.freeze({ |
||||
|
expand: require('../../assets/dropDown_expand.png'), |
||||
|
open: require('../../assets/dropDown_open.png'), |
||||
|
selected: require('../../assets/drop-selected.svg') |
||||
|
}) |
||||
|
|
||||
|
export default { |
||||
|
name: 'CustomDropdown', |
||||
|
props: { |
||||
|
width: { |
||||
|
type: String, |
||||
|
default: "200px", |
||||
|
validator: (val) => /^\d+(px|%|rem|em|vw)$/.test(val) |
||||
|
}, |
||||
|
options: { |
||||
|
type: Array, |
||||
|
default: () => [], |
||||
|
validator: (arr) => !arr.some(item => item === null || typeof item !== 'object') |
||||
|
}, |
||||
|
nullOption: { |
||||
|
type: Object, |
||||
|
default: null |
||||
|
}, |
||||
|
placeholder: { |
||||
|
type: String, |
||||
|
default: "请选择" |
||||
|
}, |
||||
|
value: { |
||||
|
type: [String, Number, Object], |
||||
|
default: null |
||||
|
}, |
||||
|
valueKey: { |
||||
|
type: String, |
||||
|
default: "value", |
||||
|
validator: (key) => typeof key === 'string' && key.trim().length > 0 |
||||
|
}, |
||||
|
displayKey: { |
||||
|
type: String, |
||||
|
default: "label", |
||||
|
validator: (key) => typeof key === 'string' && key.trim().length > 0 |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 使用非响应式数据存储关键状态 |
||||
|
data: () => ({ |
||||
|
state: Object.seal({ |
||||
|
isOpen: false, |
||||
|
lastEmittedValue: null, |
||||
|
updateDepth: 0, |
||||
|
eventLock: false |
||||
|
}), |
||||
|
icons: STATIC_ICONS |
||||
|
}), |
||||
|
|
||||
|
computed: { |
||||
|
computedWidth() { |
||||
|
return this.width.endsWith('px') ? this.width : `${parseInt(this.width)}px` |
||||
|
}, |
||||
|
filteredOptions() { |
||||
|
return Array.isArray(this.options) ? [...this.options] : [] |
||||
|
}, |
||||
|
showNullOption() { |
||||
|
return this.nullOption && !this.filteredOptions.length |
||||
|
}, |
||||
|
displayText() { |
||||
|
const current = this.findOptionByValue(this.value) |
||||
|
return current ? current[this.displayKey] : this.placeholder |
||||
|
}, |
||||
|
openIcon() { |
||||
|
return this.icons.open |
||||
|
}, |
||||
|
expandIcon() { |
||||
|
return this.icons.expand |
||||
|
}, |
||||
|
selectedIcon() { |
||||
|
return this.icons.selected |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
watch: { |
||||
|
value: { |
||||
|
immediate: true, |
||||
|
handler(newVal) { |
||||
|
if (this.state.eventLock) return |
||||
|
this.state.lastEmittedValue = JSON.stringify(newVal) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
// 安全查找方法 |
||||
|
findOptionByValue(value) { |
||||
|
if (value === null || value === undefined) return null |
||||
|
return this.filteredOptions.find(item => { |
||||
|
return JSON.stringify(item[this.valueKey]) === JSON.stringify(value) |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
isSelected(item) { |
||||
|
if (!this.value) return false |
||||
|
return JSON.stringify(item[this.valueKey]) === JSON.stringify(this.value) |
||||
|
}, |
||||
|
|
||||
|
// 防抖关闭方法 |
||||
|
handleClickAway() { |
||||
|
if (!this.state.isOpen) return |
||||
|
|
||||
|
this.state.isOpen = false |
||||
|
this.$nextTick(() => { |
||||
|
this.$emit('closed') |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 安全切换方法 |
||||
|
toggleDropdown() { |
||||
|
if (this.state.eventLock) return |
||||
|
|
||||
|
if (this.state.isOpen) { |
||||
|
this.handleClickAway() |
||||
|
} else { |
||||
|
this.$emit('opened') |
||||
|
this.state.isOpen = true |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 核心安全选择方法 |
||||
|
selectItem(item) { |
||||
|
if (this.state.updateDepth > 1) { |
||||
|
console.error('Recursion detected in dropdown selection') |
||||
|
this.state.isOpen = false |
||||
|
this.state.updateDepth = 0 |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const newValue = item[this.valueKey] |
||||
|
|
||||
|
// 值无变化时直接返回 |
||||
|
if (this.state.lastEmittedValue === JSON.stringify(newValue)) { |
||||
|
this.state.isOpen = false |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
this.state.updateDepth++ |
||||
|
this.state.eventLock = true |
||||
|
this.state.lastEmittedValue = JSON.stringify(newValue) |
||||
|
|
||||
|
// 使用setTimeout确保调用栈清空 |
||||
|
setTimeout(() => { |
||||
|
try { |
||||
|
this.$emit('input', newValue) |
||||
|
this.$emit('change', Object.freeze({ ...item })) |
||||
|
} catch (e) { |
||||
|
console.error('Dropdown emit error:', e) |
||||
|
} finally { |
||||
|
this.state.isOpen = false |
||||
|
this.state.updateDepth = 0 |
||||
|
this.state.eventLock = false |
||||
|
} |
||||
|
}, 0) |
||||
|
}, |
||||
|
|
||||
|
selectNullItem() { |
||||
|
this.state.eventLock = true |
||||
|
this.state.lastEmittedValue = null |
||||
|
|
||||
|
setTimeout(() => { |
||||
|
this.$emit('input', null) |
||||
|
this.$emit('change', null) |
||||
|
this.state.isOpen = false |
||||
|
this.state.eventLock = false |
||||
|
}, 0) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 添加性能标记 |
||||
|
created() { |
||||
|
this.$_performanceMark = `dropdown-${Date.now()}` |
||||
|
performance.mark(this.$_performanceMark) |
||||
|
}, |
||||
|
beforeDestroy() { |
||||
|
performance.measure('dropdown-lifetime', this.$_performanceMark) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
/* Your existing styles remain the same */ |
||||
|
.custom-select { |
||||
|
display: inline-block; |
||||
|
vertical-align: middle; |
||||
|
height: 38px; |
||||
|
position: relative; |
||||
|
font-family: Arial, sans-serif; |
||||
|
} |
||||
|
|
||||
|
.select-trigger { |
||||
|
border-radius: 2px; |
||||
|
opacity: 1; |
||||
|
background: #FFFFFF; |
||||
|
border: 1px solid #DFE2E6; |
||||
|
width: 100%; |
||||
|
height: 40px; |
||||
|
box-sizing: border-box; |
||||
|
padding: 10px 12px; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.is-open .select-trigger { |
||||
|
border: 1px solid #006AFF; |
||||
|
transition: all .5s; |
||||
|
outline: 3px solid #D8E9FA; |
||||
|
} |
||||
|
|
||||
|
.select-trigger:hover { |
||||
|
border-color: #006AFF; |
||||
|
transition: all .5s; |
||||
|
} |
||||
|
|
||||
|
.arrow-icon { |
||||
|
width: 12px; |
||||
|
} |
||||
|
|
||||
|
.select-dropdown { |
||||
|
position: absolute; |
||||
|
top: 100%; |
||||
|
right: 0; |
||||
|
width: 100%; |
||||
|
border: 1px solid #ccc; |
||||
|
box-sizing: border-box; |
||||
|
border-radius: 4px; |
||||
|
background-color: #fff; |
||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
||||
|
z-index: 1000; |
||||
|
margin-top: 4px; |
||||
|
max-height: 384px; |
||||
|
overflow-y: auto; |
||||
|
padding: 0 12px 12px 12px; |
||||
|
} |
||||
|
|
||||
|
.dropdown-item { |
||||
|
padding: 12px 10px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.dropdown-item:hover { |
||||
|
background: #F6F7FA; |
||||
|
} |
||||
|
|
||||
|
.dropdown-item.is-selected { |
||||
|
background-color: #F6F7FA; |
||||
|
color: #006AFF; |
||||
|
} |
||||
|
|
||||
|
/* 展开收起动画 */ |
||||
|
.slide-fade-enter-active, |
||||
|
.slide-fade-leave-active { |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.slide-fade-enter-from, |
||||
|
.slide-fade-leave-to { |
||||
|
opacity: 0; |
||||
|
transform: translateY(-10px); |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,7 @@ |
|||||
|
import GuipFormItem from './src/index.vue' |
||||
|
|
||||
|
GuipFormItem.install = function(Vue) { |
||||
|
Vue.component(GuipFormItem.name || 'GuipFormItem', GuipFormItem) |
||||
|
} |
||||
|
|
||||
|
export default GuipFormItem |
@ -0,0 +1,52 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
:class="[{'column':column},{'error':hasError},{'w510':addClass=='w510'},{'w388':addClass=='w388'},'form-item1']"> |
||||
|
<div class="form-item-top"> |
||||
|
<label v-if="label" for="">{{ label }} |
||||
|
<img src="../../assets/require.svg" v-if="required" alt=""> |
||||
|
</label> |
||||
|
<template > |
||||
|
<slot name="formLeft"></slot> |
||||
|
</template> |
||||
|
<template > |
||||
|
<slot name="formRight"></slot> |
||||
|
</template> |
||||
|
</div> |
||||
|
<div class="form-item-bottom"> |
||||
|
<template > |
||||
|
<slot name="formDom"></slot> |
||||
|
</template> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'GuipFormItem', |
||||
|
props:['label','required','addClass','column'], |
||||
|
data() { |
||||
|
return { |
||||
|
hasError: false, |
||||
|
// 目前这两个宽度用的最多,其余宽度自定义类名修改吧 |
||||
|
classList:{ |
||||
|
'w510':'w510', |
||||
|
'w388':'w388' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
// dynamicClasses() { |
||||
|
// return { |
||||
|
// active: this.isActive, // 如果isActive为true,则添加'active'类 |
||||
|
// error: this.hasError, // 如果hasError为true,则添加'error'类 |
||||
|
// highlighted: this.isHighlighted, // 如果isHighlighted为true,则添加'highlighted'类 |
||||
|
// }; |
||||
|
// } |
||||
|
}, |
||||
|
mounted(){ |
||||
|
// console.log(this.required,'required----'); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
<style lang="scss" scoped> |
||||
|
|
||||
|
</style> |
@ -0,0 +1,7 @@ |
|||||
|
import GuipSelect from './src/index.vue' |
||||
|
|
||||
|
GuipSelect.install = function(Vue) { |
||||
|
Vue.component(GuipSelect.name || 'GuipSelect', GuipSelect) |
||||
|
} |
||||
|
|
||||
|
export default GuipSelect |
@ -0,0 +1,111 @@ |
|||||
|
<template> |
||||
|
<el-form-item :style="{ ...style, height: height, ...styleObject }" :required="required" |
||||
|
:class="[{ 'column': column }, { 'w510': addClass == 'w510' }, { 'w388': addClass == 'w388' }, 'form-item']" |
||||
|
:label="label" :prop="prop" :rules="rules"> |
||||
|
<p v-if="desc" class="desc_right">{{ desc }}</p> |
||||
|
<el-select :style="{ width: width }" :placeholder="placeholder1" @change="handleChange" v-model="selectedValue" |
||||
|
v-bind="$attrs"> |
||||
|
<el-option v-for="item in processedOptions" :key="getItemValue(item)" :label="getItemLabel(item)" |
||||
|
:disabled="item.disabled" :value="getItemValue(item)"> |
||||
|
</el-option> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'GuipSelect', |
||||
|
props: { |
||||
|
value: [String, Number, Array], |
||||
|
options: { |
||||
|
type: Array, |
||||
|
default: () => [] |
||||
|
}, |
||||
|
// 新增配置字段 |
||||
|
valueKey: { |
||||
|
type: String, |
||||
|
default: 'value' |
||||
|
}, |
||||
|
labelKey: { |
||||
|
type: String, |
||||
|
default: 'label' |
||||
|
}, |
||||
|
styleObject: Object, |
||||
|
disabled: Boolean, |
||||
|
required: Boolean, |
||||
|
defaultValue: [String, Number, Array], |
||||
|
placeholder: String, |
||||
|
width: String, |
||||
|
height: String, |
||||
|
label: String, |
||||
|
type: String, |
||||
|
prop: String, |
||||
|
rules: [Object, Array], |
||||
|
column: Boolean, |
||||
|
addClass: String, |
||||
|
desc: String |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
selectedValue: '', |
||||
|
style: {}, |
||||
|
placeholder1: '请选择', |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
// 处理options为空的情况 |
||||
|
processedOptions() { |
||||
|
return this.options || [] |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
value(newVal) { |
||||
|
this.selectedValue = newVal |
||||
|
}, |
||||
|
defaultValue(newVal) { |
||||
|
if (newVal !== undefined && newVal !== null) { |
||||
|
this.selectedValue = newVal |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
// 默认值赋值 |
||||
|
if (this.defaultValue !== undefined && this.defaultValue !== null) { |
||||
|
this.selectedValue = this.defaultValue |
||||
|
} |
||||
|
if (this.value !== undefined && this.value !== null) { |
||||
|
this.selectedValue = this.value |
||||
|
} |
||||
|
// 默认提示语 |
||||
|
if (this.placeholder) { |
||||
|
this.placeholder1 = this.placeholder |
||||
|
} |
||||
|
this.$nextTick(() => { |
||||
|
let els = document.querySelectorAll('.el-input'); |
||||
|
els.forEach(item => { |
||||
|
item.onmouseover = function () { |
||||
|
item.classList.add("hoverclass") |
||||
|
} |
||||
|
item.onmouseout = function () { |
||||
|
item.classList.remove("hoverclass") |
||||
|
} |
||||
|
|
||||
|
}) |
||||
|
}) |
||||
|
}, |
||||
|
methods: { |
||||
|
// 获取value值 |
||||
|
getItemValue(item) { |
||||
|
return item[this.valueKey] |
||||
|
}, |
||||
|
// 获取label值 |
||||
|
getItemLabel(item) { |
||||
|
return item[this.labelKey] |
||||
|
}, |
||||
|
handleChange(value) { |
||||
|
this.$emit('input', value) |
||||
|
this.$emit('change', value) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 782 B |
@ -0,0 +1,75 @@ |
|||||
|
/** |
||||
|
* 复制文本到剪贴板 |
||||
|
* @param {string} text 要复制的文本 |
||||
|
* @param {Object} options 配置选项 |
||||
|
* @param {string} options.successMsg 成功提示信息 |
||||
|
* @param {string} options.errorMsg 失败提示信息 |
||||
|
* @param {Vue} options.vm Vue实例(用于调用$message) |
||||
|
* @returns {Promise<boolean>} 是否复制成功 |
||||
|
*/ |
||||
|
export function copyToClipboard(text, options = {}) { |
||||
|
const { |
||||
|
successMsg = '复制成功', |
||||
|
errorMsg = '复制失败,请手动复制', |
||||
|
vm = null |
||||
|
} = options; |
||||
|
|
||||
|
return new Promise((resolve) => { |
||||
|
// 创建textarea元素
|
||||
|
const textarea = document.createElement('textarea'); |
||||
|
textarea.value = text; |
||||
|
textarea.style.position = 'fixed'; // 防止页面滚动
|
||||
|
document.body.appendChild(textarea); |
||||
|
textarea.select(); |
||||
|
|
||||
|
try { |
||||
|
// 执行复制命令
|
||||
|
const successful = document.execCommand('copy'); |
||||
|
if (successful) { |
||||
|
if (vm && vm.$Message) { |
||||
|
vm.$Message.success(successMsg); |
||||
|
} else { |
||||
|
console.log(successMsg); |
||||
|
} |
||||
|
resolve(true); |
||||
|
} else { |
||||
|
throw new Error('Copy command was unsuccessful'); |
||||
|
} |
||||
|
} catch (err) { |
||||
|
console.error('复制失败:', err); |
||||
|
if (vm && vm.$Message) { |
||||
|
vm.$Message.error(errorMsg); |
||||
|
} else { |
||||
|
console.error(errorMsg); |
||||
|
} |
||||
|
resolve(false); |
||||
|
} finally { |
||||
|
document.body.removeChild(textarea); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} text 要复制的文本 |
||||
|
* @param {Object} options 配置选项 |
||||
|
* @returns {Promise<boolean>} 是否复制成功 |
||||
|
*/ |
||||
|
export async function modernCopyToClipboard(text, options = {}) { |
||||
|
const { |
||||
|
successMsg = '复制成功', |
||||
|
errorMsg = '复制失败,请手动复制', |
||||
|
vm = null |
||||
|
} = options; |
||||
|
if (navigator.clipboard && window.isSecureContext) { |
||||
|
await navigator?.clipboard?.writeText(text); |
||||
|
if (vm && vm.$Message) { |
||||
|
vm.$Message.success(successMsg); |
||||
|
} else { |
||||
|
console.log(errorMsg); |
||||
|
} |
||||
|
} else { |
||||
|
return copyToClipboard(text, options); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default modernCopyToClipboard; |
@ -0,0 +1,32 @@ |
|||||
|
// 设置页面元素对应高亮
|
||||
|
export function setHighActive(dom) { |
||||
|
const ele = document.getElementById(dom) |
||||
|
ele.classList.add('ceshi') |
||||
|
ele.scrollIntoView({behavior:'smooth',block:'start'}) |
||||
|
setTimeout(()=>{ |
||||
|
ele.classList.remove('ceshi') |
||||
|
},1000) |
||||
|
} |
||||
|
|
||||
|
export function getServicePriceDesc(price, price_unit, unit_num) { |
||||
|
let unit = 0; |
||||
|
let unit_str = ""; |
||||
|
|
||||
|
if (unit_num == 1) return price + price_unit +'/篇'; |
||||
|
|
||||
|
if (unit_num/10000 < 10) { |
||||
|
unit = Math.ceil(unit_num/10000); |
||||
|
unit_str = unit == 1 ? '万' : unit+'万'; |
||||
|
} |
||||
|
if (unit_num/1000 < 10) { |
||||
|
unit = Math.ceil(unit_num/1000); |
||||
|
unit_str = unit == 1 ? '千' : unit+'千'; |
||||
|
} |
||||
|
if (unit_num/100 < 10) { |
||||
|
unit = Math.ceil(unit_num/100); |
||||
|
unit_str = unit == 1 ? '百' : unit+'百'; |
||||
|
} |
||||
|
|
||||
|
return price + price_unit + "/" +unit_str + "字符"; |
||||
|
} |
||||
|
|
@ -0,0 +1,19 @@ |
|||||
|
import modernCopyToClipboard from '@/utils/clipboard'; |
||||
|
|
||||
|
export default { |
||||
|
install(Vue) { |
||||
|
Vue.directive('clipboard', { |
||||
|
bind(el, binding) { |
||||
|
el.style.cursor = 'pointer'; |
||||
|
el.addEventListener('click', async () => { |
||||
|
const text = binding.value || el.innerText; |
||||
|
const options = { |
||||
|
vm: binding.instance, |
||||
|
...(binding.arg || {}) |
||||
|
}; |
||||
|
await modernCopyToClipboard(text, options); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,11 @@ |
|||||
|
import Vue from 'vue' |
||||
|
// 创建全局事件总线
|
||||
|
const EventBus = new Vue() |
||||
|
|
||||
|
// 封装常用方法
|
||||
|
export const $on = EventBus.$on.bind(EventBus) |
||||
|
export const $once = EventBus.$once.bind(EventBus) |
||||
|
export const $off = EventBus.$off.bind(EventBus) |
||||
|
export const $emit = EventBus.$emit.bind(EventBus) |
||||
|
|
||||
|
export default EventBus |
@ -0,0 +1,17 @@ |
|||||
|
export default { |
||||
|
methods: { |
||||
|
renderHeaderWithIcon(h, { column }, iconPath) { |
||||
|
return h('div', [ |
||||
|
column.label, |
||||
|
h('img', { |
||||
|
attrs: { src: iconPath }, |
||||
|
style: { |
||||
|
width: '10px', |
||||
|
height: '10px', |
||||
|
marginLeft: '3px', |
||||
|
} |
||||
|
}) |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,93 @@ |
|||||
|
// src/utils/request.js
|
||||
|
import axios from "axios"; |
||||
|
|
||||
|
// 创建 axios 实例
|
||||
|
const service = axios.create({ |
||||
|
baseURL: process.env.VUE_APP_BASE_API, // 从环境变量中读取 API 基础地址
|
||||
|
timeout: 60000, // 请求超时时间
|
||||
|
headers: { |
||||
|
'Content-Type': 'application/x-www-form-urlencoded' |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
// 请求拦截器
|
||||
|
service.interceptors.request.use( |
||||
|
(config) => { |
||||
|
// 在发送请求之前做一些处理,例如添加 token
|
||||
|
const token = localStorage.getItem("token"); |
||||
|
if (token) { |
||||
|
config.headers["Auth"] = `${token}`; |
||||
|
} |
||||
|
return config; |
||||
|
}, |
||||
|
(error) => { |
||||
|
// 对请求错误做些什么
|
||||
|
return Promise.reject(error); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// 响应拦截器
|
||||
|
service.interceptors.response.use( |
||||
|
(response) => { |
||||
|
// 对响应数据做一些处理
|
||||
|
const res = response.data; |
||||
|
if (!res.status) { |
||||
|
// 如果返回的 status 不是 true,则视为错误
|
||||
|
// return Promise.reject(new Error(res.info || "请求失败"));
|
||||
|
} |
||||
|
return res; |
||||
|
}, |
||||
|
(error) => { |
||||
|
// 对响应错误做些什么
|
||||
|
if (error.response) { |
||||
|
switch (error.response.status) { |
||||
|
case 401: |
||||
|
// 未授权,跳转到登录页
|
||||
|
window.location.href = "/login"; |
||||
|
break; |
||||
|
case 404: |
||||
|
// 资源未找到
|
||||
|
console.error("资源未找到"); |
||||
|
break; |
||||
|
case 500: |
||||
|
// 服务器错误
|
||||
|
console.error("服务器错误"); |
||||
|
break; |
||||
|
default: |
||||
|
console.error("请求失败", error.message); |
||||
|
} |
||||
|
} |
||||
|
return Promise.reject(error); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
/** |
||||
|
* 封装请求方法 |
||||
|
* @param {string} method 请求方法 (GET, POST, PUT, DELETE 等) |
||||
|
* @param {string} url 请求地址 |
||||
|
* @param {object} data 请求参数 |
||||
|
* @param {object} config 其他 axios 配置 |
||||
|
* @returns {Promise} 返回请求结果 |
||||
|
*/ |
||||
|
const request = (method, url, data = {}, config = {}) => { |
||||
|
const lowerCaseMethod = method.toLowerCase(); |
||||
|
if (lowerCaseMethod === "get") { |
||||
|
// GET 请求将参数拼接到 URL 上
|
||||
|
return service({ |
||||
|
method: "get", |
||||
|
url, |
||||
|
params: data, |
||||
|
...config, |
||||
|
}); |
||||
|
} else { |
||||
|
// 其他请求(POST, PUT, DELETE 等)将参数放在请求体中
|
||||
|
return service({ |
||||
|
method: lowerCaseMethod, |
||||
|
url, |
||||
|
data, |
||||
|
...config, |
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default request; |
Loading…
Reference in new issue