Merge branch 'master' of ssh://github.com/bd-js/wcui

master
chenjiajian 2023-04-03 17:32:12 +08:00
commit 53aa4d89ff
6 changed files with 665 additions and 11 deletions

View File

@ -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`表单组件-多级联动

View File

@ -111,11 +111,6 @@ class Button extends Component {
w: 192px,
h: 52px,
f: 16px
),
xxxxl: (
w: 212px,
h: 64px,
f: 18px
)
);

208
src/form/star.js Normal file
View File

@ -0,0 +1,208 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @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`
<label
ref="box"
@mousemove=${this.handleMove}
@mouseleave=${this.handleLeave}
@click=${this.handleClick}
>
<wc-icon data-idx="0" name="star"></wc-icon>
<wc-icon data-idx="1" name="star"></wc-icon>
<wc-icon data-idx="2" name="star"></wc-icon>
<wc-icon data-idx="3" name="star"></wc-icon>
<wc-icon data-idx="4" name="star"></wc-icon>
<span class="text">${this['show-value'] ? val : ''}</span>
</label>
`
}
}
Star.reg('star')

View File

@ -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 {

453
src/layer/index.js Normal file
View File

@ -0,0 +1,453 @@
/**
* {弹窗组件}
* @author yutent<yutent.io@gmail.com>
* @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`
<div ref="box" #animation=${{ type: 'fade' }} class="layer">
<div
class="layer__title noselect"
style=${styleMap({ display: !!this.title ? '' : 'none' })}
>
${this.title}${this.type === 'notify'
? html`<wc-icon name="close"></wc-icon>`
: ''}
</div>
<div class="layer__content">
<slot></slot>
</div>
<div class="layer__ctrl noselect">
${this.btns.map((s, i) => html`<button data-idx=${i}>${s}</button>`)}
</div>
</div>
`
}
}
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: `<wc-input autofocus class="layer__content__input" value="${defaultValue}"></wc-input>`,
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: `
<div class="layer__content__toast style-${type}">
<wc-icon is="${ico}"></wc-icon>
<span class="toast-txt">${txt}</span>
</div>`,
type: 'toast'
})
}
Layer.reg('layer')
window.layer = layer

View File

@ -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"` 属性实现