Browse Source

组件增加vue doc

ming 2 years ago
parent
commit
edcac4fd1c
69 changed files with 5717 additions and 4684 deletions
  1. 72 57
      components/fs-action/fs-action.vue
  2. 36 26
      components/fs-avatar-group/fs-avatar-group.vue
  3. 184 138
      components/fs-avatar/fs-avatar.vue
  4. 68 58
      components/fs-back-top/fs-back-top.vue
  5. 81 66
      components/fs-badge/fs-badge.vue
  6. 265 243
      components/fs-button/fs-button.vue
  7. 94 77
      components/fs-captcha/fs-captcha.vue
  8. 104 90
      components/fs-card/fs-card.vue
  9. 98 69
      components/fs-cell-group/fs-cell-group.vue
  10. 204 171
      components/fs-cell/fs-cell.vue
  11. 87 71
      components/fs-checkbox-button/fs-checkbox-button.vue
  12. 54 46
      components/fs-checkbox-cell/fs-checkbox-cell.vue
  13. 123 98
      components/fs-checkbox-group/fs-checkbox-group.vue
  14. 92 71
      components/fs-checkbox/fs-checkbox.vue
  15. 40 31
      components/fs-col/fs-col.vue
  16. 77 71
      components/fs-collapse-item/fs-collapse-item.vue
  17. 85 62
      components/fs-collapse/fs-collapse.vue
  18. 230 226
      components/fs-comment/fs-comment.vue
  19. 35 25
      components/fs-container/fs-container.vue
  20. 37 29
      components/fs-date-format/fs-date-format.vue
  21. 20 10
      components/fs-divide-list/fs-divide-list.vue
  22. 29 23
      components/fs-divider/fs-divider.vue
  23. 30 21
      components/fs-dropdown-item/fs-dropdown-item.vue
  24. 52 38
      components/fs-empty/fs-empty.vue
  25. 137 114
      components/fs-fab/fs-fab.vue
  26. 25 1
      components/fs-field/fs-field.vue
  27. 114 99
      components/fs-form-item/fs-form-item.vue
  28. 60 49
      components/fs-form/fs-form.vue
  29. 77 64
      components/fs-grid-item/fs-grid-item.vue
  30. 63 52
      components/fs-grid/fs-grid.vue
  31. 29 17
      components/fs-gutter/fs-gutter.vue
  32. 18 0
      components/fs-icon/fs-icon.vue
  33. 157 127
      components/fs-index-list/fs-index-list.vue
  34. 62 50
      components/fs-keyboard/fs-keyboard.vue
  35. 108 95
      components/fs-license-plate/fs-license-plate.vue
  36. 68 57
      components/fs-loading/fs-loading.vue
  37. 97 82
      components/fs-loadmore/fs-loadmore.vue
  38. 65 50
      components/fs-mask/fs-mask.vue
  39. 113 105
      components/fs-message/fs-message.vue
  40. 190 158
      components/fs-modal/fs-modal.vue
  41. 137 120
      components/fs-notice-bar/fs-notice-bar.vue
  42. 33 25
      components/fs-number-box/fs-number-box.vue
  43. 45 36
      components/fs-panel/fs-panel.vue
  44. 13 0
      components/fs-popover/fs-popover.vue
  45. 28 12
      components/fs-popup/fs-popup.vue
  46. 95 76
      components/fs-radio-button/fs-radio-button.vue
  47. 62 50
      components/fs-radio-cell/fs-radio-cell.vue
  48. 83 60
      components/fs-radio-group/fs-radio-group.vue
  49. 104 84
      components/fs-radio/fs-radio.vue
  50. 119 120
      components/fs-rate/fs-rate.vue
  51. 111 96
      components/fs-readmore/fs-readmore.vue
  52. 36 27
      components/fs-row/fs-row.vue
  53. 128 116
      components/fs-scroll-list/fs-scroll-list.vue
  54. 167 143
      components/fs-search/fs-search.vue
  55. 12 0
      components/fs-select/fs-select.vue
  56. 32 17
      components/fs-sidebar/fs-sidebar.vue
  57. 13 6
      components/fs-space/fs-space.vue
  58. 11 0
      components/fs-swipe-action-group/fs-swipe-action-group.vue
  59. 15 0
      components/fs-swipe-action/fs-swipe-action.vue
  60. 49 20
      components/fs-swiper/fs-swiper.vue
  61. 17 0
      components/fs-switch/fs-switch.vue
  62. 197 189
      components/fs-tab/fs-tab.vue
  63. 159 141
      components/fs-tag/fs-tag.vue
  64. 87 80
      components/fs-text/fs-text.vue
  65. 14 6
      components/fs-timeago/fs-timeago.vue
  66. 33 18
      components/fs-timeline/fs-timeline.vue
  67. 164 153
      components/fs-upload/fs-upload.vue
  68. 172 151
      components/fs-week-bar/fs-week-bar.vue
  69. 1 1
      package.json

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

@@ -1,51 +1,66 @@
 <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>
-			<view class="fs-action-extra">
-				<slot></slot>
+			<view class="fs-action-item" v-for="(item, index) in list" :key="index" @click="handleAction(item)">
+				{{ item.name }}
 			</view>
-			<view class="fs-action-cancel" v-if="showCancel" @click="cancel">{{cancelText}}</view>
+			<view class="fs-action-extra"><slot></slot></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>
+/**
+ * 动作组件
+ * @description 动作组件
+ * @property {Array} list action列表
+ * @property {Boolean} showMask 是否展示遮罩
+ * @property {Boolean} maskClickable 遮罩是否可点击
+ * @property {Boolean} showCancel 是否展示取消action
+ * @property {String} cancelText “取消”文字
+ * @event {Function} change change事件
+ * @example <fs-action :list="list" v-model="show" @change="change"></fs-action>
+ */
+export default {
+	name: 'fs-action'
+}
+</script>
+
+<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()
 }
@@ -53,23 +68,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>

+ 36 - 26
components/fs-avatar-group/fs-avatar-group.vue

@@ -1,26 +1,36 @@
-<template>
-	<view class="fs-avatar-group">
-		<slot></slot>
-	</view>
-</template>
-
-<script setup>
-import { provide, computed } from 'vue'
-
-const props = defineProps({
-	margin: {
-		type: Number,
-		default: '-30'
-	},
-	border: Boolean,
-})
-
-const marginLeft = computed(() => -props.margin + 'rpx')
-provide('avatarGroup', props)
-</script>
-
-<style lang="scss" scoped>
-.fs-avatar-group{
-	margin-left: v-bind(marginLeft);
-}
-</style>
+<template>
+	<view class="fs-avatar-group"><slot></slot></view>
+</template>
+
+<script>
+/**
+ * 头像组组件
+ * @description 头像组组件
+ * @property {Number} margin 头像之间间隔
+ * @property {Boolean} border 是否展示边框
+ */
+export default {
+	name: 'fs-avatar-group'
+}
+</script>
+
+<script setup>
+import { provide, computed } from 'vue'
+
+const props = defineProps({
+	margin: {
+		type: Number,
+		default: '-30'
+	},
+	border: Boolean
+})
+
+const marginLeft = computed(() => -props.margin + 'rpx')
+provide('avatarGroup', props)
+</script>
+
+<style lang="scss" scoped>
+.fs-avatar-group {
+	margin-left: v-bind(marginLeft);
+}
+</style>

+ 184 - 138
components/fs-avatar/fs-avatar.vue

@@ -1,122 +1,168 @@
 <template>
-	<view
-		class="fs-avatar"
-		:class="[
-			shape,
-			{
-				radius,
-				fixed
-			}
-		]"
-		:style="{
-			width: width || size,
-			height: height || size,
-			right: fixed ? right : 0,
-			bottom: fixed ? bottom : 0,
-			border: borderStyle,
-			'margin-left': avatarGroup.margin + 'rpx'
-		}"
-		@click="handleClick"
-	>
-		<image class="fs-avatar-img" :src="errImg" v-if="errImg" :lazy-load="lazyLoad" :mode="imageMode" @click="handlePreview" />
-		<image class="fs-avatar-img" :src="src" v-if="src" :lazy-load="lazyLoad" :mode="imageMode" @click="handlePreview" @error="handleError" />
-		<view v-else class="fs-avatar-slot" :class="['bg-' + bgColorType]" :style="{backgroundColor:bgColor}">
+	<view
+		class="fs-avatar"
+		:class="[
+			shape,
+			{
+				radius,
+				fixed
+			}
+		]"
+		:style="{
+			width: width || size,
+			height: height || size,
+			right: fixed ? right : 0,
+			bottom: fixed ? bottom : 0,
+			border: borderStyle,
+			'margin-left': avatarGroup.margin + 'rpx'
+		}"
+		@click="handleClick"
+	>
+		<image
+			class="fs-avatar-img"
+			:src="errImg"
+			v-if="errImg"
+			:lazy-load="lazyLoad"
+			:mode="imageMode"
+			@click="handlePreview"
+		/>
+		<image
+			class="fs-avatar-img"
+			:src="src"
+			v-if="src"
+			:lazy-load="lazyLoad"
+			:mode="imageMode"
+			@click="handlePreview"
+			@error="handleError"
+		/>
+		<view v-else class="fs-avatar-slot" :class="['bg-' + bgColorType]" :style="{ backgroundColor: bgColor }">
 			<slot></slot>
 		</view>
 	</view>
 </template>
 
-<script setup>
-import { computed, inject, ref } from 'vue'
-import config from '@/utils/config'
-
-const props = defineProps({
-	src: String,
-	shape: {
-		type: String,
-		default: 'circle', // square, circle
-		validator(value) {
-			return ['circle', 'square'].includes(value)
-		}
-	},
-	size: {
-		type: String,
-		default: '80rpx'
-	},
-	width: String,
-	height: String,
-	bgColor: String,
-	bgColorType: {
-		type: String,
-		default: 'primary',
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-		}
-	},
-	border: Boolean,
-	borderWidth: {
-		type: String,
-		default: '4rpx'
-	},
-	borderColor: {
-		type: String,
-		default: '#fff'
-	},
-	lazyLoad: Boolean,
-	imageMode: {
-		type: String,
-		default: 'scaleToFill'
-	},
-	preview: Boolean,
-	radius: Boolean,
-	link: String,
-	linkType: {
-		type: String,
-		default: 'navigateTo'
-	},
-	fixed: Boolean,
-	right: {
-		type: String,
-		default: '40rpx'
-	},
-	bottom: {
-		type: String,
-		default: '60rpx'
-	},
-	defaultErrorImg: {
-		type: String,
-		default: config.defaultErrorImg
-	}
-})
-
-const emits = defineEmits(['click'])
-
-const avatarGroup = inject('avatarGroup', {})
-
-const borderStyle = computed(() => {
-	return (props.border || avatarGroup.border) ? `${ props.borderWidth} solid ${ props.borderColor}` : 'none'
+<script>
+/**
+ * 头像组件
+ * @description 头像组件
+ * @property {String} src 头像地址
+ * @property {String} shape = [circle | square] 头像类型
+ * @property {String} size 头像大小
+ * @property {String} width 头像宽
+ * @property {String} height 头像高
+ * @property {String} bgColor 背景颜色
+ * @property {String} bgColorType = [primary | danger | warning | info | success] 背景颜色类型
+ * @property {Boolean} border 是否展示边框
+ * @property {String} borderWidth 边框宽
+ * @property {String} borderColor 边框颜色
+ * @property {Boolean} lazyLoad 是否懒加载
+ * @property {String} imageMode 图片模式
+ * @property {Boolean} preview 是否预览图片
+ * @property {Boolean} radius 是否圆角
+ * @property {String} link 跳转地址
+ * @property {String} linkType 跳转类型
+ * @property {Boolean} fixed 是否悬浮
+ * @property {String} right 悬浮右边距(仅在fixed为true时有效)
+ * @property {String} bottom 悬浮下边距(仅在fixed为true时有效)
+ * @property {String} defaultErrorImg 图片加载失败显示的默认图片
+ * @example <fs-avatar size="100rpx" src="***"></fs-avatar>
+ */
+export default {
+	name: 'fs-avatar'
+}
+</script>
+
+<script setup>
+import { computed, inject, ref } from 'vue'
+import config from '@/utils/config'
+
+const props = defineProps({
+	src: String,
+	shape: {
+		type: String,
+		default: 'circle', // square, circle
+		validator(value) {
+			return ['circle', 'square'].includes(value)
+		}
+	},
+	size: {
+		type: String,
+		default: '80rpx'
+	},
+	width: String,
+	height: String,
+	bgColor: String,
+	bgColorType: {
+		type: String,
+		default: 'primary',
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	},
+	border: Boolean,
+	borderWidth: {
+		type: String,
+		default: '4rpx'
+	},
+	borderColor: {
+		type: String,
+		default: '#fff'
+	},
+	lazyLoad: Boolean,
+	imageMode: {
+		type: String,
+		default: 'scaleToFill'
+	},
+	preview: Boolean,
+	radius: Boolean,
+	link: String,
+	linkType: {
+		type: String,
+		default: 'navigateTo'
+	},
+	fixed: Boolean,
+	right: {
+		type: String,
+		default: '40rpx'
+	},
+	bottom: {
+		type: String,
+		default: '60rpx'
+	},
+	defaultErrorImg: {
+		type: String,
+		default: config.defaultErrorImg
+	}
 })
-
-const handleClick = () => {
+
+const emits = defineEmits(['click'])
+
+const avatarGroup = inject('avatarGroup', {})
+
+const borderStyle = computed(() => {
+	return props.border || avatarGroup.border ? `${props.borderWidth} solid ${props.borderColor}` : 'none'
+})
+
+const handleClick = () => {
 	if (props.link) {
 		uni[props.linkType]({
 			url: props.link
 		})
-	}
+	}
 	emits('click')
-}
-
-const handlePreview = () => {
-	if (props.preview) {
-		uni.previewImage({
-			urls: [props.src || errImg.value]
-		})
-	}
-}
-
-const errImg = ref('')
-const handleError = e => {
-	errImg.value = props.defaultErrorImg
+}
+
+const handlePreview = () => {
+	if (props.preview) {
+		uni.previewImage({
+			urls: [props.src || errImg.value]
+		})
+	}
+}
+
+const errImg = ref('')
+const handleError = e => {
+	errImg.value = props.defaultErrorImg
 }
 </script>
 
@@ -127,36 +173,36 @@ const handleError = e => {
 	position: relative;
 	overflow: hidden;
 	vertical-align: middle;
-	text-align: center;
-	
-	&.radius {
-		border-radius: var(--radius);
-	}
-	&.circle,
-	&.circle &-img {
-		border-radius: 50%;
-	}
-	&.fixed{
-		position: fixed;
-		z-index: 50;
-		margin-bottom: var(--window-bottom);
-	}
-	
-	&-img {
-		width: 100%;
-		height: 100%;
-		object-fit: cover;
-	}
-	
-	&-slot {
-		width: 100%;
-		height: 100%;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		color: #fff;
-		white-space: normal;
-		line-height: 1;
-	}
+	text-align: center;
+
+	&.radius {
+		border-radius: var(--radius);
+	}
+	&.circle,
+	&.circle &-img {
+		border-radius: 50%;
+	}
+	&.fixed {
+		position: fixed;
+		z-index: 50;
+		margin-bottom: var(--window-bottom);
+	}
+
+	&-img {
+		width: 100%;
+		height: 100%;
+		object-fit: cover;
+	}
+
+	&-slot {
+		width: 100%;
+		height: 100%;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		color: #fff;
+		white-space: normal;
+		line-height: 1;
+	}
 }
 </style>

+ 68 - 58
components/fs-back-top/fs-back-top.vue

@@ -1,59 +1,69 @@
-<template>
-	<view 
-		class="fs-back-top"
-		:style="{right, bottom}"
-		@click="handleClick"
-		v-if="visible">
-		<fs-icon type="icon-arrow-up" size="50rpx" color="#fff"></fs-icon>
-	</view>
-</template>
-
-<script setup>
-import { computed, watch } from 'vue'
-
-const props = defineProps({
-	scrollTop: {
-		type: [String, Number],
-		required: true
-	},
-	top: {
-		type: String,
-		default: '150px'
-	},
-	right: {
-		type: String,
-		default: '40rpx'
-	},
-	bottom: {
-		type: String,
-		default: '40rpx'
-	}
-})
-
-const visible = computed(() => {
-	return parseFloat(props.scrollTop) >= parseFloat(props.top)
-})
-
-const handleClick = () => {
-	uni.pageScrollTo({
-		scrollTop: 0,
-		duration: 100
-	})
-}
-</script>
-
-<style lang="scss" scoped>
-.fs-back-top{
-	position: fixed;
-	width: 90rpx;
-	height: 90rpx;
-	border-radius: 50%;
-	background-color: #606266;
-	opacity: .5;
-	z-index: 100;
-	display: flex;
-	justify-content: center;
-	align-items: center;
-	margin-bottom: var(--window-bottom);
-}
+<template>
+	<view class="fs-back-top" :style="{ right, bottom }" @click="handleClick" v-if="visible">
+		<fs-icon type="icon-arrow-up" size="50rpx" color="#fff"></fs-icon>
+	</view>
+</template>
+
+<script>
+/**
+ * 回到顶部组件
+ * @description 回到顶部组件
+ * @property {String, Number} scrollTop useScrollTop
+ * @property {String} top 距离顶部多大时显示返回顶部
+ * @property {String} right “返回顶部”距离页面右边距
+ * @property {String} bottom “返回顶部”距离页面下边距
+ */
+export default {
+	name: 'fs-back-top'
+}
+</script>
+
+<script setup>
+import { computed, watch } from 'vue'
+
+const props = defineProps({
+	scrollTop: {
+		type: [String, Number],
+		required: true
+	},
+	top: {
+		type: String,
+		default: '150px'
+	},
+	right: {
+		type: String,
+		default: '40rpx'
+	},
+	bottom: {
+		type: String,
+		default: '40rpx'
+	}
+})
+
+const visible = computed(() => {
+	return parseFloat(props.scrollTop) >= parseFloat(props.top)
+})
+
+const handleClick = () => {
+	uni.pageScrollTo({
+		scrollTop: 0,
+		duration: 100
+	})
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-back-top {
+	position: fixed;
+	width: 90rpx;
+	height: 90rpx;
+	border-radius: 50%;
+	background-color: #606266;
+	opacity: 0.5;
+	z-index: 100;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	margin-bottom: var(--window-bottom);
+}
 </style>

+ 81 - 66
components/fs-badge/fs-badge.vue

@@ -1,81 +1,96 @@
 <template>
 	<view class="fs-badge">
 		<slot></slot>
-		<view
-			v-if="dot" 
-			class="fs-badge-dot"
-			:class="['bg-' + bgColorType, {absolute: slots.default}]"
-			:style="{
-				backgroundColor: bgColor
-			}">
-		</view>
-		<view
-			v-else 
-			class="fs-badge-count"
-			:class="['bg-' + bgColorType, {absolute: slots.default}]"
-			:style="{
-				backgroundColor: bgColor
-			}">
-			<slot name="count">{{value}}</slot>
-		</view>
+		<view
+			v-if="dot"
+			class="fs-badge-dot"
+			:class="['bg-' + bgColorType, { absolute: slots.default }]"
+			:style="{
+				backgroundColor: bgColor
+			}"
+		></view>
+		<view
+			v-else
+			class="fs-badge-count"
+			:class="['bg-' + bgColorType, { absolute: slots.default }]"
+			:style="{
+				backgroundColor: bgColor
+			}"
+		>
+			<slot name="count">{{ value }}</slot>
+		</view>
 	</view>
 </template>
 
