
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