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>
 <template>
 	<fs-popup direction="bottom" height="auto" v-model="visible" :showMask="showMask" :maskClickable="maskClickable">
 	<fs-popup direction="bottom" height="auto" v-model="visible" :showMask="showMask" :maskClickable="maskClickable">
 		<view class="fs-action">
 		<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-item" v-for="(item, index) in list" :key="index" @click="handleAction(item)">
-			<view class="fs-action-extra">
+				{{ item.name }}
-				<slot></slot>
 			</view>
 			</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>
 		</view>
 	</fs-popup>
 	</fs-popup>
 </template>
 </template>
 
 
-<script setup>
+<script>
-import { computed } from 'vue'
+/**
-
+ * 动作组件
-const props = defineProps({
+ * @description 动作组件
-	list: Array,
+ * @property {Array} list action列表
-	modelValue: Boolean,
+ * @property {Boolean} showMask 是否展示遮罩
-	showMask: {
+ * @property {Boolean} maskClickable 遮罩是否可点击
-		type: Boolean,
+ * @property {Boolean} showCancel 是否展示取消action
-		default: true
+ * @property {String} cancelText “取消”文字
-	},
+ * @event {Function} change change事件
-	maskClickable: {
+ * @example <fs-action :list="list" v-model="show" @change="change"></fs-action>
-		type: Boolean,
+ */
-		default: true
+export default {
-	},
+	name: 'fs-action'
-	cancelText: {
+}
-		type: String,
+</script>
-		default: '取消'
+
-	},
+<script setup>
-	showCancel: {
+import { computed } from 'vue'
-		type: Boolean,
+
-		default: true
+const props = defineProps({
-	}
+	list: Array,
-})
+	modelValue: Boolean,
-const emits = defineEmits(['update:modelValue', 'change'])
+	showMask: {
-
+		type: Boolean,
-const visible = computed(
+		default: true
-	{
+	},
-		get: () => props.modelValue,
+	maskClickable: {
-		set: value => emits('update:modelValue', value)
+		type: Boolean,
-	}
+		default: true
-)
+	},
-
+	cancelText: {
-const cancel = () => {
+		type: String,
-	emits('update:modelValue', false)
+		default: '取消'
-}
+	},
-const handleAction = item =>  {
+	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)
 	emits('change', item)
 	cancel()
 	cancel()
 }
 }
@@ -53,23 +68,23 @@ const handleAction = item =>  {
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .fs-action {
 .fs-action {
-	background-color: #f8f8f8;
+	background-color: #f8f8f8;
-	
+
-	&-item {
+	&-item {
-		padding: 20rpx;
+		padding: 20rpx;
-		text-align: center;
+		text-align: center;
-		background-color: #fff;
+		background-color: #fff;
-	
+
-		&+& {
+		& + & {
-			border-top: 1px solid var(--border-color);
+			border-top: 1px solid var(--border-color);
-		}
+		}
-	}
+	}
-	
+
-	&-cancel {
+	&-cancel {
-		padding: 20rpx;
+		padding: 20rpx;
-		text-align: center;
+		text-align: center;
-		background-color: #fff;
+		background-color: #fff;
-		margin-top: 10rpx;
+		margin-top: 10rpx;
 	}
 	}
 }
 }
 </style>
 </style>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,29 +1,37 @@
-<template>
+<template>
-	<view>
+	<view>{{ dateFormat }}</view>
-		{{dateFormat}}
+</template>
-	</view>
+
-</template>
+<script>
-
+/**
-<script setup>
+ * 日期格式化组件
-import { computed } from 'vue'
+ * @description 日期格式化组件
-import dayjs from 'dayjs'
+ * @property {String, Object} date 日期
-
+ * @property {String} format 格式化
-const props = defineProps({
+ */
-	date: [String, Object],
+export default {
-	format: {
+	name: 'fs-date-format'
-		type: String,
+}
-		default: 'YYYY-MM-DD'
+</script>
-	}
+
-})
+<script setup>
-const emits = defineEmits(['click'])
+import { computed } from 'vue'
-
+import dayjs from 'dayjs'
-const dateFormat = computed(() => dayjs(props.date).format(props.format))
+
-
+const props = defineProps({
-const handleClick = () => {
+	date: [String, Object],
-	emits('click')
+	format: {
-}
+		type: String,
-</script>
+		default: 'YYYY-MM-DD'
-
+	}
-<style>
+})
-
+const emits = defineEmits(['click'])
-</style>
+
+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>
 <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">
 		<fs-grid :columnNum="list.length" :padding="false">
 			<view v-for="(item, index) in list" :key="index" class="fs-divide-list-item">
 			<view v-for="(item, index) in list" :key="index" class="fs-divide-list-item">