-<script setup>
-import { computed, useSlots } from 'vue'
-
-const props = defineProps({
-	dot: Boolean,
-	count: Number,
-	bgColor: String,
-	bgColorType: {
-		type: String,
-		default: 'danger',
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-		}
-	}
-})
-const value = computed(() => props.count > 99 ? '99+' : props.count)
-
-const slots = useSlots()
+<script>
+/**
+ * 徽章组件
+ * @description 徽章组件
+ * @property {Number} count 数字
+ * @property {Boolean} dot 是否显示圆点
+ * @property {String} bgColor 背景颜色
+ * @property {String} bgColorType = [primary | danger | warning | info | success] 背景颜色类型
+ */
+export default {
+	name: 'fs-badge'
+}
+</script>
+
+<script setup>
+import { computed, useSlots } from 'vue'
+
+const props = defineProps({
+	dot: Boolean,
+	count: Number,
+	bgColor: String,
+	bgColorType: {
+		type: String,
+		default: 'danger',
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	}
+})
+const value = computed(() => (props.count > 99 ? '99+' : props.count))
+
+const slots = useSlots()
 </script>
 
 <style lang="scss" scoped>
 .fs-badge {
 	position: relative;
 	display: inline-block;
-	vertical-align: middle;
-	
-	&-dot {
-		width: 16rpx;
-		height: 16rpx;
-		border-radius: 50%;
-		top: -8rpx;
-		right: -8rpx;
-		z-index: 10;
-	}
-	&-count {
-		color: #fff;
-		font-size: 12px;
-		font-weight: 500;
-		padding: 0 6px;
-		line-height: 36rpx;
-		min-width: 36rpx;
-		border-radius: 18rpx;
-		text-align: center;
-		box-sizing: border-box;
-		white-space: nowrap;
-		z-index: 10;
-		
-		&.absolute{
-			top: 0;
-			right: 0;
-			transform: translate(50%, -50%) scale(0.8);
-		}
-	}
-	.absolute{
-		position: absolute;
+	vertical-align: middle;
+
+	&-dot {
+		width: 16rpx;
+		height: 16rpx;
+		border-radius: 50%;
+		top: -8rpx;
+		right: -8rpx;
+		z-index: 10;
+	}
+	&-count {
+		color: #fff;
+		font-size: 12px;
+		font-weight: 500;
+		padding: 0 6px;
+		line-height: 36rpx;
+		min-width: 36rpx;
+		border-radius: 18rpx;
+		text-align: center;
+		box-sizing: border-box;
+		white-space: nowrap;
+		z-index: 10;
+
+		&.absolute {
+			top: 0;
+			right: 0;
+			transform: translate(50%, -50%) scale(0.8);
+		}
+	}
+	.absolute {
+		position: absolute;
 	}
 }
 </style>

+ 265 - 243
components/fs-button/fs-button.vue

@@ -1,244 +1,266 @@
-<template>
-  <button
-		hover-class="fs-hover"
-    class="fs-button"
-    :class="[
-      type,
-      size,
-      plain ? 'plain' : '',
-      radius ? 'radius' : '',
-      round ? 'round' : '',
-      disabled ? 'disabled' : '',
-      block ? 'block' : '',
-      full ? 'block full' : '',
-    ]"
-    :style="[
-      { width: width },
-      customStyle,
-    ]"
-		:open-type="openType"
-		:form-type="formType"
-		@getuserinfo="getuserinfo"
-		@contact="contact"
-		@getphonenumber="getphonenumber"
-		@opensetting="opensetting"
-		@error="error"
-    @click="handleClick"
-  >
-		<view class="fs-loader" v-if="loading"></view>
-    <template v-else>
-    	<slot></slot>
-    </template>
-  </button>
-</template>
-
-<script setup>
-	import { computed, useAttrs } from 'vue'
-	
-	const props = defineProps({
-		openType: String,
-		formType: String,
-    size: {
-      type: String,
-      validator(value) {
-      	return ['mini', 'small', 'medium'].includes(value)
-      }
-    },
-    type: {
-      type: String,
-      default: 'primary',
-			validator(value) {
-				return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
-			}
-    },
-    plain: Boolean,
-    radius: Boolean,
-    round: Boolean,
-    disabled: Boolean,
-    full: Boolean,
-    block: Boolean,
-    link: String,
-    linkType: {
-      type: String,
-      default: 'navigateTo'
-    },
-    width: String,
-    customStyle: {
-      type: Object,
-      default() {
-				return {}
-			}
-    },
-		loading: Boolean
-  })
-	
-	const emits = defineEmits(['click','getuserinfo','contact','getphonenumber','opensetting','error'])
-		
-	const handleClick = e =>  {
-	  if (props.link && !props.disabled) {
-	    uni[props.linkType]({
-	      url: props.link
-	    })
-	  }
-	  !props.disabled && emits('click')
-	}
-	const getuserinfo = (event) => {
-	  emits('getuserinfo', event.detail)
-	}
-	const contact = (event) => {
-	  emits('contact', event.detail)
-	}
-	const getphonenumber = (event) => {
-	  emits('getphonenumber', event.detail)
-	}
-	const opensetting = (event) => {
-	  emits('opensetting', event.detail)
-	}
-	const error = (event) => {
-	  emits('error', event.detail)
-	}
-</script>
-
-<style lang="scss" scoped>
-.fs-button {
-  font-weight: normal;
-  font-size: 14px;
-  color: #fff;
-  padding: 0 40rpx;
-  line-height: 1;
-  text-align: center;
-  border-radius: 0;
-  display: inline-block;
-  vertical-align: bottom;
-  background-color: #cfcfcf;
-  margin: 0;
-  border: 0;
-	height: 70rpx;
-	line-height: 70rpx;
-
-  &.primary,
-  &.primary.selected,
-  &.selected {
-    background-color: var(--primary);
-  }
-  &.success {
-    background-color: var(--success);
-  }
-  &.info {
-    background-color: var(--info);
-  }
-  &.warning {
-    background-color: var(--warning);
-  }
-  &.danger {
-    background-color: var(--danger);
-  }
-
-  &.plain,
-  &.plain.selected {
-    background-color: transparent;
-    box-shadow: inset 0 0 0 1px currentColor;
-    color: #999999;
-  }
-
-  &.medium {
-    // width: auto !important;
-    height: 60rpx;
-    line-height: 60rpx;
-    font-size: 13px;
-    padding: 0 30rpx;
-  }
-  &.small {
-    width: auto !important;
-    height: 50rpx;
-    line-height: 50rpx;
-    font-size: 12px;
-    padding: 0 20rpx;
-  }
-	&.mini {
-	  width: auto !important;
-	  height: 40rpx;
-	  line-height: 40rpx;
-	  font-size: 12px;
-	  padding: 0 20rpx;
-	}
-	&.block {
-	  display: block;
-	  height: 100rpx;
-	  line-height: 100rpx;
-	  margin-left: var(--gutter) !important;
-	  margin-right: var(--gutter) !important;
-	  font-size: 18px;
-	  width: auto;
-	
-	  &.radius {
-	    border-radius: 16rpx;
-	  }
-	}
-	&.full {
-	  margin-left: 0 !important;
-	  margin-right: 0 !important;
-	}
-	
-	&::after {
-		border: none;
-	}
-	
-	&.radius {
-	  border-radius: 8rpx;
-	}
-	&.round {
-	  border-radius: 30px;
-	}
-	
-	&.plain.primary,
-	&.plain.selected {
-	  color: var(--primary);
-	}
-	
-	&.plain.success {
-	  color: var(--success);
-	}
-	
-	&.plain.warning {
-	  color: var(--warning);
-	}
-	
-	&.plain.danger {
-	  color: var(--danger);
-	}
-	
-	&.plain.info {
-	  color: var(--info);
-	}
-	
-	&.disabled {
-	  opacity: 0.5;
-	}
-}
-
-.fs-hover {
-  opacity: 0.5;
-}
-
-.fs-loader {
-	display: inline-block;
-	width: 40rpx;
-	height: 40rpx;
-	color: inherit;
-	vertical-align: middle;
-	border: 4rpx solid currentcolor;
-	border-bottom-color: transparent;
-	border-radius: 50%;
-	animation: 1s loader linear infinite;
-	position: relative;
-}
-@keyframes loader {
-	0% {
-		transform: rotate(0deg);
-	}
-	100% {
-		transform: rotate(360deg);
-	}
-}
+<template>
+	<button
+		hover-class="fs-hover"
+		class="fs-button"
+		:class="[
+			type,
+			size,
+			plain ? 'plain' : '',
+			radius ? 'radius' : '',
+			round ? 'round' : '',
+			disabled ? 'disabled' : '',
+			block ? 'block' : '',
+			full ? 'block full' : ''
+		]"
+		:style="[{ width: width }, customStyle]"
+		:open-type="openType"
+		:form-type="formType"
+		@getuserinfo="getuserinfo"
+		@contact="contact"
+		@getphonenumber="getphonenumber"
+		@opensetting="opensetting"
+		@error="error"
+		@click="handleClick"
+	>
+		<view class="fs-loader" v-if="loading"></view>
+		<template v-else>
+			<slot></slot>
+		</template>
+	</button>
+</template>
+
+<script>
+/**
+ * 按钮组件
+ * @description 按钮组件
+ * @property {String} openType 参考小程序
+ * @property {String} formType 参考小程序
+ * @property {String} size = [mini | small | medium] 按钮大小
+ * @property {Boolean} plain 是否镂空
+ * @property {Boolean} radius 是否圆角
+ * @property {Boolean} round 是否半圆
+ * @property {Boolean} loading 加载效果
+ * @property {Boolean} disabled disabled
+ * @property {Boolean} full 通屏按钮
+ * @property {Boolean} block 块状按钮
+ * @property {String} link 跳转地址
+ * @property {String} linkType 跳转类型
+ * @property {String} width 按钮宽度
+ * @property {Object} customStyle 按钮自定义样式
+ * @property {String} type = [primary | danger | warning | info | success | default] 按钮颜色类型
+ */
+export default {
+	name: 'fs-button'
+}
+</script>
+
+<script setup>
+import { computed, useAttrs } from 'vue'
+
+const props = defineProps({
+	openType: String,
+	formType: String,
+	size: {
+		type: String,
+		validator(value) {
+			return ['mini', 'small', 'medium'].includes(value)
+		}
+	},
+	type: {
+		type: String,
+		default: 'primary',
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
+		}
+	},
+	plain: Boolean,
+	radius: Boolean,
+	round: Boolean,
+	disabled: Boolean,
+	full: Boolean,
+	block: Boolean,
+	link: String,
+	linkType: {
+		type: String,
+		default: 'navigateTo'
+	},
+	width: String,
+	customStyle: {
+		type: Object,
+		default() {
+			return {}
+		}
+	},
+	loading: Boolean
+})
+
+const emits = defineEmits(['click', 'getuserinfo', 'contact', 'getphonenumber', 'opensetting', 'error'])
+
+const handleClick = e => {
+	if (props.link && !props.disabled) {
+		uni[props.linkType]({
+			url: props.link
+		})
+	}
+	!props.disabled && emits('click')
+}
+const getuserinfo = event => {
+	emits('getuserinfo', event.detail)
+}
+const contact = event => {
+	emits('contact', event.detail)
+}
+const getphonenumber = event => {
+	emits('getphonenumber', event.detail)
+}
+const opensetting = event => {
+	emits('opensetting', event.detail)
+}
+const error = event => {
+	emits('error', event.detail)
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-button {
+	font-weight: normal;
+	font-size: 14px;
+	color: #fff;
+	padding: 0 40rpx;
+	line-height: 1;
+	text-align: center;
+	border-radius: 0;
+	display: inline-block;
+	vertical-align: bottom;
+	background-color: #cfcfcf;
+	margin: 0;
+	border: 0;
+	height: 70rpx;
+	line-height: 70rpx;
+
+	&.primary,
+	&.primary.selected,
+	&.selected {
+		background-color: var(--primary);
+	}
+	&.success {
+		background-color: var(--success);
+	}
+	&.info {
+		background-color: var(--info);
+	}
+	&.warning {
+		background-color: var(--warning);
+	}
+	&.danger {
+		background-color: var(--danger);
+	}
+
+	&.plain,
+	&.plain.selected {
+		background-color: transparent;
+		box-shadow: inset 0 0 0 1px currentColor;
+		color: #999999;
+	}
+
+	&.medium {
+		// width: auto !important;
+		height: 60rpx;
+		line-height: 60rpx;
+		font-size: 13px;
+		padding: 0 30rpx;
+	}
+	&.small {
+		width: auto !important;
+		height: 50rpx;
+		line-height: 50rpx;
+		font-size: 12px;
+		padding: 0 20rpx;
+	}
+	&.mini {
+		width: auto !important;
+		height: 40rpx;
+		line-height: 40rpx;
+		font-size: 12px;
+		padding: 0 20rpx;
+	}
+	&.block {
+		display: block;
+		height: 100rpx;
+		line-height: 100rpx;
+		margin-left: var(--gutter) !important;
+		margin-right: var(--gutter) !important;
+		font-size: 18px;
+		width: auto;
+
+		&.radius {
+			border-radius: 16rpx;
+		}
+	}
+	&.full {
+		margin-left: 0 !important;
+		margin-right: 0 !important;
+	}
+
+	&::after {
+		border: none;
+	}
+
+	&.radius {
+		border-radius: 8rpx;
+	}
+	&.round {
+		border-radius: 30px;
+	}
+
+	&.plain.primary,
+	&.plain.selected {
+		color: var(--primary);
+	}
+
+	&.plain.success {
+		color: var(--success);
+	}
+
+	&.plain.warning {
+		color: var(--warning);
+	}
+
+	&.plain.danger {
+		color: var(--danger);
+	}
+
+	&.plain.info {
+		color: var(--info);
+	}
+
+	&.disabled {
+		opacity: 0.5;
+	}
+}
+
+.fs-hover {
+	opacity: 0.5;
+}
+
+.fs-loader {
+	display: inline-block;
+	width: 40rpx;
+	height: 40rpx;
+	color: inherit;
+	vertical-align: middle;
+	border: 4rpx solid currentcolor;
+	border-bottom-color: transparent;
+	border-radius: 50%;
+	animation: 1s loader linear infinite;
+	position: relative;
+}
+@keyframes loader {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
 </style>

+ 94 - 77
components/fs-captcha/fs-captcha.vue

@@ -1,83 +1,100 @@
-<template>
-	<fs-button
-		v-if="type === 'button'"
-		size="medium" 
-		round
-		:plain="plain" 
-		:block="block"
-		@click="getCaptcha"
-		:style="customStyle"
-		:disabled="state.sending">
-		{{state.timerText}}
-	</fs-button>
-	<view v-else class="primary" :style="customStyle" @click="getCaptcha">{{state.timerText}}</view>
-</template>
-
-<script setup>
-import { reactive } from 'vue'
-
-const props = defineProps({
-	mobile: String,
-	seconds: {
-		type: Number,
-		default: 60
-	},
-	type: {
-		type: String,
-		default: 'button',
-		validator(value) {
-			return ['button','text'].includes(value)
-		}
-	},
-	block: Boolean,
-	plain: {
-		type: Boolean,
-		default: true
-	},
-	customStyle: {
-		type: Object,
-		default: () => {}
-	}
-})
-const emits = defineEmits(['start', 'end'])
-
-const state = reactive({
-	timerText: '获取验证码',
-	sending: false,
-	timerId: null
-})
-
-const getCaptcha = () => {
-	if (!state.sending) {
-		if (!/^1\d{10}$/.test(props.mobile)) {
-			return uni.showToast({
-				icon: 'none',
-				title: '请输入正确的手机号'
-			})
-		}
-		state.sending = true
-		
-		let timer = props.seconds
-		state.timerText = `${timer}s`
-		
-		state.timerId = setInterval(() => {
-			if (--timer > 0) {
-				state.timerText = `${timer}s`
-			} else {
-				endSendCaptcha()
-			}
-		}, 1000)
-		emits('start')
+<template>
+	<fs-button
+		v-if="type === 'button'"
+		size="medium"
+		round
+		:plain="plain"
+		:block="block"
+		@click="getCaptcha"
+		:style="customStyle"
+		:disabled="state.sending"
+	>
+		{{ state.timerText }}
+	</fs-button>
+	<view v-else class="primary" :style="customStyle" @click="getCaptcha">{{ state.timerText }}</view>
+</template>
+
+<script>
+/**
+ * 验证码组件
+ * @description 验证码组件
+ * @property {String} mobile 手机号
+ * @property {Number} seconds 倒计时(单位s)
+ * @property {String} type = [button | text] 类型
+ * @property {Boolean} plain 是否镂空
+ * @property {Boolean} block 块状
+ * @property {Object} customStyle 自定义样式
+ * @event {Function} start 倒计时开始事件
+ * @event {Function} end 倒计时结束事件
+ */
+export default {
+	name: 'fs-captcha'
+}
+</script>
+
+<script setup>
+import { reactive } from 'vue'
+
+const props = defineProps({
+	mobile: String,
+	seconds: {
+		type: Number,
+		default: 60
+	},
+	type: {
+		type: String,
+		default: 'button',
+		validator(value) {
+			return ['button', 'text'].includes(value)
+		}
+	},
+	block: Boolean,
+	plain: {
+		type: Boolean,
+		default: true
+	},
+	customStyle: {
+		type: Object,
+		default: () => {}
+	}
+})
+const emits = defineEmits(['start', 'end'])
+
+const state = reactive({
+	timerText: '获取验证码',
+	sending: false,
+	timerId: null
+})
+
+const getCaptcha = () => {
+	if (!state.sending) {
+		if (!/^1\d{10}$/.test(props.mobile)) {
+			return uni.showToast({
+				icon: 'none',
+				title: '请输入正确的手机号'
+			})
+		}
+		state.sending = true
+
+		let timer = props.seconds
+		state.timerText = `${timer}s`
+
+		state.timerId = setInterval(() => {
+			if (--timer > 0) {
+				state.timerText = `${timer}s`
+			} else {
+				endSendCaptcha()
+			}
+		}, 1000)
+		emits('start')
 	}
 }
 const endSendCaptcha = () => {
 	state.sending = false
 	state.timerText = '获取验证码'
-	clearInterval(state.timerId)
+	clearInterval(state.timerId)
 	emits('end')
-}
-</script>
-
-<style>
-
-</style>
+}
+</script>
+
+<style></style>

+ 104 - 90
components/fs-card/fs-card.vue

@@ -1,91 +1,105 @@
-<template>
-	<view class="fs-card" :class="{'fs-card-full': full, 'fs-card-gutter': gutter}" @click="handleClick">
-		<view class="fs-card-box" :class="{'fs-card-radius': radius, shadow}">
-			<view class="fs-card-title" v-if="slots.title || title" :style="titleStyle">
-				<slot name="title">{{title}}</slot>
-			</view>
-			<view :class="{'fs-card-content': contentPadding}">
-				<slot></slot>
-			</view>
-			<view class="fs-card-ft" v-if="slots.footer">
-				<slot name="footer"></slot>
-			</view>
-		</view>
-	</view>
-</template>
-
-<script setup>
-import { useSlots } from 'vue'
-const props = defineProps({
-	title: String,
-	titleStyle: {
-		type: Object,
-		default() {
-			return {}
-		}
-	},
-	full: Boolean,
-	gutter: Boolean,
-	radius: {
-		type: Boolean,
-		default: true
-	},
-	shadow: {
-		type: Boolean,
-		default: false
-	},
-	contentPadding: Boolean,
-	link: String,
-	linkType: {
-		type: String,
-		default: 'navigateTo'
-	},
-})
-
-const slots = useSlots()
-const emits = defineEmits(['click'])
-
-const handleClick = () => {
-	if (props.link) {
-		uni[props.linkType]({
-			url: props.link
-		})
-	}
-	emits('click')
-}
-</script>
-
-<style lang="scss" scoped>
-.fs-card{
-	margin-left: var(--gutter);
-	margin-right: var(--gutter);
-	
-	&-box{
-		background-color: #fff;
-		overflow: hidden;
-	}
-	&-radius{
-		border-radius: var(--radius);
-	}
-	
-	&-title{
-		padding: 20rpx var(--gutter);
-		border-bottom: 2rpx solid var(--border-color);
-	}
-	&-ft{
-		padding: 20rpx var(--gutter);
-		border-top: 2rpx solid var(--border-color);
-	}
-	&-content{
-		padding: 20rpx var(--gutter);
-	}
-	
-	&-full{
-		margin-left: 0;
-		margin-right: 0;
-	}
-	&-gutter{
-		margin-bottom: var(--gutter-v);
-	}
-}
+<template>
+	<view class="fs-card" :class="{ 'fs-card-full': full, 'fs-card-gutter': gutter }" @click="handleClick">
+		<view class="fs-card-box" :class="{ 'fs-card-radius': radius, shadow }">
+			<view class="fs-card-title" v-if="slots.title || title" :style="titleStyle">
+				<slot name="title">{{ title }}</slot>
+			</view>
+			<view :class="{ 'fs-card-content': contentPadding }"><slot></slot></view>
+			<view class="fs-card-ft" v-if="slots.footer"><slot name="footer"></slot></view>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 卡片组件
+ * @description 卡片组件
+ * @property {String} title 标题
+ * @property {Object} titleStyle 标题样式
+ * @property {Boolean} full 是否通屏
+ * @property {Boolean} gutter 是否有下边距
+ * @property {Boolean} radius 是否圆角
+ * @property {Boolean} shadow 是否带阴影
+ * @property {String} link 跳转地址
+ * @property {String} linkType 跳转类型
+ */
+export default {
+	name: 'fs-card'
+}
+</script>
+
+<script setup>
+import { useSlots } from 'vue'
+const props = defineProps({
+	title: String,
+	titleStyle: {
+		type: Object,
+		default() {
+			return {}
+		}
+	},
+	full: Boolean,
+	gutter: Boolean,
+	radius: {
+		type: Boolean,
+		default: true
+	},
+	shadow: {
+		type: Boolean,
+		default: false
+	},
+	contentPadding: Boolean,
+	link: String,
+	linkType: {
+		type: String,
+		default: 'navigateTo'
+	}
+})
+
+const slots = useSlots()
+const emits = defineEmits(['click'])
+
+const handleClick = () => {
+	if (props.link) {
+		uni[props.linkType]({
+			url: props.link
+		})
+	}
+	emits('click')
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-card {
+	margin-left: var(--gutter);
+	margin-right: var(--gutter);
+
+	&-box {
+		background-color: #fff;
+		overflow: hidden;
+	}
+	&-radius {
+		border-radius: var(--radius);
+	}
+
+	&-title {
+		padding: 20rpx var(--gutter);
+		border-bottom: 2rpx solid var(--border-color);
+	}
+	&-ft {
+		padding: 20rpx var(--gutter);
+		border-top: 2rpx solid var(--border-color);
+	}
+	&-content {
+		padding: 20rpx var(--gutter);
+	}
+
+	&-full {
+		margin-left: 0;
+		margin-right: 0;
+	}
+	&-gutter {
+		margin-bottom: var(--gutter-v);
+	}
+}
 </style>

+ 98 - 69
components/fs-cell-group/fs-cell-group.vue

@@ -1,81 +1,110 @@
 <template>
-	<view class="fs-cell-group" :class="{full, radius, 'fs-cell-group-gutter': gutter}" :style="{backgroundColor:bgColor || '#fff'}">
+	<view
+		class="fs-cell-group"
+		:class="{ full, radius, 'fs-cell-group-gutter': gutter }"
+		:style="{ backgroundColor: bgColor || '#fff' }"
+	>
 		<slot />
 	</view>
 </template>
 
-<script setup>
-import { provide, toRefs } from 'vue'
-
-const props = defineProps({
-	titleWidth: String,
-	arrow: Boolean,
-	arrowColor: {
-		type: String,
-		default: ''
-	},
-	arrowColorType: {
-		type: String,
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-		}
-	},
-	border: Boolean,
-	tighten: Boolean,
-	gutter: Boolean,
-	radius: {
-		type: Boolean,
-		default: true
-	},
-	reverse: Boolean,
-	align: {
-		type: String,
-		default: 'center'
-	},
-	justify: {
-		type: String,
-		default: 'left',
-		validator(value) {
-			return ['left', 'center', 'right'].includes(value)
-		}
-	},
-	bgColor: {
-		type: String,
-	},
-	full: Boolean
-})
+<script>
+/**
+ * 单元格组组件
+ * @description 单元格组组件
+ * @property {String} titleWidth 标题宽度
+ * @property {Boolean} arrow 是否显示箭头
+ * @property {String} arrowColor 箭头颜色
+ * @property {String} arrowColorType = [primary | danger | warning | info | success] 箭头颜色类型
+ * @property {Boolean} full 是否通屏
+ * @property {Boolean} border 是否显示边框
+ * @property {Boolean} tighten 是否紧凑
+ * @property {Boolean} gutter 是否显示间距
+ * @property {Boolean} radius 是否带圆角
+ * @property {Boolean} reverse 是否翻转
+ * @property {String} bgColor 背景颜色
+ * @property {String} align = [top | center | bottom | stretch] 垂直对齐方式
+ * @property {String} justify = [left | center | right] 水平对齐方式
+ */
+export default {
+	name: 'fs-cell-group'
+}
+</script>
 
-provide('cellGroup', props)
+<script setup>
+import { provide, toRefs } from 'vue'
 
+const props = defineProps({
+	titleWidth: String,
+	arrow: Boolean,
+	arrowColor: {
+		type: String,
+		default: ''
+	},
+	arrowColorType: {
+		type: String,
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	},
+	border: Boolean,
+	tighten: Boolean,
+	gutter: Boolean,
+	radius: {
+		type: Boolean,
+		default: true
+	},
+	reverse: Boolean,
+	align: {
+		type: String,
+		default: 'center',
+		validator(value) {
+			return ['top', 'center', 'bottom', 'stretch'].includes(value)
+		}
+	},
+	justify: {
+		type: String,
+		default: 'left',
+		validator(value) {
+			return ['left', 'center', 'right'].includes(value)
+		}
+	},
+	bgColor: {
+		type: String
+	},
+	full: Boolean
+})
+
+provide('cellGroup', props)
 </script>
 
 <style lang="scss" scoped>
-.fs-cell-group{
-	margin: 0 var(--gutter);
-	overflow: hidden;
-	position: relative;
-	
-	&::after{
-		position: absolute;
-		bottom: 0;
-		left: 0;
-		height: 2rpx;
-		width: 100%;
-		background-color: #fff;
-		z-index: 10;
-		content: '';
-	}
-	
-	&.full{
-		margin: 0;
-	}
-	
-	&.radius{
-		border-radius: var(--radius);
-	}
-	
-	&-gutter{
-		margin-bottom: var(--gutter-v);
-	}
+.fs-cell-group {
+	margin: 0 var(--gutter);
+	overflow: hidden;
+	position: relative;
+
+	&::after {
+		position: absolute;
+		bottom: 0;
+		left: 0;
+		height: 2rpx;
+		width: 100%;
+		background-color: #fff;
+		z-index: 10;
+		content: '';
+	}
+
+	&.full {
+		margin: 0;
+	}
+
+	&.radius {
+		border-radius: var(--radius);
+	}
+
+	&-gutter {
+		margin-bottom: var(--gutter-v);
+	}
 }
 </style>

+ 204 - 171
components/fs-cell/fs-cell.vue

@@ -1,115 +1,148 @@
 <template>
-	<view 
-		class="fs-cell" 
-		:class="[cls,{shadow}]" 
-		:style="{backgroundColor:bgColor}" 
-		@click="handleClick"
-	>
-		<view class="fs-cell-flex" :class="['fs-cell-align-' + align, justify,{reverse}]">
+	<view class="fs-cell" :class="[cls, { shadow }]" :style="{ backgroundColor: bgColor }" @click="handleClick">
+		<view class="fs-cell-flex" :class="['fs-cell-align-' + align, justify, { reverse }]">
 			<view class="fs-cell-title" :class="{ 'fs-cell-required': required }" :style="titleStyle">
-				<template v-if="title">{{title}}</template>
-				<slot v-else name="title"></slot>
+				<template v-if="title">
+					{{ title }}
+				</template>
+				<slot v-else name="title"></slot>
 			</view>
 			<view class="fs-cell-value">
-				<template v-if="value">{{value}}</template>
+				<template v-if="value">
+					{{ value }}
+				</template>
 				<slot v-else name="value"></slot>
 			</view>
 			<view class="fs-cell-extra">
-				<template v-if="extra">{{extra}}</template>
+				<template v-if="extra">
+					{{ extra }}
+				</template>
 				<slot v-else name="extra"></slot>
 			</view>
 		</view>
 		<view class="fs-cell-label">
-			<template v-if="label">{{label}}</template>
+			<template v-if="label">
+				{{ label }}
+			</template>
 			<slot v-else name="label"></slot>
 		</view>
-		<view class="arrow-icon">
-			<fs-icon type="icon-d-down" rotate="-90" size="28rpx" :color="arrowColor" :colorType="arrowColorType"></fs-icon>
-		</view>
+		<view class="arrow-icon">
+			<fs-icon type="icon-d-down" rotate="-90" size="28rpx" :color="arrowColor" :colorType="arrowColorType"></fs-icon>
+		</view>
 	</view>
 </template>
 
-<script setup>
-	import { computed, toRefs, inject } from 'vue'
-	
-	const props = defineProps({
-		title: String,
-		titleWidth: String,
-		value: String,
-		extra: String,
-		label: String,
-		arrow: Boolean,
-		arrowColor: {
-			type: String,
-			default: '#E8EAF2'
-		},
-		arrowColorType: {
-			type: String,
-			validator(value) {
-				return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-			}
-		},
-		border: Boolean,
-		tighten: Boolean,
-		gutter: Boolean,
-		radius: Boolean,
-		reverse: Boolean,
-		required: Boolean,
-		align: {
-			type: String,
-			default: 'center',
-			validator(value) {
-				return ['top', 'center', 'bottom', 'stretch'].includes(value)
-			}
-		},
-		justify: {
-			type: String,
-			validator(value) {
-				return ['left', 'center', 'right'].includes(value)
-			}
-		},
-		bgColor: {
-			type: String,
-		},
-		shadow: Boolean,
-		link: String,
-		linkType: {
-			type: String,
-			default: 'navigateTo'
-		},
-	})
-	
-	const emits = defineEmits(['click'])
-	const cellGroup = inject('cellGroup', {})
-	
-	const justify = props.justify || cellGroup.justify || 'left'
-	const bgColor = props.bgColor || cellGroup.bgColor || '#fff'
-	const arrowColor = props.arrowColor || cellGroup.arrowColor
-	const arrowColorType = props.arrowColorType || cellGroup.arrowColorType
-	const cls = computed(() => {
-		const classNames = [];
-			
-		(props.arrow || cellGroup.arrow) && classNames.push('arrow');
-		(props.border || cellGroup.border) && classNames.push('border');
-		(props.tighten || cellGroup.tighten) && classNames.push('tighten')
-		props.gutter && classNames.push('gutter')
-		props.radius && classNames.push('radius')
-			
-		return classNames.join(' ')
-	})
-	const titleStyle = computed(() => {
-		const width = props.titleWidth || cellGroup.titleWidth
-		return width ? `width: ${width}` : ''
-	})
-	
-	const handleClick = () => {
-		if (props.link) {
-			uni[props.linkType]({
-				url: props.link
-			})
-		}
-		emits('click')
+<script>
+/**
+ * 单元格组件
+ * @description 单元格组件
+ * @property {String} title 标题
+ * @property {String} titleWidth 标题宽度
+ * @property {String} value 值
+ * @property {String} label 描述信息
+ * @property {String} extra 额外信息
+ * @property {Boolean} arrow 是否显示箭头
+ * @property {String} arrowColor 箭头颜色
+ * @property {String} arrowColorType = [primary | danger | warning | info | success] 箭头颜色类型
+ * @property {Boolean} border 是否显示边框
+ * @property {Boolean} shadow 是否显示阴影
+ * @property {Boolean} tighten 是否紧凑
+ * @property {Boolean} gutter 是否显示间距
+ * @property {Boolean} radius 是否带圆角
+ * @property {Boolean} reverse 是否翻转
+ * @property {Boolean} required 是否必填配合表单使用
+ * @property {String} bgColor 背景颜色
+ * @property {String} align = [top | center | bottom | stretch] 垂直对齐方式
+ * @property {String} justify = [left | center | right] 水平对齐方式
+ * @property {String} link 跳转地址
+ * @property {String} linkType 跳转类型
+ */
+export default {
+	name: 'fs-cell'
+}
+</script>
+
+<script setup>
+import { computed, toRefs, inject } from 'vue'
+
+const props = defineProps({
+	title: String,
+	titleWidth: String,
+	value: String,
+	extra: String,
+	label: String,
+	arrow: Boolean,
+	arrowColor: {
+		type: String,
+		default: '#E8EAF2'
+	},
+	arrowColorType: {
+		type: String,
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	},
+	border: Boolean,
+	tighten: Boolean,
+	gutter: Boolean,
+	radius: Boolean,
+	reverse: Boolean,
+	required: Boolean,
+	align: {
+		type: String,
+		default: 'center',
+		validator(value) {
+			return ['top', 'center', 'bottom', 'stretch'].includes(value)
+		}
+	},
+	justify: {
+		type: String,
+		validator(value) {
+			return ['left', 'center', 'right'].includes(value)
+		}
+	},
+	bgColor: {
+		type: String
+	},
+	shadow: Boolean,
+	link: String,
+	linkType: {
+		type: String,
+		default: 'navigateTo'
+	}
+})
+
+const emits = defineEmits(['click'])
+const cellGroup = inject('cellGroup', {})
+
+const justify = props.justify || cellGroup.justify || 'left'
+const bgColor = props.bgColor || cellGroup.bgColor || '#fff'
+const arrowColor = props.arrowColor || cellGroup.arrowColor
+const arrowColorType = props.arrowColorType || cellGroup.arrowColorType
+const cls = computed(() => {
+	const classNames = []
+
+	;(props.arrow || cellGroup.arrow) && classNames.push('arrow')
+	;(props.border || cellGroup.border) && classNames.push('border')
+	;(props.tighten || cellGroup.tighten) && classNames.push('tighten')
+	props.gutter && classNames.push('gutter')
+	props.radius && classNames.push('radius')
+
+	return classNames.join(' ')
+})
+const titleStyle = computed(() => {
+	const width = props.titleWidth || cellGroup.titleWidth
+	return width ? `width: ${width}` : ''
+})
+
+const handleClick = () => {
+	if (props.link) {
+		uni[props.linkType]({
+			url: props.link
+		})
 	}
+	emits('click')
+}
 </script>
 
 <style lang="scss" scoped>
@@ -120,82 +153,82 @@
 	background-color: #fff;
 	line-height: 1.4;
 	width: 100%;
-	box-sizing: border-box;
-	
-	&-flex {
-		display: flex;
-		justify-content: space-between;
-	}
-	&-value {
-		flex: 1;
-		padding-left: 20rpx;
-		text-align: right;
-	}
-	&-label {
-		font-size: var(--sub-size);
-		color: var(--sub);
-	}
-	
-	&-align-top {
-		align-items: flex-start;
-	}
-	&-align-center {
-		align-items: center;
-	}
-	&-align-bottom {
-		align-items: flex-end;
-	}
-	
-	&.arrow {
-		padding-right: 50rpx;
-		
-		.arrow-icon{
-			display: block;
-		}
-	}
-	
-	&-required{
-		position: relative;
-		padding-left: 7px;
-		
-		&::before{
-			position: absolute;
-			content: '*';
-			color: red;
-			left: 0;
-		}
-	}
-		
-	&.gutter {
-		margin-bottom: var(--gutter-v);
-	}
-	&.radius {
-		border-radius: var(--radius);
-	}
-	&.reverse{
-		flex-direction: row-reverse;
-	}
-	&.tighten {
-		padding: var(--tighten-gutter);
-	}
-	&.border {
-		border-bottom: 2rpx solid var(--border-color);
+	box-sizing: border-box;
+
+	&-flex {
+		display: flex;
+		justify-content: space-between;
+	}
+	&-value {
+		flex: 1;
+		padding-left: 20rpx;
+		text-align: right;
+	}
+	&-label {
+		font-size: var(--sub-size);
+		color: var(--sub);
+	}
+
+	&-align-top {
+		align-items: flex-start;
+	}
+	&-align-center {
+		align-items: center;
 	}
-}
-.arrow-icon{
-	display: none;
-	position: absolute;
-	right: 10rpx;
-	top: 50%;
-	transform: translateY(-50%);
-}
-.left .fs-cell-value {
-	text-align: left;
-}
-.center .fs-cell-value {
-	text-align: center;
-}
-.right .fs-cell-value {
-	text-align: right;
+	&-align-bottom {
+		align-items: flex-end;
+	}
+
+	&.arrow {
+		padding-right: 50rpx;
+
+		.arrow-icon {
+			display: block;
+		}
+	}
+
+	&-required {
+		position: relative;
+		padding-left: 7px;
+
+		&::before {
+			position: absolute;
+			content: '*';
+			color: red;
+			left: 0;
+		}
+	}
+
+	&.gutter {
+		margin-bottom: var(--gutter-v);
+	}
+	&.radius {
+		border-radius: var(--radius);
+	}
+	&.reverse {
+		flex-direction: row-reverse;
+	}
+	&.tighten {
+		padding: var(--tighten-gutter);
+	}
+	&.border {
+		border-bottom: 2rpx solid var(--border-color);
+	}
+}
+.arrow-icon {
+	display: none;
+	position: absolute;
+	right: 10rpx;
+	top: 50%;
+	transform: translateY(-50%);
+}
+.left .fs-cell-value {
+	text-align: left;
+}
+.center .fs-cell-value {
+	text-align: center;
+}
+.right .fs-cell-value {
+	text-align: right;
 }
 </style>

+ 87 - 71
components/fs-checkbox-button/fs-checkbox-button.vue

@@ -1,84 +1,100 @@
 <template>
-	<view 
-		class="fs-checkbox-button" 
-		:class="[
-			selected ? checkedColorType : 'fs-checkbox-button-default',
-			{'fs-checkbox-button-radius':radius, 'fs-checkbox-button-round':round},
-			buttonSize
-		]"
-		:style="{color:checkedColor}"
-		@click="handleToggle">
-			{{label}}
-			<slot />
+	<view
+		class="fs-checkbox-button"
+		:class="[
+			selected ? checkedColorType : 'fs-checkbox-button-default',
+			{ 'fs-checkbox-button-radius': radius, 'fs-checkbox-button-round': round },
+			buttonSize
+		]"
+		:style="{ color: checkedColor }"
+		@click="handleToggle"
+	>
+		{{ label }}
+		<slot />
 	</view>
 </template>
 
-<script setup>
-import { inject, watch, toRefs, ref } from 'vue'
-	
-const props = defineProps({
-	label: String,
-	value: {
-		type: null,
-		required: true
-	},
-	checkedColor: String,
-	checkedColorType: String,
-	size: {
-	  type: String,
-	  validator(value) {
-	  	return ['mini', 'small', 'medium'].includes(value)
-	  }
-	},
-})
-
-const checkboxGroup = inject('checkboxGroup')
-const { inline, radius, round } = checkboxGroup
+<script>
+/**
+ * 多选框组件
+ * @description 多选框组件
+ * @property {String} label 文本
+ * @property {null} value 标识符(必须传)
+ * @property {String} size = [mini | small | medium] 按钮大小
+ * @property {String} checkedColor 选中颜色
+ * @property {String} checkedColorType = [primary | danger | warning | info | success] 选中颜色类型
+ */
+export default {
+	name: 'fs-checkbox-button'
+}
+</script>
+
+<script setup>
+import { inject, watch, toRefs, ref } from 'vue'
+
+const props = defineProps({
+	label: String,
+	value: {
+		type: null,
+		required: true
+	},
+	checkedColor: String,
+	checkedColorType: String,
+	size: {
+		type: String,
+		validator(value) {
+			return ['mini', 'small', 'medium'].includes(value)
+		}
+	}
+})
+
+const checkboxGroup = inject('checkboxGroup')
+const { inline, radius, round } = checkboxGroup
 const checkedColorType = props.checkedColorType || checkboxGroup.checkedColorType
 const checkedColor = props.checkedColor || checkboxGroup.checkedColor
 const buttonSize = props.size || checkboxGroup.size
-
-let selected = ref(false)
-checkboxGroup.updateChildren({
-	selected,
-	value: props.value
-})
-
-const handleToggle = () => {
-	checkboxGroup.updateValue(props.value)
-}
+
+let selected = ref(false)
+checkboxGroup.updateChildren({
+	selected,
+	value: props.value
+})
+
+const handleToggle = () => {
+	checkboxGroup.updateValue(props.value)
+}
 </script>
 
 <style lang="scss" scoped>
 .fs-checkbox-button {
-	padding: 10rpx 30rpx;
-	white-space: nowrap;
-	border: 2rpx solid currentColor;
-	margin-right: 20rpx;
-	margin-bottom: 20rpx;
-	
-	&-default {
-		color: #999999;
-	}
-	
-	&-radius{
-		border-radius: var(--radius);
-	}
-	&-round{
-		border-radius: 60rpx;
-	}
-	
-	&.medium{
-		padding: 8rpx 25rpx;
-		font-size: 13px;
-	}
-	&.small{
-		padding: 6rpx 20rpx;
-		font-size: 12px;
-	}
-	&.mini{
-		padding: 2rpx 15rpx;
-		font-size: 11px;
-	}
+	padding: 10rpx 30rpx;
+	white-space: nowrap;
+	border: 2rpx solid currentColor;
+	margin-right: 20rpx;
+	margin-bottom: 20rpx;
+
+	&-default {
+		color: #999999;
+	}
+
+	&-radius {
+		border-radius: var(--radius);
+	}
+	&-round {
+		border-radius: 60rpx;
+	}
+
+	&.medium {
+		padding: 8rpx 25rpx;
+		font-size: 13px;
+	}
+	&.small {
+		padding: 6rpx 20rpx;
+		font-size: 12px;
+	}
+	&.mini {
+		padding: 2rpx 15rpx;
+		font-size: 11px;
+	}
 }
 </style>

+ 54 - 46
components/fs-checkbox-cell/fs-checkbox-cell.vue

@@ -1,48 +1,56 @@
-<template>
-	<fs-cell
-		border
-		justify="right" 
-		:title="label" 
-		@click="handleToggle">
-		<template #title>
-			<slot></slot>
-		</template>
-		<template #value>
-			<fs-icon type="icon-right" :color="checkedColor" :colorType="checkedColorType" v-if="selected"></fs-icon>
-		</template>
-	</fs-cell>
-</template>
-
-<script setup>
-import { ref, watch, inject, toRefs } from 'vue'
-
-const props = defineProps({
-	label: String,
-	value: {
-		type: null,
-		required: true
-	},
-	checkedColor: String,
-	checkedColorType: String,
-})
-
-const checkboxGroup = inject('checkboxGroup')
+<template>
+	<fs-cell border justify="right" :title="label" @click="handleToggle">
+		<template #title>
+			<slot></slot>
+		</template>
+		<template #value>
+			<fs-icon type="icon-right" :color="checkedColor" :colorType="checkedColorType" v-if="selected"></fs-icon>
+		</template>
+	</fs-cell>
+</template>
+
+<script>
+/**
+ * 多选框组件
+ * @description 多选框组件
+ * @property {String} label 文本
+ * @property {null} value 标识符(必须传)
+ * @property {String} checkedColor 选中颜色
+ * @property {String} checkedColorType = [primary | danger | warning | info | success] 选中颜色类型
+ */
+export default {
+	name: 'fs-checkbox-cell'
+}
+</script>
+
+<script setup>
+import { ref, watch, inject, toRefs } from 'vue'
+
+const props = defineProps({
+	label: String,
+	value: {
+		type: null,
+		required: true
+	},
+	checkedColor: String,
+	checkedColorType: String
+})
+
+const checkboxGroup = inject('checkboxGroup')
 const { reverse, inline, justify } = checkboxGroup
-
+
 const checkedColorType = props.checkedColorType || checkboxGroup.checkedColorType
-const checkedColor = props.checkedColor || checkboxGroup.checkedColor
-
-let selected = ref(false)
-checkboxGroup.updateChildren({
-	selected,
-	value: props.value
-})
-
-const handleToggle = () => {
-	checkboxGroup.updateValue(props.value)
-}
-</script>
-
-<style lang="scss" scoped>
-
-</style>
+const checkedColor = props.checkedColor || checkboxGroup.checkedColor
+
+let selected = ref(false)
+checkboxGroup.updateChildren({
+	selected,
+	value: props.value
+})
+
+const handleToggle = () => {
+	checkboxGroup.updateValue(props.value)
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 123 - 98
components/fs-checkbox-group/fs-checkbox-group.vue

@@ -1,108 +1,133 @@
 <template>
-	<view class="fs-checkbox-group" :class="{inline}">
-		<slot></slot>
-	</view>
+	<view class="fs-checkbox-group" :class="{ inline }"><slot></slot></view>
 </template>
 
-<script setup>
-import { provide, reactive, watch, toRefs } from 'vue'
-
-const props = defineProps({
-	max: {
-		type: Number,
-		default: -1
-	},
-	justify: String,
-	reverse: Boolean,
-	inline: Boolean,
-	checkedColor: String,
-	checkedColorType: {
-	  type: String,
-	  default: 'primary',
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-		}
-	},
-	radius: Boolean,
-	round: Boolean,
-	size: {
-	  type: String,
-	  validator(value) {
-	  	return ['mini', 'small', 'medium'].includes(value)
-	  }
-	},
-	modelValue: {
-		type: Array,
-		default() {
-			return []
-		}
-	}
-})
-const emits = defineEmits(['update:modelValue','change'])
-
-const state = reactive({
-	selectedValue: props.modelValue,
-	children: []
-})
-watch(() => props.modelValue, val => {
-	state.selectedValue = val
-})
-
-const checkStrategy = value => {
-	state.children.forEach(item => {
-		if (typeof item.value === 'object') {
-			item.selected = state.selectedValue.filter(selected => selected.id === item.value.id).length > 0
-		} else{
-			item.selected = state.selectedValue.indexOf(item.value) > -1
+<script>
+/**
+ * 多选框组组件
+ * @description 多选框组组件
+ * @property {Number} max 最多选中几个
+ * @property {String} justify 图标对齐方式
+ * @property {Boolean} reverse 是否反转
+ * @property {Boolean} inline 单行显示
+ * @property {Boolean} radius 是否圆角(仅对按钮样式有效)
+ * @property {Boolean} round 是否半圆(仅对按钮样式有效)
+ * @property {String} checkedColor 选中颜色
+ * @property {String} checkedColorType = [primary | danger | warning | info | success] 选中颜色类型
+ * @property {String} size = [mini | small | medium] 按钮大小(仅对按钮样式有效)
+ * @event {Function} change change事件
+ */
+export default {
+	name: 'fs-checkbox-group'
+}
+</script>
+
+<script setup>
+import { provide, reactive, watch, toRefs } from 'vue'
+
+const props = defineProps({
+	max: {
+		type: Number,
+		default: -1
+	},
+	justify: String,
+	reverse: Boolean,
+	inline: Boolean,
+	checkedColor: String,
+	checkedColorType: {
+		type: String,
+		default: 'primary',
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	},
+	radius: Boolean,
+	round: Boolean,
+	size: {
+		type: String,
+		validator(value) {
+			return ['mini', 'small', 'medium'].includes(value)
+		}
+	},
+	modelValue: {
+		type: Array,
+		default() {
+			return []
+		}
+	}
+})
+const emits = defineEmits(['update:modelValue', 'change'])
+
+const state = reactive({
+	selectedValue: props.modelValue,
+	children: []
+})
+watch(
+	() => props.modelValue,
+	val => {
+		state.selectedValue = val
+	}
+)
+
+const checkStrategy = value => {
+	state.children.forEach(item => {
+		if (typeof item.value === 'object') {
+			item.selected = state.selectedValue.filter(selected => selected.id === item.value.id).length > 0
+		} else {
+			item.selected = state.selectedValue.indexOf(item.value) > -1
 		}
 	})
-}
-const updateChildren = child => {
-	state.children.push(child)
-	checkStrategy()
-}
-const updateValue = value => {
-	let index = -1
-	
-	if (typeof value === 'object') {
-		state.selectedValue.forEach((item, key) => {
-			if(item.id === value.id) {
-				index = key
-			}
-		})
-	} else{
-		index = state.selectedValue.indexOf(value)
-	}
-	
-	if (state.selectedValue.length < props.max || props.max === -1) {
-		if (index === -1) {
-			state.selectedValue.push(value)
-		} else {
-			state.selectedValue.splice(index, 1)
-		}
-	} else {
-		index > -1 && state.selectedValue.splice(index, 1)
-	}
-}
-
-watch(() => state.selectedValue, val => {
-	checkStrategy()
-	emits('update:modelValue', val)
-	emits('change', val)
-},{deep: true})
-
-provide('checkboxGroup', {
-	...toRefs(props),
-	updateChildren,
-	updateValue
-})
+}
+const updateChildren = child => {
+	state.children.push(child)
+	checkStrategy()
+}
+const updateValue = value => {
+	let index = -1
+
+	if (typeof value === 'object') {
+		state.selectedValue.forEach((item, key) => {
+			if (item.id === value.id) {
+				index = key
+			}
+		})
+	} else {
+		index = state.selectedValue.indexOf(value)
+	}
+
+	if (state.selectedValue.length < props.max || props.max === -1) {
+		if (index === -1) {
+			state.selectedValue.push(value)
+		} else {
+			state.selectedValue.splice(index, 1)
+		}
+	} else {
+		index > -1 && state.selectedValue.splice(index, 1)
+	}
+}
+
+watch(
+	() => state.selectedValue,
+	val => {
+		checkStrategy()
+		emits('update:modelValue', val)
+		emits('change', val)
+	},
+	{ deep: true }
+)
+
+provide('checkboxGroup', {
+	...toRefs(props),
+	updateChildren,
+	updateValue
+})
 </script>
 
 <style lang="scss" scoped>
-.fs-checkbox-group{
-	&.inline{
-		display: flex;
-		flex-wrap: wrap;
-	}
+.fs-checkbox-group {
+	&.inline {
+		display: flex;
+		flex-wrap: wrap;
+	}
 }
 </style>

+ 92 - 71
components/fs-checkbox/fs-checkbox.vue

@@ -1,89 +1,110 @@
 <template>
-	<view 
-		class="fs-checkbox" 
-		:class="[justify,{'fs-checkbox-reverse':reverse, 'fs-checkbox-inline': inline}]" 
-		@click="handleToggle">
-		<fs-icon 
-			v-if="icon" 
-			source="out" 
-			:type="selected ? (selectIcon || icon) : icon" 
-			:colorType="selected ? checkedColorType : 'gray'" 
-			:size="iconSize"
-			:color="checkedColor">
-		</fs-icon>
-		<fs-icon 
-			v-else 
-			:type="selected ? 'icon-squarecheck' : 'icon-square'" 
+	<view
+		class="fs-checkbox"
+		:class="[justify, { 'fs-checkbox-reverse': reverse, 'fs-checkbox-inline': inline }]"
+		@click="handleToggle"
+	>
+		<fs-icon
+			v-if="icon"
+			source="out"
+			:type="selected ? selectIcon || icon : icon"
 			:colorType="selected ? checkedColorType : 'gray'"
-			:size="iconSize"
-			:color="checkedColor">
-		</fs-icon>
+			:size="iconSize"
+			:color="checkedColor"
+		></fs-icon>
+		<fs-icon
+			v-else
+			:type="selected ? 'icon-squarecheck' : 'icon-square'"
+			:colorType="selected ? checkedColorType : 'gray'"
+			:size="iconSize"
+			:color="checkedColor"
+		></fs-icon>
 		<view class="fs-checkbox-lable">
-			{{label}}
+			{{ label }}
 			<slot />
 		</view>
 	</view>
 </template>
 
-<script setup>
-import { inject, watch, toRefs, ref, computed } from 'vue'
-	
-const props = defineProps({
-	label: String,
-	icon: String,
-	selectIcon: String,
-	iconSize: {
-		type: String,
-		default: '40rpx'
-	},
-	value: {
-		type: null,
-		required: true
-	},
-	checkedColor: String,
-	checkedColorType: {
-		type: String,
-		default: 'primary'
-	}
-})
-
-const checkboxGroup = inject('checkboxGroup')
+<script>
+/**
+ * 多选框组件
+ * @description 多选框组件
+ * @property {String} label 文本
+ * @property {String} icon 自定义图标
+ * @property {String} iconSize 图标大小
+ * @property {String} selectIcon 自定义选中图标
+ * @property {null} value 标识符(必须传)
+ * @property {String} checkedColor 选中颜色
+ * @property {String} checkedColorType = [primary | danger | warning | info | success] 选中颜色类型
+ */
+export default {
+	name: 'fs-checkbox'
+}
+</script>
+
+<script setup>
+import { inject, watch, toRefs, ref, computed } from 'vue'
+
+const props = defineProps({
+	label: String,
+	icon: String,
+	selectIcon: String,
+	iconSize: {
+		type: String,
+		default: '40rpx'
+	},
+	value: {
+		type: null,
+		required: true
+	},
+	checkedColor: String,
+	checkedColorType: {
+		type: String,
+		default: 'primary',
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	}
+})
+
+const checkboxGroup = inject('checkboxGroup')
 const { reverse, inline, justify } = checkboxGroup
-
-let selected = ref(false)
-checkboxGroup.updateChildren({
-	selected,
-	value: props.value
-})
-
-const handleToggle = () => {
-	checkboxGroup.updateValue(props.value)
-}
+
+let selected = ref(false)
+checkboxGroup.updateChildren({
+	selected,
+	value: props.value
+})
+
+const handleToggle = () => {
+	checkboxGroup.updateValue(props.value)
+}
 </script>
 
 <style lang="scss" scoped>
 .fs-checkbox {
 	display: flex;
 	align-items: center;
-	justify-content: flex-start;
-	margin-bottom: 14rpx;
-	
-	&-lable {
-		margin-left: 6rpx;
-		margin-right: 20rpx;
-	}
-	
-	&-reverse {
-		flex-direction: row-reverse;
-		justify-content: flex-end;
-	}
-	&-reverse &-lable {
-		margin-left: 0;
-		margin-right: 6rpx;
-	}
-	
-	&.right {
-		justify-content: space-between;
+	justify-content: flex-start;
+	margin-bottom: 14rpx;
+
+	&-lable {
+		margin-left: 6rpx;
+		margin-right: 20rpx;
+	}
+
+	&-reverse {
+		flex-direction: row-reverse;
+		justify-content: flex-end;
+	}
+	&-reverse &-lable {
+		margin-left: 0;
+		margin-right: 6rpx;
+	}
+
+	&.right {
+		justify-content: space-between;
 	}
 }
 </style>

+ 40 - 31
components/fs-col/fs-col.vue

@@ -1,42 +1,51 @@
 <template>
-	<view class="fs-col" :class="['fs-col-' + span,{gutter}]" :style="styleStr">
-		<slot></slot>
-	</view>
+	<view class="fs-col" :class="['fs-col-' + span, { gutter }]" :style="styleStr"><slot></slot></view>
 </template>
 
-<script setup>
-import { computed, toRefs, inject } from 'vue'
-
-const props = defineProps({
-	span: {
-		type: [Number, String],
-		default: 12
-	},
-	gutter: Boolean
-})
-
-const rowGap = inject('rowGap')
-
-const styleStr = computed(() => {
-	const padding = parseInt(rowGap) / 2
-	return padding ? `padding-left: ${padding}rpx;padding-right: ${padding}rpx;` : ''
-})
+<script>
+/**
+ * 多选框组件
+ * @description 多选框组件
+ * @property {Number, String} span 列
+ * @property {Boolean} gutter 是否有下边距
+ */
+export default {
+	name: 'fs-col'
+}
+</script>
+
+<script setup>
+import { computed, toRefs, inject } from 'vue'
+
+const props = defineProps({
+	span: {
+		type: [Number, String],
+		default: 12
+	},
+	gutter: Boolean
+})
+
+const rowGap = inject('rowGap')
+
+const styleStr = computed(() => {
+	const padding = parseInt(rowGap) / 2
+	return padding ? `padding-left: ${padding}rpx;padding-right: ${padding}rpx;` : ''
+})
 </script>
 
-<style lang="scss" scoped>
-@use "sass:math";
-
-.fs-col {
-	float: left;
-	
-	&.gutter {
-		margin-bottom: var(--gutter-v);
-	}
+<style lang="scss" scoped>
+@use "sass:math";
+
+.fs-col {
+	float: left;
+
+	&.gutter {
+		margin-bottom: var(--gutter-v);
+	}
 }
 @for $i from 1 through 12 {
 	.fs-col-#{$i} {
 		width: math.div(100%, 12) * $i;
 	}
-}
-
+}
 </style>

+ 77 - 71
components/fs-collapse-item/fs-collapse-item.vue

@@ -1,97 +1,103 @@
 <template>
-	<view class="fs-collapse-item" :class="{'fs-collapse-item-border':border}">
-		<view class="fs-title-box" :class="[{open},position]" @click="handleClick">
-			<view class="fs-item-hd" :class="[highlight]">
-				<slot name="title"></slot>
+	<view class="fs-collapse-item" :class="{ 'fs-collapse-item-border': border }">
+		<view class="fs-title-box" :class="[{ open }, position]" @click="handleClick">
+			<view class="fs-item-hd" :class="[highlight]"><slot name="title"></slot></view>
+			<view class="fs-arrow-box" :class="[{ open }, highlight]">
+				<slot name="arrow"><view class="fs-arrow"></view></slot>
 			</view>
-			<view class="fs-arrow-box" :class="[{open},highlight]">
-				<slot name="arrow">
-					<view class="fs-arrow"></view>
-				</slot>
-			</view>
-		</view>
-		<view class="content" v-if="open">
-			<slot name="content"></slot>
 		</view>
+		<view class="content" v-if="open"><slot name="content"></slot></view>
 	</view>
 </template>
 
-<script setup>
-import { computed, inject, watch, ref } from 'vue'
-
-const props = defineProps({
-	name: [String, Number],
-	disabled: Boolean
-})
-const collapse = inject('collapse')
-
-const border = collapse.border
-const open = ref(collapse.allOpen || collapse.active === props.name)
-const position = collapse.position
-const highlight = computed(() => open.value && !props.disabled && collapse.activeType)
-
-const setActive = (active) => {
+<script>
+/**
+ * 折叠面板组件
+ * @description 折叠面板组件
+ * @property {String, Number} name name
+ * @property {Boolean} disabled disabled
+ */
+export default {
+	name: 'fs-collapse-item'
+}
+</script>
+
+<script setup>
+import { computed, inject, watch, ref } from 'vue'
+
+const props = defineProps({
+	name: [String, Number],
+	disabled: Boolean
+})
+const collapse = inject('collapse')
+
+const border = collapse.border
+const open = ref(collapse.allOpen || collapse.active === props.name)
+const position = collapse.position
+const highlight = computed(() => open.value && !props.disabled && collapse.activeType)
+
+const setActive = active => {
 	open.value = active && !props.disabled
-}
-
-collapse.children.push({
-	name: props.name,
-	open,
-	setActive,
-})
-
-const handleClick = () => {
-	!props.disabled && collapse.emitEvent(props.name)
+}
+
+collapse.children.push({
+	name: props.name,
+	open,
+	setActive
+})
+
+const handleClick = () => {
+	!props.disabled && collapse.emitEvent(props.name)
 }
 </script>
 
-<style lang="scss" scoped>
+<style lang="scss" scoped>
 .fs-collapse-item {
-	&-border{
-		border-bottom: 1px solid var(--border-color);
-	}
-	.open {
-		.fs-arrow {
-			transform: rotate(135deg);
-		}
+	&-border {
+		border-bottom: 1px solid var(--border-color);
+	}
+	.open {
+		.fs-arrow {
+			transform: rotate(135deg);
+		}
 	}
 }
 
 .fs-title-box {
 	display: flex;
-	padding: 20rpx var(--gutter);
-	justify-content: space-between;
-	align-items: center;
-	
-	.fs-item-hd{
-		min-width: 0;
-		flex: 1;
-	}
-	&.left{
-		flex-direction: row-reverse;
-		.fs-item-hd{
-			padding-left: 10rpx;
-		}
-	}
-	&.right{
-		.fs-item-hd{
-			padding-right: 10rpx;
-		}
-	}
-}
-
-.fs-arrow-box{
-	line-height: 1;
+	padding: 20rpx var(--gutter);
+	justify-content: space-between;
+	align-items: center;
+
+	.fs-item-hd {
+		min-width: 0;
+		flex: 1;
+	}
+	&.left {
+		flex-direction: row-reverse;
+		.fs-item-hd {
+			padding-left: 10rpx;
+		}
+	}
+	&.right {
+		.fs-item-hd {
+			padding-right: 10rpx;
+		}
+	}
+}
+
+.fs-arrow-box {
+	line-height: 1;
 }
 
-.fs-arrow {
+.fs-arrow {
 	border-top: 2rpx solid currentColor;
 	border-right: 2rpx solid currentColor;
 	transform: rotate(45deg);
 	width: 16rpx;
 	height: 16rpx;
 	color: inherit;
-	transition: all .1s;
-	flex-shrink: 0;
+	transition: all 0.1s;
+	flex-shrink: 0;
 }
 </style>

+ 85 - 62
components/fs-collapse/fs-collapse.vue

@@ -1,71 +1,94 @@
 <template>
-	<view class="fs-collapse">
-		<slot></slot>
-	</view>
+	<view class="fs-collapse"><slot></slot></view>
 </template>
 
-<script setup>
-import { computed, provide, watch, reactive, onMounted, getCurrentInstance } from 'vue'
-
-const props = defineProps({
-	accordion: Boolean,
-	active: {
-		type: [Number, String],
-		default: '0'
-	},
-	position: {
-		type: String,
-		default: 'right',
-		validator(value) {
-			return ['left', 'right'].includes(value)
-		}
-	},
-	rotate: {
-		type: [Number, String],
-		default: 90
-	},
-	activeType: String,
-	activeColor: String,
-	allOpen: Boolean,
-	border: {
-		type: Boolean,
-		default: true
-	}
-})
-const emits = defineEmits(['change'])
-const emitEvent = (name) => {
-	setActive(name)
-	emits('change', name)
-}
-
-const children = reactive([])
-const setActive = (name) => {
-	children.forEach(item => {
-		if (props.accordion) {
-			item.setActive(name === item.name ? !item.open : false)
-		} else {
-			if (name === item.name) {
-				item.setActive(!item.open)
-			}
-		}
-	})
-}
-provide('collapse', reactive({
-	...props,
-	children,
-	emitEvent
-}))
-watch(() => props.active, value => {
-	setActive(value)
-})
-
-defineExpose({
-	children
+<script>
+/**
+ * 折叠面板组件
+ * @description 折叠面板组件
+ * @property {Boolean} accordion 手风琴效果
+ * @property {Number, String} active 激活项name
+ * @property {String} position 箭头位置
+ * @property {Number, String} rotate 旋转
+ * @property {String} activeColor 高亮颜色
+ * @property {String} activeType 高亮颜色类型
+ * @property {Boolean} allOpen 是否全部展开
+ * @property {Boolean} border 是否显示边框
+ * @event {Function} change change事件
+ */
+export default {
+	name: 'fs-collapse'
+}
+</script>
+
+<script setup>
+import { computed, provide, watch, reactive, onMounted, getCurrentInstance } from 'vue'
+
+const props = defineProps({
+	accordion: Boolean,
+	active: {
+		type: [Number, String],
+		default: '0'
+	},
+	position: {
+		type: String,
+		default: 'right',
+		validator(value) {
+			return ['left', 'right'].includes(value)
+		}
+	},
+	rotate: {
+		type: [Number, String],
+		default: 90
+	},
+	activeType: String,
+	activeColor: String,
+	allOpen: Boolean,
+	border: {
+		type: Boolean,
+		default: true
+	}
+})
+const emits = defineEmits(['change'])
+const emitEvent = name => {
+	setActive(name)
+	emits('change', name)
+}
+
+const children = reactive([])
+const setActive = name => {
+	children.forEach(item => {
+		if (props.accordion) {
+			item.setActive(name === item.name ? !item.open : false)
+		} else {
+			if (name === item.name) {
+				item.setActive(!item.open)
+			}
+		}
+	})
+}
+provide(
+	'collapse',
+	reactive({
+		...props,
+		children,
+		emitEvent
+	})
+)
+watch(
+	() => props.active,
+	value => {
+		setActive(value)
+	}
+)
+
+defineExpose({
+	children
 })
 </script>
 
 <style lang="scss" scoped>
-.fs-collapse{
-	// background-color: #fff;
+.fs-collapse {
+	// background-color: #fff;
 }
 </style>

+ 230 - 226
components/fs-comment/fs-comment.vue

@@ -1,227 +1,231 @@
-<template>
-	<view>
-		<fs-cell-group full border :radius="false">
-			<fs-cell align="top" v-for="(item,index) in list" :key="item.id">
-				<template #title>
-					<fs-avatar :src="item[keyMap.avatar]"></fs-avatar>
-				</template>
-				<template #value>
-					<view class="fs-comment-hd">
-						<view class="fs-comment-hd-item fs-comment-hd-title">{{item[keyMap.name]}}</view>
-						<view class="fs-comment-hd-item">{{item[keyMap.time]}}</view>
-					</view>
-					<view class="fs-comment-content">
-						{{item[keyMap.content]}}
-					</view>
-					<view class="fs-comment-reply">
-						<view class="fs-comment-reply-item" @click="handleLike(item)" v-if="showLike">
-							<fs-icon type="icon-like" :color="item[keyMap.isLike] ? 'red' : ''"></fs-icon>
-							<view v-if="!item[keyMap.isLike]">点赞</view>
-						</view>
-						<view class="fs-comment-reply-item" @click="initInput(item)">
-							<fs-icon type="icon-comment"></fs-icon> 回复
-						</view>
-					</view>
-					<view class="fs-comment-reply-box">
-						<fs-cell-group border bgColor="transparent" full>
-							<fs-cell align="top" v-for="reply in item[keyMap.replyList]" :key="reply.id">
-								<template #title>
-									<fs-avatar :src="reply[keyMap.avatar]"></fs-avatar>
-								</template>
-								<template #value>
-									<view class="fs-comment-hd fs-comment-hd-title">
-										<view class="fs-comment-hd-item">{{reply[keyMap.name]}}</view>
-										<view class="fs-comment-hd-item">{{reply[keyMap.time]}}</view>
-									</view>
-									<view class="fs-comment-content">
-										{{reply[keyMap.content]}}
-									</view>
-									<view class="fs-comment-reply">
-										<view class="fs-comment-reply-item" @click="handleLike(reply)" v-if="showLike">
-											<fs-icon type="icon-like" :color="reply[keyMap.isLike] ? 'red' : ''"></fs-icon>
-											<view v-if="!reply[keyMap.isLike]">点赞</view>
-										</view>
-										<view class="fs-comment-reply-item" @click="initInput(reply)">
-											<fs-icon type="icon-comment"></fs-icon> 回复
-										</view>
-									</view>
-								</template>
-							</fs-cell>
-						</fs-cell-group>
-					</view>
-				</template>
-			</fs-cell>
-		</fs-cell-group>
-		
-		<view class="fs-comment-input-box" v-if="showInput">
-			<input 
-				type="text"  
-				class="fs-comment-input" 
-				placeholder="回复..."
-				v-model="replyValue"
-				:focus="focus"
-				@confirm="handleConfirm"/>
-			<view
-				class="fs-comment-button" 
-				@click="handleConfirm">
-				发送
-			</view>
-			<!-- :style="{backgroundColor: replyValue ? '#08bf65' : '#f7f7f7',color: replyValue ? '#fff' : '#cecece'}" -->
-		</view>
-	</view>
-</template>
-
-<script>
-export default {
-	name: 'fs-comment'
-}
-</script>
-
-<script setup>
-import { ref, nextTick } from 'vue'
-
-const props = defineProps({
-	list: {
-		type: Array,
-		default() {
-			return []
-		}
-	},
-	showLike: Boolean,
-	keyMap: {
-		type: Object,
-		default() {
-			return {
-				avatar: 'avatar',
-				name: 'name',
-				time: 'time',
-				content: 'content',
-				replyList: 'children',
-				isLike: 'isLike'
-			}
-		}
-	}
-})
-const emits = defineEmits(['like', 'reply'])
-
-let focus = ref(false)
-let showInput = ref(false)
-let reply = ref(null)
-let replyValue = ref('')
-const initInput = (item) => {
-	showInput.value = true
-	focus.value = true
-	reply.value = item
-	replyValue.value = ''
-}
-const handleBlur = () => {
-	nextTick(() => {
-		showInput.value = false
-		focus.value = false
-	})
-}
-const handleConfirm = () => {
-	if (replyValue.value) {
-		emits('reply', reply.value)
-	}
-	showInput.value = false
-	focus.value = false
-}
-
-const handleLike = (item) => {
-	item[props.keyMap.isLike] = !item[props.keyMap.isLike]
-	emits('like', item)
-}
-</script>
-
-<style lang="scss">
-.fs-comment{
-	font-size: 14px;
-	
-	&-hd{
-		display: flex;
-		
-		&-title{
-			color: #222;
-		}
-		
-		&-item{
-			padding-right: 30rpx;
-			position: relative;
-			margin-bottom: 10rpx;
-			
-			& + &{
-				padding-left: 30rpx;
-				
-				&::before{
-					position: absolute;
-					left: 0;
-					top: 50%;
-					transform: translateY(-50%);
-					width: 2rpx;
-					height: 24rpx;
-					background-color: #D9D9D9;
-					content: '';
-				}
-			}
-		}
-	}
-	
-	&-content{
-		font-size: 14px;
-	}
-	
-	&-reply-box{
-		background-color: #f7f8fa;
-		margin-top: 10rpx;
-		border-radius: var(--radius);
-		overflow: hidden;
-	}
-	
-	&-reply{
-		display: flex;
-		font-size: 14px;
-		margin-top: 10rpx;
-		align-items: center;
-		
-		&-item{
-			display: flex;
-			align-items: center;
-			margin-right: 30rpx;
-		}
-	}
-	
-	&-input-box{
-		height: 110rpx;
-		position: fixed;
-		display: flex;
-		left: 0;
-		right: 0;
-		bottom: 0;
-		padding: 20rpx;
-		border-top: 1rpx solid #eaeaea;
-		background-color: #fff;
-		align-items: center;
-		z-index: 10;
-	}
-	&-input{
-		height: 100%;
-		margin-right: 20rpx;
-		flex: 1;
-		background-color: #fff;
-		border-radius: 8rpx;
-		padding-left: 20rpx;
-	}
-	
-	&-button {
-		height: 100%;
-		padding: 10rpx 20rpx;
-		border-radius: 8rpx;
-		box-sizing: border-box;
-		display: flex;
-		align-items: center;
-		background-color: #08bf65;
-		color: #fff;
-	}
-}
+<template>
+	<view>
+		<fs-cell-group full border :radius="false">
+			<fs-cell align="top" v-for="(item, index) in list" :key="item.id">
+				<template #title>
+					<fs-avatar :src="item[keyMap.avatar]"></fs-avatar>
+				</template>
+				<template #value>
+					<view class="fs-comment-hd">
+						<view class="fs-comment-hd-item fs-comment-hd-title">{{ item[keyMap.name] }}</view>
+						<view class="fs-comment-hd-item">{{ item[keyMap.time] }}</view>
+					</view>
+					<view class="fs-comment-content">{{ item[keyMap.content] }}</view>
+					<view class="fs-comment-reply">
+						<view class="fs-comment-reply-item" @click="handleLike(item)" v-if="showLike">
+							<fs-icon type="icon-like" :color="item[keyMap.isLike] ? 'red' : ''"></fs-icon>
+							<view v-if="!item[keyMap.isLike]">点赞</view>
+						</view>
+						<view class="fs-comment-reply-item" @click="initInput(item)">
+							<fs-icon type="icon-comment"></fs-icon>
+							回复
+						</view>
+					</view>
+					<view class="fs-comment-reply-box">
+						<fs-cell-group border bgColor="transparent" full>
+							<fs-cell align="top" v-for="reply in item[keyMap.replyList]" :key="reply.id">
+								<template #title>
+									<fs-avatar :src="reply[keyMap.avatar]"></fs-avatar>
+								</template>
+								<template #value>
+									<view class="fs-comment-hd fs-comment-hd-title">
+										<view class="fs-comment-hd-item">{{ reply[keyMap.name] }}</view>
+										<view class="fs-comment-hd-item">{{ reply[keyMap.time] }}</view>
+									</view>
+									<view class="fs-comment-content">{{ reply[keyMap.content] }}</view>
+									<view class="fs-comment-reply">
+										<view class="fs-comment-reply-item" @click="handleLike(reply)" v-if="showLike">
+											<fs-icon type="icon-like" :color="reply[keyMap.isLike] ? 'red' : ''"></fs-icon>
+											<view v-if="!reply[keyMap.isLike]">点赞</view>
+										</view>
+										<view class="fs-comment-reply-item" @click="initInput(reply)">
+											<fs-icon type="icon-comment"></fs-icon>
+											回复
+										</view>
+									</view>
+								</template>
+							</fs-cell>
+						</fs-cell-group>
+					</view>
+				</template>
+			</fs-cell>
+		</fs-cell-group>
+
+		<view class="fs-comment-input-box" v-if="showInput">
+			<input
+				type="text"
+				class="fs-comment-input"
+				placeholder="回复..."
+				v-model="replyValue"
+				:focus="focus"
+				@confirm="handleConfirm"
+			/>
+			<view class="fs-comment-button" @click="handleConfirm">发送</view>
+			<!-- :style="{backgroundColor: replyValue ? '#08bf65' : '#f7f7f7',color: replyValue ? '#fff' : '#cecece'}" -->
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 评论组件
+ * @description 评论组件
+ * @property {Array} list 手风琴效果
+ * @property {Boolean} showLike 激活项name
+ * @property {Object} keyMap 箭头位置
+ * @event {Function} like 点赞事件
+ * @event {Function} reply 发送事件
+ */
+export default {
+	name: 'fs-comment'
+}
+</script>
+
+<script setup>
+import { ref, nextTick } from 'vue'
+
+const props = defineProps({
+	list: {
+		type: Array,
+		default() {
+			return []
+		}
+	},
+	showLike: Boolean,
+	keyMap: {
+		type: Object,
+		default() {
+			return {
+				avatar: 'avatar',
+				name: 'name',
+				time: 'time',
+				content: 'content',
+				replyList: 'children',
+				isLike: 'isLike'
+			}
+		}
+	}
+})
+const emits = defineEmits(['like', 'reply'])
+
+let focus = ref(false)
+let showInput = ref(false)
+let reply = ref(null)
+let replyValue = ref('')
+const initInput = item => {
+	showInput.value = true
+	focus.value = true
+	reply.value = item
+	replyValue.value = ''
+}
+const handleBlur = () => {
+	nextTick(() => {
+		showInput.value = false
+		focus.value = false
+	})
+}
+const handleConfirm = () => {
+	if (replyValue.value) {
+		emits('reply', reply.value)
+	}
+	showInput.value = false
+	focus.value = false
+}
+
+const handleLike = item => {
+	item[props.keyMap.isLike] = !item[props.keyMap.isLike]
+	emits('like', item)
+}
+</script>
+
+<style lang="scss">
+.fs-comment {
+	font-size: 14px;
+
+	&-hd {
+		display: flex;
+
+		&-title {
+			color: #222;
+		}
+
+		&-item {
+			padding-right: 30rpx;
+			position: relative;
+			margin-bottom: 10rpx;
+
+			& + & {
+				padding-left: 30rpx;
+
+				&::before {
+					position: absolute;
+					left: 0;
+					top: 50%;
+					transform: translateY(-50%);
+					width: 2rpx;
+					height: 24rpx;
+					background-color: #d9d9d9;
+					content: '';
+				}
+			}
+		}
+	}
+
+	&-content {
+		font-size: 14px;
+	}
+
+	&-reply-box {
+		background-color: #f7f8fa;
+		margin-top: 10rpx;
+		border-radius: var(--radius);
+		overflow: hidden;
+	}
+
+	&-reply {
+		display: flex;
+		font-size: 14px;
+		margin-top: 10rpx;
+		align-items: center;
+
+		&-item {
+			display: flex;
+			align-items: center;
+			margin-right: 30rpx;
+		}
+	}
+
+	&-input-box {
+		height: 110rpx;
+		position: fixed;
+		display: flex;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		padding: 20rpx;
+		border-top: 1rpx solid #eaeaea;
+		background-color: #fff;
+		align-items: center;
+		z-index: 10;
+	}
+	&-input {
+		height: 100%;
+		margin-right: 20rpx;
+		flex: 1;
+		background-color: #fff;
+		border-radius: 8rpx;
+		padding-left: 20rpx;
+	}
+
+	&-button {
+		height: 100%;
+		padding: 10rpx 20rpx;
+		border-radius: 8rpx;
+		box-sizing: border-box;
+		display: flex;
+		align-items: center;
+		background-color: #08bf65;
+		color: #fff;
+	}
+}
 </style>

+ 35 - 25
components/fs-container/fs-container.vue

@@ -1,48 +1,58 @@
 <template>
 	<view class="fs-container">
-		<view class="fs-container-header">
-			<slot name="header"></slot>
-		</view>
-		<view class="fs-container-main" :style="{paddingTop: top + 'px',paddingBottom: bottom + 'px'}">
+		<view class="fs-container-header"><slot name="header"></slot></view>
+		<view class="fs-container-main" :style="{ paddingTop: top + 'px', paddingBottom: bottom + 'px' }">
 			<slot></slot>
 		</view>
-		<view class="fs-container-footer">
-			<slot name="footer"></slot>
-		</view>
+		<view class="fs-container-footer"><slot name="footer"></slot></view>
 	</view>
 </template>
 
 <script>
-	export default {
-		name:"fs-container",
-		data() {
-			return {
-				top: 0,
-				bottom: 0
-			};
-		},
-		mounted() {
-			uni.createSelectorQuery().in(this).select('.fs-container-header').boundingClientRect(data => {
+/**
+ * 容器组件
+ * @description 容器组件
+ */
+export default {
+	name: 'fs-container',
+	data() {
+		return {
+			top: 0,
+			bottom: 0
+		}
+	},
+	mounted() {
+		uni
+			.createSelectorQuery()
+			.in(this)
+			.select('.fs-container-header')
+			.boundingClientRect(data => {
 				this.top = data.height
-			}).exec()
-			uni.createSelectorQuery().in(this).select('.fs-container-footer').boundingClientRect(data => {
+			})
+			.exec()
+		uni
+			.createSelectorQuery()
+			.in(this)
+			.select('.fs-container-footer')
+			.boundingClientRect(data => {
 				this.bottom = data.height
-			}).exec()
-		}
+			})
+			.exec()
 	}
+}
 </script>
 
 <style lang="scss" scoped>
-.fs-container{
-	&-header{
+.fs-container {
+	&-header {
 		position: fixed;
 		top: var(--window-top);
 		left: 0;
 		right: 0;
 		z-index: 100;
 	}
-	
-	&-footer{
+
+	&-footer {
 		position: fixed;
 		left: 0;
 		right: 0;

+ 37 - 29
components/fs-date-format/fs-date-format.vue

@@ -1,29 +1,37 @@
-<template>
-	<view>
-		{{dateFormat}}
-	</view>
-</template>
-
-<script setup>
-import { computed } from 'vue'
-import dayjs from 'dayjs'
-
-const props = defineProps({
-	date: [String, Object],
-	format: {
-		type: String,
-		default: 'YYYY-MM-DD'
-	}
-})
-const emits = defineEmits(['click'])
-
-const dateFormat = computed(() => dayjs(props.date).format(props.format))
-
-const handleClick = () => {
-	emits('click')
-}
-</script>
-
-<style>
-
-</style>
+<template>
+	<view>{{ dateFormat }}</view>
+</template>
+
+<script>
+/**
+ * 日期格式化组件
+ * @description 日期格式化组件
+ * @property {String, Object} date 日期
+ * @property {String} format 格式化
+ */
+export default {
+	name: 'fs-date-format'
+}
+</script>
+
+<script setup>
+import { computed } from 'vue'
+import dayjs from 'dayjs'
+
+const props = defineProps({
+	date: [String, Object],
+	format: {
+		type: String,
+		default: 'YYYY-MM-DD'
+	}
+})
+const emits = defineEmits(['click'])
+
+const dateFormat = computed(() => dayjs(props.date).format(props.format))
+
+const handleClick = () => {
+	emits('click')
+}
+</script>
+
+<style></style>

+ 20 - 10
components/fs-divide-list/fs-divide-list.vue

@@ -1,15 +1,25 @@
 <template>
-	<view class="fs-divide-list" :class="{'gutter-v': gutter}">
+	<view class="fs-divide-list" :class="{ 'gutter-v': gutter }">
 		<fs-grid :columnNum="list.length" :padding="false">
 			<view v-for="(item, index) in list" :key="index" class="fs-divide-list-item">
-				<fs-grid-item>
-					<slot :item="item"></slot>
-				</fs-grid-item>
+				<fs-grid-item><slot :item="item"></slot></fs-grid-item>
 			</view>
 		</fs-grid>
 	</view>
 </template>
 
+<script>
+/**
+ * 分割列表组件
+ * @description 分割列表组件
+ * @property {Array} list 分割列表
+ * @property {Boolean} gutter 下边距
+ */
+export default {
+	name: 'fs-divide-list'
+}
+</script>
+
 <script setup>
 const props = defineProps({
 	list: {
@@ -21,22 +31,22 @@ const props = defineProps({
 </script>
 
 <style lang="scss" scoped>
-.fs-divide-list{
+.fs-divide-list {
 	background-color: #fff;
 	border-radius: var(--radius);
-	
-	&-item{
+
+	&-item {
 		position: relative;
 		padding: 20rpx;
-		& + &{
-			&::before{
+		& + & {
+			&::before {
 				position: absolute;
 				left: 0;
 				top: 50%;
 				transform: translateY(-50%);
 				width: 2rpx;
 				height: 43rpx;
-				background-color: #D9D9D9;
+				background-color: #d9d9d9;
 				content: '';
 			}
 		}

+ 29 - 23
components/fs-divider/fs-divider.vue

@@ -1,24 +1,31 @@
 <template>
 	<view class="fs-divider">
-		<view 
-			class="fs-divider-line"
-			:class="['bg-' + (lineColorType || colorType)]"
-			:style="lineStyle">
-		</view>
-		<view 
-			class="fs-divider-content" 
-			:class="textColorType || colorType"
-			:style="{color: textColor || color}">
-				<slot></slot>
-		</view>
-		<view
-			class="fs-divider-line"
-			:class="['bg-' + (lineColorType || colorType)]"
-			:style="lineStyle">
+		<view class="fs-divider-line" :class="['bg-' + (lineColorType || colorType)]" :style="lineStyle"></view>
+		<view class="fs-divider-content" :class="textColorType || colorType" :style="{ color: textColor || color }">
+			<slot></slot>
 		</view>
+		<view class="fs-divider-line" :class="['bg-' + (lineColorType || colorType)]" :style="lineStyle"></view>
 	</view>
 </template>
 
+<script>
+/**
+ * 分割线组件
+ * @description 分割线组件
+ * @property {String} lineWidth 分割线宽度
+ * @property {String} lineHeight 分割线高度
+ * @property {String} lineColor 分割线颜色(优先级高于color)
+ * @property {String} lineColorType = [primary | danger | warning | info | success] 分割线颜色类型(优先级高于colorType)
+ * @property {String} textColor 文字颜色(优先级高于color)
+ * @property {String} textColorType = [primary | danger | warning | info | success] 文字颜色类型(优先级高于color)
+ * @property {String} color 分割线、文字颜色
+ * @property {String} colorType = [primary | danger | warning | info | success] 分割线、文字颜色类型
+ */
+export default {
+	name: 'fs-divider'
+}
+</script>
+
 <script setup>
 import { computed } from 'vue'
 
@@ -35,14 +42,14 @@ const props = defineProps({
 	lineColorType: {
 		type: String,
 		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger','default'].includes(value)
+			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
 		}
 	},
 	textColor: String,
 	textColorType: {
 		type: String,
 		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger','default'].includes(value)
+			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
 		}
 	},
 	color: String,
@@ -50,9 +57,9 @@ const props = defineProps({
 		type: String,
 		default: 'default',
 		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger','default'].includes(value)
+			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
 		}
-	},
+	}
 })
 
 const lineStyle = computed(() => {
@@ -65,16 +72,15 @@ const lineStyle = computed(() => {
 </script>
 
 <style lang="scss" scoped>
-.fs-divider{
+.fs-divider {
 	display: flex;
 	align-items: center;
 	justify-content: center;
 	padding: 20rpx;
-	
-	&-content{
+
+	&-content {
 		padding: 0 20rpx;
 		white-space: nowrap;
 	}
-	
 }
 </style>

+ 30 - 21
components/fs-dropdown-item/fs-dropdown-item.vue

@@ -1,22 +1,31 @@
 <template>
 	<view class="fs-dropdown-item">
 		<view class="fs-dropdown-item-title" @click="handleToggle">
-			<view class="fs-dropdown-item-text">{{title}}</view>
-			<fs-icon class="fs-dropdown-item-icon" type="icon-sort-down" size="26rpx" :class="{visible}"></fs-icon>
-		</view>
-		
-		<view class="fs-dropdown-item-content" :class="{visible}">
-			<slot></slot>
+			<view class="fs-dropdown-item-text">{{ title }}</view>
+			<fs-icon class="fs-dropdown-item-icon" type="icon-sort-down" size="26rpx" :class="{ visible }"></fs-icon>
 		</view>
+
+		<view class="fs-dropdown-item-content" :class="{ visible }"><slot></slot></view>
 	</view>
 	<fs-mask v-model="visible" :z-index="99"></fs-mask>
 </template>
 
+<script>
+/**
+ * 下拉组件
+ * @description 下拉组件
+ * @property {String} title 标题
+ */
+export default {
+	name: 'fs-dropdown-item'
+}
+</script>
+
 <script setup>
 import { ref, inject } from 'vue'
 
 const props = defineProps({
-	title: String,
+	title: String
 })
 
 const visible = ref(false)
@@ -26,7 +35,7 @@ const updateState = () => {
 	if (flag) {
 		flag = false
 		visible.value = !visible.value
-	} else{
+	} else {
 		visible.value = false
 	}
 }
@@ -43,13 +52,13 @@ const handleToggle = () => {
 </script>
 
 <style lang="scss" scoped>
-.fs-dropdown-item{
+.fs-dropdown-item {
 	flex: 1;
 	width: 100%;
 	height: 80rpx;
 	background-color: #fff;
-	
-	&-title{
+
+	&-title {
 		display: flex;
 		justify-content: center;
 		align-items: center;
@@ -59,32 +68,32 @@ const handleToggle = () => {
 		width: 100%;
 		height: 100%;
 		background-color: #fff;
-		
-		.visible{
+
+		.visible {
 			transform: rotate(180deg);
 			transform-origin: center center;
 		}
 	}
-	&-text{
+	&-text {
 		margin-right: 10rpx;
 	}
-	&-icon{
-		transition: all .2s;
+	&-icon {
+		transition: all 0.2s;
 	}
-	
-	&-content{
+
+	&-content {
 		position: absolute;
 		left: 0;
 		right: 0;
 		top: 80rpx;
 		transform: scaleY(0);
 		transform-origin: left top;
-		transition: all .1s;
+		transition: all 0.1s;
 		z-index: 100;
 		background-color: #fff;
 		opacity: 0;
-		
-		&.visible{
+
+		&.visible {
 			transform: scaleY(1);
 			opacity: 1;
 		}

+ 52 - 38
components/fs-empty/fs-empty.vue

@@ -1,39 +1,53 @@
-<template>
-	<view class="fs-empty-box" :style="{padding: padding}">
-		<image :src="src" mode="widthFix" :style="{width: imageWidth}"></image>
-		<view class="content">{{text}}</view>
-	</view>
-</template>
-
-<script setup>
-import empty from './empty.png'
-
-const props = defineProps({
-	src: {
-		type: String,
-		default: empty
-	},
-	text: {
-		type: String,
-		default: '暂无数据'
-	},
-	padding: {
-		type: String,
-		default: '200rpx 30rpx'
-	},
-	imageWidth: {
-		type: String,
-		default: '400rpx'
-	}
-})
-</script>
-
-<style lang="scss" scoped>
-.fs-empty-box{
-	text-align: center;
-	
-	.content{
-		margin-top: 20rpx;
-	}
-}
+<template>
+	<view class="fs-empty-box" :style="{ padding: padding }">
+		<image :src="src" mode="widthFix" :style="{ width: imageWidth }"></image>
+		<view class="content">{{ text }}</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 无数据组件
+ * @description 无数据组件
+ * @property {String} src 图片地址
+ * @property {String} text 文字
+ * @property {String} padding 内边距
+ * @property {String} imageWidth 图片宽度
+ */
+export default {
+	name: 'fs-empty'
+}
+</script>
+
+<script setup>
+import empty from './empty.png'
+
+const props = defineProps({
+	src: {
+		type: String,
+		default: empty
+	},
+	text: {
+		type: String,
+		default: '暂无数据'
+	},
+	padding: {
+		type: String,
+		default: '200rpx 30rpx'
+	},
+	imageWidth: {
+		type: String,
+		default: '400rpx'
+	}
+})
+</script>
+
+<style lang="scss" scoped>
+.fs-empty-box {
+	text-align: center;
+
+	.content {
+		margin-top: 20rpx;
+	}
+}
 </style>

+ 137 - 114
components/fs-fab/fs-fab.vue

@@ -1,115 +1,138 @@
-<template>
-	<view class="fs-fab">
-		<view class="fs-fab-btn" :style="{right, bottom}">
-			<view class="fs-fab-option" :class="{'fs-fab-scale': visible}">
-				<slot name="option">
-					<fs-avatar
-						class="fs-fab-option-gutter"
-						v-for="(item,index) in options"
-						:key="index"
-						:size="size"
-						:bgColor="item.bgColor"
-						:bgColorType="item.bgColorType"
-						@click="handleOption(item)">
-						{{item.name}}
-					</fs-avatar>
-				</slot>
-			</view>
-			
-			<fs-avatar :size="size" bgColorType="danger" @click="handleToggle" v-if="visible">
-				<fs-icon type="icon-close" :size="iconSize"></fs-icon>
-			</fs-avatar>
-			<fs-avatar :size="size" :bgColor="bgColor" @click="handleToggle" v-else>
-				<slot name="icon">
-					<fs-icon type="icon-plus" :size="iconSize" class="fs-fab-plus" :class="{'fs-fab-visible':visible}"></fs-icon>
-				</slot>
-			</fs-avatar>
-		</view>
-		
-		<fs-mask v-if="showMask" v-model="visible"></fs-mask>
-	</view>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-
-const props = defineProps({
-	right: {
-		type: String,
-		default: '40rpx'
-	},
-	bottom: {
-		type: String,
-		default: '40rpx'
-	},
-	options: {
-		type: Array,
-		default: () => []
-	},
-	size: {
-		type: String,
-		default: '120rpx'
-	},
-	iconSize: {
-		type: String,
-		default: '50rpx'
-	},
-	bgColor: String,
-	showMask: Boolean
-})
-
-const emits = defineEmits(['clickOption'])
-
-const visible = ref(false)
-
-const handleToggle = () => {
-	visible.value = !visible.value
-}
-const handleOption = item => {
-	close()
-	emits('clickOption',item)
-}
-const close = () => {
-	visible.value = false
-}
-
-defineExpose({
-	close
-})
-</script>
-
-<style lang="scss" scoped>
-.fs-fab{
-	&-btn{
-		position: fixed;
-		margin-bottom: var(--window-bottom);
-		z-index: 900;
-	}
-	
-	&-plus{
-		transition: all .2s;
-	}
-	&-visible{
-		transform: rotate(315deg);
-	}
-	
-	&-option{
-		display: flex;
-		flex-direction: column;
-		margin-bottom: 30rpx;
-		transition: all .2s;
-		transform: translateY(-50%);
-		opacity: 0;
-		z-index: -1;
-		
-		&-gutter{
-			margin-top: 20rpx;
-		}
-	}
-	&-scale{
-		transform: translateY(0);
-		opacity: 1;
-		z-index: 900;
-	}
-}
+<template>
+	<view class="fs-fab">
+		<view class="fs-fab-btn" :style="{ right, bottom }">
+			<view class="fs-fab-option" :class="{ 'fs-fab-scale': visible }">
+				<slot name="option">
+					<fs-avatar
+						class="fs-fab-option-gutter"
+						v-for="(item, index) in options"
+						:key="index"
+						:size="size"
+						:bgColor="item.bgColor"
+						:bgColorType="item.bgColorType"
+						@click="handleOption(item)"
+					>
+						{{ item.name }}
+					</fs-avatar>
+				</slot>
+			</view>
+
+			<fs-avatar :size="size" bgColorType="danger" @click="handleToggle" v-if="visible">
+				<fs-icon type="icon-close" :size="iconSize"></fs-icon>
+			</fs-avatar>
+			<fs-avatar :size="size" :bgColor="bgColor" @click="handleToggle" v-else>
+				<slot name="icon">
+					<fs-icon
+						type="icon-plus"
+						:size="iconSize"
+						class="fs-fab-plus"
+						:class="{ 'fs-fab-visible': visible }"
+					></fs-icon>
+				</slot>
+			</fs-avatar>
+		</view>
+
+		<fs-mask v-if="showMask" v-model="visible"></fs-mask>
+	</view>
+</template>
+
+<script>
+/**
+ * 悬浮按钮组件
+ * @description 悬浮按钮组件
+ * @property {String} right 按钮右边距
+ * @property {String} bottom 按钮下边距
+ * @property {Array} options 选项
+ * @property {String} size 按钮大小
+ * @property {String} iconSize 图标大小
+ * @property {String} bgColor 背景色
+ * @property {Boolean} showMask 是否显示遮罩
+ */
+export default {
+	name: 'fs-fab'
+}
+</script>
+
+<script setup>
+import { ref } from 'vue'
+
+const props = defineProps({
+	right: {
+		type: String,
+		default: '40rpx'
+	},
+	bottom: {
+		type: String,
+		default: '40rpx'
+	},
+	options: {
+		type: Array,
+		default: () => []
+	},
+	size: {
+		type: String,
+		default: '120rpx'
+	},
+	iconSize: {
+		type: String,
+		default: '50rpx'
+	},
+	bgColor: String,
+	showMask: Boolean
+})
+
+const emits = defineEmits(['clickOption'])
+
+const visible = ref(false)
+
+const handleToggle = () => {
+	visible.value = !visible.value
+}
+const handleOption = item => {
+	close()
+	emits('clickOption', item)
+}
+const close = () => {
+	visible.value = false
+}
+
+defineExpose({
+	close
+})
+</script>
+
+<style lang="scss" scoped>
+.fs-fab {
+	&-btn {
+		position: fixed;
+		margin-bottom: var(--window-bottom);
+		z-index: 900;
+	}
+
+	&-plus {
+		transition: all 0.2s;
+	}
+	&-visible {
+		transform: rotate(315deg);
+	}
+
+	&-option {
+		display: flex;
+		flex-direction: column;
+		margin-bottom: 30rpx;
+		transition: all 0.2s;
+		transform: translateY(-50%);
+		opacity: 0;
+		z-index: -1;
+
+		&-gutter {
+			margin-top: 20rpx;
+		}
+	}
+	&-scale {
+		transform: translateY(0);
+		opacity: 1;
+		z-index: 900;
+	}
+}
 </style>

+ 25 - 1
components/fs-field/fs-field.vue

@@ -69,6 +69,31 @@
 	</view>
 </template>
 
+<script>
+/**
+ * 输入框组件
+ * @description 输入框组件
+ * @property {String} placeholder 占位符
+ * @property {String} type 输入框类型
+ * @property {Number, String} maxlength 输入的最大字符数
+ * @property {Boolean} disabled 是否禁用
+ * @property {Boolean} border 是否显示边框
+ * @property {Boolean} clearable 是否启用清除图标,点击清除图标后会清空输入框
+ * @property {Boolean} autoHeight 仅对type=textarea有效
+ * @property {Boolean} tighten 是否紧凑
+ * @property {Boolean} fixed 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
+ * @property {Boolean} round 是否圆角
+ * @property {String} height 输入框高度
+ * @property {String} bgColor 背景颜色
+ * @event {Function} focus 输入框聚焦事件
+ * @event {Function} blur 输入框失去焦点事件
+ * @event {Function} confirm 点击完成事件
+ */
+export default {
+	name: 'fs-field'
+}
+</script>
+
 <script setup>
 import { inject, computed, useSlots } from 'vue'
 
@@ -88,7 +113,6 @@ const props = defineProps({
 	border: Boolean,
 	clearable: Boolean,
 	autoHeight: Boolean,
-	required: Boolean,
 	tighten: Boolean,
 	fixed: Boolean,
 	round: Boolean,

+ 114 - 99
components/fs-form-item/fs-form-item.vue

@@ -1,100 +1,115 @@
-<template>
-	<view class="fs-form-item" :class="['fs-form-item-' + position, {'fs-form-item-border': border}]">
-		<view class="fs-form-item-label-before" v-if="slots.before">
-			<slot name="before"></slot>
-		</view>
-		<view 
-			class="fs-form-item-label" 
-			:class="['text-' + align, { 'fs-form-item-required': required }]" 
-			:style="{width: width}" 
-			v-if="label">
-			{{label}}
-		</view>
-		<view class="fs-form-item-right"><slot></slot></view>
-	</view>
-</template>
-
-<script setup>
-import { inject, provide, computed, useSlots } from 'vue'
-
-const props = defineProps({
-	label: String,
-	labelWidth: String,
-	labelPosition: {
-		type: String,
-		validator(value) {
-			return ['left', 'top'].includes(value)
-		}
-	},
-	labelAlign: {
-		type: String,
-		validator(value) {
-			return ['left', 'center', 'right', 'justify'].includes(value)
-		}
-	},
-	required: Boolean,
-	border: {
-		type: Boolean,
-		default: true
-	}
-})
-const slots = useSlots()
-
-const form = inject('form', {})
-const position = props.labelPosition || form.labelPosition || 'left'
-const width = props.labelWidth || form.labelWidth || '120rpx'
-const align = props.labelAlign || form.labelAlign || 'left'
-
-provide('form-item-position', position)
-</script>
-
-<style lang="scss" scoped>
-.fs-form-item{
-	display: flex;
-	background-color: #fff;
-	padding: 20rpx var(--gutter);
-	min-height: 110rpx;
-	
-	&-border{
-		border-bottom: 2rpx solid var(--border-color);
-	}
-	
-	&-left{
-		align-items: center;
-		// .label{
-		// 	text-align-last: justify;
-		// }
-	}
-	&-top{
-		flex-direction: column;
-		.form-item-label{
-			margin-bottom: 10rpx;
-			width: auto !important;
-		}
-	}
-	
-	&-label{
-		font-size: var(--content-size);
-		margin-right: 20rpx;
-		
-		&-before{
-			margin-right: 20rpx;
-		}
-	}
-	&-required{
-		position: relative;
-		padding-left: 14rpx;
-		
-		&::before{
-			position: absolute;
-			content: '*';
-			color: red;
-			left: 0;
-		}
-	}
-	
-	&-right{
-		flex: 1;
-	}
-}
+<template>
+	<view class="fs-form-item" :class="['fs-form-item-' + position, { 'fs-form-item-border': border }]">
+		<view class="fs-form-item-label-before" v-if="slots.before"><slot name="before"></slot></view>
+		<view
+			class="fs-form-item-label"
+			:class="['text-' + align, { 'fs-form-item-required': required }]"
+			:style="{ width: width }"
+			v-if="label"
+		>
+			{{ label }}
+		</view>
+		<view class="fs-form-item-right"><slot></slot></view>
+	</view>
+</template>
+
+<script>
+/**
+ * 表单项组件
+ * @description 表单项组件
+ * @property {String} label label
+ * @property {String} labelWidth label宽度
+ * @property {String} labelPosition = [left | top] label位置
+ * @property {String} labelAlign = [left | center | right | justify] label对齐方式
+ * @property {Boolean} required  是否必填,值为true时会有红色星号
+ * @property {Boolean} border 是否显示边框
+ */
+export default {
+	name: 'fs-form-item'
+}
+</script>
+
+<script setup>
+import { inject, provide, computed, useSlots } from 'vue'
+
+const props = defineProps({
+	label: String,
+	labelWidth: String,
+	labelPosition: {
+		type: String,
+		validator(value) {
+			return ['left', 'top'].includes(value)
+		}
+	},
+	labelAlign: {
+		type: String,
+		validator(value) {
+			return ['left', 'center', 'right', 'justify'].includes(value)
+		}
+	},
+	required: Boolean,
+	border: {
+		type: Boolean,
+		default: true
+	}
+})
+const slots = useSlots()
+
+const form = inject('form', {})
+const position = props.labelPosition || form.labelPosition || 'left'
+const width = props.labelWidth || form.labelWidth || '120rpx'
+const align = props.labelAlign || form.labelAlign || 'left'
+
+provide('form-item-position', position)
+</script>
+
+<style lang="scss" scoped>
+.fs-form-item {
+	display: flex;
+	background-color: #fff;
+	padding: 20rpx var(--gutter);
+	min-height: 110rpx;
+
+	&-border {
+		border-bottom: 2rpx solid var(--border-color);
+	}
+
+	&-left {
+		align-items: center;
+		// .label{
+		// 	text-align-last: justify;
+		// }
+	}
+	&-top {
+		flex-direction: column;
+		.form-item-label {
+			margin-bottom: 10rpx;
+			width: auto !important;
+		}
+	}
+
+	&-label {
+		font-size: var(--content-size);
+		margin-right: 20rpx;
+
+		&-before {
+			margin-right: 20rpx;
+		}
+	}
+	&-required {
+		position: relative;
+		padding-left: 14rpx;
+
+		&::before {
+			position: absolute;
+			content: '*';
+			color: red;
+			left: 0;
+		}
+	}
+
+	&-right {
+		flex: 1;
+	}
+}
 </style>

+ 60 - 49
components/fs-form/fs-form.vue

@@ -1,49 +1,60 @@
-<template>
-	<view>
-		<slot></slot>
-	</view>
-</template>
-
-<script setup>
-import { provide } from 'vue'
-
-const props = defineProps({
-	labelWidth: {
-		type: String,
-		default: '120rpx'
-	},
-	labelPosition: {
-		type: String,
-		default: 'left',
-		validator(value) {
-			return ['left', 'top'].includes(value)
-		}
-	},
-	labelAlign: {
-		type: String,
-		default: 'left',
-		validator(value) {
-			return ['left', 'center', 'right', 'justify'].includes(value)
-		}
-	},
-	errorType: {
-		type: String,
-		default: 'toast',
-		validator(value) {
-			return ['toast', 'message'].includes(value)
-		}
-	},
-	model: Object
-})
-
-provide('form', props)
-
-defineExpose({
-	model: props.model,
-	errorType: props.errorType
-})
-</script>
-
-<style lang="scss">
-
-</style>
+<template>
+	<view><slot></slot></view>
+</template>
+
+<script>
+/**
+ * 表单组件
+ * @description 表单组件
+ * @property {String} labelWidth label宽度
+ * @property {String} labelPosition = [left | top] label位置
+ * @property {String} labelAlign = [left | center | right | justify] label对齐方式
+ * @property {String} errorType = [toast | message] 错误提示类型
+ * @property {Object} model 数据对象,表单校验时用
+ */
+export default {
+	name: 'fs-form'
+}
+</script>
+
+<script setup>
+import { provide } from 'vue'
+
+const props = defineProps({
+	labelWidth: {
+		type: String,
+		default: '120rpx'
+	},
+	labelPosition: {
+		type: String,
+		default: 'left',
+		validator(value) {
+			return ['left', 'top'].includes(value)
+		}
+	},
+	labelAlign: {
+		type: String,
+		default: 'left',
+		validator(value) {
+			return ['left', 'center', 'right', 'justify'].includes(value)
+		}
+	},
+	errorType: {
+		type: String,
+		default: 'toast',
+		validator(value) {
+			return ['toast', 'message'].includes(value)
+		}
+	},
+	model: Object
+})
+
+provide('form', props)
+
+defineExpose({
+	model: props.model,
+	errorType: props.errorType
+})
+</script>
+
+<style lang="scss"></style>

+ 77 - 64
components/fs-grid-item/fs-grid-item.vue

@@ -1,65 +1,78 @@
-<template>
-	<view
-		class="fs-grid-item"
-		:class="{'fs-grid-item-border': border,'fs-grid-item-radius': radius}"
-		:style="{padding: padding ? '20rpx 0' : 0,backgroundColor: bgColor}"
-		@click="handleClick">
-		<slot></slot>
-	</view>
-</template>
-
-<script setup>
-import { inject } from 'vue'
-
-const props = defineProps({
-	link: String,
-	linkType: {
-		type: String,
-		default: 'navigateTo'
-	},
-})
-
-const emits = defineEmits(['click'])
-
-const gird = inject('fsGrid', {})
-
-const { border, padding, bgColor, radius } = gird
-
-const handleClick = () => {
-	if (props.link) {
-		uni[props.linkType]({
-			url: props.link
-		})
-	}
-	emits('click')
-}
-</script>
-
-<style lang="scss" scoped>
-.fs-grid-item{
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	flex-direction: column;
-	text-align: center;
-	position: relative;
-	height: 100%;
-	background-color: #fff;
-	
-	&-border::before{
-		position: absolute;
-		content: '';
-		top: 0;
-		right: 0;
-		bottom: 0;
-		left: 0;
-		border: 1px solid var(--border-color);
-		margin-left: -1px;
-		margin-top: -1px;
-	}
-	
-	&-radius{
-		border-radius: var(--radius);
-	}
-}
+<template>
+	<view
+		class="fs-grid-item"
+		:class="{ 'fs-grid-item-border': border, 'fs-grid-item-radius': radius }"
+		:style="{ padding: padding ? '20rpx 0' : 0, backgroundColor: bgColor }"
+		@click="handleClick"
+	>
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+/**
+ * 宫格项组件
+ * @description 宫格项组件
+ * @property {String} link 跳转地址
+ * @property {String} linkType 跳转类型
+ */
+export default {
+	name: 'fs-grid-item'
+}
+</script>
+
+<script setup>
+import { inject } from 'vue'
+
+const props = defineProps({
+	link: String,
+	linkType: {
+		type: String,
+		default: 'navigateTo'
+	}
+})
+
+const emits = defineEmits(['click'])
+
+const gird = inject('fsGrid', {})
+
+const { border, padding, bgColor, radius } = gird
+
+const handleClick = () => {
+	if (props.link) {
+		uni[props.linkType]({
+			url: props.link
+		})
+	}
+	emits('click')
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-grid-item {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-direction: column;
+	text-align: center;
+	position: relative;
+	height: 100%;
+	background-color: #fff;
+
+	&-border::before {
+		position: absolute;
+		content: '';
+		top: 0;
+		right: 0;
+		bottom: 0;
+		left: 0;
+		border: 1px solid var(--border-color);
+		margin-left: -1px;
+		margin-top: -1px;
+	}
+
+	&-radius {
+		border-radius: var(--radius);
+	}
+}
 </style>

+ 63 - 52
components/fs-grid/fs-grid.vue

@@ -1,53 +1,64 @@
-<template>
-	<view 
-		class="fs-grid" 
-		:class="{'fs-grid-border': border}"
-		:style="styleStr">
-		<slot></slot>
-	</view>
-</template>
-
-<script setup>
-	import { computed, provide } from 'vue'
-	
-	const props = defineProps({
-		gutter: {
-			type: [Number,String],
-			default: 0
-		},
-		border: Boolean,
-		columnNum: {
-			type: Number,
-			default: 3
-		},
-		padding: {
-			type: Boolean,
-			default: true
-		},
-		bgColor: {
-			type: String,
-			default: '#fff'
-		},
-		radius: Boolean
-	})
-	
-	const styleStr = computed(() => {
-		return `grid-template-columns: repeat(${props.columnNum},1fr);gap:${props.gutter};`
-	})
-	
-	provide('fsGrid', props)
-	provide('gridBorder', props.border)
-	provide('gridPadding', props.padding)
-	provide('gridBgColor', props.bgColor)
-</script>
-
-<style lang="scss" scoped>
-.fs-grid{
-	display: grid;
-	
-	&-border{
-		border: 1px solid var(--border-color);
-		border-bottom: none;
-	}
-}
+<template>
+	<view class="fs-grid" :class="{ 'fs-grid-border': border }" :style="styleStr"><slot></slot></view>
+</template>
+
+<script>
+/**
+ * 宫格组件
+ * @description 宫格组件
+ * @property {Number, String} gutter 间距
+ * @property {Number} columnNum 列数量
+ * @property {Boolean} padding 网格项内边距
+ * @property {String} bgColor 背景色
+ * @property {Boolean} border 是否显示边框
+ * @property {Boolean} radius 是否圆角
+ */
+export default {
+	name: 'fs-grid'
+}
+</script>
+
+<script setup>
+import { computed, provide } from 'vue'
+
+const props = defineProps({
+	gutter: {
+		type: [Number, String],
+		default: 0
+	},
+	border: Boolean,
+	columnNum: {
+		type: Number,
+		default: 3
+	},
+	padding: {
+		type: Boolean,
+		default: true
+	},
+	bgColor: {
+		type: String,
+		default: '#fff'
+	},
+	radius: Boolean
+})
+
+const styleStr = computed(() => {
+	return `grid-template-columns: repeat(${props.columnNum},1fr);gap:${props.gutter};`
+})
+
+provide('fsGrid', props)
+provide('gridBorder', props.border)
+provide('gridPadding', props.padding)
+provide('gridBgColor', props.bgColor)
+</script>
+
+<style lang="scss" scoped>
+.fs-grid {
+	display: grid;
+
+	&-border {
+		border: 1px solid var(--border-color);
+		border-bottom: none;
+	}
+}
 </style>

+ 29 - 17
components/fs-gutter/fs-gutter.vue

@@ -2,25 +2,37 @@
 	<view class="fs-gutter" :style="styleStr"></view>
 </template>
 
-<script setup>
-	import { computed } from 'vue'
-	
-	const props = defineProps({
-		height: {
-			type: String,
-			default: '20rpx'
-		},
-		bgColor: String
-	})
-	
-	const styleStr = computed(() => {
-		return `height: ${props.height};background-color:${props.bgColor}`
-	})
+<script>
+/**
+ * 垂直间隔组件
+ * @description 垂直间隔组件
+ * @property {String} height 间距高度
+ * @property {String} bgColor 背景色
+ */
+export default {
+	name: 'fs-gutter'
+}
+</script>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+	height: {
+		type: String,
+		default: '20rpx'
+	},
+	bgColor: String
+})
+
+const styleStr = computed(() => {
+	return `height: ${props.height};background-color:${props.bgColor}`
+})
 </script>
 
 <style lang="scss" scoped>
-.fs-gutter{
-  width: 100%;
-  background-color: var(--bg-color);
+.fs-gutter {
+	width: 100%;
+	background-color: var(--bg-color);
 }
 </style>

+ 18 - 0
components/fs-icon/fs-icon.vue

@@ -6,6 +6,24 @@
 	></view>
 </template>
 
+<script>
+/**
+ * 图标组件
+ * @description 图标组件
+ * @property {String} type 图标类型
+ * @property {String} size 图标大小
+ * @property {String} source = [inner | outer] 来源
+ * @property {String} rotate 旋转
+ * @property {String} color 图标颜色
+ * @property {String} colorType = [primary | danger | warning | info | success | gray] 图标颜色类型
+ * @property {String} link 跳转地址
+ * @property {String} linkType 跳转类型
+ */
+export default {
+	name: 'fs-icon'
+}
+</script>
+
 <script setup>
 import { computed } from 'vue'
 

+ 157 - 127
components/fs-index-list/fs-index-list.vue

@@ -1,126 +1,156 @@
 <template>
 	<view>
 		<view class="fs-sidebar" @touchmove="handleMove" @touchend="handleEnd" v-if="list.length">
-			<view class="fs-sidebar-item" :class="{primary: state.activeId === item}" v-for="(item, index) in letters" :key="index" @click="handleClick(item)">
-				{{item}}
+			<view
+				class="fs-sidebar-item"
+				:class="{ primary: state.activeId === item }"
+				v-for="(item, index) in letters"
+				:key="index"
+				@click="handleClick(item)"
+			>
+				{{ item }}
 			</view>
 		</view>
-		
-		<view class="fs-contact" :style="{'margin-top': showSearch ? '110rpx' : ''}">
-			<scroll-view scroll-y :scroll-into-view="state.intoView" @scroll="scroll" class="fs-contact-list">
-				<view v-for="item in list" :key="item.name" :id="item.name" class="fs-contact-list-item">
-					<view class="fs-panel-title" :class="{'fs-sticky': sticky}">{{item[titleKey]}}</view>
-					<fs-cell
-						v-for="subitem in item[childrenKey]"
-						:key="subitem.name"
-						border
-						justify="left"
-						align="center"
-						:link="link + '?id=' + subitem.id"
-						@click="handleRoute(subitem)">
-						<template #title>
-							<fs-avatar :src="subitem[avatarKey]">{{subitem.alais}}</fs-avatar>
-						</template>
-						<template #value>
-							<view class="fs-contact-hd">{{subitem[hdKey]}}</view>
-							<view class="fs-contact-bd">{{subitem[bdKey]}}</view>
-						</template>
-					</fs-cell>
-				</view>
+
+		<view class="fs-contact" :style="{ 'margin-top': showSearch ? '110rpx' : '' }">
+			<scroll-view scroll-y :scroll-into-view="state.intoView" @scroll="scroll" class="fs-contact-list">
+				<view v-for="item in list" :key="item.name" :id="item.name" class="fs-contact-list-item">
+					<view class="fs-panel-title" :class="{ 'fs-sticky': sticky }">{{ item[titleKey] }}</view>
+					<fs-cell
+						v-for="subitem in item[childrenKey]"
+						:key="subitem.name"
+						border
+						justify="left"
+						align="center"
+						:link="link + '?id=' + subitem.id"
+						@click="handleRoute(subitem)"
+					>
+						<template #title>
+							<fs-avatar :src="subitem[avatarKey]">{{ subitem.alais }}</fs-avatar>
+						</template>
+						<template #value>
+							<view class="fs-contact-hd">{{ subitem[hdKey] }}</view>
+							<view class="fs-contact-bd">{{ subitem[bdKey] }}</view>
+						</template>
+					</fs-cell>
+				</view>
 			</scroll-view>
 		</view>
-		<view class="fs-layer" v-if="state.showLayer">{{state.activeId}}</view>
+		<view class="fs-layer" v-if="state.showLayer">{{ state.activeId }}</view>
 	</view>
 </template>
 
-<script setup>
-import { reactive } from 'vue'
+<script>
+/**
+ * 索引列表组件
+ * @description 索引列表组件
+ * @property {Array} list 列表
+ * @property {String} childrenKey children Key
+ * @property {String} titleKey title Key
+ * @property {String} avatarKey avatar Key
+ * @property {String} hdKey hd Key
+ * @property {String} bdKey bd Key
+ * @property {String} link 跳转地址
+ * @property {Boolean} sticky 是否固定
+ * @property {Boolean} showSearch 是否显示搜索
+ */
+export default {
+	name: 'fs-index-list'
+}
+</script>
+
+<script setup>
+import { reactive } from 'vue'
 
 const letters = []
 for (var i = 0; i < 26; i++) {
 	letters.push(String.fromCharCode(65 + i))
-}
-
-const props = defineProps({
-	list: Array,
-	childrenKey: {
-		type: String,
-		default: 'list'
-	},
-	titleKey: {
-		type: String,
-		default: 'name'
-	},
-	avatarKey: {
-		type: String,
-		default: 'src'
-	},
-	hdKey: {
-		type: String,
-		default: 'name'
-	},
-	bdKey: {
-		type: String,
-		default: 'phone'
-	},
-	link: String,
-	sticky: {
-		type: Boolean,
-		default: true
-	},
-	showSearch: Boolean,
-})
-
-const windowHeight = uni.getSystemInfoSync().windowHeight
-const offsetHeight = 50
-const navHeight = windowHeight - offsetHeight * 2
-const letterPos = []
-const letterHeight = navHeight / letters.length
-
-letters.forEach((item, index) => {
-	letterPos.push(offsetHeight + letterHeight * index)
-})
-
-const state = reactive({
-	intoView: '',
-	activeId: '',
-	showLayer: false
-})
-const handleClick = (id) => {
-	state.intoView = id
-	state.activeId = id
-}
-const handleMove = (e) => {
-	const y = e.touches[0].clientY
-
-	for (let i = 0, len = letterPos.length; i < len; i++) {
-		if (y >= letterPos[i] && y <= letterPos[i] + letterHeight) {
-			state.intoView = letters[i]
-			state.activeId = letters[i]
-			state.showLayer = true
-			break
-		}
-	}
-}
-const handleEnd = (e) => {
-	setTimeout(() => {
-		state.showLayer = false
-	}, 200)
-}
-const scroll = (e) => {
-	uni.createSelectorQuery().selectAll('.list-item').boundingClientRect(rects => {
-		for (let i = 0; i < rects.length; i++) {
-			let rect = rects[i]
-			if (rect.top === 0 || rect.bottom > 0) {
-				state.activeId = rect.id
-				break
-			}
-		}
-	}).exec()
-}
-
-const handleRoute = (item) => {
-	getApp().globalData.addrbookDetail = item
+}
+
+const props = defineProps({
+	list: Array,
+	childrenKey: {
+		type: String,
+		default: 'list'
+	},
+	titleKey: {
+		type: String,
+		default: 'name'
+	},
+	avatarKey: {
+		type: String,
+		default: 'src'
+	},
+	hdKey: {
+		type: String,
+		default: 'name'
+	},
+	bdKey: {
+		type: String,
+		default: 'phone'
+	},
+	link: String,
+	sticky: {
+		type: Boolean,
+		default: true
+	},
+	showSearch: Boolean
+})
+
+const windowHeight = uni.getSystemInfoSync().windowHeight
+const offsetHeight = 50
+const navHeight = windowHeight - offsetHeight * 2
+const letterPos = []
+const letterHeight = navHeight / letters.length
+
+letters.forEach((item, index) => {
+	letterPos.push(offsetHeight + letterHeight * index)
+})
+
+const state = reactive({
+	intoView: '',
+	activeId: '',
+	showLayer: false
+})
+const handleClick = id => {
+	state.intoView = id
+	state.activeId = id
+}
+const handleMove = e => {
+	const y = e.touches[0].clientY
+
+	for (let i = 0, len = letterPos.length; i < len; i++) {
+		if (y >= letterPos[i] && y <= letterPos[i] + letterHeight) {
+			state.intoView = letters[i]
+			state.activeId = letters[i]
+			state.showLayer = true
+			break
+		}
+	}
+}
+const handleEnd = e => {
+	setTimeout(() => {
+		state.showLayer = false
+	}, 200)
+}
+const scroll = e => {
+	uni
+		.createSelectorQuery()
+		.selectAll('.list-item')
+		.boundingClientRect(rects => {
+			for (let i = 0; i < rects.length; i++) {
+				let rect = rects[i]
+				if (rect.top === 0 || rect.bottom > 0) {
+					state.activeId = rect.id
+					break
+				}
+			}
+		})
+		.exec()
+}
+
+const handleRoute = item => {
+	getApp().globalData.addrbookDetail = item
 }
 </script>
 
@@ -134,8 +164,8 @@ const handleRoute = (item) => {
 	right: 0;
 	align-items: center;
 	z-index: 900;
-	transform: translateY(-50%);
-	
+	transform: translateY(-50%);
+
 	&-item {
 		padding: 6rpx 20rpx;
 		flex-shrink: 1;
@@ -154,28 +184,28 @@ const handleRoute = (item) => {
 	top: var(--window-top);
 	left: var(--gutter);
 	right: 60rpx;
-	bottom: 0;
-	
-	&-list {
-		height: 100%;
-	}
-	
-	&-hd {
-		font-size: 16px;
-	}
-	
-	.fs-panel-title {
-		padding: var(--gutter);
-		color: var(--title);
-		text-align: left;
-		background-color: var(--bg-color);
+	bottom: 0;
+
+	&-list {
+		height: 100%;
+	}
+
+	&-hd {
+		font-size: 16px;
+	}
+
+	.fs-panel-title {
+		padding: var(--gutter);
+		color: var(--title);
+		text-align: left;
+		background-color: var(--bg-color);
 	}
 }
 
 .fs-layer {
 	width: 150rpx;
 	height: 150rpx;
-	background: rgba(0, 0, 0, .5);
+	background: rgba(0, 0, 0, 0.5);
 	border-radius: 50%;
 	line-height: 150rpx;
 	color: #ffffff;

+ 62 - 50
components/fs-keyboard/fs-keyboard.vue

@@ -1,50 +1,62 @@
-<template>
-	<view>
-		<keyboardCar
-			v-if="mode === 'car'"
-			:index="index"
-			v-model="modelValue"
-			@change="handleChange"
-			@close="handleClose">
-		</keyboardCar>
-		
-		<keyboardNum
-			v-if="mode === 'number'"
-			:index="index"
-			v-model="modelValue"
-			@change="handleChange"
-			@close="handleClose"
-		/>
-	</view>
-</template>
-
-<script setup>
-import keyboardCar from './car.vue'
-import keyboardNum from './number.vue'
-
-const props = defineProps({
-	modelValue: Boolean,
-	mode: {
-		type: String,
-		default: 'car'
-	},
-	index: {
-		type: Number,
-		default: 0
-	}
-})
-const emits = defineEmits(['update:modelValue', 'change', 'close'])
-
-const handleChange = item => {
-	emits('change', item)
-}
-
-const handleClose = () => {
-	emits('update:modelValue', false)
-	emits('close')
-}
-</script>
-
-<style lang="scss">
-
-</style>
+<template>
+	<view>
+		<keyboardCar
+			v-if="mode === 'car'"
+			:index="index"
+			v-model="modelValue"
+			@change="handleChange"
+			@close="handleClose"
+		></keyboardCar>
+
+		<keyboardNum
+			v-if="mode === 'number'"
+			:index="index"
+			v-model="modelValue"
+			@change="handleChange"
+			@close="handleClose"
+		/>
+	</view>
+</template>
+
+<script>
+/**
+ * 键盘组件
+ * @description 键盘组件
+ * @property {String} mode 键盘模式
+ * @property {Number} index 激活项index
+ * @event {Function} change 键盘项变化事件
+ * @event {Function} close 键盘关闭事件
+ */
+export default {
+	name: 'fs-keyboard'
+}
+</script>
+
+<script setup>
+import keyboardCar from './car.vue'
+import keyboardNum from './number.vue'
+
+const props = defineProps({
+	modelValue: Boolean,
+	mode: {
+		type: String,
+		default: 'car'
+	},
+	index: {
+		type: Number,
+		default: 0
+	}
+})
+const emits = defineEmits(['update:modelValue', 'change', 'close'])
+
+const handleChange = item => {
+	emits('change', item)
+}
+
+const handleClose = () => {
+	emits('update:modelValue', false)
+	emits('close')
+}
+</script>
+
+<style lang="scss"></style>

+ 108 - 95
components/fs-license-plate/fs-license-plate.vue

@@ -1,96 +1,109 @@
-<template>
-	<view>
-		<view class="title text-center gutter-v" style="padding: 20rpx;">请输入车牌号</view>
-		<view class="fs-plate">
-			<view
-				class="fs-plate-item"
-				:class="{'fs-plate-item-active': curIndex === index, 'fs-plate-new': index === 6 && !item}"
-				v-for="(item, index) in carNo"
-				:key="index"
-				@click="handleInput(index)">
-					{{item || ''}}
-				</view>
-		</view>
-		<fs-keyboard mode="car" v-model="visible" :index="curIndex" @change="handleChange"></fs-keyboard>
-	</view>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-
-const props = defineProps({
-	modelValue: String
-})
-const emits = defineEmits(['update:modelValue', 'change'])
-
-const carNo = ref(new Array(7))
-if (props.modelValue) {
-	for (let i = 0; i < props.modelValue.length; i++) {
-		carNo.value[i] = props.modelValue[i]
-	}
-}
-let curIndex = ref(-1)
-const handleChange = item => {
-	const length = carNo.value.filter(item => item).length
-	
-	if (item === 'del') {
-		if (length >= 1) {
-			curIndex.value--
-			carNo.value[curIndex.value] = ''
-		}
-	} else{
-		if (length <= 6) {
-			carNo.value[curIndex.value] = item
-			curIndex.value++
-		}
-	}
-	
-	emits('update:modelValue', carNo.value.join(''))
-}
-
-let visible = ref(false)
-const handleInput = (index) => {
-	curIndex.value = index
-	visible.value = true
-}
-</script>
-
-<style lang="scss" scoped>
-.fs-plate{
-	display: flex;
-	justify-content: center;
-	align-items: center;
-	
-	&-item{
-		width: 70rpx;
-		height: 80rpx;		
-		line-height: 80rpx;		
-		border-radius: 8rpx;
-		border: 1px solid #CBCBCB;
-		text-align: center;
-		
-		& + &{
-			margin-left: 20rpx;
-		}
-		
-		&-active{
-			border-color: var(--primary);
-		}
-	}
-	&-new{
-		position: relative;
-		&::after{
-			content: "新能源";
-			color: #999999;
-			width: 100%;
-			font-size: 9px;
-			text-align: center;
-			position: absolute;
-			left: 0;
-			right: 0;
-			top: 50%;
-			transform: translateY(-50%);
-		}
-	}
-}
+<template>
+	<view>
+		<view class="title text-center gutter-v" style="padding: 20rpx;">请输入车牌号</view>
+		<view class="fs-plate">
+			<view
+				class="fs-plate-item"
+				:class="{ 'fs-plate-item-active': curIndex === index, 'fs-plate-new': index === 6 && !item }"
+				v-for="(item, index) in carNo"
+				:key="index"
+				@click="handleInput(index)"
+			>
+				{{ item || '' }}
+			</view>
+		</view>
+		<fs-keyboard mode="car" v-model="visible" :index="curIndex" @change="handleChange"></fs-keyboard>
+	</view>
+</template>
+
+<script>
+/**
+ * 车牌组件
+ * @description 车牌组件
+ * @event {Function} change chagne事件
+ */
+export default {
+	name: 'fs-license-plate'
+}
+</script>
+
+<script setup>
+import { ref } from 'vue'
+
+const props = defineProps({
+	modelValue: String
+})
+const emits = defineEmits(['update:modelValue', 'change'])
+
+const carNo = ref(new Array(7))
+if (props.modelValue) {
+	for (let i = 0; i < props.modelValue.length; i++) {
+		carNo.value[i] = props.modelValue[i]
+	}
+}
+let curIndex = ref(-1)
+const handleChange = item => {
+	const length = carNo.value.filter(item => item).length
+
+	if (item === 'del') {
+		if (length >= 1) {
+			curIndex.value--
+			carNo.value[curIndex.value] = ''
+		}
+	} else {
+		if (length <= 6) {
+			carNo.value[curIndex.value] = item
+			curIndex.value++
+		}
+	}
+
+	emits('update:modelValue', carNo.value.join(''))
+	emits('change', carNo.value.join(''))
+}
+
+let visible = ref(false)
+const handleInput = index => {
+	curIndex.value = index
+	visible.value = true
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-plate {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+
+	&-item {
+		width: 70rpx;
+		height: 80rpx;
+		line-height: 80rpx;
+		border-radius: 8rpx;
+		border: 1px solid #cbcbcb;
+		text-align: center;
+
+		& + & {
+			margin-left: 20rpx;
+		}
+
+		&-active {
+			border-color: var(--primary);
+		}
+	}
+	&-new {
+		position: relative;
+		&::after {
+			content: '新能源';
+			color: #999999;
+			width: 100%;
+			font-size: 9px;
+			text-align: center;
+			position: absolute;
+			left: 0;
+			right: 0;
+			top: 50%;
+			transform: translateY(-50%);
+		}
+	}
+}
 </style>

+ 68 - 57
components/fs-loading/fs-loading.vue

@@ -1,58 +1,69 @@
-<template>
-	<view class="fs-loading" :style="{backgroundColor: bgColor}">
-		<view>
-			<view class="loader"></view>
-			<slot></slot>
-		</view>
-	</view>
-</template>
-
-<script setup>
-const props = defineProps({
-	bgColor: {
-		type: String,
-		default: 'rgba(0,0,0,.5)'
-	}
-})
-</script>
-
-<style lang="scss" scoped>
-.fs-loading{
-	position: absolute;
-	top: 0;
-	left: 0;
-	text-align: center;
-	width: 100%;
-	height: 100%;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	color: #fff;
-	z-index: 20;
-}
-.loader {
-  margin: 0 auto 20rpx;
-  font-size: 12rpx;
-  position: relative;
-  border-top: 1em solid rgba(255, 255, 255, 0.2);
-  border-right: 1em solid rgba(255, 255, 255, 0.2);
-  border-bottom: 1em solid rgba(255, 255, 255, 0.2);
-  border-left: 1em solid #fff;
-  animation: load8 1s infinite linear;
-}
-.loader,
-.loader:after {
-  border-radius: 50%;
-  width: 10em;
-  height: 10em;
-}
-
-@keyframes load8 {
-  0% {
-    transform: rotate(0deg);
-  }
-  100% {
-    transform: rotate(360deg);
-  }
-}
+<template>
+	<view class="fs-loading" :style="{ backgroundColor: bgColor }">
+		<view>
+			<view class="loader"></view>
+			<slot></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 加载组件
+ * @description 加载组件
+ * @property {String} bgColor 背景色
+ */
+export default {
+	name: 'fs-loading'
+}
+</script>
+
+<script setup>
+const props = defineProps({
+	bgColor: {
+		type: String,
+		default: 'rgba(0,0,0,.5)'
+	}
+})
+</script>
+
+<style lang="scss" scoped>
+.fs-loading {
+	position: absolute;
+	top: 0;
+	left: 0;
+	text-align: center;
+	width: 100%;
+	height: 100%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	color: #fff;
+	z-index: 20;
+}
+.loader {
+	margin: 0 auto 20rpx;
+	font-size: 12rpx;
+	position: relative;
+	border-top: 1em solid rgba(255, 255, 255, 0.2);
+	border-right: 1em solid rgba(255, 255, 255, 0.2);
+	border-bottom: 1em solid rgba(255, 255, 255, 0.2);
+	border-left: 1em solid #fff;
+	animation: load8 1s infinite linear;
+}
+.loader,
+.loader:after {
+	border-radius: 50%;
+	width: 10em;
+	height: 10em;
+}
+
+@keyframes load8 {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
 </style>

+ 97 - 82
components/fs-loadmore/fs-loadmore.vue

@@ -1,83 +1,98 @@
-<template>
-	<view>
-		<slot></slot>
-		<fs-empty v-if="!state.loading && !state.dataList.length"></fs-empty>
-		<template v-else>
-			<fs-divider v-if="!state.hasMore">{{ nomore }}</fs-divider>
-		</template>
-	</view>
-</template>
-
-<script setup>
-import { reactive, toRef } from 'vue'
-
-const props = defineProps({
-	modelValue: {
-		type: Array,
-		default() {
-			return []
-		}
-	},
-	fetchList: Function,
-	pageSize: {
-		type: Number,
-		default: 20
-	},
-	pullDownRefresh: Boolean,
-	autoCall: {
-		type: Boolean,
-		default: true
-	},
-	nomore: {
-		type: String,
-		default: '没有更多了~'
-	}
-})
-const emits = defineEmits(['update:modelValue'])
-
-const state = reactive({
-	loading: true,
-	dataList: props.modelValue,
-	pageNo: 1,
-	pageSize: props.pageSize,
-	hasMore: true
-})
-
-const query = loadmore => {
-	state.loading = true
-	if (loadmore) {
-		state.pageNo++
-	} else {
-		state.pageNo = 1
-	}
-	return props
-		.fetchList({
-			pageNo: state.pageNo,
-			pageSize: props.pageSize
-		})
-		.then(res => {
-			state.hasMore = res.length >= state.pageSize
-			state.dataList = loadmore ? [...state.dataList, ...res] : res
-			emits('update:modelValue', state.dataList)
-		})
-		.finally(() => {
-			state.loading = false
-		})
-}
-props.autoCall && query()
-
-const refresh = () => {
-	state.dataList = []
-	emits('update:modelValue', state.dataList)
-	return query()
-}
-
-defineExpose({
-	query,
-	refresh,
-	hasMore: toRef(state, 'hasMore'),
-	pullDownRefresh: props.pullDownRefresh
-})
-</script>
-
+<template>
+	<view>
+		<slot></slot>
+		<fs-empty v-if="!state.loading && !state.dataList.length"></fs-empty>
+		<template v-else>
+			<fs-divider v-if="!state.hasMore">{{ nomore }}</fs-divider>
+		</template>
+	</view>
+</template>
+
+<script>
+/**
+ * 加载更多组件
+ * @description 加载更多组件
+ * @property {Function} fetchList 获取数据的方法
+ * @property {Number} pageSize 分页大小
+ * @property {Boolean} pullDownRefresh 是否开启下拉刷新
+ * @property {Boolean} autoCall 是否自动调用查询方法
+ * @property {String} nomore 没有更多文字
+ */
+export default {
+	name: 'fs-loadmore'
+}
+</script>
+
+<script setup>
+import { reactive, toRef } from 'vue'
+
+const props = defineProps({
+	modelValue: {
+		type: Array,
+		default() {
+			return []
+		}
+	},
+	fetchList: Function,
+	pageSize: {
+		type: Number,
+		default: 20
+	},
+	pullDownRefresh: Boolean,
+	autoCall: {
+		type: Boolean,
+		default: true
+	},
+	nomore: {
+		type: String,
+		default: '没有更多了~'
+	}
+})
+const emits = defineEmits(['update:modelValue'])
+
+const state = reactive({
+	loading: true,
+	dataList: props.modelValue,
+	pageNo: 1,
+	pageSize: props.pageSize,
+	hasMore: true
+})
+
+const query = loadmore => {
+	state.loading = true
+	if (loadmore) {
+		state.pageNo++
+	} else {
+		state.pageNo = 1
+	}
+	return props
+		.fetchList({
+			pageNo: state.pageNo,
+			pageSize: props.pageSize
+		})
+		.then(res => {
+			state.hasMore = res.length >= state.pageSize
+			state.dataList = loadmore ? [...state.dataList, ...res] : res
+			emits('update:modelValue', state.dataList)
+		})
+		.finally(() => {
+			state.loading = false
+		})
+}
+props.autoCall && query()
+
+const refresh = () => {
+	state.dataList = []
+	emits('update:modelValue', state.dataList)
+	return query()
+}
+
+defineExpose({
+	query,
+	refresh,
+	hasMore: toRef(state, 'hasMore'),
+	pullDownRefresh: props.pullDownRefresh
+})
+</script>
+
 <style></style>

+ 65 - 50
components/fs-mask/fs-mask.vue

@@ -1,51 +1,66 @@
-<template>
-	<view
-		class="fs-mask"
-		:class="{'fs-mask-blur': blurable}"
-		:style="{'zIndex': zIndex}" 
-		v-if="modelValue" 
-		@click="handleMask">
-	</view>
-</template>
-
-<script setup>
-const props = defineProps({
-	modelValue: Boolean,
-	blurable: Boolean,
-	zIndex: {
-		type: Number,
-		default: 899
-	},
-	maskClickable: {
-		type: Boolean,
-		default: true
-	},
-	bgColor: {
-		type: String,
-		default: 'rgba(0, 0, 0, 0.5)'
-	}
-})
-const emits = defineEmits(['update:modelValue','close'])
-
-const handleMask = () => {
-	if (props.maskClickable) {
-		emits('update:modelValue', false)
-		emits('close')
-	}
-}
-</script>
-
-<style lang="scss">
-.fs-mask{
-	position: fixed;
-	top: var(--window-top);
-	right: 0;
-	bottom: var(--window-bottom);
-	left: 0;
-	background-color: v-bind(bgColor);
-	
-	&-blur{
-		backdrop-filter: blur(12rpx);
-	}
-}
+<template>
+	<view
+		class="fs-mask"
+		:class="{ 'fs-mask-blur': blurable }"
+		:style="{ zIndex: zIndex }"
+		v-if="modelValue"
+		@click="handleMask"
+	></view>
+</template>
+
+<script>
+/**
+ * 遮罩组件
+ * @description 遮罩组件
+ * @property {Boolean} blurable 毛玻璃效果
+ * @property {Number} zIndex 层级
+ * @property {Boolean} maskClickable 点击遮罩是否可关闭
+ * @property {Boolean} bgColor 背景色
+ * @event {Function} close 关闭事件
+ */
+export default {
+	name: 'fs-mask'
+}
+</script>
+
+<script setup>
+const props = defineProps({
+	modelValue: Boolean,
+	blurable: Boolean,
+	zIndex: {
+		type: Number,
+		default: 899
+	},
+	maskClickable: {
+		type: Boolean,
+		default: true
+	},
+	bgColor: {
+		type: String,
+		default: 'rgba(0, 0, 0, 0.5)'
+	}
+})
+const emits = defineEmits(['update:modelValue', 'close'])
+
+const handleMask = () => {
+	if (props.maskClickable) {
+		emits('update:modelValue', false)
+		emits('close')
+	}
+}
+</script>
+
+<style lang="scss">
+.fs-mask {
+	position: fixed;
+	top: var(--window-top);
+	right: 0;
+	bottom: var(--window-bottom);
+	left: 0;
+	background-color: v-bind(bgColor);
+
+	&-blur {
+		backdrop-filter: blur(12rpx);
+	}
+}
 </style>

+ 113 - 105
components/fs-message/fs-message.vue

@@ -1,108 +1,116 @@
-<template>
-	<view 
-		class="fs-message" 
-		:class="[{show:state.options.show},'bg-'+state.options.type]">
-		{{state.options.message}}
-	</view>
-</template>
-
-<script setup>
-import { reactive } from 'vue'
-
-const defaultOptions = {
-	type: 'primary',
-	duration: 3000
-}
-
-const state = reactive({
-	options: {},
-	timer: null
-})
-const formatOptions = options => {
-	if (typeof options === 'string') {
-		return {
-			message: options
-		}
-	}
-	if (options.type === 'error') {
-		options.type = 'danger'
-	}
-	return options
-}
-const show = options => {
-	state.options = {
-		...defaultOptions,
-		...formatOptions(options),
-		show: true
-	}
-	
-	if (state.timer) {
-		clearTimeout(state.timer)
-	}
-
-	if (state.options.duration > 0) {
-		state.timer = setTimeout(() => {
-			handleHide()
-			state.timer = null
-		}, state.options.duration)
-	}
-}
-const success = options => {
-	show({
-		...formatOptions(options),
-		type: 'success'
-	})
-}
-const error = options => {
-	show({
-		...formatOptions(options),
-		type: 'danger'
-	})
-}
-const warning = options => {
-	show({
-		...formatOptions(options),
-		type: 'warning'
-	})
-}
-const info = options => {
-	show({
-		...formatOptions(options),
-		type: 'info'
-	})
-}
-
-const handleHide = () => {
-	state.options = {
-		...state.options,
-		show: false
-	}
-}
-
-defineExpose({
-	show,
-	success,
-	error,
-	warning,
-	info,
-	handleHide
-})
-</script>
-
-<style lang="scss" scoped>
-.fs-message{
-	position: fixed;
-	top: var(--window-top);
-	left: 0;
-	right: 0;
-	padding: 20rpx;
+<template>
+	<view class="fs-message" :class="[{ show: state.options.show }, 'bg-' + state.options.type]">
+		{{ state.options.message }}
+	</view>
+</template>
+
+<script>
+/**
+ * 消息通知组件
+ * @description 消息通知组件
+ */
+export default {
+	name: 'fs-message'
+}
+</script>
+
+<script setup>
+import { reactive } from 'vue'
+
+const defaultOptions = {
+	type: 'primary',
+	duration: 3000
+}
+
+const state = reactive({
+	options: {},
+	timer: null
+})
+const formatOptions = options => {
+	if (typeof options === 'string') {
+		return {
+			message: options
+		}
+	}
+	if (options.type === 'error') {
+		options.type = 'danger'
+	}
+	return options
+}
+const show = options => {
+	state.options = {
+		...defaultOptions,
+		...formatOptions(options),
+		show: true
+	}
+
+	if (state.timer) {
+		clearTimeout(state.timer)
+	}
+
+	if (state.options.duration > 0) {
+		state.timer = setTimeout(() => {
+			handleHide()
+			state.timer = null
+		}, state.options.duration)
+	}
+}
+const success = options => {
+	show({
+		...formatOptions(options),
+		type: 'success'
+	})
+}
+const error = options => {
+	show({
+		...formatOptions(options),
+		type: 'danger'
+	})
+}
+const warning = options => {
+	show({
+		...formatOptions(options),
+		type: 'warning'
+	})
+}
+const info = options => {
+	show({
+		...formatOptions(options),
+		type: 'info'
+	})
+}
+
+const handleHide = () => {
+	state.options = {
+		...state.options,
+		show: false
+	}
+}
+
+defineExpose({
+	show,
+	success,
+	error,
+	warning,
+	info,
+	handleHide
+})
+</script>
+
+<style lang="scss" scoped>
+.fs-message {
+	position: fixed;
+	top: var(--window-top);
+	left: 0;
+	right: 0;
+	padding: 20rpx;
 	color: #fff;
-	transition: all .1s;
+	transition: all 0.1s;
 	transform: translateY(-100%);
-	text-align: center;
-	z-index: 900;
-}
-.show{
-  transform: translateY(0);
-}
+	text-align: center;
+	z-index: 900;
+}
+.show {
+	transform: translateY(0);
+}
 </style>

+ 190 - 158
components/fs-modal/fs-modal.vue

@@ -1,118 +1,150 @@
-<template>
-	<view class="fs-modal">
-		<view class="fs-modal-box" :class="{show:modelValue}" :style="{width}">
-			<view class="fs-modal-title title" v-if="showTitle">{{title}}</view>
-			<view class="fs-modal-content">
-				<slot>{{content}}</slot>
-			</view>
-			<view class="fs-modal-ft">
-				<view class="fs-modal-ft-btn fs-modal-ft-cancel" v-if="showCancel" @click="handleCancel">{{cancelText}}</view>
-				<view 
-					v-if="showConfirm"
-					class="fs-modal-ft-btn fs-modal-ft-confirm"
-					:class="[confirmTextColorType]"
-					:style="{color: confirmTextColor}"
-					@click="handleConfirm">
-					<view class="fs-loader" v-if="loading"></view>
-					<template v-else>
-						{{confirmText}}
-					</template>
-				</view>
-			</view>
-			<view class="fs-modal-close" v-if="showClose" @click="handleClose">
-				<fs-icon type="icon-close-circle" color="#fff" size="50rpx"></fs-icon>
-			</view>
-		</view>
-		<fs-mask :modelValue="modelValue" @close="handleClose" :maskClickable="maskClickable" :blurable="blurable"></fs-mask>
-	</view>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-
-const props = defineProps({
-	modelValue: Boolean,
-	width: {
-		type: String,
-		default: '80vw'
-	},
-	maskClickable: {
-		type: Boolean,
-		default: true
-	},
-	blurable: Boolean,
-	title: {
-		type: String,
-		default: '提示'
-	},
-	showTitle: {
-		type: Boolean,
-		default: true
-	},
-	content: String,
-	showCancel: {
-		type: Boolean,
-		default: true
-	},
-	cancelText: {
-		type: String,
-		default: '取消'
-	},
-	showConfirm: {
-		type: Boolean,
-		default: true
-	},
-	confirmText: {
-		type: String,
-		default: '确定'
-	},
-	confirmTextColor: {
-		type: String
-	},
-	confirmTextColorType: {
-		type: String,
-		default: 'primary',
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-		}
-	},
-	showClose: {
-		type: Boolean,
-		default: false
-	},
-	beforeClose: Function
-})
-
-const emits = defineEmits(['update:modelValue','confirm','cancel'])
-const loading = ref(false)
-
-const handleClose = () => {
-	emits('update:modelValue', false)
-}
-const handleMask = () => {
-	if(props.maskClickable) {
-		handleClose()
-	}
-}
-const handleCancel = () => {
-	handleClose()
-	emits('cancel')
-}
-const handleConfirm = () => {
-	if (props.beforeClose) {
-		loading.value = true
-		props.beforeClose('confirm', (flag = true) => {
-			loading.value = false
-			flag && handleClose()
-		})
-	} else{
-		handleClose()
-		emits('confirm', false)
-	}
-}
-</script>
-
-<style lang="scss" scoped>
+<template>
+	<view class="fs-modal">
+		<view class="fs-modal-box" :class="{ show: modelValue }" :style="{ width }">
+			<view class="fs-modal-title title" v-if="showTitle">{{ title }}</view>
+			<view class="fs-modal-content">
+				<slot>{{ content }}</slot>
+			</view>
+			<view class="fs-modal-ft">
+				<view class="fs-modal-ft-btn fs-modal-ft-cancel" v-if="showCancel" @click="handleCancel">{{ cancelText }}</view>
+				<view
+					v-if="showConfirm"
+					class="fs-modal-ft-btn fs-modal-ft-confirm"
+					:class="[confirmTextColorType]"
+					:style="{ color: confirmTextColor }"
+					@click="handleConfirm"
+				>
+					<view class="fs-loader" v-if="loading"></view>
+					<template v-else>
+						{{ confirmText }}
+					</template>
+				</view>
+			</view>
+			<view class="fs-modal-close" v-if="showClose" @click="handleClose">
+				<fs-icon type="icon-close-circle" color="#fff" size="50rpx"></fs-icon>
+			</view>
+		</view>
+		<fs-mask
+			:modelValue="modelValue"
+			@close="handleClose"
+			:maskClickable="maskClickable"
+			:blurable="blurable"
+		></fs-mask>
+	</view>
+</template>
+
+<script>
+/**
+ * 模态框组件
+ * @description 模态框组件
+ * @property {String} width 模态框宽度
+ * @property {String} title 标题
+ * @property {Boolean} showTitle 是否显示标题
+ * @property {Boolean} maskClickable 点击遮罩是否可关闭
+ * @property {String} content 内容
+ * @property {Boolean} blurable 毛玻璃效果
+ * @property {Boolean} showClose 是否显示关闭
+ * @property {Function} beforeClose 异步关闭
+ * @property {String} cancelText 取消按钮文字
+ * @property {Boolean} showCancel 是否显示取消按钮
+ * @property {String} confirmText 确定按钮文字
+ * @property {Boolean} showConfirm 是否显示确定按钮
+ * @property {String} confirmTextColor 确定按钮文字颜色
+ * @property {Boolean} confirmTextColorType = [primary | danger | warning | info | success] 确定按钮文字颜色类型
+ * @event {Function} confirm 确定事件
+ * @event {Function} cancel 取消事件
+ */
+export default {
+	name: 'fs-model'
+}
+</script>
+
+<script setup>
+import { ref } from 'vue'
+
+const props = defineProps({
+	modelValue: Boolean,
+	width: {
+		type: String,
+		default: '80vw'
+	},
+	maskClickable: {
+		type: Boolean,
+		default: true
+	},
+	blurable: Boolean,
+	title: {
+		type: String,
+		default: '提示'
+	},
+	showTitle: {
+		type: Boolean,
+		default: true
+	},
+	content: String,
+	showCancel: {
+		type: Boolean,
+		default: true
+	},
+	cancelText: {
+		type: String,
+		default: '取消'
+	},
+	showConfirm: {
+		type: Boolean,
+		default: true
+	},
+	confirmText: {
+		type: String,
+		default: '确定'
+	},
+	confirmTextColor: {
+		type: String
+	},
+	confirmTextColorType: {
+		type: String,
+		default: 'primary',
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	},
+	showClose: {
+		type: Boolean,
+		default: false
+	},
+	beforeClose: Function
+})
+
+const emits = defineEmits(['update:modelValue', 'confirm', 'cancel'])
+const loading = ref(false)
+
+const handleClose = () => {
+	emits('update:modelValue', false)
+}
+const handleMask = () => {
+	if (props.maskClickable) {
+		handleClose()
+	}
+}
+const handleCancel = () => {
+	handleClose()
+	emits('cancel')
+}
+const handleConfirm = () => {
+	if (props.beforeClose) {
+		loading.value = true
+		props.beforeClose('confirm', (flag = true) => {
+			loading.value = false
+			flag && handleClose()
+		})
+	} else {
+		handleClose()
+		emits('confirm', false)
+	}
+}
+</script>
+
+<style lang="scss" scoped>
 .fs-modal {
 	&-box {
 		position: fixed;
@@ -121,10 +153,10 @@ const handleConfirm = () => {
 		top: 50%;
 		left: 50%;
 		transform: translate(-50%, -50%) scale(0);
-		border-radius: var(--radius);
-		
-		&.show{
-			transform: translate(-50%, -50%) scale(1);
+		border-radius: var(--radius);
+
+		&.show {
+			transform: translate(-50%, -50%) scale(1);
 		}
 	}
 
@@ -133,25 +165,25 @@ const handleConfirm = () => {
 		text-align: center;
 	}
 	&-content {
-		padding: 20rpx 20rpx 40rpx;
+		padding: 20rpx 20rpx 40rpx;
 		text-align: center;
-	}
-	&-ft{
-		display: flex;
-		
-		&-btn{
-			flex: 1;
-			height: 80rpx;
-			line-height: 80rpx;
-			text-align: center;
-			border-top: 2rpx solid var(--border-color);
-			cursor: pointer;
-			
-			& + &{
-				border-left: 2rpx solid var(--border-color);
-			}
-		}
-	}
+	}
+	&-ft {
+		display: flex;
+
+		&-btn {
+			flex: 1;
+			height: 80rpx;
+			line-height: 80rpx;
+			text-align: center;
+			border-top: 2rpx solid var(--border-color);
+			cursor: pointer;
+
+			& + & {
+				border-left: 2rpx solid var(--border-color);
+			}
+		}
+	}
 
 	&-close {
 		position: absolute;
@@ -170,25 +202,25 @@ const handleConfirm = () => {
 			content: '';
 		}
 	}
-}
-.fs-loader {
-	display: inline-block;
-	width: 40rpx;
-	height: 40rpx;
-	color: inherit;
-	vertical-align: middle;
-	border: 4rpx solid currentcolor;
-	border-bottom-color: transparent;
-	border-radius: 50%;
-	animation: 1s loader linear infinite;
-	position: relative;
-}
-@keyframes loader {
-	0% {
-		transform: rotate(0deg);
-	}
-	100% {
-		transform: rotate(360deg);
-	}
-}
+}
+.fs-loader {
+	display: inline-block;
+	width: 40rpx;
+	height: 40rpx;
+	color: inherit;
+	vertical-align: middle;
+	border: 4rpx solid currentcolor;
+	border-bottom-color: transparent;
+	border-radius: 50%;
+	animation: 1s loader linear infinite;
+	position: relative;
+}
+@keyframes loader {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
 </style>

+ 137 - 120
components/fs-notice-bar/fs-notice-bar.vue

@@ -1,121 +1,138 @@
-<template>
-	<view class="fs-notice-bar" :animation="animationData" :style="{backgroundColor: bgColor, color}" v-if="visible">
-		<fs-icon v-if="showIcon" class="fs-notice-bar-notice" type="icon-sound" :color="color"></fs-icon>
-		<swiper
-			class="fs-notice-bar-swiper"
-			autoplay
-			circular 
-			:vertical="vertical"
-			:interval="interval" 
-			:duration="1000">
-			<swiper-item v-for="(item, index) in list" :key="index">
-				<view class="fs-notice-bar-item line1" @click="handleRoute(item)">{{item[titleKey]}}</view>
-			</swiper-item>
-		</swiper>
-		<fs-icon
-			v-if="showClose"
-			class="fs-notice-bar-close"
-			:color="color"
-			type="icon-close-circle"
-			@click="handleClose">
-		</fs-icon>
-	</view>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-
-const props = defineProps({
-	list: {
-		type: Array,
-		default() {
-			return []
-		}
-	},
-	titleKey: {
-		type: String,
-		default: 'title'
-	},
-	urlKey: {
-		type: String,
-		default: 'url'
-	},
-	bgColor: {
-		type: String,
-		default: '#fffbe8'
-	},
-	color: {
-		type: String,
-		default: '#de8c17'
-	},
-	vertical: Boolean,
-	interval: {
-		type: Number,
-		default: 5000
-	},
-	showClose: {
-		type: Boolean,
-		default: true
-	},
-	showIcon: {
-		type: Boolean,
-		default: true
-	},
-	linkType: {
-		type: String,
-		default: 'navigateTo'
-	}
-})
-const emits = defineEmits(['click'])
-
-let visible = ref(true)
-let animationData = ref(null)
-const animation = uni.createAnimation({
-	duration: 200,
-})
-
-const handleClose = () => {
-	animation.scale(0.7, 0.7).opacity(0).step()
-	animationData.value = animation.export()
-
-	setTimeout(() => {
-		visible.value = false
-	}, 200)
-}
-
-const handleRoute = item => {
-	if (item[urlKey]) {
-		uni[props.linkType]({
-			url: item[urlKey]
-		})
-	}
-	emits('click')
-}
-</script>
-
-<style lang="scss" scoped>
-.fs-notice-bar{
-	margin: 0 var(--gutter);
-	position: relative;
-	display: flex;
-	align-items: center;
-	
-	&-notice{
-		margin-left: var(--gutter);
-	}
-	
-	&-swiper{
-		height: 80rpx;
-		flex: 1;
-	}
-	&-item{
-		height: 80rpx;
-		line-height: 80rpx;
-		padding: 0 var(--gutter);
-	}
-	
-	&-close{
-		margin-right: var(--gutter);
-	}
-}
+<template>
+	<view class="fs-notice-bar" :animation="animationData" :style="{ backgroundColor: bgColor, color }" v-if="visible">
+		<fs-icon v-if="showIcon" class="fs-notice-bar-notice" type="icon-sound" :color="color"></fs-icon>
+		<swiper class="fs-notice-bar-swiper" autoplay circular :vertical="vertical" :interval="interval" :duration="1000">
+			<swiper-item v-for="(item, index) in list" :key="index">
+				<view class="fs-notice-bar-item line1" @click="handleRoute(item)">{{ item[titleKey] }}</view>
+			</swiper-item>
+		</swiper>
+		<fs-icon
+			v-if="showClose"
+			class="fs-notice-bar-close"
+			:color="color"
+			type="icon-close-circle"
+			@click="handleClose"
+		></fs-icon>
+	</view>
+</template>
+
+<script>
+/**
+ * 通告栏组件
+ * @description 通告栏组件
+ * @property {Array} list 通告栏列表
+ * @property {String} titleKey 展示标题的key
+ * @property {String} urlKey 跳转路径的key
+ * @property {String} bgColor 背景色
+ * @property {String} color 文字颜色
+ * @property {Number} vertical 是否垂直滚动
+ * @property {String} interval 滚动间隔
+ * @property {Boolean} showClose 是否显示关闭按钮
+ * @property {Boolean} showIcon 是否显示通知icon
+ * @property {String} linkType  跳转类型
+ */
+export default {
+	name: 'fs-notice-bar'
+}
+</script>
+
+<script setup>
+import { ref } from 'vue'
+
+const props = defineProps({
+	list: {
+		type: Array,
+		default() {
+			return []
+		}
+	},
+	titleKey: {
+		type: String,
+		default: 'title'
+	},
+	urlKey: {
+		type: String,
+		default: 'url'
+	},
+	bgColor: {
+		type: String,
+		default: '#fffbe8'
+	},
+	color: {
+		type: String,
+		default: '#de8c17'
+	},
+	vertical: Boolean,
+	interval: {
+		type: Number,
+		default: 5000
+	},
+	showClose: {
+		type: Boolean,
+		default: true
+	},
+	showIcon: {
+		type: Boolean,
+		default: true
+	},
+	linkType: {
+		type: String,
+		default: 'navigateTo'
+	}
+})
+const emits = defineEmits(['click'])
+
+let visible = ref(true)
+let animationData = ref(null)
+const animation = uni.createAnimation({
+	duration: 200
+})
+
+const handleClose = () => {
+	animation
+		.scale(0.7, 0.7)
+		.opacity(0)
+		.step()
+	animationData.value = animation.export()
+
+	setTimeout(() => {
+		visible.value = false
+	}, 200)
+}
+
+const handleRoute = item => {
+	if (item[urlKey]) {
+		uni[props.linkType]({
+			url: item[urlKey]
+		})
+	}
+	emits('click')
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-notice-bar {
+	margin: 0 var(--gutter);
+	position: relative;
+	display: flex;
+	align-items: center;
+
+	&-notice {
+		margin-left: var(--gutter);
+	}
+
+	&-swiper {
+		height: 80rpx;
+		flex: 1;
+	}
+	&-item {
+		height: 80rpx;
+		line-height: 80rpx;
+		padding: 0 var(--gutter);
+	}
+
+	&-close {
+		margin-right: var(--gutter);
+	}
+}
 </style>

+ 33 - 25
components/fs-number-box/fs-number-box.vue

@@ -1,8 +1,6 @@
 <template>
-	<view class="fs-number-box" :class="{'fs-number-box-round': round}">
-		<view 
-			class="fs-number-box-item fs-number-box-left"
-			@click="minus">
+	<view class="fs-number-box" :class="{ 'fs-number-box-round': round }">
+		<view class="fs-number-box-item fs-number-box-left" @click="minus">
 			<fs-icon type="icon-minus" size="32rpx" :color="minusDisabled ? '#c8c9cc' : ''"></fs-icon>
 		</view>
 		<input
@@ -12,18 +10,25 @@
 			@blur="handleChange"
 			:disabled="disableInput"
 		/>
-		<view
-			class="fs-number-box-item fs-number-box-right"
-			@click="add">
+		<view class="fs-number-box-item fs-number-box-right" @click="add">
 			<fs-icon type="icon-plus" size="32rpx" :color="addDisabled ? '#c8c9cc' : ''"></fs-icon>
 		</view>
 	</view>
 </template>
 
 <script>
-	export default {
-		name: 'fs-number-box',
-	}
+/**
+ * 步进器组件
+ * @description 步进器组件
+ * @property {Number} min 最小值
+ * @property {Number} max 最大值
+ * @property {Number} step 步幅
+ * @property {Boolean} round 是否圆角
+ * @property {Boolean} disableInput 禁止输入框输入
+ */
+export default {
+	name: 'fs-number-box'
+}
 </script>
 
 <script setup>
@@ -47,15 +52,18 @@ const props = defineProps({
 		default: 1
 	},
 	round: Boolean,
-	disableInput: Boolean,
+	disableInput: Boolean
 })
 
-const emits = defineEmits(['update:modelValue','change'])
+const emits = defineEmits(['update:modelValue', 'change'])
 
 let initValue = ref(props.modelValue)
-watch(() => props.modelValue, val => {
-	initValue.value = val
-})
+watch(
+	() => props.modelValue,
+	val => {
+		initValue.value = val
+	}
+)
 watch(initValue, val => {
 	emits('update:modelValue', val)
 	emits('change', val)
@@ -74,7 +82,7 @@ const minus = () => {
 }
 const handleChange = e => {
 	initValue.value = Number(e.detail.value) || props.min
-	
+
 	if (initValue.value < props.min) {
 		initValue.value = props.min
 	} else if (initValue.value > props.max) {
@@ -87,29 +95,29 @@ const addDisabled = computed(() => initValue.value === props.max)
 </script>
 
 <style lang="scss" scoped>
-.fs-number-box{
+.fs-number-box {
 	display: inline-flex;
 	border: 2rpx solid var(--border-color);
 	height: 60rpx;
 	background-color: #fff;
-	
-	&-round{
+
+	&-round {
 		border-radius: 30rpx;
 	}
-	
-	&-item{
+
+	&-item {
 		display: flex;
 		height: 100%;
 		justify-content: center;
 		align-items: center;
 	}
-	
+
 	&-left,
-	&-right{
+	&-right {
 		width: 60rpx;
 	}
-	
-	&-middle{
+
+	&-middle {
 		box-sizing: border-box;
 		width: 80rpx;
 		border-left: 2rpx solid var(--border-color);

+ 45 - 36
components/fs-panel/fs-panel.vue

@@ -1,38 +1,47 @@
-<template>
-	<view class="fs-panel">
-	  <view class="fs-panel-title">
-	    <view v-if="title">{{title}}</view>
-	    <view v-else><slot name="title"></slot></view>
-	  </view>
-	  <view 
-			class="fs-panel-content" 
-			:class="{'fs-panel-padding': padding}" 
-			:style="{'background-color':bgColor}"
-		>
-			<slot name="content"></slot>
-		</view>
-	</view>
-</template>
-
-<script setup>
-	const props = defineProps({
-		title: String,
-		padding: Boolean,
-		bgColor: {
-			type: String,
-			default: '#fff'
-		}
-	})
-</script>
-
-<style lang="scss">
-.fs-panel-title{
-  padding: 20rpx var(--gutter);
-  color: var(--title);
-  text-align: left;
-  background-color: var(--bg-color);
+<template>
+	<view class="fs-panel">
+		<view class="fs-panel-title">
+			<view v-if="title">{{ title }}</view>
+			<view v-else><slot name="title"></slot></view>
+		</view>
+		<view class="fs-panel-content" :class="{ 'fs-panel-padding': padding }" :style="{ 'background-color': bgColor }">
+			<slot name="content"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 面板组件
+ * @description 面板组件
+ * @property {String} title 标题
+ * @property {Boolean} padding 内容区域是否带padding
+ * @property {String} bgColor 内容区域背景色
+ */
+export default {
+	name: 'fs-panel'
+}
+</script>
+
+<script setup>
+const props = defineProps({
+	title: String,
+	padding: Boolean,
+	bgColor: {
+		type: String,
+		default: '#fff'
+	}
+})
+</script>
+
+<style lang="scss">
+.fs-panel-title {
+	padding: 20rpx var(--gutter);
+	color: var(--title);
+	text-align: left;
+	background-color: var(--bg-color);
+}
+.fs-panel-padding {
+	padding: var(--gutter);
 }
-.fs-panel-padding{
-  padding: var(--gutter);
-}
 </style>

+ 13 - 0
components/fs-popover/fs-popover.vue

@@ -27,6 +27,19 @@
 </template>
 
 <script>
+/**
+ * 气泡弹出层组件
+ * @description 气泡弹出层组件
+ * @property {String} valueKey 作为 value 唯一标识的键名
+ * @property {Array} actions 选项列表
+ * @property {String} placement = [top | top-start | top-end | bottom | bottom-start | bottom-end,left | left-start | left-end,right | right-start | right-end] 弹出位置
+ * @property {String} bgColor 背景色
+ * @property {String} textColor 文字颜色
+ * @property {String} activeColor 文字选中颜色
+ * @property {String} borderColor 边框颜色
+ * @property {String} width 气泡弹出层宽度
+ * @property {String} actionWidth 选项列表宽度
+ */
 export default {
 	name: 'fs-popover'
 }

+ 28 - 12
components/fs-popup/fs-popup.vue

@@ -1,12 +1,28 @@
 <template>
 	<view class="fs-popup">
-		<view class="fs-popup-drawer" :class="[direction,{show:modelValue}]" :style="[style,customStyle]">
+		<view class="fs-popup-drawer" :class="[direction, { show: modelValue }]" :style="[style, customStyle]">
 			<slot></slot>
 		</view>
 		<fs-mask v-if="showMask" :modelValue="modelValue" @close="handleClose" :maskClickable="maskClickable"></fs-mask>
 	</view>
 </template>
 
+<script>
+/**
+ * 弹出层组件
+ * @description 弹出层组件
+ * @property {String} direction = [left | right | top | bottom] 弹出位置
+ * @property {Boolean} showMask 是否显示遮罩
+ * @property {Boolean} maskClickable 遮罩是否可点击
+ * @property {String} width 弹出层宽度(仅direction为left\right有效)
+ * @property {String} height 弹出层高度(仅direction为top\bottom有效)
+ * @property {Object} customStyle 自定义样式
+ */
+export default {
+	name: 'fs-popup'
+}
+</script>
+
 <script setup>
 import { computed } from 'vue'
 
@@ -36,8 +52,8 @@ const props = defineProps({
 		default: true
 	},
 	customStyle: {
-	  type: Object,
-	  default() {
+		type: Object,
+		default() {
 			return {}
 		}
 	}
@@ -62,40 +78,40 @@ const handleClose = () => {
 
 <style lang="scss" scoped>
 .fs-popup {
-	&-drawer{
+	&-drawer {
 		position: fixed;
 		background-color: #fff;
 		z-index: 900;
-		transition: all .3s;
+		transition: all 0.3s;
 		overflow: auto;
 	}
-	
-	.left{
+
+	.left {
 		top: var(--window-top);
 		bottom: var(--window-bottom);
 		left: 0;
 		transform: translateX(-100%);
 	}
-	.right{
+	.right {
 		top: var(--window-top);
 		bottom: var(--window-bottom);
 		right: 0;
 		transform: translateX(100%);
 	}
-	.top{
+	.top {
 		top: var(--window-top);
 		right: 0;
 		left: 0;
 		transform: translateY(-200%);
 	}
-	.bottom{
+	.bottom {
 		left: 0;
 		bottom: var(--window-bottom);
 		right: 0;
 		transform: translateY(100%);
 	}
-	
-	.show{
+
+	.show {
 		transform: translateX(0);
 	}
 }

+ 95 - 76
components/fs-radio-button/fs-radio-button.vue

@@ -1,89 +1,108 @@
 <template>
-	<view
-		class="fs-radio-button" 
-		:class="[
-			selected ? checkedColorType : 'fs-radio-button-default',
-			{'fs-radio-button-radius':radius, 'fs-radio-button-round':round},
-			buttonSize
-		]"
-		:style="{color:checkedColor}"
-		@click="handleToggle">
-			{{label}}
-			<slot />
+	<view
+		class="fs-radio-button"
+		:class="[
+			selected ? checkedColorType : 'fs-radio-button-default',
+			{ 'fs-radio-button-radius': radius, 'fs-radio-button-round': round },
+			buttonSize
+		]"
+		:style="{ color: checkedColor }"
+		@click="handleToggle"
+	>
+		{{ label }}
+		<slot />
 	</view>
 </template>
 
-<script setup>
-import { inject, reactive, watch, ref } from 'vue'
-
-const props = defineProps({
-	label: String,
-	value: {
-		type: null,
-		required: true
-	},
-	checkedColor: String,
-	checkedColorType: String,
-	size: {
-	  type: String,
-	  validator(value) {
-	  	return ['mini', 'small', 'medium'].includes(value)
-	  }
-	},
-	checked: Boolean
-})
-
-const radioGroup = inject('radioGroup')
-const { radius, round } = radioGroup
+<script>
+/**
+ * 单选框组件
+ * @description 单选框组件
+ * @property {String} label 文本
+ * @property {null} value 标识符(必须传)
+ * @property {String} size = [mini | small | medium] 按钮大小
+ * @property {String} checkedColor 选中颜色
+ * @property {String} checkedColorType = [primary | danger | warning | info | success] 选中颜色类型
+ */
+export default {
+	name: 'fs-radio-button'
+}
+</script>
+
+<script setup>
+import { inject, reactive, watch, ref } from 'vue'
+
+const props = defineProps({
+	label: String,
+	value: {
+		type: null,
+		required: true
+	},
+	checkedColor: String,
+	checkedColorType: String,
+	size: {
+		type: String,
+		validator(value) {
+			return ['mini', 'small', 'medium'].includes(value)
+		}
+	},
+	checked: Boolean
+})
+
+const radioGroup = inject('radioGroup')
+const { radius, round } = radioGroup
 const checkedColorType = props.checkedColorType || radioGroup.checkedColorType
 const checkedColor = props.checkedColor || radioGroup.checkedColor
-const buttonSize = props.size || radioGroup.size
-
-let selected = ref(props.checked)
-watch(() => props.checked, val => {
-	selected.value = val
-})
-
-radioGroup.updateChildren({
-	selected,
-	value: props.value
-})
-
-const handleToggle = () => {
+const buttonSize = props.size || radioGroup.size
+
+let selected = ref(props.checked)
+watch(
+	() => props.checked,
+	val => {
+		selected.value = val
+	}
+)
+
+radioGroup.updateChildren({
+	selected,
+	value: props.value
+})
+
+const handleToggle = () => {
 	radioGroup.updateValue(props.value)
 }
 </script>
 
 <style lang="scss" scoped>
 .fs-radio-button {
-	padding: 10rpx 30rpx;
-	white-space: nowrap;
-	border: 2rpx solid currentColor;
-	margin-right: 20rpx;
-	margin-bottom: 20rpx;
-	
-	&-default {
-		color: #999999;
-	}
-	
-	&-radius{
-		border-radius: var(--radius);
-	}
-	&-round{
-		border-radius: 60rpx;
-	}
-	
-	&.medium{
-		padding: 8rpx 25rpx;
-		font-size: 13px;
-	}
-	&.small{
-		padding: 6rpx 20rpx;
-		font-size: 12px;
-	}
-	&.mini{
-		padding: 2rpx 15rpx;
-		font-size: 11px;
-	}
-}
+	padding: 10rpx 30rpx;
+	white-space: nowrap;
+	border: 2rpx solid currentColor;
+	margin-right: 20rpx;
+	margin-bottom: 20rpx;
+
+	&-default {
+		color: #999999;
+	}
+
+	&-radius {
+		border-radius: var(--radius);
+	}
+	&-round {
+		border-radius: 60rpx;
+	}
+
+	&.medium {
+		padding: 8rpx 25rpx;
+		font-size: 13px;
+	}
+	&.small {
+		padding: 6rpx 20rpx;
+		font-size: 12px;
+	}
+	&.mini {
+		padding: 2rpx 15rpx;
+		font-size: 11px;
+	}
+}
 </style>

+ 62 - 50
components/fs-radio-cell/fs-radio-cell.vue

@@ -1,52 +1,64 @@
-<template>
-	<fs-cell
-		border
-		justify="right" 
-		@click="handleToggle">
-		<template #title>
-			<view :class="selected ? checkedColorType : ''" :style="{color: selected ? checkedColor : ''}">
-				<slot>{{label}}</slot>
-			</view>
-		</template>
-		<template #value>
-			<fs-icon type="icon-right" :colorType="checkedColorType" :color="checkedColor" v-if="selected"></fs-icon>
-		</template>
-	</fs-cell>
-</template>
-
-<script setup>
-import { ref, watch, inject } from 'vue'
-
-const props = defineProps({
-	label: String,
-	value: {
-		type: null,
-		required: true
-	},
-	checkedColor: String,
-	checkedColorType: String,
-	checked: Boolean
-})
-
-let selected = ref(props.checked)
-watch(() => props.checked, val => {
-	selected.value = val
-})
-
-const radioGroup = inject('radioGroup')
+<template>
+	<fs-cell border justify="right" @click="handleToggle">
+		<template #title>
+			<view :class="selected ? checkedColorType : ''" :style="{ color: selected ? checkedColor : '' }">
+				<slot>{{ label }}</slot>
+			</view>
+		</template>
+		<template #value>
+			<fs-icon type="icon-right" :colorType="checkedColorType" :color="checkedColor" v-if="selected"></fs-icon>
+		</template>
+	</fs-cell>
+</template>
+
+<script>
+/**
+ * 单选框组件
+ * @description 单选框组件
+ * @property {String} label 文本
+ * @property {null} value 标识符(必须传)
+ * @property {String} checkedColor 选中颜色
+ * @property {String} checkedColorType = [primary | danger | warning | info | success] 选中颜色类型
+ */
+export default {
+	name: 'fs-radio-cell'
+}
+</script>
+
+<script setup>
+import { ref, watch, inject } from 'vue'
+
+const props = defineProps({
+	label: String,
+	value: {
+		type: null,
+		required: true
+	},
+	checkedColor: String,
+	checkedColorType: String,
+	checked: Boolean
+})
+
+let selected = ref(props.checked)
+watch(
+	() => props.checked,
+	val => {
+		selected.value = val
+	}
+)
+
+const radioGroup = inject('radioGroup')
 const checkedColorType = props.checkedColorType || radioGroup.checkedColorType
-const checkedColor = props.checkedColor || radioGroup.checkedColor
-
-radioGroup.updateChildren({
-	selected,
-	value: props.value
-})
-
-const handleToggle = () => {
+const checkedColor = props.checkedColor || radioGroup.checkedColor
+
+radioGroup.updateChildren({
+	selected,
+	value: props.value
+})
+
+const handleToggle = () => {
 	radioGroup.updateValue(props.value)
-}
-</script>
-
-<style lang="scss" scoped>
-
-</style>
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 83 - 60
components/fs-radio-group/fs-radio-group.vue

@@ -1,73 +1,96 @@
 <template>
-	<view class="fs-radio-group" :class="{inline}">
-		<slot></slot>
-	</view>
+	<view class="fs-radio-group" :class="{ inline }"><slot></slot></view>
 </template>
 
-<script setup>
-import { provide, reactive, watch } from 'vue'
-
-const props = defineProps({
-	justify: String,
-	reverse: Boolean,
-	inline: Boolean,
-	checkedColor: String,
-	checkedColorType: {
-	  type: String,
-	  default: 'primary',
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-		}
-	},
-	radius: Boolean,
-	round: Boolean,
-	size: {
-	  type: String,
-	  validator(value) {
-	  	return ['mini', 'small', 'medium'].includes(value)
-	  }
-	},
-	modelValue: String
-})
-const emits = defineEmits(['update:modelValue', 'change'])
-
-const state = reactive({
-	selectedValue: props.modelValue,
-	children: []
-})
-watch(() => props.modelValue, val => {
-	state.selectedValue = val
-})
-
+<script>
+/**
+ * 单选框组组件
+ * @description 单选框组组件
+ * @property {String} justify 图标对齐方式
+ * @property {Boolean} reverse 是否反转
+ * @property {Boolean} inline 单行显示
+ * @property {Boolean} radius 是否圆角(仅对按钮样式有效)
+ * @property {Boolean} round 是否半圆(仅对按钮样式有效)
+ * @property {String} checkedColor 选中颜色
+ * @property {String} checkedColorType = [primary | danger | warning | info | success] 选中颜色类型
+ * @property {String} size = [mini | small | medium] 按钮大小(仅对按钮样式有效)
+ * @event {Function} change change事件
+ */
+export default {
+	name: 'fs-radio-group'
+}
+</script>
+
+<script setup>
+import { provide, reactive, watch } from 'vue'
+
+const props = defineProps({
+	justify: String,
+	reverse: Boolean,
+	inline: Boolean,
+	checkedColor: String,
+	checkedColorType: {
+		type: String,
+		default: 'primary',
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	},
+	radius: Boolean,
+	round: Boolean,
+	size: {
+		type: String,
+		validator(value) {
+			return ['mini', 'small', 'medium'].includes(value)
+		}
+	},
+	modelValue: String
+})
+const emits = defineEmits(['update:modelValue', 'change'])
+
+const state = reactive({
+	selectedValue: props.modelValue,
+	children: []
+})
+watch(
+	() => props.modelValue,
+	val => {
+		state.selectedValue = val
+	}
+)
+
 const radioStrategy = value => {
 	state.children.forEach(item => {
 		item.selected = item.value === state.selectedValue
 	})
-}
-const updateChildren = child => {
-	state.children.push(child)
-	radioStrategy()
-}
-const updateValue = value => state.selectedValue = value
-
-watch(() => state.selectedValue, val => {
-	radioStrategy()
-	emits('update:modelValue', val)
-	emits('change', val)
-})
-
-provide('radioGroup', {
-	...props,
-	updateChildren,
-	updateValue
+}
+const updateChildren = child => {
+	state.children.push(child)
+	radioStrategy()
+}
+const updateValue = value => (state.selectedValue = value)
+
+watch(
+	() => state.selectedValue,
+	val => {
+		radioStrategy()
+		emits('update:modelValue', val)
+		emits('change', val)
+	}
+)
+
+provide('radioGroup', {
+	...props,
+	updateChildren,
+	updateValue
 })
 </script>
 
 <style lang="scss">
-.fs-radio-group{
-	&.inline{
-		display: flex;
-		flex-wrap: wrap;
-	}
+.fs-radio-group {
+	&.inline {
+		display: flex;
+		flex-wrap: wrap;
+	}
 }
 </style>

+ 104 - 84
components/fs-radio/fs-radio.vue

@@ -1,94 +1,114 @@
 <template>
-	<view 
-		class="fs-radio" 
-		:class="['fs-radio-' + justify,{'fs-radio-reverse': reverse,'fs-radio-inline': inline}]" 
-		@click="handleToggle">
-	  <fs-icon 
-			v-if="icon" 
-			source="out" 
-			:type="selected ? (selectIcon || icon) : icon" 
-			:color-type="selected ? checkedColorType : 'gray'" 
-			:size="iconSize" 
-			:color="checkedColor">
-		</fs-icon>
-	  <fs-icon 
-			v-else 
-			:type="selected ? 'icon-checked' : 'icon-uncheck'" 
-			:color-type="selected ? checkedColorType : 'gray'" 
-			:size="iconSize" 
-			:color="checkedColor">
-		</fs-icon>
-	  <view class="fs-radio-lable">
-	    {{label}}
-	    <slot />
-	  </view>
+	<view
+		class="fs-radio"
+		:class="['fs-radio-' + justify, { 'fs-radio-reverse': reverse, 'fs-radio-inline': inline }]"
+		@click="handleToggle"
+	>
+		<fs-icon
+			v-if="icon"
+			source="out"
+			:type="selected ? selectIcon || icon : icon"
+			:color-type="selected ? checkedColorType : 'gray'"
+			:size="iconSize"
+			:color="checkedColor"
+		></fs-icon>
+		<fs-icon
+			v-else
+			:type="selected ? 'icon-checked' : 'icon-uncheck'"
+			:color-type="selected ? checkedColorType : 'gray'"
+			:size="iconSize"
+			:color="checkedColor"
+		></fs-icon>
+		<view class="fs-radio-lable">
+			{{ label }}
+			<slot />
+		</view>
 	</view>
 </template>
 
-<script setup>
-import { inject, reactive, watch, toRefs, ref } from 'vue'
-
-const props = defineProps({
-	label: String,
-	iconSize: {
-		type: String,
-		default: '42rpx'
-	},
-	value: {
-		type: null,
-		required: true
-	},
-	icon: String,
-	selectIcon: String,
-	checkedColor: String,
-	checkedColorType: {
-		type: String,
-		default: 'primary'
-	},
-	checked: Boolean
-})
-const emits = defineEmits(['change'])
-
-const radioGroup = inject('radioGroup')
-const { reverse, justify, inline } = radioGroup
-
-let selected = ref(props.checked)
-watch(() => props.checked, val => {
-	selected.value = val
-})
-
-radioGroup.updateChildren({
-	selected,
-	value: props.value
-})
-
-const handleToggle = () => {
+<script>
+/**
+ * 单选框组件
+ * @description 单选框组件
+ * @property {String} label 文本
+ * @property {String} icon 自定义图标
+ * @property {String} iconSize 图标大小
+ * @property {String} selectIcon 自定义选中图标
+ * @property {null} value 标识符(必须传)
+ * @property {String} checkedColor 选中颜色
+ * @property {String} checkedColorType = [primary | danger | warning | info | success] 选中颜色类型
+ */
+export default {
+	name: 'fs-radio'
+}
+</script>
+
+<script setup>
+import { inject, reactive, watch, toRefs, ref } from 'vue'
+
+const props = defineProps({
+	label: String,
+	iconSize: {
+		type: String,
+		default: '42rpx'
+	},
+	value: {
+		type: null,
+		required: true
+	},
+	icon: String,
+	selectIcon: String,
+	checkedColor: String,
+	checkedColorType: {
+		type: String,
+		default: 'primary'
+	}
+})
+const emits = defineEmits(['change'])
+
+const radioGroup = inject('radioGroup')
+const { reverse, justify, inline } = radioGroup
+
+let selected = ref(props.checked)
+watch(
+	() => props.checked,
+	val => {
+		selected.value = val
+	}
+)
+
+radioGroup.updateChildren({
+	selected,
+	value: props.value
+})
+
+const handleToggle = () => {
 	radioGroup.updateValue(props.value)
 }
 </script>
 
 <style lang="scss" scoped>
-.fs-radio{
-  display: flex;
-  align-items: center;
-  justify-content: flex-start;
-	padding: 10rpx 0;
-	
-	&-lable{
-	  margin-left: 6rpx;
-	  margin-right: 20rpx;
-	}
-	
-	&-reverse{
-	  flex-direction: row-reverse;
-	  justify-content: flex-end;
-	}
-	&-reverse &-lable {
-		margin-left: 20rpx;
-		margin-right: 6rpx;
-	}
-	&-right{
-	  justify-content: space-between;
-	}
-}
+.fs-radio {
+	display: flex;
+	align-items: center;
+	justify-content: flex-start;
+	padding: 10rpx 0;
+
+	&-lable {
+		margin-left: 6rpx;
+		margin-right: 20rpx;
+	}
+
+	&-reverse {
+		flex-direction: row-reverse;
+		justify-content: flex-end;
+	}
+	&-reverse &-lable {
+		margin-left: 20rpx;
+		margin-right: 6rpx;
+	}
+	&-right {
+		justify-content: space-between;
+	}
+}
 </style>

+ 119 - 120
components/fs-rate/fs-rate.vue

@@ -1,121 +1,120 @@
-<template>
-	<view class="fs-rate">
-		<view class="fs-rate-item" @click="handleClick(index)" v-for="(item,index) in count" :key="index">
-			<fs-icon
-				:type="index < modelValue ? activeIcon : inactiveIcon"
-				:color="index < modelValue ? activeColor : inactiveColor"
-				:size="iconSize"
-				:source="source"
-			>
-			</fs-icon>
-			<view
-				v-if="allowHalf && index < modelValue && (index + 1) > modelValue"
-				class="fs-rate-half">
-				<view class="fs-rate-half-active" :style="{width: size / 2 + 'rpx'}">
-					<fs-icon
-						:type="activeIcon"
-						:color="activeColor"
-						:size="iconSize"
-						:source="source"
-					>
-					</fs-icon>
-				</view>
-				<view class="fs-rate-half-inactive">
-					<fs-icon
-						:type="activeIcon"
-						:color="inactiveColor"
-						:size="iconSize"
-						:source="source"
-					>
-					</fs-icon>
-				</view>
-			</view>
-		</view>
-	</view>
-</template>
-
-<script>
-export default {
-	name: 'fs-rate',
-}
-</script>
-
-<script setup>
-import { computed } from 'vue'
-
-const props = defineProps({
-	modelValue: {
-		type: Number,
-		default: 0
-	},
-	count: {
-		type: Number,
-		default: 5
-	},
-	activeColor: {
-		type: String,
-		default: '#F53F3F'
-	},
-	inactiveColor: {
-		type: String,
-		default: '#999999'
-	},
-	size: {
-		type: Number,
-		default: '40'
-	},
-	disabled: Boolean,
-	activeIcon: {
-		type: String,
-		default: 'icon-star-fill'
-	},
-	inactiveIcon: {
-		type: String,
-		default: 'icon-star'
-	},
-	source: {
-		type: String,
-		default: 'inner'
-	},
-	allowHalf: Boolean,
-})
-const emits = defineEmits(['update:modelValue', 'change'])
-
-const iconSize = computed(() =>props.size + 'rpx')
-
-const handleClick = index => {
-	if (!props.disabled) {
-		emits('update:modelValue', index + 1)
-		emits('change', index + 1)
-	}
-}
-</script>
-
-<style lang="scss">
-.fs-rate{
-	display: flex;
-	
-	&-item{
-		position: relative;
-		& + &{
-			margin-left: 8rpx;
-		}
-	}
-	
-	&-half{
-		position: absolute;
-		left: 0;
-		top: 0;
-		z-index: 1;
-		overflow: hidden;
-		
-		&-active{
-			position: absolute;
-			top: 0;
-			left: 0;
-			z-index: 1;
-			overflow: hidden;
-		}
-	}
-}
+<template>
+	<view class="fs-rate">
+		<view class="fs-rate-item" @click="handleClick(index)" v-for="(item, index) in count" :key="index">
+			<fs-icon
+				:type="index < modelValue ? activeIcon : inactiveIcon"
+				:color="index < modelValue ? activeColor : inactiveColor"
+				:size="iconSize"
+				:source="source"
+			></fs-icon>
+			<view v-if="allowHalf && index < modelValue && index + 1 > modelValue" class="fs-rate-half">
+				<view class="fs-rate-half-active" :style="{ width: size / 2 + 'rpx' }">
+					<fs-icon :type="activeIcon" :color="activeColor" :size="iconSize" :source="source"></fs-icon>
+				</view>
+				<view class="fs-rate-half-inactive">
+					<fs-icon :type="activeIcon" :color="inactiveColor" :size="iconSize" :source="source"></fs-icon>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 评分组件
+ * @description 评分组件
+ * @property {Number} count 图标数量
+ * @property {String} activeColor 图标选中的颜色
+ * @property {String} inactiveColor 图标未选中的颜色
+ * @property {String} activeIcon 选中的图标
+ * @property {String} inactiveIcon 未选中的图标
+ * @property {String} size 图标大小(单位rpx)
+ * @property {Boolean} allowHalf 是否允许半星
+ * @property {Boolean} disabled 是否禁用
+ * @property {String} source 图标来源
+ * @event {Function} change change事件
+ */
+export default {
+	name: 'fs-rate'
+}
+</script>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+	modelValue: {
+		type: Number,
+		default: 0
+	},
+	count: {
+		type: Number,
+		default: 5
+	},
+	activeColor: {
+		type: String,
+		default: '#F53F3F'
+	},
+	inactiveColor: {
+		type: String,
+		default: '#999999'
+	},
+	size: {
+		type: Number,
+		default: '40'
+	},
+	disabled: Boolean,
+	activeIcon: {
+		type: String,
+		default: 'icon-star-fill'
+	},
+	inactiveIcon: {
+		type: String,
+		default: 'icon-star'
+	},
+	source: {
+		type: String,
+		default: 'inner'
+	},
+	allowHalf: Boolean
+})
+const emits = defineEmits(['update:modelValue', 'change'])
+
+const iconSize = computed(() => props.size + 'rpx')
+
+const handleClick = index => {
+	if (!props.disabled) {
+		emits('update:modelValue', index + 1)
+		emits('change', index + 1)
+	}
+}
+</script>
+
+<style lang="scss">
+.fs-rate {
+	display: flex;
+
+	&-item {
+		position: relative;
+		& + & {
+			margin-left: 8rpx;
+		}
+	}
+
+	&-half {
+		position: absolute;
+		left: 0;
+		top: 0;
+		z-index: 1;
+		overflow: hidden;
+
+		&-active {
+			position: absolute;
+			top: 0;
+			left: 0;
+			z-index: 1;
+			overflow: hidden;
+		}
+	}
+}
 </style>

+ 111 - 96
components/fs-readmore/fs-readmore.vue

@@ -1,97 +1,112 @@
-<template>
-	<view
-		class="fs-readmore" 
-		:style="{height: isOpen ? 'auto' : `${height + 70}rpx`,paddingBottom: isOpen ? '70rpx' : 0,color}"
-	>
-		<view class="fs-readmore-content layout-box" :style="{backgroundColor:bgColor}">
-			<slot></slot>
-		</view>
-		
-		<view class="fs-readmore-footer" :style="{backgroundColor:bgColor}" @click="handleToggle" v-if="visible">
-			<view>{{isOpen ? '收起' : label}}</view>
-			<fs-icon class="fs-readmore-icon" type="icon-d-down" size="26rpx" :class="{isOpen}"></fs-icon>
-		</view>
-	</view>
-</template>
-
-<script>
-	export default {
-		props: {
-			// 限定多高时展示更多按钮
-			maxHeight: {
-				type: Number,
-				default: 100
-			},
-			label: {
-				type: String,
-				default: '展开更多'
-			},
-			open: Boolean,
-			bgColor: {
-				type: String,
-				default: '#fff'
-			},
-			color: {
-				type: String,
-				default: '#666666'
-			}
-		},
-		data() {
-			return {
-				isOpen: false,
-				visible: false
-			}
-		},
-		mounted() {
-			this.updateHeight()
-		},
-		computed: {
-			height() {
-				return parseFloat(this.maxHeight)
-			}
-		},
-		methods: {
-			handleToggle() {
-				this.isOpen = !this.isOpen
-			},
-			updateHeight() {
-				uni.createSelectorQuery().in(this).select('.fs-readmore-content').boundingClientRect(data => {
-					this.visible = data.height > this.height
-				}).exec()
-			}
-		}
-	}
-</script>
-
-<style lang="scss" scoped>
-.fs-readmore{
-	position: relative;
-	overflow: hidden;
-	
-	&-content{
-		overflow: hidden;
-	}
-	
-	&-footer{
-		display: flex;
-		align-items: center;
-		justify-content: center;
-		position: absolute;
-		z-index: 10;
-		width: 100%;
-		bottom: 0;
-		font-size: 13px;
-		height: 70rpx;
-		line-height: 70rpx;
-		// box-shadow: 0 -6rpx 2rpx rgba(65, 65, 70, 0.2);
-	}
-	
-	&-icon{
-		transition: all .2s;
-		margin-left: 6rpx;
-	}
-	.isOpen{
-		transform: rotate(180deg);
-	}
-}
+<template>
+	<view
+		class="fs-readmore"
+		:style="{ height: isOpen ? 'auto' : `${height + 70}rpx`, paddingBottom: isOpen ? '70rpx' : 0, color }"
+	>
+		<view class="fs-readmore-content layout-box" :style="{ backgroundColor: bgColor }"><slot></slot></view>
+
+		<view class="fs-readmore-footer" :style="{ backgroundColor: bgColor }" @click="handleToggle" v-if="visible">
+			<view>{{ isOpen ? '收起' : label }}</view>
+			<fs-icon class="fs-readmore-icon" type="icon-d-down" size="26rpx" :class="{ isOpen }"></fs-icon>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 展开更多组件
+ * @description 展开更多组件
+ * @property {Number} maxHeight 限定多高时展示更多按钮(单位rpx)
+ * @property {String} label "更多"文本
+ * @property {Boolean} open 是否展开
+ * @property {String} bgColor 背景色
+ * @property {String} color 文本颜色
+ */
+export default {
+	name: 'fs-readmore'
+}
+export default {
+	props: {
+		// 限定多高时展示更多按钮
+		maxHeight: {
+			type: Number,
+			default: 100
+		},
+		label: {
+			type: String,
+			default: '展开更多'
+		},
+		open: Boolean,
+		bgColor: {
+			type: String,
+			default: '#fff'
+		},
+		color: {
+			type: String,
+			default: '#666666'
+		}
+	},
+	data() {
+		return {
+			isOpen: false,
+			visible: false
+		}
+	},
+	mounted() {
+		this.updateHeight()
+	},
+	computed: {
+		height() {
+			return parseFloat(this.maxHeight)
+		}
+	},
+	methods: {
+		handleToggle() {
+			this.isOpen = !this.isOpen
+		},
+		updateHeight() {
+			uni
+				.createSelectorQuery()
+				.in(this)
+				.select('.fs-readmore-content')
+				.boundingClientRect(data => {
+					this.visible = data.height > this.height
+				})
+				.exec()
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-readmore {
+	position: relative;
+	overflow: hidden;
+
+	&-content {
+		overflow: hidden;
+	}
+
+	&-footer {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		position: absolute;
+		z-index: 10;
+		width: 100%;
+		bottom: 0;
+		font-size: 13px;
+		height: 70rpx;
+		line-height: 70rpx;
+		// box-shadow: 0 -6rpx 2rpx rgba(65, 65, 70, 0.2);
+	}
+
+	&-icon {
+		transition: all 0.2s;
+		margin-left: 6rpx;
+	}
+	.isOpen {
+		transform: rotate(180deg);
+	}
+}
 </style>

+ 36 - 27
components/fs-row/fs-row.vue

@@ -1,28 +1,37 @@
-<template>
-	<view class="fs-row" :style="{marginLeft: margin,marginRight:margin}">
-		<slot></slot>
-	</view>
-</template>
-
-<script setup>
-import { computed, toRefs, provide } from 'vue'
-
-const props = defineProps({
-	gutter: {
-		type: [Number,String],
-		default: 0
-	}
-})
-
-const margin = computed(() => `${parseInt(props.gutter) / -2}rpx`)
-
-provide('rowGap', props.gutter)
-</script>
-
-<style lang="scss" scoped>
-.fs-row::after{
-  content: '';
-  display: table;
-  clear: both;
-}
+<template>
+	<view class="fs-row" :style="{ marginLeft: margin, marginRight: margin }"><slot></slot></view>
+</template>
+
+<script>
+/**
+ * 栅格布局组件
+ * @description 栅格布局组件
+ * @property {Number,String} gutter 间距(单位rpx)
+ */
+export default {
+	name: 'fs-row'
+}
+</script>
+
+<script setup>
+import { computed, toRefs, provide } from 'vue'
+
+const props = defineProps({
+	gutter: {
+		type: [Number, String],
+		default: 0
+	}
+})
+
+const margin = computed(() => `${parseInt(props.gutter) / -2}rpx`)
+
+provide('rowGap', props.gutter)
+</script>
+
+<style lang="scss" scoped>
+.fs-row::after {
+	content: '';
+	display: table;
+	clear: both;
+}
 </style>

+ 128 - 116
components/fs-scroll-list/fs-scroll-list.vue

@@ -1,117 +1,129 @@
-<template>
-	<view class="fs-scroll-list">
-		<scroll-view 
-			scroll-x
-			:show-scrollbar="false"
-			:lower-threshold="0" 
-			:upper-threshold="0" 
-			@scrolltoupper="handleToUpper" 
-			@scrolltolower="handleToLower"
-			@scroll="handleScroll"
-		>
-			<view class="fs-scroll-list-content">
-				<slot></slot>
-			</view>
-		</scroll-view>
-		
-		<view class="fs-scroll-indicator" v-if="indicator">
-			<view class="fs-scroll-indicator-line">
-				<view class="fs-scroll-indicator-bar" :style="barStyle"></view>
-			</view>
-		</view>
-	</view>
-</template>
-
-<script>
-export default {
-	name: 'fs-scroll-list'
-}
-</script>
-
-<script setup>
-import { onMounted, reactive, ref, computed, getCurrentInstance } from 'vue'
-
-const props = defineProps({
-	indicator: {
-		type: Boolean,
-		default: true
-	},
-	indicatorWidth: {
-		type: String,
-		default: '30px'
-	},
-	indicatorBarWidth: {
-		type: String,
-		default: '15px'
-	},
-	indicatorColor: {
-		type: String,
-		default: '#f2f2f2'
-	},
-	indicatorActiveColor: {
-		type: String,
-		default: '#3c9cff'
-	}
-})
-const emits = defineEmits(['left', 'right'])
-
-const state = reactive({
-	listWidth: 0,
-	contentWidth: 0,
-	scrollWidth: 0
-})
-onMounted(() => {
-	uni.createSelectorQuery().in(getCurrentInstance().ctx).select('.fs-scroll-list').boundingClientRect(data => {
-		state.listWidth = data.width
-	}).exec()
-})
-
-const indicatorLeftWidth = computed(() => {
-	return parseInt(props.indicatorWidth) - parseInt(props.indicatorBarWidth)
-})
-const listLeftWidth = computed(() => {
-	return state.contentWidth - state.listWidth
-})
-const barStyle = computed(() => {
-	const x = state.scrollWidth * indicatorLeftWidth.value / listLeftWidth.value  + 'px'
-	return `transform: translateX(${x})`
-})
-
-const handleScroll = event => {
-	state.scrollWidth = event.detail.scrollLeft
-	state.contentWidth = event.detail.scrollWidth
-}
-
-const handleToUpper = () => emits('left')
-const handleToLower = () => emits('right')
-</script>
-
-<style lang="scss" scoped>
-.fs-scroll-list{
-	&-box{
-		display: flex;
-	}
-	&-content{
-		display: flex;
-		flex-wrap: nowrap;
-	}
-}
-.fs-scroll-indicator{
-	display: flex;
-	justify-content: center;
-	
-	&-line{
-		background-color: v-bind(indicatorColor);
-		width: v-bind(indicatorWidth);
-		height: 8rpx;
-		border-radius: 50px;
-		overflow: hidden;
-	}
-	&-bar{
-		background-color: v-bind(indicatorActiveColor);
-		height: 8rpx;
-		width: v-bind(indicatorBarWidth);
-		border-radius: 50px;
-	}
-}
+<template>
+	<view class="fs-scroll-list">
+		<scroll-view
+			scroll-x
+			:show-scrollbar="false"
+			:lower-threshold="0"
+			:upper-threshold="0"
+			@scrolltoupper="handleToUpper"
+			@scrolltolower="handleToLower"
+			@scroll="handleScroll"
+		>
+			<view class="fs-scroll-list-content"><slot></slot></view>
+		</scroll-view>
+
+		<view class="fs-scroll-indicator" v-if="indicator">
+			<view class="fs-scroll-indicator-line"><view class="fs-scroll-indicator-bar" :style="barStyle"></view></view>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 滚动列表组件
+ * @description 滚动列表组件
+ * @property {Boolean} indicator 是否显示面板指示器
+ * @property {String} indicatorColor 指示器背景颜色
+ * @property {String} indicatorActiveColor 指示器滑块颜色
+ * @property {String} indicatorWidth 指示器的整体宽度(传值的时候需要带单位px)
+ * @property {String} indicatorBarWidth 滑块的宽度(传值的时候需要带单位px)
+ * @event {Function} left 滑到左边事件
+ * @event {Function} right 滑动右边事件
+ */
+export default {
+	name: 'fs-scroll-list'
+}
+</script>
+
+<script setup>
+import { onMounted, reactive, ref, computed, getCurrentInstance } from 'vue'
+
+const props = defineProps({
+	indicator: {
+		type: Boolean,
+		default: true
+	},
+	indicatorWidth: {
+		type: String,
+		default: '30px'
+	},
+	indicatorBarWidth: {
+		type: String,
+		default: '15px'
+	},
+	indicatorColor: {
+		type: String,
+		default: '#f2f2f2'
+	},
+	indicatorActiveColor: {
+		type: String,
+		default: '#3c9cff'
+	}
+})
+const emits = defineEmits(['left', 'right'])
+
+const state = reactive({
+	listWidth: 0,
+	contentWidth: 0,
+	scrollWidth: 0
+})
+onMounted(() => {
+	uni
+		.createSelectorQuery()
+		.in(getCurrentInstance().ctx)
+		.select('.fs-scroll-list')
+		.boundingClientRect(data => {
+			state.listWidth = data.width
+		})
+		.exec()
+})
+
+const indicatorLeftWidth = computed(() => {
+	return parseInt(props.indicatorWidth) - parseInt(props.indicatorBarWidth)
+})
+const listLeftWidth = computed(() => {
+	return state.contentWidth - state.listWidth
+})
+const barStyle = computed(() => {
+	const x = (state.scrollWidth * indicatorLeftWidth.value) / listLeftWidth.value + 'px'
+	return `transform: translateX(${x})`
+})
+
+const handleScroll = event => {
+	state.scrollWidth = event.detail.scrollLeft
+	state.contentWidth = event.detail.scrollWidth
+}
+
+const handleToUpper = () => emits('left')
+const handleToLower = () => emits('right')
+</script>
+
+<style lang="scss" scoped>
+.fs-scroll-list {
+	&-box {
+		display: flex;
+	}
+	&-content {
+		display: flex;
+		flex-wrap: nowrap;
+	}
+}
+.fs-scroll-indicator {
+	display: flex;
+	justify-content: center;
+
+	&-line {
+		background-color: v-bind(indicatorColor);
+		width: v-bind(indicatorWidth);
+		height: 8rpx;
+		border-radius: 50px;
+		overflow: hidden;
+	}
+	&-bar {
+		background-color: v-bind(indicatorActiveColor);
+		height: 8rpx;
+		width: v-bind(indicatorBarWidth);
+		border-radius: 50px;
+	}
+}
 </style>

+ 167 - 143
components/fs-search/fs-search.vue

@@ -1,170 +1,194 @@
 <template>
-	<view class="fs-search-box" :style="{backgroundColor: bgColor}">
+	<view class="fs-search-box" :style="{ backgroundColor: bgColor }">
 		<view class="fs-search-box-left"><slot name="left"></slot></view>
-		<view class="fs-input-box" :class="[{round}]" @click="handleLink" :style="{backgroundColor: inputBgColor}">
-			<view class="sub fs-input" v-if="link">{{placeholder}}</view>
-			<input
-				v-else 
-				class="fs-input"
-				:value="modelValue"
-				:type="type" 
-				:placeholder="placeholder" 
-				@input="handleChange" 
-				@focus="handleFocus" 
-				@blur="handleBlur" 
-				:focus="autoFocus"
+		<view class="fs-input-box" :class="[{ round }]" @click="handleLink" :style="{ backgroundColor: inputBgColor }">
+			<view class="sub fs-input" v-if="link">{{ placeholder }}</view>
+			<input
+				v-else
+				class="fs-input"
+				:value="modelValue"
+				:type="type"
+				:placeholder="placeholder"
+				@input="handleChange"
+				@focus="handleFocus"
+				@blur="handleBlur"
+				:focus="autoFocus"
 			/>
-			<view class="fs-icon fs-icon-search">
-				<slot name="icon">
-					<fs-icon type="icon-search" color="#666666" size="28rpx"></fs-icon>
-				</slot>
-			</view>
-			<view class="fs-icon fs-icon-close" v-if="modelValue" @click="handleClear">
-				<fs-icon type="icon-close-circle" color="#666666"></fs-icon>
+			<view class="fs-icon fs-icon-search">
+				<slot name="icon"><fs-icon type="icon-search" color="#666666" size="28rpx"></fs-icon></slot>
 			</view>
+			<view class="fs-icon fs-icon-close" v-if="modelValue" @click="handleClear">
+				<fs-icon type="icon-close-circle" color="#666666"></fs-icon>
+			</view>
+		</view>
+		<view
+			v-if="showAction"
+			class="fs-cancel"
+			:class="[actionColorType]"
+			:style="{ color: actionColor }"
+			@click="handleAction"
+		>
+			{{ actionText }}
 		</view>
-		<view 
-			v-if="showAction"
-			class="fs-cancel" 
-			:class="[actionColorType]" 
-			:style="{color:actionColor}"
-			@click="handleAction" 
-		>
-			{{actionText}}
-		</view>
 		<view class="fs-search-box-right"><slot name="right"></slot></view>
 	</view>
 </template>
 
-<script setup>
-	import { ref } from 'vue'
-	
-	const props = defineProps({
-		placeholder: {
-			type: String,
-			default: '搜索'
-		},
-		actionColor: String,
-		actionColorType: {
-			type: String,
-			validator(value) {
-				return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-			}
-		},
-		actionText: {
-			type: String,
-			default: '取消'
-		},
-		autoFocus: Boolean,
-		showAction: Boolean,
-		round: Boolean,
-		type: {
-			type: String,
-			default: 'text'
-		},
-		bgColor: {
-			type: String,
-			default: '#fff'
-		},
-		inputBgColor: {
-			type: String,
-			default: '#f0f0f0'
-		},
-		link: String,
-		linkType: {
-			type: String,
-			default: 'navigateTo'
-		},
-		modelValue: String
-	})
-	
-	const emits = defineEmits(['action', 'focus', 'blur', 'update:modelValue','change'])
-	
-	const handleChange = (e) => {
-		emits('update:modelValue', e.detail.value)
-		emits('change', e.detail.value)
-	}
-	const handleFocus = () => {
-		emits('focus')
-	}
-	const handleBlur = () => {
-		emits('blur')
-	}
-	const handleClear = () => {
-		emits('update:modelValue', '')
-		emits('change', '')
-	}
-	const handleAction = () => {
-		emits('action', props.modelValue)
-	}
-	const handleLink = () => {
-		if (props.link) {
-			uni[props.linkType]({
-				url: props.link
-			})
+<script>
+/**
+ * 搜索组件
+ * @description 搜索组件
+ * @property {String} placeholder 占位符
+ * @property {String} type 输入框类型
+ * @property {String} actionColor 操作文字颜色
+ * @property {String} actionColorType = [primary | danger | warning | info | success] 操作文字颜色类型
+ * @property {String} actionText 操作文字
+ * @property {String} bgColor 背景颜色
+ * @property {String} inputBgColor 输入框背景颜色
+ * @property {String} showAction 是否显示操作
+ * @property {String} round 是否圆角
+ * @property {String} autoFocus 是否圆角
+ * @property {String} link 跳转地址
+ * @property {String} linkType 跳转类型
+ * @event {Function} focus 输入框聚焦事件
+ * @event {Function} blur 输入框失去焦点事件
+ * @event {Function} change 输入框内容变化事件
+ * @event {Function} action 点击操作事件
+ */
+export default {
+	name: 'fs-search'
+}
+</script>
+
+<script setup>
+import { ref } from 'vue'
+
+const props = defineProps({
+	placeholder: {
+		type: String,
+		default: '搜索'
+	},
+	actionColor: String,
+	actionColorType: {
+		type: String,
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
 		}
-	}
+	},
+	actionText: {
+		type: String,
+		default: '取消'
+	},
+	autoFocus: Boolean,
+	showAction: Boolean,
+	round: Boolean,
+	type: {
+		type: String,
+		default: 'text'
+	},
+	bgColor: {
+		type: String,
+		default: '#fff'
+	},
+	inputBgColor: {
+		type: String,
+		default: '#f0f0f0'
+	},
+	link: String,
+	linkType: {
+		type: String,
+		default: 'navigateTo'
+	},
+	modelValue: String
+})
+
+const emits = defineEmits(['action', 'focus', 'blur', 'update:modelValue', 'change'])
+
+const handleChange = e => {
+	emits('update:modelValue', e.detail.value)
+	emits('change', e.detail.value)
+}
+const handleFocus = () => {
+	emits('focus')
+}
+const handleBlur = () => {
+	emits('blur')
+}
+const handleClear = () => {
+	emits('update:modelValue', '')
+	emits('change', '')
+}
+const handleAction = () => {
+	emits('action', props.modelValue)
+}
+const handleLink = () => {
+	if (props.link) {
+		uni[props.linkType]({
+			url: props.link
+		})
+	}
+}
 </script>
 
 <style lang="scss" scoped>
-.fs-search-box{
-  width: 100%;
-  height: 110rpx;
-  padding: 20rpx var(--gutter);
-  display: flex;
-  background-color: #fff;
-  box-sizing: border-box;
-  align-items: center;
+.fs-search-box {
+	width: 100%;
+	height: 110rpx;
+	padding: 20rpx var(--gutter);
+	display: flex;
+	background-color: #fff;
+	box-sizing: border-box;
+	align-items: center;
 }
 .fs-input-box {
-  position: relative;
-  height: 100%;
-  width: 100%;
-  flex: 1;
-  background-color: #f0f0f0;
-	
-	.sub{
-	  line-height: 70rpx;
-	  color: var(--sub);
-	}
-	
-	&.round{
-		border-radius: 20px;
-		.fs-input{
-			border-radius: inherit;
+	position: relative;
+	height: 100%;
+	width: 100%;
+	flex: 1;
+	background-color: #f0f0f0;
+
+	.sub {
+		line-height: 70rpx;
+		color: var(--sub);
+	}
+
+	&.round {
+		border-radius: 20px;
+		.fs-input {
+			border-radius: inherit;
 		}
 	}
 }
-.fs-input{
-  height: 100%;
-  width: 100%;
-  padding-left: 68rpx;
-  padding-right: 70rpx;
-  border-radius: 6rpx;
-  box-sizing: border-box;
-	outline: none;
-	
-	/* #ifdef H5 */
-	background-color: inherit;
-	border: none;
+.fs-input {
+	height: 100%;
+	width: 100%;
+	padding-left: 68rpx;
+	padding-right: 70rpx;
+	border-radius: 6rpx;
+	box-sizing: border-box;
+	outline: none;
+
+	/* #ifdef H5 */
+	background-color: inherit;
+	border: none;
 	/* #endif */
 }
 
-.fs-cancel{
-  margin-left: 20rpx;
+.fs-cancel {
+	margin-left: 20rpx;
 }
-.fs-icon{
-  position: absolute;
-  top: 50%;
-  transform: translateY(-50%);
-  color: var(--sub);
-  z-index: 10;
+.fs-icon {
+	position: absolute;
+	top: 50%;
+	transform: translateY(-50%);
+	color: var(--sub);
+	z-index: 10;
 }
-.fs-icon-search{
-  left: 20rpx;
+.fs-icon-search {
+	left: 20rpx;
 	line-height: 1;
 }
-.fs-icon-close{
-  right: 20rpx;
+.fs-icon-close {
+	right: 20rpx;
 }
 </style>

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

@@ -20,6 +20,18 @@
 </template>
 
 <script>
+/**
+ * 选择器组件
+ * @description 选择器组件
+ * @property {Array} actions 选项列表
+ * @property {String} valueKey 作为 value 唯一标识的键名
+ * @property {String} placement = [top | top-start | top-end | bottom | bottom-start | bottom-end,left | left-start | left-end,right | right-start | right-end] 弹出位置
+ * @property {String} placeholder 占位符
+ * @property {String} width select宽度
+ * @property {String} actionWidth 选项列表宽度
+ * @property {Boolean} border 显示边框
+ * @event {Function} change 选项变化事件
+ */
 export default {
 	name: 'fs-select'
 }

+ 32 - 17
components/fs-sidebar/fs-sidebar.vue

@@ -1,21 +1,36 @@
 <template>
 	<view class="fs-side-bar">
-		<view class="fs-side-bar-left" :style="{width: width}">
-			<view 
+		<view class="fs-side-bar-left" :style="{ width: width }">
+			<view
 				class="fs-side-bar-item line1"
-				:class="{'fs-side-bar-active': activeId ? (activeId === item[valueKey]) : (index === 0)}"
+				:class="{ 'fs-side-bar-active': activeId ? activeId === item[valueKey] : index === 0 }"
 				v-for="(item, index) in list"
 				:key="item[valueKey]"
-				@click="handleClick(item, index)">
-				{{item[titleKey]}}
+				@click="handleClick(item, index)"
+			>
+				{{ item[titleKey] }}
 			</view>
 		</view>
-		<view class="fs-side-bar-right">
-			<slot></slot>
-		</view>
+		<view class="fs-side-bar-right"><slot></slot></view>
 	</view>
 </template>
 
+<script>
+/**
+ * 侧边栏组件
+ * @description 侧边栏组件
+ * @property {Array} list sidebar列表
+ * @property {String} value 默认激活项的value(通常为id)
+ * @property {String} valueKey 激活项value的key
+ * @property {String} titleKey 显示内容的key
+ * @property {String} width 侧边栏组宽度
+ * @event {Function} change 激活项变化事件
+ */
+export default {
+	name: 'fs-sidebar'
+}
+</script>
+
 <script setup>
 import { ref } from 'vue'
 
@@ -53,24 +68,24 @@ const handleClick = (item, index) => {
 </script>
 
 <style lang="scss" scoped>
-.fs-side-bar{
+.fs-side-bar {
 	display: flex;
 	height: 100%;
-	
-	&-left{
+
+	&-left {
 		flex-shrink: 0;
 		background-color: #fafafa;
 		overflow: auto;
 	}
-	
-	&-item{
+
+	&-item {
 		padding: 30rpx var(--gutter);
 		position: relative;
 	}
-	&-active{
+	&-active {
 		background-color: #fff;
 		color: var(--primary);
-		&::before{
+		&::before {
 			position: absolute;
 			content: '';
 			left: 0;
@@ -80,8 +95,8 @@ const handleClick = (item, index) => {
 			background-color: currentColor;
 		}
 	}
-	
-	&-right{
+
+	&-right {
 		flex: 1;
 		background-color: #fff;
 		overflow: auto;

+ 13 - 6
components/fs-space/fs-space.vue

@@ -1,14 +1,21 @@
 <template>
-	<view class="fs-space" :class="{'gutter-v': gutter}" :style="{gap: size}">
-		<slot></slot>
-	</view>
+	<view class="fs-space" :class="{ 'gutter-v': gutter }" :style="{ gap: size }"><slot></slot></view>
 </template>
 
 <script>
+/**
+ * 间距组件
+ * @description 间距组件(设置组件之间的间距)
+ * @property {String} size 间距大小
+ * @property {String} direction 间距方向
+ * @property {String} justify 间距方向
+ * @property {Boolean} gutter 下边距
+ */
 export default {
-	name: "fs-space"
+	name: 'fs-space'
 }
 </script>
+
 <script setup>
 const props = defineProps({
 	size: {
@@ -31,11 +38,11 @@ const props = defineProps({
 </script>
 
 <style lang="scss" scoped>
-.fs-space{
+.fs-space {
 	display: flex;
 	flex-direction: v-bind(direction);
 	flex-wrap: wrap;
 	justify-content: v-bind(justify);
 	align-items: center;
 }
-</style>
+</style>

+ 11 - 0
components/fs-swipe-action-group/fs-swipe-action-group.vue

@@ -2,6 +2,17 @@
 	<view><slot></slot></view>
 </template>
 
+<script>
+/**
+ * 滑动面板组件
+ * @description 滑动面板组件
+ * @property {Boolean} autoClose 是否自动关闭其他swipe按钮组
+ */
+export default {
+	name: 'fs-swipe-action-group'
+}
+</script>
+
 <script setup>
 import { provide, reactive } from 'vue'
 

+ 15 - 0
components/fs-swipe-action/fs-swipe-action.vue

@@ -27,6 +27,21 @@
 	</movable-area>
 </template>
 
+<script>
+/**
+ * 滑动面板组件
+ * @description 滑动面板组件
+ * @property {Array} options 操作选项
+ * @property {null} optionData 操作选项数据
+ * @property {Number} optionWidth 操作选项宽度(单位px)
+ * @property {Boolean} disabled 操作选项数据
+ * @property {Number} swipeRate 滑动比例阈值,大于此值会自动打开或关闭
+ */
+export default {
+	name: 'fs-swipe-action'
+}
+</script>
+
 <script setup>
 import { inject, onMounted, reactive, ref, nextTick, getCurrentInstance } from 'vue'
 

+ 49 - 20
components/fs-swiper/fs-swiper.vue

@@ -4,9 +4,9 @@
 		:indicator-dots="indicatorDots"
 		:indicator-color="indicatorColor"
 		:indicator-active-color="indicatorActiveColor"
-		:autoplay="autoplay" 
-		:interval="interval" 
-		:duration="duration" 
+		:autoplay="autoplay"
+		:interval="interval"
+		:duration="duration"
 		:circular="circular"
 		:vertical="vertical"
 		:previous-margin="previousMargin"
@@ -14,13 +14,14 @@
 		@change="handleChange"
 		@transition="handleTransition"
 		class="fs-swiper"
-		:class="{'fs-swiper-card': mode === 'card', 'gutter-v': gutter}"
-		:style="{height: height}">
+		:class="{ 'fs-swiper-card': mode === 'card', 'gutter-v': gutter }"
+		:style="{ height: height }"
+	>
 		<template v-if="mode === 'card'">
 			<swiper-item class="fs-swiper-item-box" v-for="(item, index) in list" :key="index" @click="handleClick">
-				<view class="fs-swiper-item" :class="{'card-cur': index === curIndex}">
+				<view class="fs-swiper-item" :class="{ 'card-cur': index === curIndex }">
 					<fs-avatar shape="square" radius width="100%" height="100%" :src="item[keyMap.src]"></fs-avatar>
-					<view class="fs-swiper-item-text line1" v-if="showTitle">{{item[keyMap.title]}}</view>
+					<view class="fs-swiper-item-text line1" v-if="showTitle">{{ item[keyMap.title] }}</view>
 				</view>
 			</swiper-item>
 		</template>
@@ -28,13 +29,41 @@
 			<swiper-item class="fs-swiper-item-box" v-for="(item, index) in list" :key="index" @click="handleClick">
 				<view class="fs-swiper-item">
 					<fs-avatar shape="square" width="100%" height="100%" :src="item[keyMap.src]"></fs-avatar>
-					<view class="fs-swiper-item-text line1" v-if="showTitle">{{item[keyMap.title]}}</view>
+					<view class="fs-swiper-item-text line1" v-if="showTitle">{{ item[keyMap.title] }}</view>
 				</view>
 			</swiper-item>
 		</template>
 	</swiper>
 </template>
 
+<script>
+/**
+ * 轮播图组件
+ * @description 轮播图组件
+ * @property {Array} list 滚动列表
+ * @property {Object} keyMap 属性映射
+ * @property {Number} current 当前所在滑块的index
+ * @property {Boolean} indicatorDots 是否显示指示器
+ * @property {String} indicatorColor 指示器颜色
+ * @property {String} indicatorActiveColor 指示器高亮颜色
+ * @property {Boolean} autoplay 自动播放
+ * @property {Boolean} vertical 竖直滚动
+ * @property {Number} interval 播放间隔时长(单位ms)
+ * @property {Number} duration 滚动动画持续时长(单位ms)
+ * @property {String} previousMargin 前边距,可用于露出前一项的一小部分
+ * @property {String} nextMargin 后边距,可用于露出后一项的一小部分
+ * @property {String} height 滚动项高度
+ * @property {String} mode 模式
+ * @property {Boolean} gutter 是否有下边距
+ * @property {Boolean} showTitle 是否显示标题
+ * @event {Function} change change事件
+ * @event {Function} transition transition事件
+ */
+export default {
+	name: 'fs-swiper'
+}
+</script>
+
 <script setup>
 import { ref } from 'vue'
 
@@ -124,17 +153,17 @@ const handleClick = item => {
 </script>
 
 <style lang="scss" scoped>
-.fs-swiper{
-	&-item{
+.fs-swiper {
+	&-item {
 		width: 100%;
 		height: 100%;
-		
-		&-text{
+
+		&-text {
 			position: absolute;
 			left: 0;
 			right: 0;
 			bottom: 0;
-			background-color: rgba(0, 0, 0, .5);
+			background-color: rgba(0, 0, 0, 0.5);
 			color: #fff;
 			padding: 10rpx 20rpx;
 			font-size: 14px;
@@ -143,19 +172,19 @@ const handleClick = item => {
 	}
 }
 
-.fs-swiper-card{
-	.fs-swiper-item-box{
+.fs-swiper-card {
+	.fs-swiper-item-box {
 		width: 610rpx !important;
 		left: 70rpx;
 		position: relative;
 	}
-	.fs-swiper-item{
-		transform: scale(.9);
+	.fs-swiper-item {
+		transform: scale(0.9);
 		transition: all 0.2s ease-in 0s;
 		overflow: hidden;
 	}
-	.card-cur{
-		transform: scale(1)
+	.card-cur {
+		transform: scale(1);
 	}
 }
-</style>
+</style>

+ 17 - 0
components/fs-switch/fs-switch.vue

@@ -8,6 +8,23 @@
 	</view>
 </template>
 
+<script>
+/**
+ * 开关组件
+ * @description 开关组件
+ * @property {String} activeColor switch的值为 on 的颜色
+ * @property {String} inactiveColor switch的值为 off 的颜色
+ * @property {Boolean} disabled 是否禁用
+ * @property {String} activeText switch的值为 on 的文字
+ * @property {String} inactiveText switch的值为 off 的文字
+ * @property {String} size = [small | default | large] 开关大小
+ * @event {Function} change change事件
+ */
+export default {
+	name: 'fs-switch'
+}
+</script>
+
 <script setup>
 import { watch, ref } from 'vue'
 

+ 197 - 189
components/fs-tab/fs-tab.vue

@@ -1,89 +1,97 @@
 <template>
-	<view class="text-center" :class="[sticky ? 'fs-tab-sticky' : '',gutter ? 'fs-tab-gutter' : '']">
-		<scroll-view
-			:scroll-x="scrollable" 
-			:style="{'background-color':bgColor}">
-			<view
-			class="fs-tab"
-			:class="[
-				'fs-tab-' + type,
-				colorType,
-				round ? 'round' : '',
-				center ? 'fs-tab-center' : '',
-			]">
-				<view
-					class="fs-tab-item" 
-					:class="[type+'-item', type + '-item-' + colorType, {active: index == curIndex}]" 
-					:style="itemStyle"
-					v-for="(item, index) in tabs"
-					:key="index"
-					@click="setActive(index)">
-						<slot :item="item" :index="index">{{item.name}}</slot>
-						<view 
-							v-if="type === 'line' && index == curIndex" 
-							class="fs-tab-item-bar" 
-							:style="{width: barWidth}">
-						</view>
-				</view>
+	<view class="text-center" :class="[sticky ? 'fs-tab-sticky' : '', gutter ? 'fs-tab-gutter' : '']">
+		<scroll-view :scroll-x="scrollable" :style="{ 'background-color': bgColor }">
+			<view class="fs-tab" :class="['fs-tab-' + type, colorType, round ? 'round' : '', center ? 'fs-tab-center' : '']">
+				<view
+					class="fs-tab-item"
+					:class="[type + '-item', type + '-item-' + colorType, { active: index == curIndex }]"
+					:style="itemStyle"
+					v-for="(item, index) in tabs"
+					:key="index"
+					@click="setActive(index)"
+				>
+					<slot :item="item" :index="index">{{ item.name }}</slot>
+					<view v-if="type === 'line' && index == curIndex" class="fs-tab-item-bar" :style="{ width: barWidth }"></view>
+				</view>
 			</view>
 		</scroll-view>
 	</view>
 </template>
 
-<script setup>
-import { reactive, computed } from 'vue'
-
-const props = defineProps({
-	color: String,
-	colorType: {
-		type: String,
-		default: 'primary',
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-		}
-	},
-	bgColor: {
-		type: String,
-		default: '#fff'
-	},
-	barWidth: {
-		type: String,
-		default: '100%'
-	},
-	scrollThreshold: {
-		type: Number,
-		default: 5
-	},
-	type: {
-		type: String,
-		default: 'line',
-		validator(value) {
-			return ['line', 'card'].includes(value)
-		}
-	},
-	tabs: {
-		type: Array,
-		default() {
-			return []
-		}
-	},
-	center: Boolean,
-	round: Boolean,
-	sticky: Boolean,
-	gutter: Boolean,
-	modelValue: {
-		type: [String, Number],
-		default: 0
-	}
-})
-const emits = defineEmits(['change', 'update:modelValue'])
-
+<script>
+/**
+ * 标签页组件
+ * @description 标签页组件
+ * @property {String} type = [line | card] 类型
+ * @property {Array} tabs 列表
+ * @property {String} colorType 高亮颜色类型
+ * @property {String} bgColor 背景颜色
+ * @property {String} barWidth 指示线宽度
+ * @property {Number} scrollThreshold 滑动阈值
+ * @property {Boolean} center 是否居中
+ * @property {Boolean} round 半角
+ * @property {Boolean} sticky 是否吸顶
+ * @property {Boolean} gutter 下边距
+ * @event {Function} change change事件
+ */
+export default {
+	name: 'fs-tab'
+}
+</script>
+
+<script setup>
+import { reactive, computed } from 'vue'
+
+const props = defineProps({
+	colorType: {
+		type: String,
+		default: 'primary',
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	},
+	bgColor: {
+		type: String,
+		default: '#fff'
+	},
+	barWidth: {
+		type: String,
+		default: '100%'
+	},
+	scrollThreshold: {
+		type: Number,
+		default: 5
+	},
+	type: {
+		type: String,
+		default: 'line',
+		validator(value) {
+			return ['line', 'card'].includes(value)
+		}
+	},
+	tabs: {
+		type: Array,
+		default() {
+			return []
+		}
+	},
+	center: Boolean,
+	round: Boolean,
+	sticky: Boolean,
+	gutter: Boolean,
+	modelValue: {
+		type: [String, Number],
+		default: 0
+	}
+})
+const emits = defineEmits(['change', 'update:modelValue'])
+
 const scrollable = computed(() => props.scrollThreshold <= props.tabs.length)
-const itemStyle = computed(() => scrollable.value ? `flex: 0 0 ${88 / props.scrollThreshold}%;` : '')
-const curIndex = computed(() => props.modelValue)
-
-const setActive = index => {
-	emits('update:modelValue', index)
+const itemStyle = computed(() => (scrollable.value ? `flex: 0 0 ${88 / props.scrollThreshold}%;` : ''))
+const curIndex = computed(() => props.modelValue)
+
+const setActive = index => {
+	emits('update:modelValue', index)
 	emits('change', index)
 }
 </script>
@@ -94,133 +102,133 @@ const setActive = index => {
 	height: 90rpx;
 	line-height: 90rpx;
 	background-color: #fff;
-	text-align: center;
-	white-space: nowrap;
-	
-	&-line{
-		&.primary .active {
-			color: var(--primary);
-		}
-		&.danger .active {
-			color: var(--danger);
-		}
-		&.warning .active {
-			color: var(--warning);
-		}
-		&.success .active {
-			color: var(--success);
-		}
-		&.info .active {
-			color: var(--info);
-		}
-		&::after {
-			content: '';
-			position: absolute;
-			bottom: 0;
-			left: 0;
-			width: 200%;
-			transform: scale(0.5);
-			transform-origin: 0 0;
-			height: 1px;
-			background-color: var(--border-color);
-		}
-	}
-	
-	&-card {
-		border: 2rpx solid var(--primary);
-		border-radius: var(--radius);
-		overflow: hidden;
-	
-		&.round {
-			border-radius: 30px;
-		}
-		&.danger {
-			border-color: var(--danger);
-		}	
-		&.warning {
-			border-color: var(--warning);
-		}	
-		&.info {
-			border-color: var(--info);
-		}	
-		&.success {
-			border-color: var(--success);
-		}
-		
-		& .active {
-			color: #fff;
-		}
-		&.primary .active {
-			background-color: var(--primary);
-		}
-		&.danger .active {
-			background-color: var(--danger);
-		}
-		&.warning .active {
-			background-color: var(--warning);
-		}
-		&.success .active {
-			background-color: var(--success);
-		}
-		&.info .active {
-			background-color: var(--info);
-		}
-	}
-	
-	&-center {
-		display: inline-flex;
-		width: auto;
-		
-		& .fs-tab-item {
-			padding: 0 30px;
-		}
-	}
-	&-sticky {
-		position: sticky;
-		top: 0;
-		z-index: 10;
-	}
-	&-gutter {
-		margin-bottom: var(--gutter);
-	}
+	text-align: center;
+	white-space: nowrap;
+
+	&-line {
+		&.primary .active {
+			color: var(--primary);
+		}
+		&.danger .active {
+			color: var(--danger);
+		}
+		&.warning .active {
+			color: var(--warning);
+		}
+		&.success .active {
+			color: var(--success);
+		}
+		&.info .active {
+			color: var(--info);
+		}
+		&::after {
+			content: '';
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			width: 200%;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			height: 1px;
+			background-color: var(--border-color);
+		}
+	}
+
+	&-card {
+		border: 2rpx solid var(--primary);
+		border-radius: var(--radius);
+		overflow: hidden;
+
+		&.round {
+			border-radius: 30px;
+		}
+		&.danger {
+			border-color: var(--danger);
+		}
+		&.warning {
+			border-color: var(--warning);
+		}
+		&.info {
+			border-color: var(--info);
+		}
+		&.success {
+			border-color: var(--success);
+		}
+
+		& .active {
+			color: #fff;
+		}
+		&.primary .active {
+			background-color: var(--primary);
+		}
+		&.danger .active {
+			background-color: var(--danger);
+		}
+		&.warning .active {
+			background-color: var(--warning);
+		}
+		&.success .active {
+			background-color: var(--success);
+		}
+		&.info .active {
+			background-color: var(--info);
+		}
+	}
+
+	&-center {
+		display: inline-flex;
+		width: auto;
+
+		& .fs-tab-item {
+			padding: 0 30px;
+		}
+	}
+	&-sticky {
+		position: sticky;
+		top: 0;
+		z-index: 10;
+	}
+	&-gutter {
+		margin-bottom: var(--gutter);
+	}
 }
 
 .fs-tab-item {
-	flex: 1;
+	flex: 1;
 	position: relative;
 	box-sizing: border-box;
-	color: var(--content);
-	
-	&-bar {
-		position: absolute;
-		bottom: 0;
-		height: 2px;
-		background-color: currentColor;
-		width: 100%;
-		left: 50%;
-		transform: translateX(-50%);
-		animation: width .5s;
-		z-index: 10;
+	color: var(--content);
+
+	&-bar {
+		position: absolute;
+		bottom: 0;
+		height: 2px;
+		background-color: currentColor;
+		width: 100%;
+		left: 50%;
+		transform: translateX(-50%);
+		animation: width 0.5s;
+		z-index: 10;
 	}
 }
 
-.card-item-primary+.card-item-primary {
+.card-item-primary + .card-item-primary {
 	border-left: 1px solid var(--primary);
 }
 
-.card-item-danger+.card-item-danger {
+.card-item-danger + .card-item-danger {
 	border-left: 1px solid var(--danger);
 }
 
-.card-item-warning+.card-item-warning {
+.card-item-warning + .card-item-warning {
 	border-left: 1px solid var(--warning);
 }
 
-.card-item-success+.card-item-success {
+.card-item-success + .card-item-success {
 	border-left: 1px solid var(--success);
 }
 
-.card-item-info+.card-item-info {
+.card-item-info + .card-item-info {
 	border-left: 1px solid var(--info);
 }
 
@@ -243,9 +251,9 @@ scroll-view ::v-deep ::-webkit-scrollbar {
 	height: 0 !important;
 	-webkit-appearance: none;
 	background: transparent;
-}
-::v-deep .fs-tab .uni-scroll-view-content{
-	display: flex;
+}
+::v-deep .fs-tab .uni-scroll-view-content {
+	display: flex;
 }
 /* #endif */
 </style>

+ 159 - 141
components/fs-tag/fs-tag.vue

@@ -1,143 +1,161 @@
-<template>
-	<view
-		class="fs-tag" 
-		:class="[
-			{
-				'fs-tag-round': round,
-				'fs-tag-plain': plain,
-				'fs-tag-mark': mark,
-				'fs-tag-mark-reverse': markReverse,
-				'fs-tag-none': closed
-			},
-			'fs-tag-' + size,
-			'bg-' + type,
-		]" 
-		:style="[
-			{color:color,backgroundColor:bgColor},
-			customStyle
-		]">
-		<slot></slot>
-		<fs-icon
-			class="fs-tag-close"
-			size="13px"
-			type="icon-close"
-			v-if="closable"
-			@click="handleClosed">
-		</fs-icon>
-	</view>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-
-const props = defineProps({
-	plain: Boolean,
-	round: Boolean,
-	mark: Boolean,
-	markReverse: Boolean,
-	closable: Boolean,
-	type: {
-		type: String,
-		default: 'primary',
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
-		}
-	},
-	color: String,
-	bgColor: String,
-	size: String,
-	customStyle: {
-	  type: Object,
-	  default() {
-			return {}
-		}
-	},
-})
-
-const closed = ref(false)
-const handleClosed = () => {
-	closed.value = true
-}
-</script>
-
-<style lang="scss" scoped>
-.fs-tag{
-  display: inline-flex;
-  height: 38rpx;
-  line-height: 38rpx;
-  padding: 0 10rpx;
-  font-size: 11px;
-  color: #fff;
-  vertical-align: middle;
-  border-radius: 4rpx;
-	align-items: center;
-	white-space: nowrap;
-	
-	&-plain{
-	  background-color: transparent !important;
-	  line-height: 32rpx;
-		border: 2rpx solid currentColor;
-		
-		&.bg-default{
-			color: var(--disabled);
-		}
-		&.bg-primary{
-		  color: var(--primary);
-		}
-		&.bg-success{
-		  color: var(--success);
-		}
-		&.bg-warning{
-		  color: var(--warning);
-		}
-		&.bg-info{
-		  color: var(--info);
-		}
-		&.bg-danger{
-		  color: var(--danger);
-		}
-	}
-	
-	&-round{
-	  border-radius: 30rpx;
-	}
-	&-mark{
-	  border-radius: 0 25rpx 25rpx 0;
-	}
-	&-mark-reverse{
-		border-radius: 25rpx 0 0 25rpx;
-	}
-	
-	&-medium{
-	  font-size: 12px;
-	  height: 48rpx;
-	  line-height: 48rpx;
-	  padding: 0 20rpx;
-		&.plain{
-		  line-height: 48rpx;
-		}
-	}
-	&-large{
-	  font-size: 13px;
-	  height: 58rpx;
-	  line-height: 58rpx;
-	  padding: 0 30rpx;
-		&.fs-tag-plain{
-		  line-height: 58rpx;
-		}
-		&.fs-tag-mark-reverse{
-			border-radius: 50rpx 0 0 50rpx;
-		}
-	}
-	
-	&-close{
-		margin-left: 8rpx;
-	}
-	&-none{
-		display: none;
+<template>
+	<view
+		class="fs-tag"
+		:class="[
+			{
+				'fs-tag-round': round,
+				'fs-tag-plain': plain,
+				'fs-tag-mark': mark,
+				'fs-tag-mark-reverse': markReverse,
+				'fs-tag-none': closed
+			},
+			'fs-tag-' + size,
+			'bg-' + type
+		]"
+		:style="[{ color: color, backgroundColor: bgColor }, customStyle]"
+	>
+		<slot></slot>
+		<fs-icon class="fs-tag-close" size="13px" type="icon-close" v-if="closable" @click="handleClosed"></fs-icon>
+	</view>
+</template>
+
+<script>
+/**
+ * 标签组件
+ * @description 标签组件
+ * @property {String} size = [default | medium | large] 标签大小
+ * @property {String} color 文字颜色
+ * @property {String} bgColor 背景色
+ * @property {Boolean} plain 是否镂空
+ * @property {Boolean} mark 标记
+ * @property {Boolean} markReverse 标记反转
+ * @property {Boolean} round 圆角
+ * @property {Boolean} closable 标签是否可移除
+ * @property {Object} customStyle 标签自定义样式
+ * @property {String} type = [primary | danger | warning | info | success | default] 标签颜色类型
+ */
+export default {
+	name: 'fs-tag'
+}
+</script>
+
+<script setup>
+import { ref } from 'vue'
+
+const props = defineProps({
+	plain: Boolean,
+	round: Boolean,
+	mark: Boolean,
+	markReverse: Boolean,
+	closable: Boolean,
+	type: {
+		type: String,
+		default: 'primary',
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
+		}
+	},
+	color: String,
+	bgColor: String,
+	size: {
+		type: String,
+		default: 'default',
+		validator(value) {
+			return ['default', 'medium', 'large'].includes(value)
+		}
+	},
+	customStyle: {
+		type: Object,
+		default() {
+			return {}
+		}
 	}
-}
-.bg-default{
-	background-color: #cfcfcf;
-}
+})
+
+const closed = ref(false)
+const handleClosed = () => {
+	closed.value = true
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-tag {
+	display: inline-flex;
+	height: 38rpx;
+	line-height: 38rpx;
+	padding: 0 10rpx;
+	font-size: 11px;
+	color: #fff;
+	vertical-align: middle;
+	border-radius: 4rpx;
+	align-items: center;
+	white-space: nowrap;
+
+	&-plain {
+		background-color: transparent !important;
+		line-height: 32rpx;
+		border: 2rpx solid currentColor;
+
+		&.bg-default {
+			color: var(--disabled);
+		}
+		&.bg-primary {
+			color: var(--primary);
+		}
+		&.bg-success {
+			color: var(--success);
+		}
+		&.bg-warning {
+			color: var(--warning);
+		}
+		&.bg-info {
+			color: var(--info);
+		}
+		&.bg-danger {
+			color: var(--danger);
+		}
+	}
+
+	&-round {
+		border-radius: 30rpx;
+	}
+	&-mark {
+		border-radius: 0 25rpx 25rpx 0;
+	}
+	&-mark-reverse {
+		border-radius: 25rpx 0 0 25rpx;
+	}
+
+	&-medium {
+		font-size: 12px;
+		height: 48rpx;
+		line-height: 48rpx;
+		padding: 0 20rpx;
+		&.plain {
+			line-height: 48rpx;
+		}
+	}
+	&-large {
+		font-size: 13px;
+		height: 58rpx;
+		line-height: 58rpx;
+		padding: 0 30rpx;
+		&.fs-tag-plain {
+			line-height: 58rpx;
+		}
+		&.fs-tag-mark-reverse {
+			border-radius: 50rpx 0 0 50rpx;
+		}
+	}
+
+	&-close {
+		margin-left: 8rpx;
+	}
+	&-none {
+		display: none;
+	}
+}
+.bg-default {
+	background-color: #cfcfcf;
+}
 </style>

+ 87 - 80
components/fs-text/fs-text.vue

@@ -1,80 +1,87 @@
-<template>
-	<text 
-		:class="[colorType,decoration,{block}]" 
-		:style="styleStr"
-		@click="handleClick">
-			{{formatText(text)}}
-		</text>
-</template>
-
-<script>
-export default {
-	name: 'fs-text'
-}
-</script>
-
-<script setup>
-import { computed } from 'vue'
-
-const props = defineProps({
-	text: String,
-	mode: {
-		type: String,
-		validator(value) {
-			return ['price', 'phone', 'name'].includes(value)
-		}
-	},
-	colorType: {
-	  type: String,
-		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
-		}
-	},
-	color: String,
-	link: String,
-	linkType: {
-		type: String,
-		default: 'navigateTo'
-	},
-	block: Boolean,
-	encrypt: Boolean,
-	decoration: {
-		type: String,
-		validator(value) {
-			return ['underline', 'line-through'].includes(value)
-		}
-	}
-})
-const emits = defineEmits(['click'])
-
-const styleStr = computed(() => {
-	return props.color ? `color: ${props.color};` : ''
-})
-
-const formatText = text => {
-	if (props.mode === 'price') {
-		return '¥' + text
-	} else if (props.mode === 'name') {
-		return text[0] + '**'
-	} else if (props.mode === 'phone' && props.encrypt) {
-		return text.slice(0, 3) + '****' + text.slice(-4)
-	}
-	return text
-}
-const handleClick = () => {
-	if (props.mode === 'phone') {
-		uni.makePhoneCall({
-			phoneNumber: props.text
-		})
-	} else if (props.link) {
-		uni[props.linkType]({
-			url: props.link
-		})
-	}
-	emits('click')
-}
-</script>
-
-<style lang="scss" scoped>
-
-</style>
+<template>
+	<text :class="[colorType, decoration, { block }]" :style="styleStr" @click="handleClick">{{ formatText(text) }}</text>
+</template>
+
+<script>
+/**
+ * 文本组件
+ * @description 文本组件
+ * @property {String} text 显示的内容文本
+ * @property {String} mode = [price | phone | name] 文本处理的匹配模式
+ * @property {String} color 文本颜色
+ * @property {String} colorType = [primary | danger | warning | info | success | default] 文本亮颜色类型
+ * @property {String} link 跳转地址
+ * @property {String} linkType 跳转类型
+ * @property {Boolean} bolck 块状
+ * @property {Boolean} encrypt 是否加密(仅对mode=phone有效)
+ * @property {String} decoration = [underline | line-through] 文字装饰
+ 
+ */
+export default {
+	name: 'fs-text'
+}
+</script>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+	text: String,
+	mode: {
+		type: String,
+		validator(value) {
+			return ['price', 'phone', 'name'].includes(value)
+		}
+	},
+	colorType: {
+		type: String,
+		validator(value) {
+			return ['primary', 'success', 'info', 'warning', 'danger'].includes(value)
+		}
+	},
+	color: String,
+	link: String,
+	linkType: {
+		type: String,
+		default: 'navigateTo'
+	},
+	block: Boolean,
+	encrypt: Boolean,
+	decoration: {
+		type: String,
+		validator(value) {
+			return ['underline', 'line-through'].includes(value)
+		}
+	}
+})
+const emits = defineEmits(['click'])
+
+const styleStr = computed(() => {
+	return props.color ? `color: ${props.color};` : ''
+})
+
+const formatText = text => {
+	if (props.mode === 'price') {
+		return '¥' + text
+	} else if (props.mode === 'name') {
+		return text[0] + '**'
+	} else if (props.mode === 'phone' && props.encrypt) {
+		return text.slice(0, 3) + '****' + text.slice(-4)
+	}
+	return text
+}
+const handleClick = () => {
+	if (props.mode === 'phone') {
+		uni.makePhoneCall({
+			phoneNumber: props.text
+		})
+	} else if (props.link) {
+		uni[props.linkType]({
+			url: props.link
+		})
+	}
+	emits('click')
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 14 - 6
components/fs-timeago/fs-timeago.vue

@@ -1,9 +1,19 @@
 <template>
-	<view @click="handleClick">
-		{{timeago(dateTime, format)}}
-	</view>
+	<view @click="handleClick">{{ timeago(dateTime, format) }}</view>
 </template>
 
+<script>
+/**
+ * 多久之前组件
+ * @description 多久之前组件
+ * @property {String, Number, Object} dateTime 日期
+ * @property {String} format 格式化
+ */
+export default {
+	name: 'fs-timeago'
+}
+</script>
+
 <script setup>
 import timeago from '@/utils/timeago'
 
@@ -21,6 +31,4 @@ const handleClick = () => {
 }
 </script>
 
-<style>
-
-</style>
+<style></style>

+ 33 - 18
components/fs-timeline/fs-timeline.vue

@@ -3,12 +3,11 @@
 		<view class="fs-timeline-item" v-for="(item, index) in options" :key="index">
 			<view class="fs-dot-box">
 				<slot name="dot" :item="item" :index="index">
-					<view 
-						class="fs-dot" 
-						:class="index === 0 ? 'bg-' + activeColorType : ''" 
-						:style="{backgroundColor: index === 0 ? activeColor : '#969799'}"
-					>
-					</view>
+					<view
+						class="fs-dot"
+						:class="index === 0 ? 'bg-' + activeColorType : ''"
+						:style="{ backgroundColor: index === 0 ? activeColor : color }"
+					></view>
 				</slot>
 			</view>
 			<view class="fs-timeline-line"></view>
@@ -17,11 +16,27 @@
 	</view>
 </template>
 
+<script>
+/**
+ * 时间轴组件
+ * @description 时间轴组件
+ * @property {Array} options 选项列表
+ * @property {String} color 颜色
+ * @property {String} activeColor 高亮颜色
+ * @property {String} activeColorType = [primary | danger | warning | info | success] 高亮颜色类型
+ */
+export default {
+	name: 'fs-timeline'
+}
+</script>
+
 <script setup>
 const props = defineProps({
 	options: Array,
-	colorType: String,
-	color: String,
+	color: {
+		type: String,
+		default: '#969799'
+	},
 	activeColor: String,
 	activeColorType: {
 		type: String,
@@ -36,8 +51,8 @@ const props = defineProps({
 <style lang="scss" scoped>
 .fs-timeline {
 	padding-left: 40rpx;
-	
-	&-line{
+
+	&-line {
 		position: absolute;
 		top: 0;
 		left: -20rpx;
@@ -45,18 +60,18 @@ const props = defineProps({
 		height: 100%;
 		background-color: #ebedf0;
 	}
-	
-	&-item{
+
+	&-item {
 		position: relative;
-		
-		&:last-child{
-			.timeline-line{
+
+		&:last-child {
+			.timeline-line {
 				display: none;
 			}
 		}
 	}
 }
-.fs-dot-box{
+.fs-dot-box {
 	position: absolute;
 	left: -20rpx;
 	top: 30rpx;
@@ -64,13 +79,13 @@ const props = defineProps({
 	z-index: 10;
 	transform: translateX(-50%);
 }
-.fs-dot{
+.fs-dot {
 	width: 15rpx;
 	height: 15rpx;
 	border-radius: 50%;
 	// background-color: #969799;
 }
-.content{
+.content {
 	padding: var(--gutter) 0;
 }
 </style>

+ 164 - 153
components/fs-upload/fs-upload.vue

@@ -1,169 +1,180 @@
-<template>
-	<view class="fs-file-list">
-		<view v-for="(item, index) in modelValue" :key="index" class="fs-file-box">
-			<fs-icon
-				type="icon-close"
-				size="20px"
-				colorType="danger"
-				class="fs-file-del"
-				@click="handleDel(index)">
-			</fs-icon>
-			<fs-avatar
-				v-if="mediaType === 'image'"
-				:src="formatPath(item)"
-				:shape="shape"
-				:size="size"
-				:width="width"
-				:height="height"
-				radius
-				@click="handlePreview(formatPath(item))">
-			</fs-avatar>
-			<video v-else :src="formatPath(item)" controls class="fs-file-video"></video>
-		</view>
-		<view class="fs-file-box" @click="upload" v-if="modelValue.length < count">
-			<slot>
-				<fs-avatar
-					:shape="shape"
-					:size="size"
-					:width="width"
-					:height="height"
-					radius
-					bgColor="#EBEFF5"
-				>
-					<fs-icon type="icon-plus" size="50px"></fs-icon>
-				</fs-avatar>
-			</slot>
-		</view>
-	</view>
-</template>
-
-<script setup>
-import utils from '@/utils/utils'
-import config from '@/utils/config'
-
-const props = defineProps({
-	action: String,
-	name: {
-		type: String,
-		default: 'file'
-	},
-	header: {
-		type: Object,
-		default: () => {}
+<template>
+	<view class="fs-file-list">
+		<view v-for="(item, index) in modelValue" :key="index" class="fs-file-box">
+			<fs-icon type="icon-close" size="20px" colorType="danger" class="fs-file-del" @click="handleDel(index)"></fs-icon>
+			<fs-avatar
+				v-if="mediaType === 'image'"
+				:src="formatPath(item)"
+				:shape="shape"
+				:size="size"
+				:width="width"
+				:height="height"
+				radius
+				@click="handlePreview(formatPath(item))"
+			></fs-avatar>
+			<video v-else :src="formatPath(item)" controls class="fs-file-video"></video>
+		</view>
+		<view class="fs-file-box" @click="upload" v-if="modelValue.length < count">
+			<slot>
+				<fs-avatar :shape="shape" :size="size" :width="width" :height="height" radius bgColor="#EBEFF5">
+					<fs-icon type="icon-plus" size="50px"></fs-icon>
+				</fs-avatar>
+			</slot>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 上传组件
+ * @description 上传组件
+ * @property {String} action 上传地址
+ * @property {String} name 文件对应的key,开发者在服务器端通过这个key可以获取到文件二进制内容
+ * @property {Object} HTTP 请求 Header, header 中不能设置 Referer
+ * @property {String} mediaType = [image | video] 媒体类型
+ * @property {Number} count 最多上传数量
+ * @property {String} shape 上传框形状
+ * @property {String} size 上传框大小
+ * @property {String} width 上传框宽度(优先级高于size)
+ * @property {String} height 上传框高度(优先级高于size)
+ * @property {String} height 上传框高度(优先级高于size)
+ * @property {String} height 上传框高度(优先级高于size)
+ * @property {Object} chooseDatachooseImage/chooseVideo参数
+ * @property {Object} formData 上传数据
+ * @property {String} pathKey	接口返回的图片路径的key(如果没有可设置成空)
+ * @property {String} cloudUpload	是否云上传(需配置云服务空间)
+ */
+export default {
+	name: 'fs-upload'
+}
+</script>
+
+<script setup>
+import utils from '@/utils/utils'
+import config from '@/utils/config'
+
+const props = defineProps({
+	action: String,
+	name: {
+		type: String,
+		default: 'file'
+	},
+	header: {
+		type: Object,
+		default: () => {}
 	},
 	count: {
 		type: Number,
 		default: 1
-	},
-	chooseData: {
-		type: Object,
-		default: () => {}
+	},
+	chooseData: {
+		type: Object,
+		default: () => {}
 	},
 	modelValue: {
 		type: Array,
 		default: () => []
-	},
-	mediaType: {
-		type: String,
-		default: 'image',
-		validator(value) {
-			return ['image', 'video'].includes(value)
-		}
-	},
-	shape: {
-		type: String,
-		default: 'square',
-		validator(value) {
-			return ['square', 'circle'].includes(value)
-		}
-	},
-	size: {
-		type: String,
-		default: '150rpx'
-	},
-	width: {
-		type: String,
-		default: '150rpx'
-	},
-	height: {
-		type: String,
-		default: '150rpx'
-	},
-	formData: {
-		type: Object,
-		default() {
-			return {}
-		}
-	},
-	pathKey: {
-		type: String,
-		default: 'filePath'
-	},
-	cloudUpload: Boolean
-})
-const emit = defineEmits(['update:modelValue'])
-
-const handlePreview = current => {
+	},
+	mediaType: {
+		type: String,
+		default: 'image',
+		validator(value) {
+			return ['image', 'video'].includes(value)
+		}
+	},
+	shape: {
+		type: String,
+		default: 'square',
+		validator(value) {
+			return ['square', 'circle'].includes(value)
+		}
+	},
+	size: {
+		type: String,
+		default: '150rpx'
+	},
+	width: {
+		type: String,
+		default: '150rpx'
+	},
+	height: {
+		type: String,
+		default: '150rpx'
+	},
+	formData: {
+		type: Object,
+		default() {
+			return {}
+		}
+	},
+	pathKey: {
+		type: String,
+		default: 'filePath'
+	},
+	cloudUpload: Boolean
+})
+const emit = defineEmits(['update:modelValue'])
+
+const handlePreview = current => {
 	uni.previewImage({
 		current,
 		urls: props.modelValue.map(item => formatPath(item))
-	})
-}
-const upload = async () => {
-	let methods = ''
-	if (props.mediaType === 'image') {
-		methods = 'chooseAndUploadImage'
-	} else {
-		methods = 'chooseAndUploadVideo'
-	}
-	utils[methods](
-		{
-			count: props.count,
-			...props.chooseData
-		}, 
-		{
-			url: props.action,
-			name: props.name,
-			header: props.header,
-			formData: props.formData
-		},
-		props.cloudUpload
-	).then(res => {
-		const fileList = [...props.modelValue, ...res]
-		fileList.length > props.count && fileList.splice(props.count)
-		console.log(fileList);
-		emit('update:modelValue', fileList)
-	})
-}
-const handleDel = index => {
+	})
+}
+const upload = async () => {
+	let methods = ''
+	if (props.mediaType === 'image') {
+		methods = 'chooseAndUploadImage'
+	} else {
+		methods = 'chooseAndUploadVideo'
+	}
+	utils[methods](
+		{
+			count: props.count,
+			...props.chooseData
+		},
+		{
+			url: props.action,
+			name: props.name,
+			header: props.header,
+			formData: props.formData
+		},
+		props.cloudUpload
+	).then(res => {
+		const fileList = [...props.modelValue, ...res]
+		fileList.length > props.count && fileList.splice(props.count)
+		console.log(fileList)
+		emit('update:modelValue', fileList)
+	})
+}
+const handleDel = index => {
 	props.modelValue.splice(index, 1)
-	emit('update:modelValue',props.modelValue)
-}
-
-
-const formatPath = item => {
-	const path = props.pathKey ? item[props.pathKey] : item
-	return utils.isHttp(path) ? path : config.baseUrl + path
-}
-</script>
-
-<style lang="scss" scoped>
-.fs-file{
-	&-list{
+	emit('update:modelValue', props.modelValue)
+}
+
+const formatPath = item => {
+	const path = props.pathKey ? item[props.pathKey] : item
+	return utils.isHttp(path) ? path : config.baseUrl + path
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-file {
+	&-list {
 		display: flex;
 		flex-wrap: wrap;
 		padding-top: var(--gutter);
-	}
-	&-box{
-		position: relative;
-		margin-bottom: var(--gutter);
-		margin-left: var(--gutter);
-	}
-	&-video{
-		width: 300rpx;
-		height: 200rpx;
-	}
-	&-del{
+	}
+	&-box {
+		position: relative;
+		margin-bottom: var(--gutter);
+		margin-left: var(--gutter);
+	}
+	&-video {
+		width: 300rpx;
+		height: 200rpx;
+	}
+	&-del {
 		position: absolute;
 		top: 0;
 		right: 0;
@@ -171,6 +182,6 @@ const formatPath = item => {
 		font-size: 22px;
 		color: var(--sub);
 		z-index: 10;
-	}
-}
+	}
+}
 </style>

+ 172 - 151
components/fs-week-bar/fs-week-bar.vue

@@ -1,152 +1,173 @@
-<template>
-	<view class="fs-week-bar">
-		<view class="fs-week-arrow" @click="handleChange('left')">
-			<fs-icon type="icon-d-down" rotate="90" size="28rpx" :color="textColor"></fs-icon>
-		</view>
-		<view class="fs-week-list">
-			<view 
-				class="fs-week-item" 
-				:class="{'fs-week-item-active': state.activeDate === item.fullDate}" 
-				v-for="(item, index) in state.week" 
-				:key="index"
-				@click="handleClick(item)"
-			>
-				<view class="fs-week-item-hd">周{{item.day}}</view>
-				<view class="fs-week-item-bd">{{item.date}}</view>
-			</view>
-		</view>
-		<view class="fs-week-arrow" @click="handleChange('right')">
-			<fs-icon type="icon-d-down" rotate="-90" size="28rpx" :color="textColor"></fs-icon>
-		</view>
-	</view>
-</template>
-
-<script>
-export default {
-	name: "fs-week-bar"
-}
-</script>
-<script setup>
-import { ref, reactive, watch, nextTick } from 'vue'
-import dayjs from 'dayjs'
-
-const props = defineProps({
-	bgColor: {
-		type: String,
-		default: '#fff'
-	},
-	textColor: {
-		type: String,
-		default: '#666666'
-	},
-	activeColor: {
-		type: String,
-		default: '#165DFF'
-	},
-	activeDate: {
-		type: String,
-		default: dayjs().format('YYYY-MM-DD')
-	}
-})
-const emits = defineEmits(['change', 'click'])
-
-const state = reactive({
-	week: [],
-	radix: 0,
-	activeDate: '',
-	curDay: '',
-	curDate: ''
-})
-
-const dayMap = {
-	1: '一',
-	2: '二',
-	3: '三',
-	4: '四',
-	5: '五',
-	6: '六',
-	7: '日',
-}
-const initWeek = () => {
-	state.week = []
-	for (let i = 1; i < state.curDay; i++) {
-		const diffDay = state.curDay - i
-		const date = dayjs(state.curDate).subtract(diffDay, 'day').add(7 * state.radix, 'day')
-		state.week.push({
-			day: dayMap[i],
-			date: date.format('MM-DD'),
-			fullDate: date.format('YYYY-MM-DD'),
-		})
-	}
-	for (let i = state.curDay; i <= 7; i++) {
-		const diffDay = i - state.curDay
-		const date = dayjs(state.curDate).add(diffDay, 'day').add(7 * state.radix, 'day')
-		state.week.push({
-			day: dayMap[i],
-			date: date.format('MM-DD'),
-			fullDate: date.format('YYYY-MM-DD'),
-		})
-	}
-}
-const stopWatch = watch(() => props.activeDate, val => {
-	const date = val || undefined
-	
-	state.activeDate = dayjs(date).format('YYYY-MM-DD')
-	state.curDate = dayjs(date).format('YYYY-MM-DD')
-	state.curDay = dayjs(date).day() || 7
-	
-	// state.radix = Math.floor((dayjs(date).diff(dayjs(), 'day') + state.curDay) / 7)
-	initWeek()
-}, { immediate: true })
-
-const handleChange = type => {
-	type === 'left' ? state.radix-- : state.radix++
-}
-watch(() => state.radix, (val) => {
-	initWeek()
-	emits('change', state.week)
-})
-
-const handleClick= item => {
-	stopWatch()
-	state.activeDate = item.fullDate
-	emits('click', item.fullDate)
-}
-</script>
-
-<style lang="scss" scoped>
-.fs-week{
-	&-bar{
-		display: flex;
-		align-items: center;
-		background-color: v-bind(bgColor);
-		padding: 20rpx 0;
-	}
-	
-	&-arrow{
-		padding: 0 10rpx;
-	}
-	
-	&-list{
-		display: flex;
-		flex: 1;
-	}
-	&-item{
-		text-align: center;
-		flex: 1;
-		color: v-bind(textColor);
-		
-		&-active{
-			color: v-bind(activeColor);
-		}
-		
-		&-hd{
-			font-size: 15px;
-			font-weight: bold;
-		}
-		&-bd{
-			font-size: 13px;
-		}
-	}
-}
+<template>
+	<view class="fs-week-bar">
+		<view class="fs-week-arrow" @click="handleChange('left')">
+			<fs-icon type="icon-d-down" rotate="90" size="28rpx" :color="textColor"></fs-icon>
+		</view>
+		<view class="fs-week-list">
+			<view
+				class="fs-week-item"
+				:class="{ 'fs-week-item-active': state.activeDate === item.fullDate }"
+				v-for="(item, index) in state.week"
+				:key="index"
+				@click="handleClick(item)"
+			>
+				<view class="fs-week-item-hd">周{{ item.day }}</view>
+				<view class="fs-week-item-bd">{{ item.date }}</view>
+			</view>
+		</view>
+		<view class="fs-week-arrow" @click="handleChange('right')">
+			<fs-icon type="icon-d-down" rotate="-90" size="28rpx" :color="textColor"></fs-icon>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 时间周组件
+ * @description 时间周组件
+ * @property {String} bgColor 背景颜色
+ * @property {String} textColor	文字颜色
+ * @property {String} activeColor 激活颜色
+ * @property {String} activeDate	激活日期
+ * @event {Function} change 左右箭头切换事件
+ */
+export default {
+	name: 'fs-week-bar'
+}
+</script>
+
+<script setup>
+import { ref, reactive, watch, nextTick } from 'vue'
+import dayjs from 'dayjs'
+
+const props = defineProps({
+	bgColor: {
+		type: String,
+		default: '#fff'
+	},
+	textColor: {
+		type: String,
+		default: '#666666'
+	},
+	activeColor: {
+		type: String,
+		default: '#165DFF'
+	},
+	activeDate: {
+		type: String,
+		default: dayjs().format('YYYY-MM-DD')
+	}
+})
+const emits = defineEmits(['change', 'click'])
+
+const state = reactive({
+	week: [],
+	radix: 0,
+	activeDate: '',
+	curDay: '',
+	curDate: ''
+})
+
+const dayMap = {
+	1: '一',
+	2: '二',
+	3: '三',
+	4: '四',
+	5: '五',
+	6: '六',
+	7: '日'
+}
+const initWeek = () => {
+	state.week = []
+	for (let i = 1; i < state.curDay; i++) {
+		const diffDay = state.curDay - i
+		const date = dayjs(state.curDate)
+			.subtract(diffDay, 'day')
+			.add(7 * state.radix, 'day')
+		state.week.push({
+			day: dayMap[i],
+			date: date.format('MM-DD'),
+			fullDate: date.format('YYYY-MM-DD')
+		})
+	}
+	for (let i = state.curDay; i <= 7; i++) {
+		const diffDay = i - state.curDay
+		const date = dayjs(state.curDate)
+			.add(diffDay, 'day')
+			.add(7 * state.radix, 'day')
+		state.week.push({
+			day: dayMap[i],
+			date: date.format('MM-DD'),
+			fullDate: date.format('YYYY-MM-DD')
+		})
+	}
+}
+const stopWatch = watch(
+	() => props.activeDate,
+	val => {
+		const date = val || undefined
+
+		state.activeDate = dayjs(date).format('YYYY-MM-DD')
+		state.curDate = dayjs(date).format('YYYY-MM-DD')
+		state.curDay = dayjs(date).day() || 7
+
+		// state.radix = Math.floor((dayjs(date).diff(dayjs(), 'day') + state.curDay) / 7)
+		initWeek()
+	},
+	{ immediate: true }
+)
+
+const handleChange = type => {
+	type === 'left' ? state.radix-- : state.radix++
+}
+watch(
+	() => state.radix,
+	val => {
+		initWeek()
+		emits('change', state.week)
+	}
+)
+
+const handleClick = item => {
+	stopWatch()
+	state.activeDate = item.fullDate
+	emits('click', item.fullDate)
+}
+</script>
+
+<style lang="scss" scoped>
+.fs-week {
+	&-bar {
+		display: flex;
+		align-items: center;
+		background-color: v-bind(bgColor);
+		padding: 20rpx 0;
+	}
+
+	&-arrow {
+		padding: 0 10rpx;
+	}
+
+	&-list {
+		display: flex;
+		flex: 1;
+	}
+	&-item {
+		text-align: center;
+		flex: 1;
+		color: v-bind(textColor);
+
+		&-active {
+			color: v-bind(activeColor);
+		}
+
+		&-hd {
+			font-size: 15px;
+			font-weight: bold;
+		}
+		&-bd {
+			font-size: 13px;
+		}
+	}
+}
 </style>

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "fs-uni",
-	"version": "2.6.1",
+	"version": "2.6.2",
 	"description": "",
 	"main": "main.js",
 	"dependencies": {