Browse Source

新增select组件

ming 2 years ago
parent
commit
5ecfd52ccc

+ 57 - 55
components/fs-action/fs-action.vue

@@ -1,49 +1,51 @@
 <template>
 	<fs-popup direction="bottom" height="auto" v-model="visible" :showMask="showMask" :maskClickable="maskClickable">
 		<view class="fs-action">
-			<view class="fs-action-item" v-for="(item, index) in list" :key="index" @click="handleAction(item)">
-				{{ item.name }}
+			<view class="fs-action-item" v-for="(item, index) in list" :key="index" @click="handleAction(item)">{{item.name}}</view>
+			<view class="fs-action-extra">
+				<slot></slot>
 			</view>
-			<view class="fs-action-extra"><slot></slot></view>
-			<view class="fs-action-cancel" v-if="showCancel" @click="cancel">{{ cancelText }}</view>
+			<view class="fs-action-cancel" v-if="showCancel" @click="cancel">{{cancelText}}</view>
 		</view>
 	</fs-popup>
 </template>
 
-<script setup>
-import { computed } from 'vue'
-
-const props = defineProps({
-	list: Array,
-	modelValue: Boolean,
-	showMask: {
-		type: Boolean,
-		default: true
-	},
-	maskClickable: {
-		type: Boolean,
-		default: true
-	},
-	cancelText: {
-		type: String,
-		default: '取消'
-	},
-	showCancel: {
-		type: Boolean,
-		default: true
-	}
-})
-const emits = defineEmits(['update:modelValue', 'change'])
-
-const visible = computed({
-	get: () => props.modelValue,
-	set: value => emits('update:modelValue', value)
-})
-
-const cancel = () => {
-	emits('update:modelValue', false)
-}
-const handleAction = item => {
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+	list: Array,
+	modelValue: Boolean,
+	showMask: {
+		type: Boolean,
+		default: true
+	},
+	maskClickable: {
+		type: Boolean,
+		default: true
+	},
+	cancelText: {
+		type: String,
+		default: '取消'
+	},
+	showCancel: {
+		type: Boolean,
+		default: true
+	}
+})
+const emits = defineEmits(['update:modelValue', 'change'])
+
+const visible = computed(
+	{
+		get: () => props.modelValue,
+		set: value => emits('update:modelValue', value)
+	}
+)
+
+const cancel = () => {
+	emits('update:modelValue', false)
+}
+const handleAction = item =>  {
 	emits('change', item)
 	cancel()
 }
@@ -51,23 +53,23 @@ const handleAction = item => {
 
 <style lang="scss" scoped>
 .fs-action {
-	background-color: #f8f8f8;
-
-	&-item {
-		padding: 20rpx;
-		text-align: center;
-		background-color: #fff;
-
-		& + & {
-			border-top: 1px solid var(--border-color);
-		}
-	}
-
-	&-cancel {
-		padding: 20rpx;
-		text-align: center;
-		background-color: #fff;
-		margin-top: 10rpx;
+	background-color: #f8f8f8;
+	
+	&-item {
+		padding: 20rpx;
+		text-align: center;
+		background-color: #fff;
+	
+		&+& {
+			border-top: 1px solid var(--border-color);
+		}
+	}
+	
+	&-cancel {
+		padding: 20rpx;
+		text-align: center;
+		background-color: #fff;
+		margin-top: 10rpx;
 	}
 }
 </style>

+ 150 - 150
components/fs-field/fs-field.vue

@@ -1,203 +1,203 @@
 <template>
-	<view class="fs-field" 
-		:class="{
-			'fs-field-tighten': tighten,
-		}"
-		:style="{'background-color': bgColor,borderRadius: round ? '80rpx': ''}">
-		<view 
-			v-if="type === 'textarea'" 
-			class="fs-field-textarea" 
-			:class="{'fs-field-padding': !formItemPosition}" 
-			:style="{height: height}">
+	<view
+		class="fs-field"
+		:class="{
+			'fs-field-tighten': tighten
+		}"
+		:style="{ 'background-color': bgColor, borderRadius: round ? '80rpx' : '' }"
+	>
+		<view
+			v-if="type === 'textarea'"
+			class="fs-field-textarea"
+			:class="{ 'fs-field-padding': !formItemPosition }"
+			:style="{ height: height }"
+		>
 			<slot name="before"></slot>
