diff --git a/Readme.md b/Readme.md index 67c5b45..bc23dcd 100644 --- a/Readme.md +++ b/Readme.md @@ -30,7 +30,7 @@ - [x] `wc-passwd`表单组件-文本输入框 - [x] `wc-textarea`表单组件-多行文本输入框 - [x] `wc-number`表单组件-步进数字输入 -- [ ] `wc-star`表单组件-评分 +- [x] `wc-star`表单组件-评分 - [x] `wc-radio`表单组件-单选框 - [ ] `wc-select`表单组件-下拉选择 - [ ] `wc-cascadar`表单组件-多级联动 diff --git a/src/form/button.js b/src/form/button.js index e2dd437..89f5b2d 100644 --- a/src/form/button.js +++ b/src/form/button.js @@ -111,11 +111,6 @@ class Button extends Component { w: 192px, h: 52px, f: 16px - ), - xxxxl: ( - w: 212px, - h: 64px, - f: 18px ) ); diff --git a/src/form/star.js b/src/form/star.js new file mode 100644 index 0000000..725e195 --- /dev/null +++ b/src/form/star.js @@ -0,0 +1,208 @@ +/** + * {} + * @author yutent + * @date 2023/03/21 16:14:10 + */ + +import { nextTick, css, html, Component, classMap } from '@bd/core' +import '../icon/index.js' + +class Star extends Component { + static props = { + value: 0, + text: [], + 'allow-half': false, + 'show-value': false, + disabled: false, + clearable: false + } + + static styles = [ + css` + :host { + display: flex; + font-size: 14px; + --size: 32px; + cursor: pointer; + user-select: none; + } + label { + display: flex; + align-items: center; + line-height: 1; + cursor: inherit; + + wc-icon { + margin: 0 3px; + transition: transform 0.15s linear, color 0.15s linear; + + &[name='star'] { + color: var(--color-plain-3); + } + + @for $i from 1 through 5 { + &:nth-child(#{$i}) { + &[name='star-full'], + &[name='star-half'] { + color: var(--star-active-#{$i}, var(--color-teal-1)); + } + } + } + &:hover { + transform: scale(1.05); + } + } + + span { + padding: 2px 8px 0; + margin: 0 6px; + color: var(--color-dark-1); + } + } + `, + // 尺寸 + css` + @use 'sass:map'; + $sizes: ( + s: ( + w: 52px, + h: 20px, + f: 12px + ), + m: ( + w: 72px, + h: 24px, + f: 12px + ), + l: ( + w: 108px, + h: 32px, + f: 14px + ) + ); + + @loop $s, $v in $sizes { + :host([size='#{$s}']) { + --size: #{map.get($v, 'h')}; + font-size: map.get($v, 'f'); + } + } + ` + ] + + #width = 32 + #value = { i: 0, f: 0 } + #stars = [] + + /** + * 更新图标渲染 + * i: int + * f: float + */ + #updateDraw(i, f = 0) { + let lastOne = 'star-half' + let value = this.value + let tmp = this.#value + + if (i === -1) { + i = Math.floor(value) + f = +(value % 1).toFixed(1) + if (i > 0 && i === value) { + i-- + f = 1 + } + } else { + f = f < 0.5 ? 0.5 : 1 + } + + if (!this['allow-half']) { + f = f > 0 ? 1 : 0 + } + // 数值没变化, 直接终止 + if (i === tmp.i && f === tmp.f) { + return + } + + if (f > 0.5) { + lastOne = 'star-full' + } + + this.#stars.forEach((it, k) => { + it.name = k < i ? 'star-full' : 'star' + }) + + if (f > 0) { + this.#stars[i].name = lastOne + } + + // 缓存结果 + this.#value = { i, f } + } + + handleMove(ev) { + if (this.disabled) { + return + } + + if (ev.target.tagName === 'WC-ICON') { + let idx = +ev.target.dataset.idx + this.#updateDraw(idx, +(ev.offsetX / this.#width).toFixed(1)) + } + } + + handleLeave() { + if (this.disabled) { + return + } + this.#updateDraw(-1) + } + + handleClick(ev) { + let tmp = this.#value + if (this.disabled) { + return + } + if (ev.target.tagName === 'WC-ICON') { + if (this.clearable && this.value === tmp.i + tmp.f) { + tmp.i = 0 + tmp.f = 0 + this.value = 0 + this.#stars.forEach(it => ((it.name = 'star'), (it.style.color = ''))) + } else { + this.value = tmp.i + tmp.f + } + this.$emit('change', { value: this.value }) + } + } + + mounted() { + this.#stars = [...this.$refs.box.children] + this.#width = this.#stars[0].clientWidth + } + + render() { + let val = this.value + if (this.text.length === 5) { + val = this.text[Math.ceil(val) - 1] + } else { + val = val || '' + } + return html` + + ` + } +} + +Star.reg('star') diff --git a/src/icon/index.js b/src/icon/index.js index c7df197..f4d5e52 100644 --- a/src/icon/index.js +++ b/src/icon/index.js @@ -57,8 +57,7 @@ class Icon extends Component { 'l': 32px, 'xl': 36px, 'xxl': 44px, - 'xxxl': 52px, - 'xxxxl': 64px + 'xxxl': 52px ); @loop $k, $v in $gaps { diff --git a/src/layer/index.js b/src/layer/index.js new file mode 100644 index 0000000..5a911cf --- /dev/null +++ b/src/layer/index.js @@ -0,0 +1,453 @@ +/** + * {弹窗组件} + * @author yutent + * @date 2023/03/06 15:17:25 + */ + +import { css, html, Component, nextTick, styleMap } from '@bd/core' +import '../form/input.js' +import Drag from '../drag/core.js' + +let uniqueInstance = null // 缓存当前打开的alert/confirm/prompt类型的弹窗 +let toastInstance = null // 缓存toast的实例 + +const LANG_TITLE = '提示' +const LANG_BTNS = ['取消', '确定'] +// 要保证弹层唯一的类型 +const UNIQUE_TYPES = ['alert', 'confirm', 'prompt'] + +class Layer extends Component { + static props = { + type: '', + mask: false, + title: { type: String, default: LANG_TITLE, attribute: false }, + content: { type: String, default: '', attribute: false }, + btns: ['取消', '确定'] + } + + static styles = [ + css` + :host { + display: none; + justify-content: center; + align-items: center; + position: fixed; + z-index: 65534; + left: 0; + top: 0; + width: 100%; + } + :host([type]) { + display: flex; + } + .noselect { + -webkit-touch-callout: none; + user-select: none; + img, + a { + -webkit-user-drag: none; + } + } + `, + + css` + .layer { + overflow: hidden; + flex: 0 auto; + position: absolute; + z-index: 65535; + border-radius: 3px; + color: #666; + font-size: 14px; + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3); + transition: opacity 0.2s ease-in-out, left 0.2s ease-in-out, + right 0.2s ease-in-out, top 0.2s ease-in-out, bottom 0.2s ease-in-out; + + &.scale { + transform: scale(1.01); + transition: transform 0.1s linear; + } + &.blur { + backdrop-filter: blur(5px); + } + + &:active { + z-index: 65536; + } + } + `, + + /* 弹层样式 */ + css` + .layer { + &__title { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 60px; + padding: 15px; + font-size: 16px; + color: var(--color-dark-2); + + wc-icon { + --size: 14px; + + &:hover { + color: var(--color-red-1); + } + } + } + + &__content { + display: flex; + position: relative; + width: 100%; + height: auto; + min-height: 50px; + word-break: break-all; + word-wrap: break-word; + + ::slotted(&__input) { + flex: 1; + height: 36px; + } + + ::slotted(&__toast) { + display: flex; + align-items: center; + width: 300px; + padding: 0 10px !important; + border-radius: 3px; + font-weight: normal; + text-indent: 8px; + --size: 16px; + color: var(--color-dark-1); + } + + ::slotted(&__toast.style-info) { + border: 1px solid #ebeef5; + background: #edf2fc; + color: var(--color-grey-3); + } + ::slotted(&__toast.style-success) { + border: 1px solid #e1f3d8; + background: #f0f9eb; + color: var(--color-green-3); + } + ::slotted(&__toast.style-warning) { + border: 1px solid #faebb4; + background: #faecd8; + color: var(--color-red-1); + } + ::slotted(&__toast.style-error) { + border: 1px solid #f5c4c4; + background: #fde2e2; + color: var(--color-red-1); + } + } + + &__ctrl { + display: none; + justify-content: flex-end; + width: 100%; + height: 60px; + padding: 15px; + line-height: 30px; + font-size: 14px; + color: #454545; + text-align: right; + + button { + min-width: 64px; + height: 30px; + padding: 0 10px; + margin: 0 5px; + border: 1px solid var(--color-plain-3); + border-radius: 2px; + white-space: nowrap; + background: #fff; + font-size: inherit; + font-family: inherit; + outline: none; + color: inherit; + + &:hover { + background: var(--color-plain-1); + } + + &:active { + border-color: var(--color-grey-1); + } + + &:focus { + box-shadow: 0 0 0 2px var(--color-plain-a); + } + + &:last-child { + color: #fff; + background: var(--color-teal-2); + border-color: transparent; + + &:hover { + background: var(--color-teal-1); + } + &:active { + background: var(--color-teal-3); + } + &:focus { + box-shadow: 0 0 0 2px var(--color-teal-a); + } + } + + &::-moz-focus-inner { + border: none; + } + } + } + } + `, + + css` + :host([mask]) { + height: 100%; + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(5px); + } + + :host([type='alert']), + :host([type='confirm']), + :host([type='prompt']) { + .layer { + max-width: 600px; + min-width: 300px; + background: #fff; + + &__content { + padding: 0 15px; + } + } + } + :host([type='notify']) { + .layer { + width: 300px; + height: 120px; + + &__content { + padding: 0 15px; + } + } + } + :host([type='toast']) { + .layer { + box-shadow: none; + + &__content { + min-height: 40px; + } + } + } + ` + ] + + mounted() { + this.$refs.box.$anim.start() + } + + updated() { + this.$refs.box.$anim.start() + } + + render() { + return html` +
+
+ ${this.title}${this.type === 'notify' + ? html`` + : ''} +
+
+ +
+
+ ${this.btns.map((s, i) => html``)} +
+
+ ` + } +} + +function layer(opt = {}) { + let layDom = document.createElement('wc-layer') + let { type = 'common', content = '' } = opt + + if (type === 'toast') { + opt = { + type, + content, + from: { top: 0 }, + to: { top: '30px' } + } + + if (toastInstance) { + toastInstance.close(true) + } + toastInstance = layDom + } else { + layDom.mask = opt.mask + + if (opt.btns === false) { + layDom.btns = [] + } else if (opt.btns && opt.btns.length) { + layDom.btns = opt.btns + } else { + layDom.btns = LANG_BTNS.concat() + } + + if (opt.intercept && typeof opt.intercept === 'function') { + layDom.intercept = opt.intercept + } + + layDom.mask = opt.mask + layDom['mask-close'] = opt['mask-close'] + + if (opt.hasOwnProperty('overflow')) { + layDom.overflow = opt.overflow + } + + /* 额外样式 */ + layDom['mask-color'] = opt['mask-color'] + + layDom.blur = opt.blur + + layDom.radius = opt.radius + layDom.background = opt.background + + if (opt.size && typeof opt.size === 'object') { + layDom.size = opt.size + } + + // 这3种类型, 只允许同时存在1个, 如果之前有弹出则关闭 + if (UNIQUE_TYPES.includes(opt.type)) { + if (uniqueInstance) { + uniqueInstance.close(true) + } + uniqueInstance = layDom + } + } + + if (opt.to && typeof opt.to === 'object') { + layDom.to = opt.to + if (opt.from && typeof opt.from === 'object') { + layDom.from = opt.from + } else { + layDom.from = opt.to + } + } + + layDom.type = opt.type + layDom.title = opt.title + if (opt.hasOwnProperty('fixed')) { + layDom.fixed = opt.fixed + } + + layDom.innerHTML = content + layDom.wrapped = false // 用于区分是API创建的还是包裹现有的节点 + document.body.appendChild(layDom) + + return layDom.promise +} + +layer.alert = function (content, title = LANG_TITLE, btns) { + if (typeof title === 'object') { + btns = title + title = LANG_TITLE + } + return this({ + type: 'alert', + title, + content, + mask: true, + btns + }) +} + +layer.confirm = function (content, title = LANG_TITLE, btns) { + if (typeof title === 'object') { + btns = title + title = LANG_TITLE + } + return this({ + type: 'confirm', + title, + content, + mask: true, + btns + }) +} + +layer.prompt = function (title = LANG_TITLE, defaultValue = '', intercept) { + if (typeof defaultValue === 'function') { + intercept = defaultValue + defaultValue = '' + } + + if (!intercept) { + intercept = function (val, done) { + if (val) { + done() + } + } + } + return this({ + type: 'prompt', + title, + content: ``, + mask: true, + intercept + }) +} + +layer.notify = function (content) { + return this({ + type: 'notify', + title: '通知', + content, + blur: true, + from: { right: '-300px', top: 0 }, + to: { right: 0 } + }) +} + +layer.toast = function (txt, type = 'info') { + var ico = type + switch (type) { + case 'info': + case 'warning': + break + case 'error': + ico = 'deny' + break + case 'success': + ico = 'get' + break + default: + ico = 'info' + } + + return this({ + content: ` +
+ + ${txt} +
`, + type: 'toast' + }) +} + +Layer.reg('layer') +window.layer = layer diff --git a/开发规范.md b/开发规范.md index 963c1a2..6f543ca 100644 --- a/开发规范.md +++ b/开发规范.md @@ -13,15 +13,14 @@ - `type=help` 灰色 3. 尺寸, 主要指 高度 - > 需要注意的是, 这里的高度, 仅为组件本身应该占的高度, 而非"可视内容"的真实高度, 比如 开头按钮, 实际上就可以不需要那么大。 + > 需要注意的是, 这里的高度, 仅为组件本身应该占的高度, 而非"可视内容"的真实高度, 比如 开关按钮, 实际上就可以不需要那么大。 - `size=s` 小号, 20px - `size=m` 中号, 24px - `size=l` 大号, 32px (默认值) - `size=xl` 加大号, 36px - `size=xxl` 加加大号, 44px - - `size=xxxl` 加加加加大号, 52px - - `size=xxxxl` 特大号, 64px + - `size=xxxl` 加加加大号, 52px 4. 自带点击事件的组件, 统一增加"节流/防抖"逻辑 > 统一为增加 `lazy="1000"` 属性实现 \ No newline at end of file