@nmgolden/zui
Version:
mobile ui framework
1 lines • 99.2 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/attachment/ZuiAttachment.vue","../src/attachment/index.js","../src/button/index.js","../src/calendar/ZuiCalendar.vue","../src/calendar/index.js","../src/deletable/ZuiDeletable.vue","../src/deletable/index.js","../src/input/ZuiInput.vue","../src/input/index.js","../src/field/ZuiField.vue","../src/field/index.js","../src/field-group/ZuiFieldGroup.vue","../src/field-group/index.js","../src/input-field/ZuiInputField.vue","../src/input-field/index.js","../src/image-uploader/Index.vue","../src/image-uploader/index.ts","../src/loading/index.js","../src/month/ZuiMonth.vue","../src/month/index.js","../src/placeholder/ZuiPlaceholder.vue","../src/placeholder/index.js","../src/switch/ZuiSwitch.vue","../src/switch/index.js","../src/switch-field/ZuiSwitchField.vue","../src/switch-field/index.js","../src/sortable-inputs/ZuiSortableInputs.vue","../src/sortable-inputs/index.js","../src/sortable-inputs-field/ZuiSortableInputsField.vue","../src/sortable-inputs-field/index.js","../src/selector/ZuiSelector.vue","../src/selector/index.js","../src/selector-field/ZuiSelectorField.vue","../src/selector-field/index.js","../src/tabs/ZuiTabs.vue","../src/tabs/index.js","../src/popup/ZuiPopup.vue","../src/popup/index.js","../src/popup-selector/ZuiPopupSelector.vue","../src/popup-selector/index.js","../src/license-plate-input/Index.vue","../src/license-plate-input/index.js","../src/user-picker/SearchBar.vue","../src/assets/avatar-default.png","../src/user-picker/SearchView.vue","../src/user-picker/ZuiUserPicker.vue","../src/user-picker/index.js","../src/red-dot/ZuiRedDot.vue","../src/red-dot/index.js"],"sourcesContent":["<template>\n <div class=\"zui-attachment\" :class=\"{ disabled }\">\n <ul v-if=\"attachments && attachments.length\" class=\"attachments\">\n <li v-for=\"(item, idx) in attachments\" :key=\"idx\" class=\"attachment clickable\" @click=\"onAttachmentClick(item)\">\n <span class=\"name\">{{ item.name }}</span>\n <i\n v-if=\"!readonly && editActions.includes('remove')\"\n class=\"remove iconfont-zui icon-zui-x clickable\"\n @click.stop=\"!disabled && onRemoveAttachment(idx)\"\n ></i>\n </li>\n </ul>\n <div\n v-if=\"!readonly && (attachments && attachments.length) < max && editActions.includes('add')\"\n class=\"add-btn\"\n :class=\"[disabled]\"\n >\n <div class=\"icon iconfont-zui icon-zui-plus\"></div>\n <input class=\"add\" type=\"file\" accept=\"*/*\" @change=\"onFileChange\" ref=\"fileDom\" :disabled=\"disabled\" />\n </div>\n <div v-if=\"readonly && (!attachments || !attachments.length)\" class=\"empty\">未上传</div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, watch } from 'vue'\n\nconst emit = defineEmits(['update:modelValue', 'fileSelect', 'attachmentClick'])\n\nconst props = defineProps({\n modelValue: {\n type: Array as () => Array<any>,\n default: () => []\n },\n max: { type: Number, default: 3 },\n /**\n * 可执行操作 add:增加,remove:移除\n */\n editActions: { type: Array, default: ['add', 'remove'] },\n disabled: Boolean,\n readonly: Boolean\n})\n\nconst attachments = ref(props.modelValue)\nwatch(attachments, (val) => {\n emit('update:modelValue', val)\n})\n\nwatch(\n () => props.modelValue,\n (val) => {\n attachments.value = val\n },\n { immediate: true }\n)\n\nconst fileDom = ref()\n\nfunction onFileChange() {\n const file = fileDom.value.files[0]\n emit('fileSelect', file)\n fileDom.value.value = ''\n}\n\nfunction onRemoveAttachment(idx) {\n attachments.value.splice(idx, 1)\n}\n\nfunction onAttachmentClick(attachment) {\n emit('attachmentClick', attachment)\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.zui-attachment {\n width: 100%;\n box-sizing: border-box;\n background: #ffffff;\n &.disabled {\n opacity: 0.5;\n }\n .attachments {\n &:not(:last-child) {\n margin-bottom: 8px;\n }\n .attachment {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-radius: var(--zui-size-border-radius-small);\n padding: 6px 16px;\n line-height: var(--zui-size-line-height-regular);\n background-color: var(--zui-color-fill-light);\n color: var(--zui-color-primary);\n\n &:not(:first-child) {\n margin-top: 8px;\n }\n\n .name {\n flex: 1;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n }\n .remove {\n flex: none;\n margin-left: 16px;\n color: var(--zui-color-text-secondary);\n font-size: var(--size-text-regular);\n }\n }\n }\n .add-btn {\n position: relative;\n overflow: hidden;\n height: 32px;\n width: 100%;\n border: 1px solid rgba($color: #e4e7ed, $alpha: 0.61);\n border-radius: 6px;\n box-sizing: border-box;\n box-shadow: var(--zui-box-shadow);\n .icon:not(.disabled):active {\n background: var(--zui-color-gray-light);\n }\n .icon {\n display: inline-block;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n line-height: 30px;\n text-align: center;\n color: var(--zui-color-text-secondary);\n font-size: var(--zui-size-text-regular);\n }\n .add {\n opacity: 0;\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n }\n }\n .empty {\n color: var(--zui-color-text-secondary);\n line-height: var(--zui-size-line-height-regular);\n padding: 6px 12px;\n background-color: var(--zui-color-fill-light);\n }\n}\n</style>\n","// 单文件引入方式\n\nimport Attachment from './ZuiAttachment.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiAttachment', Attachment)\n }\n}\n\nexport { Attachment }\n","// 单文件引入方式\n\nimport Button from './ZuiButton.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiButton', Button)\n }\n}\n\nexport { Button }\n","<!-- updated at 2022/04/11 -->\n<template>\n <div class=\"zui-calendar\">\n <div class=\"week\">\n <span class=\"day\" v-for=\"day in '一二三四五六日'\" :key=\"day\">\n {{ day }}\n </span>\n </div>\n <div class=\"dates\">\n <div class=\"date-block\" v-for=\"i in dayOffset\" :key=\"i\"></div>\n <div class=\"date-block\" v-for=\"day in days\" :key=\"day.num\">\n <div\n class=\"date\"\n :class=\"{ highlight: day.bottomText, today: isToday(day.date), exceeded: isExceeded(day.date) }\"\n @click=\"onDateClick(day)\"\n >\n <span class=\"date-num\" :class=\"{ active: isCurDate(day.date) }\">{{ day.text }}</span>\n <span v-if=\"day.bottomText\" class=\"bottom-text\"> {{ day.bottomText }}</span>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup>\nimport dateUtils from '@nmgolden/date-utils'\nimport { computed, ref, watch } from 'vue'\n\nconst emit = defineEmits(['dateClick', 'dateChange', 'update:curDate'])\nconst props = defineProps({\n monthDate: {\n type: Object,\n default: () => new Date()\n },\n curDate: {\n type: Object,\n default: () => new Date()\n },\n // 最小可选日期,默认60天前\n minDate: { type: Date, default: dateUtils.getDateByOffset(-60) },\n // 最大可选日期,默认今天\n maxDate: { type: Date, default: new Date() },\n formatter: Function\n})\n\nconst curDate = ref()\n\nconst days = computed(() => {\n const lastDate = dateUtils.getLastDateOfMonth(props.monthDate)\n const amount = lastDate.getDate()\n\n // 构造日期数组\n const days = []\n for (let i = 0; ++i <= amount; ) {\n days.push({ date: getDateByDateNum(i), num: i, text: i.toString(), bottomText: '' })\n }\n\n // 调用外部格式化方法格式化日期数据\n if (props.formatter) {\n return days.map((item) => {\n return props.formatter(item)\n })\n } else {\n return days\n }\n\n function getDateByDateNum(dateNum) {\n const [year, month, day] = [props.monthDate.getFullYear(), props.monthDate.getMonth(), dateNum]\n return new Date(year, month, day)\n }\n})\n\nconst dayOffset = computed(() => {\n const firstDate = dateUtils.getFirstDateOfMonth(props.monthDate)\n const firstDay = firstDate.getDay()\n return (firstDay - 1 + 7) % 7\n})\n\nwatch(\n () => props.curDate,\n (value) => {\n curDate.value = value\n }\n)\n\nwatch(curDate, (value) => {\n emit('dateChange', value)\n emit('update:curDate', value)\n})\n\nfunction isToday(date) {\n return dateUtils.isToday(date)\n}\n\nfunction isCurDate(date) {\n return dateUtils.isSameDate(curDate.value, date)\n}\n\nfunction isExceeded(date) {\n return date > props.maxDate || date < props.minDate\n}\n\nfunction onDateClick(day) {\n if (isExceeded(day.date)) {\n return\n }\n curDate.value = day.date\n emit('dateClick', day)\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.zui-calendar {\n .week {\n color: var(--zui-color-text-secondary);\n background: var(--zui-color-gray-light);\n padding: 4px 0;\n .day {\n display: inline-block;\n width: 14.28%;\n text-align: center;\n }\n }\n .dates {\n display: flex;\n flex-wrap: wrap;\n .date-block {\n display: block;\n width: 14.28%;\n min-height: 60px;\n padding: 8px 2px 0 2px;\n box-sizing: border-box;\n .date {\n display: flex;\n flex-direction: column;\n align-items: center;\n width: 100%;\n padding: 10px 0 10px 0px;\n\n &.highlight {\n border-top: 1px solid var(--zui-color-primary);\n background: rgba(var(--zui-color-primary-rgb), 0.08);\n padding-bottom: 4px;\n padding-top: 4px;\n }\n &.today {\n .date-num {\n color: var(--zui-color-primary);\n font-weight: bold;\n }\n }\n &.exceeded {\n .date-num {\n color: var(--zui-color-text-placeholder);\n }\n }\n .date-num {\n width: 24px;\n height: 24px;\n line-height: 24px;\n border-radius: 14px;\n text-align: center;\n &.active {\n background: var(--zui-color-primary);\n color: #ffffff;\n font-weight: bold;\n }\n }\n .bottom-text {\n font-size: var(--zui-size-text-xsmall);\n color: var(--zui-color-text-secondary);\n margin-top: 2px;\n }\n }\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport Calendar from './ZuiCalendar.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiCalendar', Calendar)\n }\n}\n\nexport { Calendar }\n","<!-- updated 2022/1/30-->\n<template>\n <div class=\"zui-deletable\" :class=\"{ 'hover-shadow': hoverShadow }\">\n <slot></slot>\n <!-- <div v-if=\"show\" class=\"delete-wrap\">\n <el-popconfirm\n v-if=\"needPrompt\"\n :confirm-button-text=\"confirmText\"\n :cancel-button-text=\"cancelText\"\n icon-color=\"red\"\n :title=\"promptTitle\"\n @confirm=\"onDelete\"\n >\n <template #reference>\n <button class=\"delete-btn\" :class=\"[size]\"><i class=\"iconfont icon-x\"></i></button>\n </template>\n </el-popconfirm>\n <button v-else class=\"delete-btn\" :class=\"[size]\" @click=\"onDelete\"><i class=\"iconfont icon-x\"></i></button>\n </div> -->\n </div>\n</template>\n\n<script>\nexport default {\n emits: ['delete'],\n props: {\n /**\n * 大小【large:默认,small】\n */\n size: {\n type: String,\n default: 'large'\n },\n show: {\n type: Boolean,\n default: true\n },\n hoverShadow: {\n type: Boolean,\n default: false\n },\n needPrompt: {\n type: Boolean,\n default: true\n },\n promptTitle: {\n type: String,\n default: '确认删除?'\n },\n confirmText: {\n type: String,\n default: '删除'\n },\n cancelText: {\n type: String,\n default: '取消'\n }\n },\n methods: {\n onDelete() {\n this.$emit('delete')\n }\n }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.zui-deletable {\n position: relative;\n cursor: pointer;\n transition: all 0.2s ease-in;\n\n &.hover-shadow:hover {\n opacity: 0.8;\n box-shadow: 0px 0px 12px 3px rgba($color: var(--zui-color-primary), $alpha: 0.4);\n }\n\n .delete-btn {\n position: absolute;\n top: -16px;\n right: -16px;\n width: 24px;\n height: 24px;\n border: 1px solid var(--zui-color-border-light);\n border-radius: 12px;\n box-sizing: border-box;\n background: #ffffff;\n box-shadow: var(--zui-box-shadow);\n color: var(--zui-color-text-placeholder);\n transition: all 0.2s ease-in-out;\n font-weight: bold;\n color: var(--zui-color-red);\n &.small {\n top: -12px;\n right: -12px;\n width: 20px;\n height: 20px;\n .iconfont {\n font-size: 12px;\n }\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport Deletable from './ZuiDeletable.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiDeletable', Deletable)\n }\n}\nexport { Deletable }\n","<template>\n <template v-if=\"!preview\">\n <div\n class=\"zui-input\"\n :class=\"{\n round,\n border: !hideBorder,\n disabled,\n 'zui-input-large': size === 'large',\n 'zui-input-small': size === 'small',\n multiline: multiline\n }\"\n >\n <slot name=\"prefix\"></slot>\n <input\n class=\"input\"\n v-if=\"!autoHeight && !multiline\"\n :style=\"inputStyle\"\n :type=\"showPassword ? 'text' : type\"\n :placeholder=\"placeholder\"\n v-model=\"value\"\n :maxlength=\"maxWordLength\"\n :disabled=\"disabled\"\n :readonly=\"readonly\"\n @blur=\"$emit('blur')\"\n ref=\"inputRef\"\n />\n <textarea\n v-else\n class=\"textarea\"\n :style=\"inputStyle\"\n :type=\"type\"\n :placeholder=\"placeholder\"\n v-model=\"value\"\n :rows=\"autoHeight ? 1 : lines\"\n :disabled=\"disabled\"\n :readonly=\"readonly\"\n ref=\"textareaRef\"\n @blur=\"$emit('blur')\"\n ></textarea>\n <span v-if=\"showWordLimit\" class=\"word-limit\" :class=\"{ danger: maxReached }\" :style=\"wordLimitPaddingStyle\"\n ><span class=\"current\">{{ !value ? 0 : String(value).length }}</span\n >/{{ maxWordLength }}</span\n >\n <span class=\"clear clickable\" :style=\"clearCss\" v-if=\"value && clearable\" @click.stop=\"onClearTap\">\n <i class=\"icon iconfont-zui icon-zui-clear\"></i>\n </span>\n <div v-if=\"showEye\" class=\"eye-wrap\" @click=\"onEyeClick\">\n <span\n v-if=\"type == 'password'\"\n class=\"icon iconfont-zui\"\n :class=\"{\n 'icon-zui-eye': showPassword,\n 'icon-zui-eye-slash': !showPassword\n }\"\n ></span>\n </div>\n <slot name=\"suffix\"></slot>\n </div>\n </template>\n <template v-else>\n <div class=\"zui-input preview\">\n <span v-if=\"modelValue\">{{ modelValue }}</span>\n <span v-else class=\"empty\">未填写</span>\n </div>\n </template>\n</template>\n<script lang=\"ts\" setup>\nimport { ref, computed, watch, CSSProperties, reactive, nextTick, onMounted } from 'vue'\n\nconst emit = defineEmits(['update:modelValue', 'blur'])\n\nconst props = defineProps({\n textAlign: {\n type: String,\n default: 'left'\n },\n // input 的 type 目前只支持 text、 number、password\n type: String,\n // 尺寸大小 small/(default)/large\n size: String,\n // 自动行高,true 时自动忽略 multiline 配置\n autoHeight: Boolean,\n // 多行显示\n multiline: Boolean,\n // 行高 默认3行\n lines: { type: Number, default: 3 },\n hideBorder: Boolean,\n round: Boolean,\n padding: { type: String, default: '6px 8px 6px 12px' },\n placeholder: String,\n modelValue: { type: [String, Number], default: '' },\n showWordLimit: Boolean,\n maxWordLength: {\n type: Number,\n default: 99999\n },\n clearable: Boolean,\n disabled: Boolean,\n readonly: Boolean,\n preview: Boolean,\n // 显示眼睛\n showEye: Boolean,\n autoFocus: Boolean\n})\n\n//***** 输入内容 *****/\nconst inputRef = ref()\nconst value = ref(props.modelValue)\nwatch(\n () => props.modelValue,\n (val) => {\n value.value = val\n }\n)\nwatch(value, (val) => {\n emit('update:modelValue', val)\n})\n\n//***** 组件样式 *****/\nconst inputStyle: CSSProperties = {\n textAlign: props.textAlign,\n padding: props.padding\n} as CSSProperties\n\nconst paddingRight = computed(() => {\n const paddingStyle = inputStyle.padding || '0'\n const paddings = paddingStyle.toString().split(' ')\n if (paddings.length == 4) {\n return paddings[1]\n } else if (paddings.length == 2) {\n return paddings[1]\n } else if (paddings.length == 1) {\n return paddings[0]\n }\n return '8px'\n})\n\n//***** 多行文本 (自动高度)*****/\nconst forceUpdateKey = ref(0)\nconst textareaRef = ref()\nwatch(value, (newVal, oldVal) => {\n if (oldVal == newVal) {\n return\n }\n if (props.autoHeight) {\n forceUpdateKey.value += 1\n nextTick(() => {\n changeTextareaHeight()\n })\n }\n\n function changeTextareaHeight() {\n if (!textareaRef.value) {\n return\n }\n var scrollHeight = textareaRef.value.scrollHeight\n var height = textareaRef.value.offsetHeight\n\n textareaRef.value.style.height = 'auto'\n // 最高 200 px\n textareaRef.value.style.height = Math.min(textareaRef.value.scrollHeight - 8, 200) + 'px'\n if (height <= scrollHeight) {\n }\n }\n})\n\n//***** 字数限制 *****/\nconst maxReached = computed(() => {\n return value.value && String(value.value).length >= props.maxWordLength\n})\n\nwatch(value, (inputValue) => {\n if (maxReached.value) {\n value.value = String(inputValue).substring(0, props.maxWordLength)\n }\n})\n\nconst wordLimitPaddingStyle = computed(() => {\n return {\n paddingLeft: '0px',\n paddingRight: value.value && props.clearable ? '0px' : paddingRight.value\n } as CSSProperties\n})\n\n//***** 清空 ******/\nconst clearCss: CSSProperties = {\n paddingLeft: '8px',\n paddingRight: paddingRight.value\n} as CSSProperties\n\nfunction onClearTap() {\n value.value = ''\n}\n\n//***** 眼睛 *****//\nconst showPassword = ref(false)\nfunction onEyeClick() {\n showPassword.value = !showPassword.value\n console.log('showPassword.value :>> ', showPassword.value)\n}\n\n//***** 自动聚焦 *****//\n\nonMounted(() => {\n if (props.autoFocus) {\n const dom = inputRef.value || textareaRef.value\n dom && dom.focus()\n }\n})\n</script>\n\n<style lang=\"scss\" scoped>\n.zui-input {\n width: 100%;\n box-sizing: border-box;\n display: flex;\n background-color: #ffffff;\n box-sizing: border-box;\n transition: border 0.2s ease-out;\n .input {\n width: 100%;\n box-sizing: border-box;\n line-height: var(--zui-size-line-height-regular);\n &::placeholder {\n color: var(--zui-color-text-placeholder);\n }\n }\n .textarea {\n width: 100%;\n resize: none;\n line-height: var(--zui-size-line-height-regular);\n &::placeholder {\n color: var(--zui-color-text-placeholder);\n }\n }\n .word-limit {\n display: flex;\n align-items: center;\n color: var(--zui-color-text-placeholder);\n font-size: var(--zui-size-text-small);\n line-height: var(--zui-size-line-height-regular);\n &.danger {\n color: var(--zui-color-red);\n }\n }\n .clear {\n display: flex;\n align-items: center;\n color: var(--zui-color-text-placeholder);\n opacity: 0.6;\n .icon {\n font-weight: normal;\n font-size: var(--zui-size-text-large);\n }\n }\n .eye-wrap {\n display: flex;\n flex: none;\n width: 32px;\n align-items: center;\n text-align: left;\n cursor: pointer;\n .icon {\n color: var(--zui-color-text-secondary);\n font-size: 18px;\n }\n }\n &.zui-input-small {\n font-size: var(--zui-size-text-small);\n input {\n line-height: var(--zui-size-line-height-small);\n padding: 3px 12px;\n }\n }\n &.disabled {\n opacity: 0.5;\n }\n &.border {\n border: 1px solid var(--zui-color-border);\n &:not(.disabled):hover {\n border-color: var(--zui-color-primary);\n }\n }\n &.round {\n border-radius: var(--zui-size-border-radius-small);\n }\n &.preview {\n line-height: var(--zui-size-line-height-regular);\n border-radius: var(--zui-size-border-radius-small);\n padding: 6px 12px;\n background-color: var(--zui-color-fill-light);\n user-select: text;\n .empty {\n color: var(--zui-color-text-secondary);\n }\n }\n &.multiline {\n position: relative;\n .word-limit {\n right: 0;\n bottom: 0;\n position: absolute;\n background-color: #ffffff;\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport Input from './ZuiInput.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiInput', Input)\n }\n}\n\nexport { Input }\n","<template>\n <div\n class=\"zui-field\"\n :class=\"{\n 'label-position-top': labelPosition == 'top',\n round,\n shadow: showShadow,\n padding,\n clickable,\n 'fade-label': fadeLabel && value\n }\"\n :style=\"style\"\n >\n <h1\n v-if=\"!hideLabel\"\n class=\"label-wrap\"\n :class=\"[labelStyle, label ? 'label-min-width' : '']\"\n :style=\"{ color: labelColor }\"\n >\n <label>{{ label }}</label\n ><span v-if=\"required\" class=\"required\">*</span>\n </h1>\n <slot name=\"accessory\">\n <div class=\"accessory\">\n <span class=\"value-wrap\">\n <h5 v-if=\"value\" class=\"value\">{{ value }}</h5>\n <span v-else class=\"placeholder\">{{ placeholder }}</span>\n </span>\n\n <i v-if=\"showArrowRight\" class=\"arrow-right iconfont-zui icon-zui-arrow-right-round\" />\n </div>\n </slot>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nconst props = defineProps({\n round: Boolean,\n showShadow: Boolean,\n // 内边距\n padding: String,\n hideLabel: Boolean,\n // label 位置 top/left\n labelPosition: {\n type: String,\n default: 'left'\n },\n clickable: {\n type: Boolean,\n default: true\n },\n label: String,\n labelColor: {\n type: String,\n default: ''\n },\n // 标签样式 weak/normal/strong/xstrong\n labelStyle: {\n type: String,\n default: 'strong'\n },\n required: Boolean,\n fadeLabel: Boolean,\n value: [String, Number, Boolean],\n placeholder: String,\n showArrowRight: {\n type: Boolean,\n default: true\n }\n})\n\nconst style = { padding: props.padding || '12px 20px' }\n</script>\n\n<style lang=\"scss\" scoped>\n.zui-field {\n position: relative;\n display: flex;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n box-sizing: border-box;\n background: #ffffff;\n &.label-position-top {\n flex-direction: column;\n align-items: flex-start;\n\n .accessory {\n margin-top: 8px;\n width: 100%;\n justify-content: space-between;\n .value-wrap {\n justify-content: space-between;\n }\n }\n }\n &.round {\n border-radius: var(--zui-size-border-radius-regular);\n }\n &.shadow {\n box-shadow: var(--zui-box-shadow);\n }\n &.clickable:active {\n background: rgba($color: #ffffff, $alpha: 0.49);\n }\n\n &.fade-label {\n .left {\n h1 {\n font-size: var(--zui-size-text-regular);\n color: var(--zui-color-text-secondary);\n font-weight: normal;\n }\n }\n .accessory {\n h5 {\n font-size: var(--zui-size-text-large);\n }\n }\n }\n .label-wrap {\n text-align: left;\n transition: all 0.2s ease-in-out;\n line-height: var(--zui-size-line-height-regular);\n font-size: var(--zui-size-text-regular);\n &.label-min-width {\n min-width: 30%;\n }\n &.strong {\n font-weight: bold;\n font-size: var(--zui-size-text-regular);\n }\n &.xstrong {\n font-weight: bold;\n font-size: var(--zui-size-text-large);\n }\n &.weak {\n color: var(--color-text-secondary);\n }\n .required {\n padding: 0 4px;\n color: var(--zui-color-red);\n }\n }\n .accessory {\n color: var(--zui-color-text-primary);\n flex: 1 auto;\n text-align: justify;\n text-align-last: left;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n .arrow-right {\n font-size: var(--zui-size-text-small);\n color: var(--zui-color-text-secondary);\n margin-left: 4px;\n }\n\n .placeholder {\n color: var(--zui-color-text-placeholder);\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport Field from './ZuiField.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiField', Field)\n }\n}\n\nexport { Field }\n","<template>\n <!-- 字段分组 -->\n <div\n class=\"zui-field-group\"\n :class=\"{\n 'top-border': borders === 'top' || borders === 'all',\n 'bottom-border': borders === 'bottom' || borders === 'all'\n }\"\n :style=\"style\"\n >\n <header>\n <slot name=\"header\">\n <h1 v-if=\"title\" class=\"title\" :class=\"[titleStyle, titlePosition]\">{{ title }}</h1>\n </slot>\n </header>\n <main class=\"fields\" :class=\"{ divider }\">\n <slot></slot>\n </main>\n </div>\n</template>\n\n<script setup>\nconst props = defineProps({\n title: String,\n // 标题样式 weak/(default)/strong\n titleStyle: {\n type: String,\n default: 'strong'\n },\n // 标题位置 inner/outer\n titlePosition: {\n type: String,\n default: 'inner'\n },\n // 边框 all/top/bottom/none\n borders: {\n type: String,\n default: 'none'\n },\n // 内部分隔线\n divider: {\n type: Boolean,\n default: true\n }\n})\n\nconst style = { 'border-top-width': props.titlePosition == 'outer' ? '0.5px' : '' }\n</script>\n\n<style lang=\"scss\" scoped>\n.zui-field-group {\n overflow: hidden;\n border-color: var(--zui-color-divider);\n border-style: solid;\n\n &.top-border {\n border-top-width: 0.5px;\n }\n &.bottom-border {\n border-bottom-width: 0.5px;\n }\n header {\n .title {\n padding: 0 20px 8px 20px;\n line-height: var(--zui-size-line-height-regular);\n color: var(--zui-color-text-secondary);\n\n &.weak {\n line-height: var(--zui-size-line-height-small);\n font-size: var(--zui-size-text-small);\n }\n &.outer {\n background-color: #ffffff;\n }\n }\n }\n :deep(.fields) {\n .zui-field {\n position: relative;\n border-radius: 0;\n box-shadow: none;\n }\n }\n :deep(.fields.divider) {\n > * {\n position: relative;\n &:not(:last-child):after {\n content: ' ';\n display: block;\n position: absolute;\n bottom: 0;\n right: 0;\n left: 20px;\n height: 0.5px;\n background: var(--zui-color-divider);\n }\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport FieldGroup from './ZuiFieldGroup.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiFieldGroup', FieldGroup)\n }\n}\n\nexport { FieldGroup }\n","<template>\n <div class=\"zui-input-field\" :class=\"{ 'label-position-top': labelPosition == 'top' }\">\n <zui-field\n class=\"field\"\n :padding=\"fieldPadding\"\n :required=\"required\"\n :label-position=\"labelPosition\"\n :label-style=\"labelStyle\"\n :round=\"round\"\n :show-shadow=\"showShadow\"\n :label=\"label\"\n :clickable=\"false\"\n >\n <template #accessory>\n <zui-input\n padding=\"8px 0\"\n class=\"input\"\n :type=\"type\"\n :show-border=\"false\"\n v-model=\"value\"\n :multiline=\"multiline\"\n :show-word-limit=\"showWordLimit\"\n :max-word-length=\"maxWordLength\"\n :placeholder=\"placeholder\"\n :clearable=\"clearable\"\n :textAlign=\"textAlign\"\n :readonly=\"readonly\"\n />\n </template>\n </zui-field>\n </div>\n</template>\n<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\n\nconst emit = defineEmits(['update:modelValue'])\n\nconst props = defineProps({\n modelValue: String,\n type: String,\n round: Boolean,\n showShadow: Boolean,\n labelPosition: String,\n //weak/normal/strong\n labelStyle: String,\n label: String,\n required: Boolean,\n multiline: Boolean,\n showWordLimit: Boolean,\n maxWordLength: Number,\n placeholder: String,\n clearable: Boolean,\n textAlign: String,\n readonly: Boolean\n})\n\nconst fieldPadding = props.labelPosition == 'top' ? '16px 20px 12px 20px' : '4px 20px'\n\nconst value = ref(props.modelValue)\nwatch(\n () => props.modelValue,\n (val) => {\n value.value = val\n }\n)\nwatch(value, (inputValue) => {\n emit('update:modelValue', inputValue)\n})\n</script>\n<style lang=\"scss\" scoped>\n.zui-input-field {\n &.label-position-top {\n .input {\n margin-top: 4px;\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport InputField from './ZuiInputField.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiInputField', InputField)\n }\n}\n\nexport { InputField }\n","<template>\n <div class=\"zui-image-uploader\" :class=\"{ disabled }\">\n <ul class=\"images\">\n <li v-for=\"(url, idx) in imageUrls\" :key=\"idx\" class=\"image-wrap clickable\" @click=\"onImageClick(idx)\">\n <img class=\"image\" :src=\"url\" />\n <div class=\"tags-wrap\">\n <ul v-if=\"images[idx]['tags']\" class=\"tags\">\n <li v-for=\"(tag, tagIdx) in images[idx]['tags']\" :key=\"tagIdx\">{{ tag }}</li>\n </ul>\n <zui-loading v-if=\"images[idx]['loading']\" class=\"loading\" position=\"inline\" size=\"xsmall\" />\n </div>\n\n <span v-if=\"!readonly && editActions.includes('remove')\" class=\"remove-wrap\">\n <i class=\"remove iconfont-zui icon-zui-x clickable\" @click.stop=\"onRemoveClick(idx)\"></i>\n </span>\n </li>\n\n <li\n v-if=\"!readonly && imageUrls.length < max && editActions.includes('add')\"\n class=\"image-wrap add-btn\"\n :class=\"{ loading: loading }\"\n @click=\"!loading && onAddClick()\"\n >\n <div v-if=\"!loading\" class=\"icon iconfont-zui icon-zui-camera\"></div>\n <zui-loading v-else class=\"loading\" position=\"inline\" size=\"xsmall\" />\n </li>\n </ul>\n <div v-if=\"readonly && !images.length\" class=\"empty\">未上传</div>\n </div>\n</template>\n\n<script setup>\nimport { ref, watch, computed } from 'vue'\nimport ZuiLoading from '../loading/ZuiLoading.vue'\n\nconst emit = defineEmits(['update:modelValue', 'addClick', 'imageClick', 'remove'])\n\nconst props = defineProps({\n modelValue: { type: Array, default: [] },\n disabled: Boolean,\n readonly: Boolean,\n loading: Boolean,\n padding: { type: String, default: '16px 20px' },\n /**\n * 可执行操作 add:增加,remove:移除\n */\n editActions: { type: Array, default: ['add', 'remove'] },\n max: {\n type: Number,\n default: 9\n }\n})\n\nconst images = ref()\nconst imageUrls = computed(() => {\n return images.value.map((item) => item.url)\n})\n\nwatch(images, (val) => [emit('update:modelValue', val)])\nwatch(\n () => props.modelValue,\n (val) => {\n images.value = val\n },\n {\n immediate: true\n }\n)\n\n//***** 上传 *****/\nasync function onAddClick() {\n emit('addClick')\n}\n\n//***** 移除 *****/\nfunction onRemoveClick(idx) {\n emit('remove', idx)\n}\n\n//***** 预览 *****/\nfunction onImageClick(idx) {\n emit('imageClick', idx)\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.zui-image-uploader {\n width: 100%;\n box-sizing: border-box;\n &.disabled {\n opacity: 0.5;\n }\n .images {\n text-align: left;\n margin: -8px 0 0 -8px;\n\n .image-wrap {\n display: inline-block;\n position: relative;\n margin-left: 8px;\n margin-top: 8px;\n width: 64px;\n height: 64px;\n background-color: var(--zui-color-background);\n border-radius: var(--zui-size-border-radius-small);\n\n .image {\n object-fit: cover;\n width: 100%;\n height: 100%;\n border-radius: var(--zui-size-border-radius-small);\n }\n .tags-wrap {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n border-radius: var(--size-border-radius-small);\n overflow: hidden;\n .tags {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n padding: 4px 0;\n font-size: var(--zui-size-text-small);\n line-height: 1.2em;\n color: #ffffff;\n text-align: center;\n background-color: rgba(0, 0, 0, 0.5);\n }\n }\n\n .remove-wrap {\n display: block;\n position: absolute;\n top: -8px;\n right: -8px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n box-shadow: var(--zui-box-shadow);\n background-color: #ffffff;\n .remove {\n width: 16px;\n height: 16px;\n line-height: 16px;\n text-align: center;\n background-color: var(--zui-color-text-secondary);\n border-radius: 50%;\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translateX(-50%) translateY(-50%);\n font-size: var(--zui-size-text-xsmall);\n color: #ffffff;\n }\n }\n\n &.add-btn {\n position: relative;\n .icon {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translateX(-50%) translateY(-50%);\n color: var(--zui-color-text-secondary);\n font-size: 32px;\n }\n &.loading {\n cursor: not-allowed;\n .loading {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translateX(-50%) translateY(-50%);\n }\n }\n }\n }\n }\n .empty {\n margin-top: 8px;\n border-radius: var(--zui-size-border-radius-small);\n line-height: var(--zui-size-line-height-regular);\n padding: 6px 12px;\n background-color: var(--zui-color-fill-light);\n color: var(--zui-color-text-secondary);\n }\n}\n</style>\n","// 单文件引入方式\n\nimport ImageUploader from './Index.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiImageUploader', ImageUploader)\n }\n}\n\nexport { ImageUploader }\n","// 单文件引入方式\n\nimport Loading from './ZuiLoading.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiLoading', Loading)\n }\n}\n\nexport { Loading }\n","<template>\n <div class=\"zui-month\">\n <div v-if=\"showYear\" class=\"year-wrap\">\n <div class=\"left btn-wrap\">\n <zui-button circle type=\"secondary\" size=\"small\" :disabled=\"!canPrevYearClick\" @click=\"onPrevYearClick\"\n ><i class=\"icon-left iconfont-zui icon-zui-arrow-left\"></i\n ></zui-button>\n </div>\n <span class=\"year\">{{ curYear }}年</span>\n\n <div class=\"right btn-wrap\">\n <zui-button circle type=\"secondary\" :disabled=\"!canNextYearClick\" size=\"small\" @click=\"onNextYearClick\"\n ><i class=\"icon-right iconfont-zui icon-zui-arrow-right\"></i\n ></zui-button>\n </div>\n </div>\n <ul class=\"month-wrap\">\n <li\n class=\"month\"\n :class=\"{ active: isCurMonth(month), disabled: !allowExceed && exceeded(month) }\"\n v-for=\"month in 12\"\n :key=\"month\"\n @click=\"onMonthClick(month, $event)\"\n ref=\"monthRefs\"\n >\n {{ month }}{{ !isCurMonth(month) ? '月' : '' }}\n </li>\n </ul>\n </div>\n</template>\n<script setup>\nimport { computed, nextTick, onMounted, ref, watch } from 'vue'\nimport dateUtils from '@nmgolden/date-utils'\n\nconst emit = defineEmits(['update:modelValue', 'yearChange', 'monthChange'])\n\nconst props = defineProps({\n modelValue: { type: Date, default: () => new Date() },\n maxDate: {\n type: Date,\n default: dateUtils.getFirstDateOfMonth(dateUtils.getToday())\n },\n minDate: {\n type: Date,\n default: new Date(2021, 0, 1)\n },\n showYear: { type: Boolean, default: true },\n allowExceed: Boolean\n})\n\nconst curDate = ref(props.modelValue)\nconst monthRefs = ref()\nconst curYear = computed(() => {\n if (!curDate.value) {\n return\n }\n return curDate.value.getFullYear()\n})\n\nconst curMonth = computed(() => {\n if (!curDate.value) {\n return\n }\n return curDate.value.getMonth() + 1\n})\n\nconst canPrevYearClick = computed(() => {\n return curYear.value > props.minDate.getFullYear()\n})\n\nconst canNextYearClick = computed(() => {\n return curYear.value < props.maxDate.getFullYear()\n})\n\nwatch(\n () => props.modelValue,\n (value) => {\n curDate.value = value\n }\n)\nwatch(curDate, (value) => {\n emit('update:modelValue', value)\n})\nwatch(curYear, (value, old) => {\n emit('yearChange', value)\n})\nwatch(curMonth, (val, old) => {\n emit('monthChange', val)\n nextTick(() => {\n monthRefs.value[val - 1].scrollIntoView()\n })\n})\n\nonMounted(() => {\n curDate.value = props.modelValue\n})\n\n// 初始化\nif (!curDate.value) {\n curDate.value = dateUtils.getFirstDateOfMonth(dateUtils.getToday())\n}\n\nfunction isCurMonth(month) {\n if (!curDate.value) {\n return false\n }\n return month - 1 == curDate.value.getMonth()\n}\n\nfunction exceeded(month) {\n const exceedMax =\n curYear.value > props.maxDate.getFullYear() ||\n (curYear.value == props.maxDate.getFullYear() && month - 1 > props.maxDate.getMonth())\n const exceedMin =\n curYear.value < props.minDate.getFullYear() ||\n (curYear.value == props.minDate.getFullYear() && month - 1 < props.minDate.getMonth())\n return exceedMax || exceedMin\n}\n\nfunction onMonthClick(month) {\n if (!props.allowExceed && exceeded(month)) {\n return\n }\n const newDate = new Date(Date.UTC(curYear.value, month - 1, 1))\n curDate.value = newDate\n}\nfunction onPrevYearClick() {\n const newDate = new Date(Date.UTC(curYear.value - 1, 0, 1))\n curDate.value = newDate\n}\nfunction onNextYearClick() {\n const newDate = new Date(Date.UTC(curYear.value + 1, 0, 1))\n curDate.value = newDate\n}\n</script>\n<style lang=\"scss\">\n.zui-month {\n .year-wrap {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 16px;\n .btn-wrap {\n .iconfont-zui {\n font-size: var(--zui-size-text-small);\n line-height: 28px;\n }\n }\n }\n .month-wrap {\n width: 100%;\n overflow: auto;\n white-space: nowrap;\n padding-bottom: 16px;\n padding-right: 20px;\n box-sizing: border-box;\n .month {\n display: inline-block;\n width: 28px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n &:not(:first-child) {\n margin-left: 20px;\n }\n &.active {\n background: var(--zui-color-primary);\n border-radius: 16px;\n color: #ffffff;\n }\n &.disabled {\n color: var(--zui-color-text-placeholder);\n }\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport Month from './ZuiMonth.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiMonth', Month)\n }\n}\n\nexport { Month }\n","<!--updated at 2022/04/09-->\n<template>\n <div class=\"zui-placeholder\">\n <div class=\"content-wrap\">\n <i v-if=\"type == 'empty'\" class=\"icon iconfont-zui icon-zui-empty\"></i>\n <i v-else-if=\"type == 'network-error'\" class=\"icon iconfont-zui icon-zui-network-error\"></i>\n <h5 class=\"title\">{{ computedTitle }}</h5>\n <zui-button v-if=\"showButton\" class=\"button\" round size=\"small\" type=\"primary\" @click=\"onBtnTap\">\n {{ computedButtonText }}\n </zui-button>\n </div>\n </div>\n</template>\n\n<script>\n// 载入此组件时就加载图片,避免后期网络错误时才加载图片导致的图片无法显示问题!\nconst types = { EMPTY: 'empty', NETWORK_ERROR: 'network-error' }\nexport default {\n emits: ['buttonClick'],\n components: {},\n props: {\n // 状态 empty空,network-error网络异常\n type: {\n type: String,\n default: types.EMPTY\n },\n title: String,\n showButton: Boolean,\n buttonText: String\n },\n data() {\n return {}\n },\n computed: {\n computedButtonText() {\n if (this.buttonText) {\n return this.buttonText\n }\n if (this.type == types.EMPTY) {\n return '添加'\n }\n\n if (this.type == types.NETWORK_ERROR) {\n return '重新加载'\n }\n },\n computedTitle() {\n if (this.title) {\n return this.title\n } else {\n if (this.type == types.EMPTY) {\n return '暂无内容'\n } else if (this.type == types.NETWORK_ERROR) {\n return '网络错误请重试'\n }\n }\n return '空'\n }\n },\n methods: {\n onBtnTap() {\n this.$emit('buttonClick')\n }\n }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.zui-placeholder {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n .content-wrap {\n display: flex;\n flex-direction: column;\n align-items: center;\n transform: translateY(-10%);\n .icon {\n font-size: 48px;\n line-height: 1em;\n color: var(--zui-color-primary);\n }\n .title {\n color: var(--zui-color-text-placeholder);\n margin-top: 8px;\n line-height: 1em;\n font-size: var(--zui-size-text-small);\n }\n .button {\n margin-top: 24px;\n padding: 0 16px;\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport Placeholder from './ZuiPlaceholder.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiPlaceholder', Placeholder)\n }\n}\n\nexport { Placeholder }\n","<!-- updated 2022/1/31 -->\n<template>\n <div class=\"zui-switch\">\n <div class=\"switch\" :class=\"{ negative: !value }\" @click=\"value = !value\">\n <em></em>\n <span class=\"yes\">{{ yesText }}</span>\n <span class=\"no\">{{ noText }}</span>\n </div>\n </div>\n</template>\n\n<script setup>\nimport { ref, watch } from 'vue'\nconst emit = defineEmits(['update:modelValue'])\nconst props = defineProps({\n modelValue: Boolean,\n yesText: {\n type: String,\n default: '是'\n },\n noText: {\n type: String,\n default: '否'\n }\n})\n\nconst value = ref(!!props.modelValue)\nwatch(\n () => props.modelValue,\n (val) => {\n value.value = val\n }\n)\nwatch(value, (val) => {\n emit('update:modelValue', val)\n})\n</script>\n\n<style scoped lang=\"scss\">\n.zui-switch {\n cursor: pointer;\n display: inline-block;\n .switch {\n width: 100px;\n height: 32px;\n line-height: 32px;\n box-shadow: var(--zui-box-shadow);\n position: relative;\n color: var(--zui-color-text-primary);\n padding: 0 5px;\n box-sizing: border-box;\n border-radius: 3px;\n > span {\n display: inline-block;\n width: 50%;\n position: relative;\n text-align: center;\n }\n > h1 {\n text-align: right;\n }\n > em {\n position: absolute;\n display: inline-block;\n width: 44px;\n height: 26px;\n background: var(--zui-color-primary);\n left: 5px;\n top: 3px;\n border-radius: 3px;\n transition: left 0.2s ease-in-out;\n }\n .yes {\n color: #ffffff;\n transition: color 0.2s ease-in-out;\n }\n .no {\n color: var(--zui-color-text-primary);\n transition: color 0.2s ease-in-out;\n }\n }\n\n .negative {\n > em {\n left: 51px;\n }\n .yes {\n color: var(--zui-color-text-primary);\n }\n .no {\n color: #ffffff !important;\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport Switch from './ZuiSwitch.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiSwitch', Switch)\n }\n}\n\nexport { Switch }\n","<template>\n <div class=\"zui-switch-field\" :class=\"{ 'label-position-top': labelPosition == 'top' }\">\n <zui-field\n class=\"field\"\n :padding=\"fieldPadding\"\n :label-position=\"labelPosition\"\n :label-style=\"labelStyle\"\n :round=\"round\"\n :show-shadow=\"showShadow\"\n :label=\"label\"\n :clickable=\"false\"\n >\n <template #accessory>\n <zui-switch v-model=\"value\" :yes-Text=\"yesText\" :no-text=\"noText\" />\n </template>\n </zui-field>\n </div>\n</template>\n<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\n\nconst emit = defineEmits(['update:modelValue'])\nconst props = defineProps({\n modelValue: Boolean,\n round: Boolean,\n showShadow: Boolean,\n labelPosition: String,\n // weak/normal/strong\n labelStyle: String,\n label: String,\n yesText: String,\n noText: String\n})\n\nconst value = ref(props.modelValue)\nwatch(\n () => props.modelValue,\n (val) => {\n value.value = val\n }\n)\nwatch(value, (inputValue) => {\n emit('update:modelValue', inputValue)\n})\n\nconst fieldPadding = props.labelPosition == 'top' ? '16px 20px 12px 20px' : '4px 20px'\n</script>\n<style lang=\"scss\" scoped></style>\n","// 单文件引入方式\n\nimport SwitchField from './ZuiSwitchField.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiSwitchField', SwitchField)\n }\n}\n\nexport { SwitchField }\n","<template>\n <div class=\"sortable-inputs\">\n <div class=\"items\" ref=\"items\">\n <div v-for=\"(item, idx) in datas\" :key=\"(item as string)\" class=\"item\">\n <label v-if=\"idx != datas.length - 1\" class=\"remove clickable\"\n ><i class=\"icon iconfont-zui icon-zui-remove\" @click=\"onRemove(idx, item)\"></i\n ></label>\n <label v-if=\"showSequence\" class=\"sequence\">{{ idx + 1 }}.</label>\n <zui-input\n class=\"input\"\n clear\n placeholder=\"点击输入\"\n text-align=\"left\"\n :show-border=\"false\"\n v-model=\"datas[idx]['text']\"\n padding=\"12px 8px\"\n clearable\n />\n <label v-if=\"idx != datas.length - 1\" class=\"sort clickable\">\n <i class=\"icon iconfont-zui icon-zui-sort\"></i\n ></label>\n </div>\n </div>\n </div>\n</template>\n<script setup lang=\"ts\">\nimport Sortable from 'sortablejs'\nimport { nextTick, ref, watch, CSSProperties } from 'vue'\n\nconst emit = defineEmits(['update:modelValue'])\n\nconst props = defineProps({\n modelValue: { default: () => [], type: Array },\n showSequence: Boolean\n})\n\n//***** 数据 *****/\nconst datas = ref([...props.modelValue, { text: '' }])\nwatch(\n () => props.modelValue,\n (value) => {\n datas.value = [...value, { text: '' }]\n }\n)\n//***** 排序 *****/\nconst items = ref()\n\nnextTick(() => {\n Sortable.create(items.value, {\n handle: '.sort',\n onEnd: function (evt) {\n // 元素放到目标位置,其余元素(拖动开始到拖动结束位置直接的元素)向上顶或向下压\n const { oldIndex: startIndex, newIndex: endIndex } = evt\n const dragData = datas.value[startIndex]\n datas.value.splice(startIndex, 1)\n datas.value.splice(endIndex, 0, dragData)\n }\n })\n})\n\n//***** 保证末尾有一个空元素 *****/\nwatch(\n datas,\n (newValue: any) => {\n const lastData = newValue[newValue.length - 1]\n if (lastData.text) {\n datas.value.push({ text: '' })\n }\n if (newValue.length > 1) {\n const penultData = newValue[newValue.length - 2]\n if (!penultData.text) {\n datas.value.pop()\n }\n }\n },\n { deep: true }\n)\n\n//***** 移除 *****/\nfunction onRemove(idx, item) {\n datas.value.splice(idx, 1)\n}\n\n//***** 监听条目变化 发送更新事件*****/\nwatch(\n datas,\n (newValue) => {\n const resolvedValue = newValue.slice(0, newValue.length - 1)\n if (JSON.stringify(resolvedValue) == JSON.stringify(props.modelValue)) {\n return\n }\n\n emit('update:modelValue', resolvedValue)\n },\n { deep: true }\n)\n</script>\n<style lang=\"scss\" scoped>\n.sortable-inputs {\n width: 100%;\n .item {\n position: relative;\n display: flex;\n align-items: center;\n background-color: #ffffff;\n &.item:not(:last-child)::after {\n position: absolute;\n bottom: 0;\n left: 20px;\n height: 0.5px;\n width: calc(100% - 20px);\n content: '';\n display: inline-block;\n background-color: var(--color-divider-light);\n }\n &.item:not(:first-child):last-child {\n padding-left: 22px;\n }\n .remove {\n margin-right: 8px;\n width: 16px;\n .icon {\n color: var(--zui-color-red);\n }\n }\n .sequence {\n min-width: 20px;\n color: var(--zui-color-text-placeholder);\n }\n .input {\n padding: 0 16px 0 0;\n }\n .sort {\n color: var(--zui-color-primary);\n .icon {\n font-size: 20px;\n }\n }\n }\n}\n</style>\n","// 单文件引入方式\n\nimport SortableInputs from './ZuiSortableInputs.vue'\n\nexport default {\n install(Vue) {\n Vue.component('ZuiSortableInputs', SortableInputs)\n }\n}\n\nexport { SortableInputs }\n","<template>\n <div class=\"zui-sortable-inputs-field\" :class=\"{ 'label-position-top': labelPosition == 'top' }\">\n <zui-field\n class=\"field\"\n :padding=\"fieldPadding\"\n :label-position=\"labelPosition\"\n :label-style=\"labelStyle\"\n :round=\"round\"\n :show-shadow=\"showShadow\"\n :label=\"label\"\n :clickable=\"false\"\n :required=\"required\"\n >\n <template #accessory>\n <zui-sortable-inputs class=\"inputs\" v-model=\"value\" :options=\"options\" :multiple=\"multiple\" itemPadding=\"0px\" />\n </template>\n </zui-field>\n </div>\n</template>\n<script setup lang=\"ts\">\nimport { ref, watch }