-			<textarea 
+			<textarea
 				class="fs-textarea"
-			 	placeholder-class="fs-ph-class"
-				:class="{clearable, 'fs-field-border': border}" 
+				placeholder-class="fs-ph-class"
+				:class="{ clearable, 'fs-field-border': border }"
 				:name="name"
-				:placeholder="placeholder"
-			 	:maxlength="maxlength"
-			 	:disabled="disabled"
+				:placeholder="placeholder"
+				:maxlength="maxlength"
+				:disabled="disabled"
 				:value="modelValue"
-				:auto-height="autoHeight"
+				:auto-height="autoHeight"
 				:fixed="fixed"
 				@input="handleInput"
 				@focus="handleFocus"
 				@blur="handleBlur"
 				@confirm="handleConfirm"
 			/>
-			<fs-icon 
-				class="fs-field-icon fs-field-icon-close" 
-				type="icon-guanbi2fill" 
-				size="20px" 
-				@touchstart="handleClear"
-				v-if="clearable">
-			</fs-icon>
+			<fs-icon
+				class="fs-field-icon fs-field-icon-close"
+				type="icon-guanbi2fill"
+				size="20px"
+				@touchstart="handleClear"
+				v-if="clearable"
+			></fs-icon>
 			<slot name="after"></slot>
 		</view>
-		
-		<view 
-			class="fs-field-input" 
-			:class="{'fs-field-padding': !formItemPosition}"
-			v-else>
+
+		<view class="fs-field-input" :class="{ 'fs-field-padding': !formItemPosition }" v-else>
 			<view v-if="slots.before" style="padding-right: 10rpx;"><slot name="before"></slot></view>
-			<input 
-				class="fs-input" 
-			 	placeholder-class="fs-ph-class"
-				:class="{clearable, 'fs-field-border': border}" 
-				:value="modelValue" 
+			<input
+				class="fs-input"
+				placeholder-class="fs-ph-class"
+				:class="{ clearable, 'fs-field-border': border }"
+				:value="modelValue"
 				:type="type"
 				:password="type === 'password'"
-				:placeholder="placeholder"
-				:name="name" 
-				:maxlength="maxlength" 
-				:disabled="disabled" 
-				@input="handleInput" 
+				:placeholder="placeholder"
+				:name="name"
+				:maxlength="maxlength"
+				:disabled="disabled"
+				@input="handleInput"
 				@focus="handleFocus"
-			 	@blur="handleBlur" 
-				@confirm="handleConfirm" 
+				@blur="handleBlur"
+				@confirm="handleConfirm"
 			/>
-			<fs-icon 
-				class="fs-field-icon fs-field-icon-close" 
-				type="icon-guanbi2fill" 
-				size="20px"
-				@touchstart="handleClear" 
-				v-if="clearable">
-			</fs-icon>
+			<fs-icon
+				class="fs-field-icon fs-field-icon-close"
+				type="icon-close-circle"
+				size="20px"
+				@touchstart="handleClear"
+				v-if="clearable && modelValue"
+			></fs-icon>
 			<view v-if="slots.after" style="padding-left: 10rpx;"><slot name="after"></slot></view>
 		</view>
 	</view>
 </template>
 