-				<fs-grid-item>
+				<fs-grid-item><slot :item="item"></slot></fs-grid-item>
-					<slot :item="item"></slot>
-				</fs-grid-item>
 			</view>
 			</view>
 		</fs-grid>
 		</fs-grid>
 	</view>
 	</view>
 </template>
 </template>
 
 
+<script>
+/**
+ * 分割列表组件
+ * @description 分割列表组件
+ * @property {Array} list 分割列表
+ * @property {Boolean} gutter 下边距
+ */
+export default {
+	name: 'fs-divide-list'
+}
+</script>
+
 <script setup>
 <script setup>
 const props = defineProps({
 const props = defineProps({
 	list: {
 	list: {
@@ -21,22 +31,22 @@ const props = defineProps({
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-.fs-divide-list{
+.fs-divide-list {
 	background-color: #fff;
 	background-color: #fff;
 	border-radius: var(--radius);
 	border-radius: var(--radius);
-	
+
-	&-item{
+	&-item {
 		position: relative;
 		position: relative;
 		padding: 20rpx;
 		padding: 20rpx;
-		& + &{
+		& + & {
-			&::before{
+			&::before {
 				position: absolute;
 				position: absolute;
 				left: 0;
 				left: 0;
 				top: 50%;
 				top: 50%;
 				transform: translateY(-50%);
 				transform: translateY(-50%);
 				width: 2rpx;
 				width: 2rpx;
 				height: 43rpx;
 				height: 43rpx;
-				background-color: #D9D9D9;
+				background-color: #d9d9d9;
 				content: '';
 				content: '';
 			}
 			}
 		}
 		}

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

@@ -1,24 +1,31 @@
 <template>
 <template>
 	<view class="fs-divider">
 	<view class="fs-divider">
-		<view 
+		<view class="fs-divider-line" :class="['bg-' + (lineColorType || colorType)]" :style="lineStyle"></view>
-			class="fs-divider-line"
+		<view class="fs-divider-content" :class="textColorType || colorType" :style="{ color: textColor || color }">
-			:class="['bg-' + (lineColorType || colorType)]"
+			<slot></slot>
-			: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>
+		<view class="fs-divider-line" :class="['bg-' + (lineColorType || colorType)]" :style="lineStyle"></view>
 	</view>
 	</view>
 </template>
 </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>
 <script setup>
 import { computed } from 'vue'
 import { computed } from 'vue'
 
 
@@ -35,14 +42,14 @@ const props = defineProps({
 	lineColorType: {
 	lineColorType: {
 		type: String,
 		type: String,
 		validator(value) {
 		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger','default'].includes(value)
+			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
 		}
 		}
 	},
 	},
 	textColor: String,
 	textColor: String,
 	textColorType: {
 	textColorType: {
 		type: String,
 		type: String,
 		validator(value) {
 		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger','default'].includes(value)
+			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
 		}
 		}
 	},
 	},
 	color: String,
 	color: String,
@@ -50,9 +57,9 @@ const props = defineProps({
 		type: String,
 		type: String,
 		default: 'default',
 		default: 'default',
 		validator(value) {
 		validator(value) {
-			return ['primary', 'success', 'info', 'warning', 'danger','default'].includes(value)
+			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
 		}
 		}
-	},
+	}
 })
 })
 
 
 const lineStyle = computed(() => {
 const lineStyle = computed(() => {
@@ -65,16 +72,15 @@ const lineStyle = computed(() => {
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-.fs-divider{
+.fs-divider {
 	display: flex;
 	display: flex;
 	align-items: center;
 	align-items: center;
 	justify-content: center;
 	justify-content: center;
 	padding: 20rpx;
 	padding: 20rpx;
-	
+
-	&-content{
+	&-content {
 		padding: 0 20rpx;
 		padding: 0 20rpx;
 		white-space: nowrap;
 		white-space: nowrap;
 	}
 	}
-	
 }
 }
 </style>
 </style>

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

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

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

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

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

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

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

@@ -69,6 +69,31 @@
 	</view>
 	</view>
 </template>
 </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>
 <script setup>
 import { inject, computed, useSlots } from 'vue'
 import { inject, computed, useSlots } from 'vue'
 
 
@@ -88,7 +113,6 @@ const props = defineProps({
 	border: Boolean,
 	border: Boolean,
 	clearable: Boolean,
 	clearable: Boolean,
 	autoHeight: Boolean,
 	autoHeight: Boolean,
-	required: Boolean,
 	tighten: Boolean,
 	tighten: Boolean,
 	fixed: Boolean,
 	fixed: Boolean,
 	round: Boolean,
 	round: Boolean,

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

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

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

@@ -1,49 +1,60 @@
-<template>
+<template>
-	<view>
+	<view><slot></slot></view>
-		<slot></slot>
+</template>
-	</view>
+
-</template>
+<script>
-
+/**
-<script setup>
+ * 表单组件
-import { provide } from 'vue'
+ * @description 表单组件
-
+ * @property {String} labelWidth label宽度
-const props = defineProps({
+ * @property {String} labelPosition = [left | top] label位置
-	labelWidth: {
+ * @property {String} labelAlign = [left | center | right | justify] label对齐方式
-		type: String,
+ * @property {String} errorType = [toast | message] 错误提示类型
-		default: '120rpx'
+ * @property {Object} model 数据对象,表单校验时用
-	},
+ */
-	labelPosition: {
+export default {
-		type: String,
+	name: 'fs-form'
-		default: 'left',
+}
-		validator(value) {
+</script>
-			return ['left', 'top'].includes(value)
+
-		}
+<script setup>
-	},
+import { provide } from 'vue'
-	labelAlign: {
+
-		type: String,
+const props = defineProps({
-		default: 'left',
+	labelWidth: {
-		validator(value) {
+		type: String,
-			return ['left', 'center', 'right', 'justify'].includes(value)
+		default: '120rpx'
-		}
+	},
-	},
+	labelPosition: {
-	errorType: {
+		type: String,
-		type: String,
+		default: 'left',
-		default: 'toast',
+		validator(value) {
-		validator(value) {
+			return ['left', 'top'].includes(value)
-			return ['toast', 'message'].includes(value)
+		}
-		}
+	},
-	},
+	labelAlign: {
-	model: Object
+		type: String,
-})
+		default: 'left',
-
+		validator(value) {
-provide('form', props)
+			return ['left', 'center', 'right', 'justify'].includes(value)
-
+		}
-defineExpose({
+	},
-	model: props.model,
+	errorType: {
-	errorType: props.errorType
+		type: String,
-})
+		default: 'toast',
-</script>
+		validator(value) {
-
+			return ['toast', 'message'].includes(value)
-<style lang="scss">
+		}
-
+	},
-</style>
+	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>
+<template>
-	<view
+	<view
-		class="fs-grid-item"
+		class="fs-grid-item"
-		:class="{'fs-grid-item-border': border,'fs-grid-item-radius': radius}"
+		:class="{ 'fs-grid-item-border': border, 'fs-grid-item-radius': radius }"
-		:style="{padding: padding ? '20rpx 0' : 0,backgroundColor: bgColor}"
+		:style="{ padding: padding ? '20rpx 0' : 0, backgroundColor: bgColor }"
-		@click="handleClick">
+		@click="handleClick"
-		<slot></slot>
+	>
-	</view>
+		<slot></slot>
-</template>
+	</view>
-
+</template>
-<script setup>
+
-import { inject } from 'vue'
+<script>
-
+/**
-const props = defineProps({
+ * 宫格项组件
-	link: String,
+ * @description 宫格项组件
-	linkType: {
+ * @property {String} link 跳转地址
-		type: String,
+ * @property {String} linkType 跳转类型
-		default: 'navigateTo'
+ */
-	},
+export default {
-})
+	name: 'fs-grid-item'
-
+}
-const emits = defineEmits(['click'])
+</script>
-
+
-const gird = inject('fsGrid', {})
+<script setup>
-
+import { inject } from 'vue'
-const { border, padding, bgColor, radius } = gird
+
-
+const props = defineProps({
-const handleClick = () => {
+	link: String,
-	if (props.link) {
+	linkType: {
-		uni[props.linkType]({
+		type: String,
-			url: props.link
+		default: 'navigateTo'
-		})
+	}
-	}
+})
-	emits('click')
+
-}
+const emits = defineEmits(['click'])
-</script>
+
-
+const gird = inject('fsGrid', {})
-<style lang="scss" scoped>
+
-.fs-grid-item{
+const { border, padding, bgColor, radius } = gird
-	display: flex;
+
-	align-items: center;
+const handleClick = () => {
-	justify-content: center;
+	if (props.link) {
-	flex-direction: column;
+		uni[props.linkType]({
-	text-align: center;
+			url: props.link
-	position: relative;
+		})
-	height: 100%;
+	}
-	background-color: #fff;
+	emits('click')
-	
+}
-	&-border::before{
+</script>
-		position: absolute;
+
-		content: '';
+<style lang="scss" scoped>
-		top: 0;
+.fs-grid-item {
-		right: 0;
+	display: flex;
-		bottom: 0;
+	align-items: center;
-		left: 0;
+	justify-content: center;
-		border: 1px solid var(--border-color);
+	flex-direction: column;
-		margin-left: -1px;
+	text-align: center;
-		margin-top: -1px;
+	position: relative;
-	}
+	height: 100%;
-	
+	background-color: #fff;
-	&-radius{
+
-		border-radius: var(--radius);
+	&-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>
 </style>

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

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

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

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

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

@@ -6,6 +6,24 @@
 	></view>
 	></view>
 </template>
 </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>
 <script setup>
 import { computed } from 'vue'
 import { computed } from 'vue'
 
 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -27,6 +27,19 @@
 </template>
 </template>
 
 
 <script>
 <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 {
 export default {
 	name: 'fs-popover'
 	name: 'fs-popover'
 }
 }

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

@@ -1,12 +1,28 @@
 <template>
 <template>
 	<view class="fs-popup">
 	<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>
 			<slot></slot>
 		</view>
 		</view>
 		<fs-mask v-if="showMask" :modelValue="modelValue" @close="handleClose" :maskClickable="maskClickable"></fs-mask>
 		<fs-mask v-if="showMask" :modelValue="modelValue" @close="handleClose" :maskClickable="maskClickable"></fs-mask>
 	</view>
 	</view>
 </template>
 </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>
 <script setup>
 import { computed } from 'vue'
 import { computed } from 'vue'
 
 
@@ -36,8 +52,8 @@ const props = defineProps({
 		default: true
 		default: true
 	},
 	},
 	customStyle: {
 	customStyle: {
-	  type: Object,
+		type: Object,
-	  default() {
+		default() {
 			return {}
 			return {}
 		}
 		}
 	}
 	}
@@ -62,40 +78,40 @@ const handleClose = () => {
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .fs-popup {
 .fs-popup {
-	&-drawer{
+	&-drawer {
 		position: fixed;
 		position: fixed;
 		background-color: #fff;
 		background-color: #fff;
 		z-index: 900;
 		z-index: 900;
-		transition: all .3s;
+		transition: all 0.3s;
 		overflow: auto;
 		overflow: auto;
 	}
 	}
-	
+
-	.left{
+	.left {
 		top: var(--window-top);
 		top: var(--window-top);
 		bottom: var(--window-bottom);
 		bottom: var(--window-bottom);
 		left: 0;
 		left: 0;
 		transform: translateX(-100%);
 		transform: translateX(-100%);
 	}
 	}
-	.right{
+	.right {
 		top: var(--window-top);
 		top: var(--window-top);
 		bottom: var(--window-bottom);
 		bottom: var(--window-bottom);
 		right: 0;
 		right: 0;
 		transform: translateX(100%);
 		transform: translateX(100%);
 	}
 	}
-	.top{
+	.top {
 		top: var(--window-top);
 		top: var(--window-top);
 		right: 0;
 		right: 0;
 		left: 0;
 		left: 0;
 		transform: translateY(-200%);
 		transform: translateY(-200%);
 	}
 	}
-	.bottom{
+	.bottom {
 		left: 0;
 		left: 0;
 		bottom: var(--window-bottom);
 		bottom: var(--window-bottom);
 		right: 0;
 		right: 0;
 		transform: translateY(100%);
 		transform: translateY(100%);
 	}
 	}
-	
+
-	.show{
+	.show {
 		transform: translateX(0);
 		transform: translateX(0);
 	}
 	}
 }
 }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -20,6 +20,18 @@
 </template>
 </template>
 
 
 <script>
 <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 {
 export default {
 	name: 'fs-select'
 	name: 'fs-select'
 }
 }

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

@@ -1,21 +1,36 @@
 <template>
 <template>
 	<view class="fs-side-bar">
 	<view class="fs-side-bar">
-		<view class="fs-side-bar-left" :style="{width: width}">
+		<view class="fs-side-bar-left" :style="{ width: width }">
-			<view 
+			<view
 				class="fs-side-bar-item line1"
 				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"
 				v-for="(item, index) in list"
 				:key="item[valueKey]"
 				:key="item[valueKey]"
-				@click="handleClick(item, index)">
+				@click="handleClick(item, index)"
-				{{item[titleKey]}}
+			>
+				{{ item[titleKey] }}
 			</view>
 			</view>
 		</view>
 		</view>
-		<view class="fs-side-bar-right">
+		<view class="fs-side-bar-right"><slot></slot></view>
-			<slot></slot>
-		</view>
 	</view>
 	</view>
 </template>
 </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>
 <script setup>
 import { ref } from 'vue'
 import { ref } from 'vue'
 
 
@@ -53,24 +68,24 @@ const handleClick = (item, index) => {
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-.fs-side-bar{
+.fs-side-bar {
 	display: flex;
 	display: flex;
 	height: 100%;
 	height: 100%;
-	
+
-	&-left{
+	&-left {
 		flex-shrink: 0;
 		flex-shrink: 0;
 		background-color: #fafafa;
 		background-color: #fafafa;
 		overflow: auto;
 		overflow: auto;
 	}
 	}
-	
+
-	&-item{
+	&-item {
 		padding: 30rpx var(--gutter);
 		padding: 30rpx var(--gutter);
 		position: relative;
 		position: relative;
 	}
 	}
-	&-active{
+	&-active {
 		background-color: #fff;
 		background-color: #fff;
 		color: var(--primary);
 		color: var(--primary);
-		&::before{
+		&::before {
 			position: absolute;
 			position: absolute;
 			content: '';
 			content: '';
 			left: 0;
 			left: 0;
@@ -80,8 +95,8 @@ const handleClick = (item, index) => {
 			background-color: currentColor;
 			background-color: currentColor;
 		}
 		}
 	}
 	}
-	
+
-	&-right{
+	&-right {
 		flex: 1;
 		flex: 1;
 		background-color: #fff;
 		background-color: #fff;
 		overflow: auto;
 		overflow: auto;

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

@@ -1,14 +1,21 @@
 <template>
 <template>
-	<view class="fs-space" :class="{'gutter-v': gutter}" :style="{gap: size}">
+	<view class="fs-space" :class="{ 'gutter-v': gutter }" :style="{ gap: size }"><slot></slot></view>
-		<slot></slot>
-	</view>
 </template>
 </template>
 
 
 <script>
 <script>
+/**
+ * 间距组件
+ * @description 间距组件(设置组件之间的间距)
+ * @property {String} size 间距大小
+ * @property {String} direction 间距方向
+ * @property {String} justify 间距方向
+ * @property {Boolean} gutter 下边距
+ */
 export default {
 export default {
-	name: "fs-space"
+	name: 'fs-space'
 }
 }
 </script>
 </script>
+
 <script setup>
 <script setup>
 const props = defineProps({
 const props = defineProps({
 	size: {
 	size: {
@@ -31,11 +38,11 @@ const props = defineProps({
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-.fs-space{
+.fs-space {
 	display: flex;
 	display: flex;
 	flex-direction: v-bind(direction);
 	flex-direction: v-bind(direction);
 	flex-wrap: wrap;
 	flex-wrap: wrap;
 	justify-content: v-bind(justify);
 	justify-content: v-bind(justify);
 	align-items: center;
 	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>
 	<view><slot></slot></view>
 </template>
 </template>
 
 
+<script>
+/**
+ * 滑动面板组件
+ * @description 滑动面板组件
+ * @property {Boolean} autoClose 是否自动关闭其他swipe按钮组
+ */
+export default {
+	name: 'fs-swipe-action-group'
+}
+</script>
+
 <script setup>
 <script setup>
 import { provide, reactive } from 'vue'
 import { provide, reactive } from 'vue'
 
 

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

@@ -27,6 +27,21 @@
 	</movable-area>
 	</movable-area>
 </template>
 </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>
 <script setup>
 import { inject, onMounted, reactive, ref, nextTick, getCurrentInstance } from 'vue'
 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-dots="indicatorDots"
 		:indicator-color="indicatorColor"
 		:indicator-color="indicatorColor"
 		:indicator-active-color="indicatorActiveColor"
 		:indicator-active-color="indicatorActiveColor"
-		:autoplay="autoplay" 
+		:autoplay="autoplay"
-		:interval="interval" 
+		:interval="interval"
-		:duration="duration" 
+		:duration="duration"
 		:circular="circular"
 		:circular="circular"
 		:vertical="vertical"
 		:vertical="vertical"
 		:previous-margin="previousMargin"
 		:previous-margin="previousMargin"
@@ -14,13 +14,14 @@
 		@change="handleChange"
 		@change="handleChange"
 		@transition="handleTransition"
 		@transition="handleTransition"
 		class="fs-swiper"
 		class="fs-swiper"
-		:class="{'fs-swiper-card': mode === 'card', 'gutter-v': gutter}"
+		:class="{ 'fs-swiper-card': mode === 'card', 'gutter-v': gutter }"
-		:style="{height: height}">
+		:style="{ height: height }"
+	>
 		<template v-if="mode === 'card'">
 		<template v-if="mode === 'card'">
 			<swiper-item class="fs-swiper-item-box" v-for="(item, index) in list" :key="index" @click="handleClick">
 			<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>
 					<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>
 				</view>
 			</swiper-item>
 			</swiper-item>
 		</template>
 		</template>
@@ -28,13 +29,41 @@
 			<swiper-item class="fs-swiper-item-box" v-for="(item, index) in list" :key="index" @click="handleClick">
 			<swiper-item class="fs-swiper-item-box" v-for="(item, index) in list" :key="index" @click="handleClick">
 				<view class="fs-swiper-item">
 				<view class="fs-swiper-item">
 					<fs-avatar shape="square" width="100%" height="100%" :src="item[keyMap.src]"></fs-avatar>
 					<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>
 				</view>
 			</swiper-item>
 			</swiper-item>
 		</template>
 		</template>
 	</swiper>
 	</swiper>
 </template>
 </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>
 <script setup>
 import { ref } from 'vue'
 import { ref } from 'vue'
 
 
@@ -124,17 +153,17 @@ const handleClick = item => {
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-.fs-swiper{
+.fs-swiper {
-	&-item{
+	&-item {
 		width: 100%;
 		width: 100%;
 		height: 100%;
 		height: 100%;
-		
+
-		&-text{
+		&-text {
 			position: absolute;
 			position: absolute;
 			left: 0;
 			left: 0;
 			right: 0;
 			right: 0;
 			bottom: 0;
 			bottom: 0;
-			background-color: rgba(0, 0, 0, .5);
+			background-color: rgba(0, 0, 0, 0.5);
 			color: #fff;
 			color: #fff;
 			padding: 10rpx 20rpx;
 			padding: 10rpx 20rpx;
 			font-size: 14px;
 			font-size: 14px;
@@ -143,19 +172,19 @@ const handleClick = item => {
 	}
 	}
 }
 }
 
 
-.fs-swiper-card{
+.fs-swiper-card {
-	.fs-swiper-item-box{
+	.fs-swiper-item-box {
 		width: 610rpx !important;
 		width: 610rpx !important;
 		left: 70rpx;
 		left: 70rpx;
 		position: relative;
 		position: relative;
 	}
 	}
-	.fs-swiper-item{
+	.fs-swiper-item {
-		transform: scale(.9);
+		transform: scale(0.9);
 		transition: all 0.2s ease-in 0s;
 		transition: all 0.2s ease-in 0s;
 		overflow: hidden;
 		overflow: hidden;
 	}
 	}
-	.card-cur{
+	.card-cur {
-		transform: scale(1)
+		transform: scale(1);
 	}
 	}
 }
 }
-</style>
+</style>

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

@@ -8,6 +8,23 @@
 	</view>
 	</view>
 </template>
 </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>
 <script setup>
 import { watch, ref } from 'vue'
 import { watch, ref } from 'vue'
 
 

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

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

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

@@ -1,143 +1,161 @@
-<template>
+<template>
-	<view
+	<view
-		class="fs-tag" 
+		class="fs-tag"
-		:class="[
+		:class="[
-			{
+			{
-				'fs-tag-round': round,
+				'fs-tag-round': round,
-				'fs-tag-plain': plain,
+				'fs-tag-plain': plain,
-				'fs-tag-mark': mark,
+				'fs-tag-mark': mark,
-				'fs-tag-mark-reverse': markReverse,
+				'fs-tag-mark-reverse': markReverse,
-				'fs-tag-none': closed
+				'fs-tag-none': closed
-			},
+			},
-			'fs-tag-' + size,
+			'fs-tag-' + size,
-			'bg-' + type,
+			'bg-' + type
-		]" 
+		]"
-		:style="[
+		:style="[{ color: color, backgroundColor: bgColor }, customStyle]"
-			{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>
-		<slot></slot>
+	</view>
-		<fs-icon
+</template>
-			class="fs-tag-close"
+
-			size="13px"
+<script>
-			type="icon-close"
+/**
-			v-if="closable"
+ * 标签组件
-			@click="handleClosed">
+ * @description 标签组件
-		</fs-icon>
+ * @property {String} size = [default | medium | large] 标签大小
-	</view>
+ * @property {String} color 文字颜色
-</template>
+ * @property {String} bgColor 背景色
-
+ * @property {Boolean} plain 是否镂空
-<script setup>
+ * @property {Boolean} mark 标记
-import { ref } from 'vue'
+ * @property {Boolean} markReverse 标记反转
-
+ * @property {Boolean} round 圆角
-const props = defineProps({
+ * @property {Boolean} closable 标签是否可移除
-	plain: Boolean,
+ * @property {Object} customStyle 标签自定义样式
-	round: Boolean,
+ * @property {String} type = [primary | danger | warning | info | success | default] 标签颜色类型
-	mark: Boolean,
+ */
-	markReverse: Boolean,
+export default {
-	closable: Boolean,
+	name: 'fs-tag'
-	type: {
+}
-		type: String,
+</script>
-		default: 'primary',
+
-		validator(value) {
+<script setup>
-			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
+import { ref } from 'vue'
-		}
+
-	},
+const props = defineProps({
-	color: String,
+	plain: Boolean,
-	bgColor: String,
+	round: Boolean,
-	size: String,
+	mark: Boolean,
-	customStyle: {
+	markReverse: Boolean,
-	  type: Object,
+	closable: Boolean,
-	  default() {
+	type: {
-			return {}
+		type: String,
-		}
+		default: 'primary',
-	},
+		validator(value) {
-})
+			return ['primary', 'success', 'info', 'warning', 'danger', 'default'].includes(value)
-
+		}
-const closed = ref(false)
+	},
-const handleClosed = () => {
+	color: String,
-	closed.value = true
+	bgColor: String,
-}
+	size: {
-</script>
+		type: String,
-
+		default: 'default',
-<style lang="scss" scoped>
+		validator(value) {
-.fs-tag{
+			return ['default', 'medium', 'large'].includes(value)
-  display: inline-flex;
+		}
-  height: 38rpx;
+	},
-  line-height: 38rpx;
+	customStyle: {
-  padding: 0 10rpx;
+		type: Object,
-  font-size: 11px;
+		default() {
-  color: #fff;
+			return {}
-  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;
+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>
 </style>

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

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

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

@@ -1,9 +1,19 @@
 <template>
 <template>
-	<view @click="handleClick">
+	<view @click="handleClick">{{ timeago(dateTime, format) }}</view>
-		{{timeago(dateTime, format)}}
-	</view>
 </template>
 </template>
 
 
+<script>
+/**
+ * 多久之前组件
+ * @description 多久之前组件
+ * @property {String, Number, Object} dateTime 日期
+ * @property {String} format 格式化
+ */
+export default {
+	name: 'fs-timeago'
+}
+</script>
+
 <script setup>
 <script setup>
 import timeago from '@/utils/timeago'
 import timeago from '@/utils/timeago'
 
 
@@ -21,6 +31,4 @@ const handleClick = () => {
 }
 }
 </script>
 </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-timeline-item" v-for="(item, index) in options" :key="index">
 			<view class="fs-dot-box">
 			<view class="fs-dot-box">
 				<slot name="dot" :item="item" :index="index">
 				<slot name="dot" :item="item" :index="index">
-					<view 
+					<view
-						class="fs-dot" 
+						class="fs-dot"
-						:class="index === 0 ? 'bg-' + activeColorType : ''" 
+						:class="index === 0 ? 'bg-' + activeColorType : ''"
-						:style="{backgroundColor: index === 0 ? activeColor : '#969799'}"
+						:style="{ backgroundColor: index === 0 ? activeColor : color }"
-					>
+					></view>
-					</view>
 				</slot>
 				</slot>
 			</view>
 			</view>
 			<view class="fs-timeline-line"></view>
 			<view class="fs-timeline-line"></view>
@@ -17,11 +16,27 @@
 	</view>
 	</view>
 </template>
 </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>
 <script setup>
 const props = defineProps({
 const props = defineProps({
 	options: Array,
 	options: Array,
-	colorType: String,
+	color: {
-	color: String,
+		type: String,
+		default: '#969799'
+	},
 	activeColor: String,
 	activeColor: String,
 	activeColorType: {
 	activeColorType: {
 		type: String,
 		type: String,
@@ -36,8 +51,8 @@ const props = defineProps({
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .fs-timeline {
 .fs-timeline {
 	padding-left: 40rpx;
 	padding-left: 40rpx;
-	
+
-	&-line{
+	&-line {
 		position: absolute;
 		position: absolute;
 		top: 0;
 		top: 0;
 		left: -20rpx;
 		left: -20rpx;
@@ -45,18 +60,18 @@ const props = defineProps({
 		height: 100%;
 		height: 100%;
 		background-color: #ebedf0;
 		background-color: #ebedf0;
 	}
 	}
-	
+
-	&-item{
+	&-item {
 		position: relative;
 		position: relative;
-		
+
-		&:last-child{
+		&:last-child {
-			.timeline-line{
+			.timeline-line {
 				display: none;
 				display: none;
 			}
 			}
 		}
 		}
 	}
 	}
 }
 }
-.fs-dot-box{
+.fs-dot-box {
 	position: absolute;
 	position: absolute;
 	left: -20rpx;
 	left: -20rpx;
 	top: 30rpx;
 	top: 30rpx;
@@ -64,13 +79,13 @@ const props = defineProps({
 	z-index: 10;
 	z-index: 10;
 	transform: translateX(-50%);
 	transform: translateX(-50%);
 }
 }
-.fs-dot{
+.fs-dot {
 	width: 15rpx;
 	width: 15rpx;
 	height: 15rpx;
 	height: 15rpx;
 	border-radius: 50%;
 	border-radius: 50%;
 	// background-color: #969799;
 	// background-color: #969799;
 }
 }
-.content{
+.content {
 	padding: var(--gutter) 0;
 	padding: var(--gutter) 0;
 }
 }
 </style>
 </style>

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

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

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

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

+ 1 - 1
package.json

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