-<script setup>
-import { inject, computed, useSlots } from 'vue'
-
-const props = defineProps({
-	modelValue: String,
-	placeholder: String,
-	name: String,
-	type: {
-		type: String,
-		default: 'text'
-	},
-	maxlength: {
-		type: [Number, String],
-		default: 140
-	},
-	disabled: Boolean,
-	border: Boolean,
-	clearable: Boolean,
-	autoHeight: Boolean,
-	required: Boolean,
-	tighten: Boolean,
-	fixed: Boolean,
-	round: Boolean,
-	height: {
-		type: String,
-		default: '70rpx'
-	},
-	bgColor: {
-		type: String,
-		default: '#fff'
-	}
-})
-const emits = defineEmits(['update:modelValue','focus','blur','confirm'])
-const slots = useSlots()
-
-const formItemPosition = inject('form-item-position', '')
-const position = computed(() => props.labelPosition || formItemPosition)
-
+<script setup>
+import { inject, computed, useSlots } from 'vue'
+
+const props = defineProps({
+	modelValue: String,
+	placeholder: String,
+	name: String,
+	type: {
+		type: String,
+		default: 'text'
+	},
+	maxlength: {
+		type: [Number, String],
+		default: 140
+	},
+	disabled: Boolean,
+	border: Boolean,
+	clearable: Boolean,
+	autoHeight: Boolean,
+	required: Boolean,
+	tighten: Boolean,
+	fixed: Boolean,
+	round: Boolean,
+	height: {
+		type: String,
+		default: '70rpx'
+	},
+	bgColor: {
+		type: String,
+		default: '#fff'
+	}
+})
+const emits = defineEmits(['update:modelValue', 'focus', 'blur', 'confirm'])
+const slots = useSlots()
+
+const formItemPosition = inject('form-item-position', '')
+const position = computed(() => props.labelPosition || formItemPosition)
+
 const handleInput = e => {
 	emits('update:modelValue', e.detail.value)
-}
-const handleFocus = () => {
-	emits('focus')
-}
-const handleBlur = e => {
-	emits('blur', e.detail.value)
-}
-const handleConfirm = () => {
-	emits('confirm')
-}
-const handleClear = () => {
-	emits('update:modelValue', '')
+}
+const handleFocus = () => {
+	emits('focus')
+}
+const handleBlur = e => {
+	emits('blur', e.detail.value)
+}
+const handleConfirm = () => {
+	emits('confirm')
+}
+const handleClear = () => {
+	emits('update:modelValue', '')
 }
 </script>
 
 <style lang="scss" scoped>
 .fs-field {
 	box-sizing: border-box;
-	font-size: var(--content-size);
+	font-size: var(--content-size);
 	overflow: hidden;
-	
+
 	&-input {
 		display: flex;
 		height: 70rpx;
 		align-items: center;
-		position: relative;
+		position: relative;
 		background-color: inherit;
 	}
-	&-textarea{
+	&-textarea {
 		display: flex;
 		width: 100%;
 		align-items: center;
 		position: relative;
-	}
-	&-padding{
-		padding: 20rpx 30rpx;
-		height: 90rpx;
-	}
-	
-	&-border{
-		border: 2rpx solid var(--border-color) !important;
-		border-radius: 4rpx !important;
-		&.fs-input,
-		&.fs-textarea{
-			padding: 20rpx;
-		}
-	}
-	
-	&-icon {
-		position: absolute;
-		top: 50%;
-		transform: translateY(-50%);
-		color: var(--sub);
-		z-index: 10;
-	}
-	&-icon-close {
-		right: var(--gutter);
-	}
-	
-	&-opacity {
-		background-color: rgba(255, 255, 255, .5);
-		color: #fff;
-		
-		.fs-ph-class {
-			color: #fff;
-		}
-	}
-	
-	&-tighten {
-		padding: 0 var(--tighten-gutter);
-	}
-	&-round {
-		border-radius: 35px;
+	}
+	&-padding {
+		padding: 20rpx 30rpx;
+		height: 90rpx;
+	}
+
+	&-border {
+		border: 2rpx solid var(--border-color) !important;
+		border-radius: 4rpx !important;
+		&.fs-input,
+		&.fs-textarea {
+			padding: 20rpx;
+		}
+	}
+
+	&-icon {
+		position: absolute;
+		top: 50%;
+		transform: translateY(-50%);
+		color: var(--sub);
+		z-index: 10;
+	}
+	&-icon-close {
+		right: var(--gutter);
+	}
+
+	&-opacity {
+		background-color: rgba(255, 255, 255, 0.5);
+		color: #fff;
+
+		.fs-ph-class {
+			color: #fff;
+		}
+	}
+
+	&-tighten {
+		padding: 0 var(--tighten-gutter);
+	}
+	&-round {
+		border-radius: 35px;
 	}
 }
 
-.fs-textarea,
-.fs-input{
+.fs-textarea,
+.fs-input {
 	width: 100%;
 	height: 100%;
 	flex: 1;
-	box-sizing: border-box !important;
-	border-radius: 6rpx;
-	border: none;
+	box-sizing: border-box !important;
+	border-radius: 6rpx;
+	border: none;
 	outline: none;
 }
 .fs-input {
-	background: transparent;
-	
+	background: transparent;
+
 	&.fs-clearable {
 		padding-right: 70rpx;
 	}
@@ -207,4 +207,4 @@ const handleClear = () => {
 	color: #c0c5ce;
 	font-size: var(--content-size);
 }
-</style>
+</style>

+ 54 - 56
components/fs-icon/fs-icon.vue

@@ -1,72 +1,70 @@
 <template>
-	<view 
-		:class="[type, colorType, source === 'inner' ? 'fsfont' : 'iconfont']"
-		:style="style"
-		@click="handleClick"
-	>
-	</view>
+	<view
+		:class="[type, colorType, source === 'inner' ? 'fsfont' : 'iconfont']"
+		:style="style"
+		@click="handleClick"
+	></view>
 </template>
 
-<script setup>
-import { computed } from 'vue'
-
-const props = defineProps({
-	type: String,
-	size: {
-		type: String,
-		default: '36rpx',
-	},
-	color: String,
-	colorType: {
-		type: String,
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger', 'gray'].includes(value)
-		}
-	},
-	source: {
-		type: String,
-		default: 'inner'
-	},
-	link: String,
-	linkType: {
-		type: String,
-		default: 'navigateTo'
-	},
-	rotate: String
-})
-
-const emits = defineEmits(['click'])
-
-const style = computed(() => {
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+	type: String,
+	size: {
+		type: String,
+		default: '36rpx'
+	},
+	color: String,
+	colorType: {
+		type: String,
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger', 'gray'].includes(value)
+		}
+	},
+	source: {
+		type: String,
+		default: 'inner'
+	},
+	link: String,
+	linkType: {
+		type: String,
+		default: 'navigateTo'
+	},
+	rotate: String
+})
+
+const emits = defineEmits(['click'])
+
+const style = computed(() => {
 	let style = `font-size: ${props.size};`
 
 	if (props.color) {
 		style += `color: ${props.color};`
-	}
-	if (props.rotate) {
-		style += `transform: rotate(${props.rotate}deg);`
+	}
+	if (props.rotate) {
+		style += `transform: rotate(${props.rotate}deg);`
 	}
 	return style
-}) 
-
-const handleClick = () => {
-	if (props.link) {
-		uni[props.linkType]({
-			url: props.link
-		})
-	}
-	emits('click')
+})
+
+const handleClick = () => {
+	if (props.link) {
+		uni[props.linkType]({
+			url: props.link
+		})
+	}
+	emits('click')
 }
 </script>
 
-<style lang="scss" scoped>
+<style lang="scss" scoped>
 @import '../../common/iconfont.css';
-@import './icon.css';
+@import './icon.css';
 
-.fsfont, .iconfont{
-  display: inline-block;
-  vertical-align: middle;
-	line-height: 1;
+.fsfont,
+.iconfont {
+	display: inline-block;
+	vertical-align: middle;
 }
 </style>
-

+ 165 - 111
components/fs-popover/fs-popover.vue

@@ -1,35 +1,65 @@
 <template>
-	<view class="fs-popover">
-		<view class="fs-popover-refer" @click="handleClickRefer">
-			<slot name="refer"></slot>
-		</view>
-		<view class="fs-popover-action" v-if="actionVisible" :class="['fs-popover-' + placement]" @click="handleClose">
+	<view class="fs-popover" :style="{ 'z-index': zIndex }">
+		<view class="fs-popover-refer" @click="handleClickRefer"><slot name="refer"></slot></view>
+		<view
+			class="fs-popover-action"
+			v-if="actionVisible"
+			:class="['fs-popover-' + placement]"
+			:style="{ 'z-index': zIndex }"
+			@click="handleClose"
+		>
 			<view class="fs-popover-arrow"></view>
 			<slot>
-				<view class="fs-popover-action-item line1" v-for="(item, index) in actions" :key="index" @click="handleClickAction(item)">{{item.text}}</view>
+				<view
+					class="fs-popover-action-item line1"
+					:class="{ 'fs-popover-active': item[valueKey] && item[valueKey] === modelValue[valueKey] }"
+					v-for="(item, index) in actions"
+					:key="index"
+					@click="handleClickAction(item)"
+				>
+					{{ item.text }}
+				</view>
 			</slot>
 		</view>
-		
-		<fs-mask v-model="actionVisible" :z-index="100" bgColor="rgba(0,0,0,0)"></fs-mask>
+
+		<fs-mask v-model="actionVisible" :z-index="1" bgColor="rgba(0,0,0,0)"></fs-mask>
 	</view>
 </template>
 
 <script>
 export default {
-	name:"fs-popover",
+	name: 'fs-popover'
 }
 </script>
 
 <script setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, watch } from 'vue'
 import utils from '@/utils/utils'
 
 const props = defineProps({
+	modelValue: [Object],
+	valueKey: {
+		type: String,
+		default: 'id'
+	},
 	placement: {
 		type: String,
 		default: 'bottom',
 		validator(value) {
-			return ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end','left', 'left-start', 'left-end','right', 'right-start', 'right-end'].includes(value)
+			return [
+				'top',
+				'top-start',
+				'top-end',
+				'bottom',
+				'bottom-start',
+				'bottom-end',
+				'left',
+				'left-start',
+				'left-end',
+				'right',
+				'right-start',
+				'right-end'
+			].includes(value)
 		}
 	},
 	actions: Array,
@@ -41,17 +71,24 @@ const props = defineProps({
 		type: String,
 		default: '#666'
 	},
+	activeColor: {
+		type: String,
+		default: '#165DFF'
+	},
 	borderColor: {
 		type: String,
 		default: '#E8EAF2'
 	},
-	zIndex: {
-		type: Number,
-		default: 99
+	width: {
+		type: String,
+		default: 'auto'
+	},
+	actionWidth: {
+		type: String,
+		default: 'auto'
 	}
 })
-const emits = defineEmits(['clickAction'])
-
+const emits = defineEmits(['clickAction', 'visibleChange', 'update:modelValue'])
 const uuid = utils.uuid()
 
 const actionVisible = ref(false)
@@ -59,17 +96,25 @@ const handleClickRefer = () => {
 	uni.$emit('popoverClose', { uuid: uuid })
 	actionVisible.value = !actionVisible.value
 }
+watch(actionVisible, () => {
+	emits('visibleChange')
+})
 
 const handleClickAction = item => {
+	emits('update:modelValue', item)
 	emits('clickAction', item)
 }
 const handleClose = () => {
 	actionVisible.value = false
 }
 
+const zIndex = ref(10)
 uni.$on('popoverClose', res => {
 	if (uuid !== res.uuid) {
+		zIndex.value = 10
 		handleClose()
+	} else {
+		zIndex.value = 20
 	}
 })
 
@@ -81,194 +126,203 @@ defineExpose({
 <style lang="scss" scoped>
 $margin: 20rpx;
 $arrowOffset: 30rpx;
+$arrowSize: 24rpx;
 
-.fs-popover{
-	display: inline-block;
+.fs-popover {
 	position: relative;
-	z-index: v-bind(zIndex);
-	
-	&-action{
+	display: inline-block;
+	width: v-bind(width);
+
+	&-action {
 		position: absolute;
 		min-width: 260rpx;
-		z-index: calc(v-bind(zIndex) + 1);
+		max-width: 100%;
+		width: v-bind(actionWidth);
 		background-color: v-bind(bgColor);
-		transition: opacity .15s,transform .15s;
+		transition: opacity 0.15s, transform 0.15s;
 		border-radius: 12rpx;
 		padding: 0 30rpx;
 		color: v-bind(textColor);
-		
-		&-item{
+		border: 2rpx solid v-bind(borderColor);
+
+		&-item {
 			padding: 20rpx;
-			& + &{
+			& + & {
 				border-top: 2rpx solid v-bind(borderColor);
 			}
 		}
 	}
-	
-	&-arrow{
+
+	&-active {
+		color: v-bind(activeColor);
+	}
+
+	&-arrow {
 		position: absolute;
-		width: 0;
-		height: 0;
-		border-color: transparent;
-		border-style: solid;
-		border-width: 12rpx;
+		width: $arrowSize;
+		height: $arrowSize;
+		transform: rotate(45deg);
+		z-index: 1000;
+		background-color: v-bind(bgColor);
+		border: 2rpx solid transparent;
+		margin-top: -$arrowSize / 2;
+		margin-left: -$arrowSize / 2;
 	}
-	
-	&-top{
+
+	&-top {
 		left: 50%;
 		bottom: 100%;
 		transform: translateX(-50%);
 		margin-bottom: $margin;
-		.fs-popover-arrow{
-			border-bottom-width: 0;
-			border-top-color: v-bind(bgColor);
-			bottom: 0;
+		.fs-popover-arrow {
+			border-bottom-color: v-bind(borderColor);
+			border-right-color: v-bind(borderColor);
+			bottom: -$arrowSize / 2;
 			left: 50%;
-			transform: translate(-50%, 100%);
 		}
 	}
-	&-top-start{
+	&-top-start {
 		left: 0;
 		bottom: 100%;
 		margin-bottom: $margin;
-		.fs-popover-arrow{
-			border-bottom-width: 0;
-			border-top-color: v-bind(bgColor);
-			bottom: 0;
+		.fs-popover-arrow {
+			border-bottom-color: v-bind(borderColor);
+			border-right-color: v-bind(borderColor);
+			bottom: -$arrowSize / 2;
 			left: $arrowOffset;
-			transform: translateY(100%);
+			margin-left: 0;
 		}
 	}
-	&-top-end{
+	&-top-end {
 		right: 0;
 		bottom: 100%;
 		margin-bottom: $margin;
-		.fs-popover-arrow{
-			border-bottom-width: 0;
-			border-top-color: v-bind(bgColor);
-			bottom: 0;
+		.fs-popover-arrow {
+			border-bottom-color: v-bind(borderColor);
+			border-right-color: v-bind(borderColor);
+			bottom: -$arrowSize / 2;
 			right: $arrowOffset;
-			transform: translateY(100%);
+			margin-left: 0;
 		}
 	}
-	
-	&-bottom{
+
+	&-bottom {
 		left: 50%;
 		top: 100%;
 		transform: translateX(-50%);
 		margin-top: $margin;
-		
-		.fs-popover-arrow{
-			border-top-width: 0;
-			border-bottom-color: v-bind(bgColor);
+
+		.fs-popover-arrow {
+			border-top-color: v-bind(borderColor);
+			border-left-color: v-bind(borderColor);
 			top: 0;
 			left: 50%;
-			transform: translate(-50%, -100%);
 		}
 	}
-	&-bottom-start{
+	&-bottom-start {
 		left: 0;
 		top: 100%;
 		margin-top: $margin;
-		
-		.fs-popover-arrow{
-			border-top-width: 0;
-			border-bottom-color: v-bind(bgColor);
+
+		.fs-popover-arrow {
+			border-top-color: v-bind(borderColor);
+			border-left-color: v-bind(borderColor);
 			top: 0;
 			left: $arrowOffset;
-			transform: translateY(-100%);
+			margin-left: 0;
 		}
 	}
-	&-bottom-end{
+	&-bottom-end {
 		right: 0;
 		top: 100%;
 		margin-top: $margin;
-		
-		.fs-popover-arrow{
-			border-top-width: 0;
-			border-bottom-color: v-bind(bgColor);
+
+		.fs-popover-arrow {
+			border-top-color: v-bind(borderColor);
+			border-left-color: v-bind(borderColor);
 			top: 0;
 			right: $arrowOffset;
-			transform: translateY(-100%);
+			margin-left: 0;
 		}
 	}
-	
-	&-left{
+
+	&-left {
 		left: -$margin;
 		top: 50%;
 		transform: translate(-100%, -50%);
-		.fs-popover-arrow{
-			border-right-width: 0;
-			border-left-color: v-bind(bgColor);
+		.fs-popover-arrow {
+			border-bottom-color: v-bind(borderColor);
+			border-right-color: v-bind(borderColor);
 			top: 50%;
-			right: 0;
-			transform: translate(100%, -50%);
+			right: -$arrowSize / 2;
+			transform: rotate(-45deg);
 		}
 	}
-	&-left-start{
+	&-left-start {
 		left: -$margin;
 		top: 0;
 		transform: translateX(-100%);
-		
-		.fs-popover-arrow{
-			border-right-width: 0;
-			border-left-color: v-bind(bgColor);
+
+		.fs-popover-arrow {
+			border-bottom-color: v-bind(borderColor);
+			border-right-color: v-bind(borderColor);
 			top: $arrowOffset;
-			right: 0;
-			transform: translateX(100%);
+			right: -$arrowSize / 2;
+			transform: rotate(-45deg);
+			margin-top: 0;
 		}
 	}
-	&-left-end{
+	&-left-end {
 		left: -$margin;
 		bottom: 0;
 		transform: translateX(-100%);
-		
-		.fs-popover-arrow{
-			border-right-width: 0;
-			border-left-color: v-bind(bgColor);
+
+		.fs-popover-arrow {
+			border-bottom-color: v-bind(borderColor);
+			border-right-color: v-bind(borderColor);
 			bottom: $arrowOffset;
-			right: 0;
-			transform: translateX(100%);
+			right: -$arrowSize / 2;
+			transform: rotate(-45deg);
+			margin-top: 0;
 		}
 	}
-	
-	&-right{
+
+	&-right {
 		left: 100%;
-		top: 0;
+		top: 50%;
 		transform: translateY(-50%);
 		margin-left: $margin;
-		
-		.fs-popover-arrow{
-			border-left-width: 0;
-			border-right-color: v-bind(bgColor);
+
+		.fs-popover-arrow {
+			border-bottom-color: v-bind(borderColor);
+			border-left-color: v-bind(borderColor);
 			top: 50%;
 			left: 0;
-			transform: translateX(-50%);
 		}
 	}
-	&-right-start{
+	&-right-start {
 		left: 100%;
 		top: 0;
 		margin-left: $margin;
-		.fs-popover-arrow{
-			border-left-width: 0;
-			border-right-color: v-bind(bgColor);
+		.fs-popover-arrow {
+			border-bottom-color: v-bind(borderColor);
+			border-left-color: v-bind(borderColor);
 			top: $arrowOffset;
 			left: 0;
-			transform: translateX(-100%);
+			margin-top: 0;
 		}
 	}
-	&-right-end{
+	&-right-end {
 		left: 100%;
 		bottom: 0;
 		margin-left: $margin;
-		.fs-popover-arrow{
-			border-left-width: 0;
-			border-right-color: v-bind(bgColor);
+		.fs-popover-arrow {
+			border-bottom-color: v-bind(borderColor);
+			border-left-color: v-bind(borderColor);
 			left: 0;
 			bottom: $arrowOffset;
-			transform: translateX(-100%);
+			margin-top: 0;
 		}
 	}
 }
-</style>
+</style>

+ 126 - 0
components/fs-select/fs-select.vue

@@ -0,0 +1,126 @@
+<template>
+	<fs-popover
+		v-model="selectedAction"
+		:placement="placement"
+		:actions="actions"
+		:width="width"
+		:actionWidth="actionWidth"
+		:valueKey="valueKey"
+		@clickAction="handleClickAction"
+		@visibleChange="handleChange"
+	>
+		<template #refer>
+			<view class="fs-select" :class="[{ 'fs-select-border': border }]">
+				<view class="fs-select-content line1">{{ selectedAction.text || placeholder }}</view>
+				<view class="fs-select-icon"><fs-icon type="icon-d-down" :rotate="state.rotate" size="30rpx"></fs-icon></view>
+			</view>
+		</template>
+		<!-- <slot></slot> -->
+	</fs-popover>
+</template>
+
+<script>
+export default {
+	name: 'fs-select'
+}
+</script>
+
+<script setup>
+import { ref, reactive, computed } from 'vue'
+
+const props = defineProps({
+	modelValue: [Object],
+	actions: Array,
+	border: Boolean,
+	placeholder: String,
+	width: {
+		type: String,
+		default: '100%'
+	},
+	actionWidth: {
+		type: String,
+		default: 'auto'
+	},
+	placement: {
+		type: String,
+		default: 'bottom',
+		validator(value) {
+			return [
+				'top',
+				'top-start',
+				'top-end',
+				'bottom',
+				'bottom-start',
+				'bottom-end',
+				'left',
+				'left-start',
+				'left-end',
+				'right',
+				'right-start',
+				'right-end'
+			].includes(value)
+		}
+	},
+	valueKey: {
+		type: String,
+		default: 'id'
+	}
+})
+const emits = defineEmits(['change', 'update:modelValue'])
+
+const selectedAction = computed({
+	get: () => props.modelValue,
+	set: value => {
+		emits('update:modelValue', value)
+	}
+})
+
+const state = reactive({
+	rotate: '0'
+})
+
+const handleClickAction = item => {
+	emits('update:modelValue', item)
+	emits('change', item)
+}
+const handleChange = () => {
+	state.rotate = state.rotate === '0' ? '180' : '0'
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-select {
+	display: flex;
+	height: 70rpx;
+	align-items: center;
+	justify-content: space-between;
+	position: relative;
+	background-color: inherit;
+	position: relative;
+	padding-right: 20rpx;
+	max-width: 100%;
+
+	&-border {
+		border: 2rpx solid var(--border-color) !important;
+		border-radius: 4rpx !important;
+		padding-left: 20rpx;
+		&.fs-select-content {
+			padding: 20rpx;
+		}
+	}
+
+	&-content {
+		flex: 1;
+		width: 400rpx;
+	}
+	&-icon {
+		padding-left: 20rpx;
+		flex-shrink: 0;
+	}
+
+	.visible {
+		transform: rotate(180deg);
+		transform-origin: center center;
+	}
+}
+</style>