master
宇天 2020-12-31 12:19:49 +08:00
commit 5673d107e7
48 changed files with 5611 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
.DS_Store
.AppleDouble
.LSOverride
.idea
._*
.Spotlight-V100
.Trashes
build
build/**
node_modules
node_modules/**
package-lock.json

16
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
},
"args": ["."]
}
]
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
README.md Normal file
View File

@ -0,0 +1,4 @@
## E-pub Reader
> E-pub Reader, 它不是功能最强的, 但可能是UI最耐看的, 最适合看技术类电子书的。

BIN
icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
icons/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
icons/app.icns Normal file

Binary file not shown.

BIN
icons/epub.psd Normal file

Binary file not shown.

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "org.bytedo.epub",
"version": "2.1.2",
"description": "E-pub Reader",
"main": "src/main.js",
"scripts": {
"start": "electron .",
"pack": "electron-builder",
"pack:mac": "electron-builder --mac"
},
"author": {
"name": "yutent",
"email": "yutent.io@gmail.com"
},
"homepage": "https://yutent.top",
"license": "MIT",
"build": {
"appId": "org.bytedo.epub",
"productName": "E-pub Reader",
"copyright": "Copyright © 2019 ${author}",
"directories": {
"buildResources": "icons",
"output": "build"
},
"electronDownload": {
"version": "11.0.4",
"mirror": "https://npm.taobao.org/mirrors/electron/"
},
"files": ["src/**/*", "node_modules/iofs/*"],
"mac": {
"category": "public.app-category.developer-tools",
"target": "dmg",
"icon": "icons/app.icns",
"darkModeSupport": false
}
},
"devDependencies": {
"electron": "11.0.4",
"electron-builder": "^22.1.0"
},
"dependencies": {
"iofs": "^1.3.2"
}
}

1
src/css/app.css Normal file
View File

@ -0,0 +1 @@
html{font-size:12.8px;width:100%;height:100vh}body{overflow:hidden;display:flex;flex-direction:column;width:100%;height:100%;line-height:1.25;font-size:14px;color:var(--color-dark-1);background:rgba(255,255,255,0.3)}.app-drag{-webkit-app-region:drag;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.app-nodrag{-webkit-app-region:no-drag}.app{position:relative;display:flex;height:100%}.app .sidebar{display:flex;flex-direction:column;justify-content:space-between;width:76px;height:100%;padding:48px 22px 24px;background:var(--color-dark-1);color:var(--color-plain-1)}.app .sidebar .item{cursor:pointer}.app .sidebar .item:hover,.app .sidebar .item.active{color:var(--color-orange-1)}.app .sidebar .item:active{color:var(--color-orange-2)}.app .select-box{display:flex;flex-direction:column;width:200px;height:100%;background:rgba(255,255,255,0.5)}.app .select-box .form{display:flex;align-items:center;width:100%;height:35px;padding:0 6px;background:#fff;border-bottom:1px solid var(--color-plain-2)}.app .select-box .form wc-input{flex:1}.app .select-box .list{flex:1}.app .select-box .list .item{display:flex;flex-direction:column;justify-content:center;height:48px;padding:6px;border-bottom:1px solid rgba(200,200,200,0.3);transition:color 0.15s ease-in-out, background 0.15s ease-in-out;cursor:pointer}.app .select-box .list .item section{display:flex;justify-content:space-between;align-items:center}.app .select-box .list .item cite{color:var(--color-grey-2)}.app .select-box .list .item .percent{padding:0 4px;border-radius:2px;font-size:12px;font-weight:bold;color:var(--color-grey-1)}.app .select-box .list .item .percent.red{color:var(--color-red-1)}.app .select-box .list .item .percent.green{color:var(--color-green-3)}.app .select-box .list .item:last-child{border-bottom:0}.app .select-box .list .item:hover{color:var(--color-blue-1);background:rgba(255,255,255,0.7)}.app .select-box .list .item.active{color:var(--color-plain-1);background:var(--color-blue-1)}.app .select-box .list .item.active cite{color:inherit}.app .select-box .list .item.active .percent{color:#fff}.app .detail{position:relative;flex:1;height:100%;border-left:1px solid var(--color-plain-2);background:#fff}.app .detail .title{display:flex;justify-content:space-between;align-items:center;width:100%;height:35px;padding:0 16px;font-size:16px;font-weight:bold}.app .detail .title span{display:inline-flex}.app .detail .title wc-button{margin:0 6px}.app .detail .card{width:96%;padding:12px 12px 16px;margin:12px 2% 24px;border:0;background:#fff;box-shadow:0 0 8px rgba(0,0,0,0.075)}.app .detail .card legend{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#64b5f6}.app .detail.blur::after{display:flex;justify-content:center;align-items:center;position:absolute;left:0;top:0;z-index:999;width:100%;height:100%;content:'搞基数据';background:#fff;color:#f7f8fb;font-size:100px}.app .preferences{width:640px;height:360px;border-radius:10px}.app .preferences .titlebar{width:100%;height:72px;border-bottom:1px solid var(--color-plain-3);background:var(--color-plain-1)}.app .preferences .titlebar .title{width:100%;height:24px;line-height:24px;text-align:center}.app .preferences .titlebar nav{display:flex;width:100%;height:40px;padding:0 16px;--size: 18px}.app .preferences .titlebar nav span{display:flex;flex-direction:column;align-items:center;justify-content:center;width:52px;height:40px;margin:0 6px;border-radius:6px;font-size:12px}.app .preferences .titlebar nav span.active{background:var(--color-plain-2);color:var(--color-blue-1)}.app .preferences .titlebar nav span:hover{background:var(--color-plain-2)}.app .preferences .tab-panel{padding:64px}.app .preferences .tab-panel p{margin-bottom:16px}.app .preferences .tab-panel .field{display:flex;align-items:center;height:64px}.app .preferences .tab-panel .field .label{width:200px;color:var(--color-grey-1)}.app.loading::after{display:flex;justify-content:center;align-items:center;position:absolute;left:0;top:0;z-index:999;width:100%;height:100%;content:'🌑';background:rgba(255,255,255,0.01);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);color:var(--color-orange-1);font-size:50px;-webkit-animation:loading 1s infinite;animation:loading 1s infinite}@-webkit-keyframes loading{1%,100%{content:'🌑'}12.5%{content:'🌒'}25%{content:'🌓'}37.5%{content:'🌔'}50%{content:'🌕'}62.5%{content:'🌖'}75%{content:'🌗'}87.5%{content:'🌘'}}@keyframes loading{1%,100%{content:'🌑'}12.5%{content:'🌒'}25%{content:'🌓'}37.5%{content:'🌔'}50%{content:'🌕'}62.5%{content:'🌖'}75%{content:'🌗'}87.5%{content:'🌘'}}

319
src/css/app.scss Normal file
View File

@ -0,0 +1,319 @@
@charset "UTF-8";
/**
*
* @authors yutent<yutent@doui.cc>
* @date 2018/12/16 17:15:07
*/
html {
font-size: 12.8px;
width: 100%;
height: 100vh;
}
body {
overflow: hidden;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
line-height: 1.25;
font-size: 14px;
color: var(--color-dark-1);
background: rgba(255, 255, 255, 0.3);
}
.app-drag {
-webkit-app-region: drag;
user-select: none;
}
.app-nodrag {
-webkit-app-region: no-drag;
}
.app {
position: relative;
display: flex;
height: 100%;
.sidebar {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 76px;
height: 100%;
padding: 48px 22px 24px;
background: var(--color-dark-1);
color: var(--color-plain-1);
.item {
cursor: pointer;
&:hover,
&.active {
color: var(--color-orange-1);
}
&:active {
color: var(--color-orange-2);
}
}
}
.select-box {
display: flex;
flex-direction: column;
width: 200px;
height: 100%;
background: rgba(255, 255, 255, 0.5);
.form {
display: flex;
align-items: center;
width: 100%;
height: 35px;
padding: 0 6px;
background: #fff;
border-bottom: 1px solid var(--color-plain-2);
wc-input {
flex: 1;
}
}
.list {
flex: 1;
.item {
display: flex;
flex-direction: column;
justify-content: center;
height: 48px;
padding: 6px;
border-bottom: 1px solid rgba(200, 200, 200, 0.3);
transition: color 0.15s ease-in-out, background 0.15s ease-in-out;
cursor: pointer;
section {
display: flex;
justify-content: space-between;
align-items: center;
}
cite {
color: var(--color-grey-2);
}
.percent {
padding: 0 4px;
border-radius: 2px;
font-size: 12px;
font-weight: bold;
color: var(--color-grey-1);
&.red {
color: var(--color-red-1);
}
&.green {
color: var(--color-green-3);
}
}
&:last-child {
border-bottom: 0;
}
&:hover {
color: var(--color-blue-1);
background: rgba(255, 255, 255, 0.7);
}
&.active {
color: var(--color-plain-1);
background: var(--color-blue-1);
cite {
color: inherit;
}
.percent {
color: #fff;
}
}
}
}
}
.detail {
position: relative;
flex: 1;
height: 100%;
border-left: 1px solid var(--color-plain-2);
background: #fff;
.title {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 35px;
padding: 0 16px;
font-size: 16px;
font-weight: bold;
span {
display: inline-flex;
}
wc-button {
margin: 0 6px;
}
}
.card {
width: 96%;
padding: 12px 12px 16px;
margin: 12px 2% 24px;
border: 0;
background: #fff;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.075);
legend {
-webkit-touch-callout: none;
user-select: none;
color: #64b5f6;
}
}
&.blur {
&::after {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
z-index: 999;
width: 100%;
height: 100%;
content: '搞基数据';
background: #fff;
color: #f7f8fb;
font-size: 100px;
}
}
}
.preferences {
width: 640px;
height: 360px;
border-radius: 10px;
.titlebar {
width: 100%;
height: 72px;
border-bottom: 1px solid var(--color-plain-3);
background: var(--color-plain-1);
.title {
width: 100%;
height: 24px;
line-height: 24px;
text-align: center;
}
nav {
display: flex;
width: 100%;
height: 40px;
padding: 0 16px;
span {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 52px;
height: 40px;
margin: 0 6px;
border-radius: 6px;
font-size: 12px;
&.active {
background: var(--color-plain-2);
color: var(--color-blue-1);
}
&:hover {
background: var(--color-plain-2);
}
}
--size: 18px;
}
}
.tab-panel {
padding: 64px;
p {
margin-bottom: 16px;
}
.field {
display: flex;
align-items: center;
height: 64px;
.label {
width: 200px;
color: var(--color-grey-1);
}
}
}
}
&.loading {
&::after {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
z-index: 999;
width: 100%;
height: 100%;
content: '🌑';
background: rgba(255, 255, 255, 0.01);
backdrop-filter: blur(2px);
color: var(--color-orange-1);
font-size: 50px;
animation: loading 1s infinite;
}
}
}
@keyframes loading {
1%,
100% {
content: '🌑';
}
12.5% {
content: '🌒';
}
25% {
content: '🌓';
}
37.5% {
content: '🌔';
}
50% {
content: '🌕';
}
62.5% {
content: '🌖';
}
75% {
content: '🌗';
}
87.5% {
content: '🌘';
}
}

1
src/css/float.css Normal file
View File

@ -0,0 +1 @@
html{font-size:12.8px;width:100%;height:100vh}body{display:flex;flex-direction:column;width:100%;height:100%;line-height:1.25;font-size:14px;color:var(--color-dark-1);background:rgba(255,255,255,0.3)}.app-drag{-webkit-app-region:drag;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.app-nodrag{-webkit-app-region:no-drag}.app{position:relative;display:flex;flex-direction:column;height:100%;padding:6px 0}.app .list{flex:1}.app .list .item{display:flex;align-items:center;height:54px;padding:10px 12px;line-height:15px;border-bottom:1px solid rgba(155,155,155,0.3)}.app .list .item:last-child{border-bottom:0}.app .list .item .info{overflow:hidden;flex:1}.app .list .item .info h3{font-size:14px}.app .list .item .info cite{color:var(--color-grey-2)}.app .list .item .last-days{display:flex;width:64px;height:30px;margin:0 6px}.app .list .item .today{width:52px;font-size:12px;text-align:right}.app .list .item .today span{display:block;padding:0 4px}.app .list .item .today .percent{border-radius:2px;color:#fff}.app .list .item .today .percent.red{background:var(--color-red-1)}.app .list .item .today .percent.green{background:var(--color-green-3)}

97
src/css/float.scss Normal file
View File

@ -0,0 +1,97 @@
@charset "UTF-8";
/**
*
* @authors yutent<yutent@doui.cc>
* @date 2018/12/16 17:15:07
*/
html {
font-size: 12.8px;
width: 100%;
height: 100vh;
}
body {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
line-height: 1.25;
font-size: 14px;
color: var(--color-dark-1);
background: rgba(255, 255, 255, 0.3);
}
.app-drag {
-webkit-app-region: drag;
user-select: none;
}
.app-nodrag {
-webkit-app-region: no-drag;
}
.app {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
padding: 6px 0;
.list {
flex: 1;
.item {
display: flex;
align-items: center;
height: 54px;
padding: 10px 12px;
line-height: 15px;
border-bottom: 1px solid rgba(155, 155, 155, 0.3);
&:last-child {
border-bottom: 0;
}
.info {
overflow: hidden;
flex: 1;
h3 {
font-size: 14px;
}
cite {
color: var(--color-grey-2);
}
}
.last-days {
display: flex;
width: 64px;
height: 30px;
margin: 0 6px;
}
.today {
width: 52px;
font-size: 12px;
text-align: right;
span {
display: block;
padding: 0 4px;
}
.percent {
border-radius: 2px;
color: #fff;
&.red {
background: var(--color-red-1);
}
&.green {
background: var(--color-green-3);
}
}
}
}
}
}

BIN
src/images/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src/images/tray@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

BIN
src/images/tray@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

120
src/index.html Normal file
View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<title></title>
<link href="/lib/css/reset-basic.css" rel="stylesheet">
<link href="/css/app.css" rel="stylesheet">
<script src="/js/app.js" type="module"></script>
</head>
<body class="noselect">
<div class="app" anot="app" :class="{loading: loading}">
<aside class="sidebar app-drag">
<wc-icon class="app-nodrag item stat active" is="chart"></wc-icon>
<wc-icon class="app-nodrag item opt" is="menu-dot" @click="showPreferencesPanel"></wc-icon>
</aside>
<div class="select-box">
<section class="form">
<wc-input
maxlength="6"
placeholder="输入编号搞新基"
round
@submit="addGay"
:duplex="input"
size="mini">
</wc-input>
</section>
<wc-scroll class="list">
<item
class="item"
:class="{active: curr.code === it.code}"
@click="viewGay(it)"
:for="it in list">
<strong class="text-ell" :text="it.name"></strong>
<section>
<cite :text="it.code"></cite>
<span
class="percent"
:class="{red: it.cp > 0, green: it.cp < 0}"
:text="it.cp + '%'">
</span>
</section>
</item>
</wc-scroll>
</div>
<div class="detail" :class="{blur: !curr.code}">
<section class="title app-drag">
<span>[{{curr.code}}] {{curr.name}}</span>
<span>
<wc-button
circle
size="mini"
@click="removeGay"
icon="trash">
</wc-button>
<wc-button
circle
color="red"
size="mini"
@click="updateGays"
icon="eye">
</wc-button>
</span>
</section>
<fieldset class="card">
<legend>实时数据</legend>
<wc-rank :attr-stat="curr.stat"></wc-rank>
</fieldset>
<fieldset class="card">
<legend>单位净值走势</legend>
<wc-line :attr-list="curr.line"></wc-line>
</fieldset>
</div>
<wc-layer ref="pre" mask mask-close radius="10px" >
<div class="preferences">
<div class="titlebar">
<div class="title">hello</div>
<nav>
<span :class="{active: preferences.tab === 1}" @click="switchTab(1)">
<wc-icon is="setting"></wc-icon>
常规
</span>
<span :class="{active: preferences.tab === 2}" @click="switchTab(2)">
<wc-icon is="info"></wc-icon>
关于
</span>
</nav>
</div>
<div class="tab-panel" :visible="preferences.tab === 1">
<section class="field">
<span class="label">神奇的2点半提醒</span>
<wc-switch :duplex="preferences.notify"></wc-switch>
</section>
</div>
<div class="tab-panel" :visible="preferences.tab === 2">
<p>“搞基爱啪啪” 是一款开源的, 非专业的搞基软件, 上面的数据全来自网络, 不对准确性作任何保证. </p>
<p>搞基有风险, 入行需谨慎. 你亏了别找我, 赚了可以给我发红包.</p>
</div>
</div>
</wc-layer>
</div>
</body>
</html>

311
src/js/app.js Normal file
View File

@ -0,0 +1,311 @@
/**
* {sonist app}
*
* @format
* @author yutent<yutent@doui.cc>
* @date 2018/12/16 17:15:57
*/
import '/lib/anot.js'
import '/lib/form/button.js'
import '/lib/form/switch.js'
import '/lib/scroll/index.js'
import '/lib/chart/rank.js'
import '/lib/chart/line.js'
import layer from '/lib/layer/index.js'
import Utils from '/lib/utils.js'
import app from '/lib/socket.js'
const log = console.log
function getJsonp(str) {
if (~str.indexOf('jsonpgz')) {
return new Function(`function jsonpgz(d){return d}; return ${str}`)()
}
return false
}
function getLineStat(str) {
return new Function(`${str}; return {line: Data_netWorthTrend.map(it => ({
x: ~~(it.x/1000),
y: +(it.y * 10000).toFixed(0),
p: it.equityReturn
})), e1: +syl_1y, e3: +syl_3y, e6: +syl_6y, e12: +syl_1n}`)()
}
function sleep(ms) {
return new Promise(_ => setTimeout(_, ms))
}
window.app = app
Anot({
$id: 'app',
state: {
input: '',
curr: {
code: '',
name: '',
stat: '',
line: ''
},
list: [],
$dict: {},
loading: false,
preferences: {
tab: 1,
notify: Anot.ls('notify') === '1'
}
},
watch: {
'preferences.notify'(v) {
Anot.ls('notify', v ^ 0)
if (v) {
app.dispatch('notify')
}
}
},
mounted() {
var old = this.syncOldStat()
if (old === false) {
this.reloadGays()
}
if (this.preferences.notify) {
app.dispatch('notify')
}
app.on('data-reload', data => {
this.reloadGays()
})
},
methods: {
reloadGays() {
var gays = Anot.ls('gays') || '{}'
var list = []
gays = JSON.parse(gays)
for (let code in gays) {
let { name, cm, cp, t } = gays[code]
list.push({ code, name, cm, cp, t: t || 0 })
this.$dict[code] = 1
}
list.sort((a, b) => b.cp - a.cp)
this.list = list
},
syncOldStat() {
var old = Anot.ls('watch_list')
var list = []
var dict = {}
if (old) {
old = JSON.parse(old)
for (let it of old) {
dict[it.code] = {
name: it.name,
cm: +it.curr,
cp: it.percent,
t: Date.now()
}
list.push({ code: it.code, ...dict[it.code] })
}
list.sort((a, b) => b.cp - a.cp)
this.list = list
Anot.ls('gays', dict)
Anot.ls('watch_list', null)
return true
}
return false
},
showPreferencesPanel() {
this.$refs.pre.show()
},
switchTab(n) {
this.preferences.tab = n
},
getGayStat(id) {
var res = app.dispatch(
'fetch',
`https://fundgz.1234567.com.cn/js/${id}.js`
)
return getJsonp(res)
},
addGay() {
var code = this.input
var gay
if (this.$dict[code]) {
layer.toast('这个鸡精在列表呢~~~', 'warn')
this.input = ''
return
}
if (code === 'debug') {
this.input = ''
return app.dispatch('devtools')
}
if (code.length < 6) {
return
}
if (/[^\d]/.test(code)) {
layer.toast('只能通过鸡精编号添加', 'error')
this.input = ''
return
}
gay = this.getGayStat(code)
if (gay) {
let tmp = {
code: gay.fundcode,
name: gay.name,
cm: +gay.gsz,
cp: +gay.gszzl,
t: Date.now()
}
this.input = ''
this.list.push(tmp)
this.$dict[tmp.code] = 1
this.list.sort((a, b) => b.cp - a.cp)
this.saveCache()
} else {
layer.toast('鸡精不存在', 'error')
}
},
async updateGays() {
var { code, stat } = this.curr
this.loading = true
for (let it of this.list) {
//
let info = this.getGayStat(it.code)
let time, needUpdate
it.cm = +info.gsz
it.cp = +info.gszzl
time = new Date(info.gztime.slice(0, 10) + ' 00:00:00')
time = ~~(time.getTime() / 1000) - 24 * 3600
// 如果走势最后的日期比当前最新的小, 则全量更新
if (it.t < time) {
it.t = time
needUpdate = this.updateLine(it.code)
}
if (it.code === code) {
stat = JSON.parse(stat)
stat.cm = it.cm
stat.cp = it.cp
if (needUpdate) {
stat.rank = needUpdate.line.slice(-60).map(_ => _.p)
stat.e1 = needUpdate.e1
stat.e3 = needUpdate.e3
stat.e6 = needUpdate.e6
stat.e12 = needUpdate.e12
this.curr.line = JSON.stringify(needUpdate.line)
}
this.curr.stat = JSON.stringify(stat)
}
await sleep(500)
}
//
this.loading = false
Anot.ss('last_update', Date.now())
layer.toast('数据更新成功', 'success')
this.list.sort((a, b) => b.cp - a.cp)
this.saveCache()
},
removeGay() {
var { code, name } = this.curr
layer
.confirm(`是否移除「${name}」?`)
.then(_ => {
for (let it of this.list) {
if (it.code === code) {
this.list.remove(it)
delete this.$dict[code]
Anot.ls(code, null)
this.saveCache()
break
}
}
this.viewGay(this.list[0])
})
.catch(Anot.noop)
},
saveCache() {
var dict = {}
for (let it of this.list) {
var { code, name, cm, cp, t } = it
dict[code] = { name, cm, cp, t }
}
Anot.ls('gays', dict)
},
updateLine(code) {
var gay = app.dispatch(
'fetch',
`http://fund.eastmoney.com/pingzhongdata/${code}.js?v=${Date.now()}`
)
gay = getLineStat(gay)
Anot.ls(code, JSON.stringify(gay))
return gay
},
viewGay(item) {
var gay = Anot.ls(item.code)
var rank, line
var { cm, cp, t } = item
this.curr.code = item.code
this.curr.name = item.name
if (gay) {
gay = JSON.parse(gay)
var last = gay.line[gay.line.length - 1].x
if (last < t) {
gay = null
}
}
if (!gay) {
gay = this.updateLine(item.code)
item.t = gay.line[gay.line.length - 1].x
this.saveCache()
}
rank = gay.line.slice(-60).map(_ => _.p)
line = JSON.stringify(gay.line)
this.curr.stat = JSON.stringify({
rank,
e1: gay.e1,
e3: gay.e3,
e6: gay.e6,
e12: gay.e12,
cm,
cp
})
this.curr.line = line
}
}
})

94
src/js/float.js Normal file
View File

@ -0,0 +1,94 @@
/**
*
* @author yutent<yutent.io@gmail.com>
* @date 2020/12/10 19:53:05
*/
import '/lib/anot.js'
import '/lib/scroll/index.js'
import layer from '/lib/layer/index.js'
import app from '/lib/socket.js'
function getJsonp(str) {
if (~str.indexOf('jsonpgz')) {
return new Function(`function jsonpgz(d){return d}; return ${str}`)()
}
return false
}
Anot({
$id: 'app',
state: {
list: []
},
mounted() {
this.reloadGays()
app.on('float-visible', data => {
var time = +Anot.ss('last_update') || 0
var now = Date.now()
// 有触发小窗口显示时, 更新通知提醒
if (Anot.ls('notify') === '1') {
app.dispatch('notify')
}
this.reloadGays()
setTimeout(() => {
// 如果离上次更新超过15分钟, 则自动更新
if (now - time > 15 * 60 * 1000) {
this.updateGays()
Anot.ss('last_update', now)
}
}, 500)
})
},
methods: {
reloadGays() {
var gays = Anot.ls('gays') || '{}'
var list = []
gays = JSON.parse(gays)
for (let code in gays) {
let { name, cm, cp, t } = gays[code]
list.push({ code, name, cm, cp, t })
}
list.sort((a, b) => b.cp - a.cp)
this.list = list
},
getGayStat(id) {
var res = app.dispatch(
'fetch',
`https://fundgz.1234567.com.cn/js/${id}.js`
)
return getJsonp(res)
},
updateGay(item) {
var info = this.getGayStat(item.code)
item.cm = +info.gsz
item.cp = +info.gszzl
},
updateGays() {
for (let it of this.list) {
this.updateGay(it)
}
this.list.sort((a, b) => b.cp - a.cp)
this.saveCache()
app.dispatch('data-reload')
},
saveCache() {
var dict = {}
for (let it of this.list) {
var { code, name, cm, cp, t } = it
dict[code] = { name, cm, cp, t }
}
Anot.ls('gays', dict)
}
}
})

8
src/lib/anot.js Normal file

File diff suppressed because one or more lines are too long

253
src/lib/chart/line.js Normal file
View File

@ -0,0 +1,253 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import $ from '../utils.js'
import '../form/button.js'
const DARK = '#62778d'
const BLUE = '#64b5f6'
const PLAIN = '#f2f5fc'
export default class Line extends HTMLElement {
static get observedAttributes() {
return ['list']
}
props = {
list: []
}
state = {
key: 1,
list: []
}
constructor() {
super()
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
display: flex;
width: 680px; }
.container {
position: relative;
padding: 24px 0 0; }
canvas {
width: 680px;
height: 230px; }
section {
position: absolute;
right: 0;
top: 0; }
</style>
<div class="container">
<canvas></canvas>
<section>
<wc-button color="blue" data-key="1" size="mini">1</wc-button>
<wc-button data-key="3" size="mini">3</wc-button>
<wc-button data-key="6" size="mini">半年</wc-button>
<wc-button data-key="12" size="mini">1</wc-button>
<wc-button data-key="36" size="mini">3</wc-button>
<wc-button data-key="999" size="mini">所有</wc-button>
</section>
</div>
`
var elem = this.root.children[1]
this.__SCENE__ = elem.firstElementChild
this.__FILTER__ = elem.lastElementChild
this.__CTX__ = this.__SCENE__.getContext('2d')
this.__SCENE__.width = 680
this.__SCENE__.height = 230
}
_getTime(n) {
var now = new Date()
var time = { getTime: _ => 0 }
var Y = now.getFullYear()
var m = now.getMonth()
var d = now.getDate()
switch (n) {
case 1:
time = new Date(Y, m - 1, d, 0, 0, 0)
break
case 3:
time = new Date(Y, m - 3, d, 0, 0, 0)
break
case 6:
time = new Date(Y, m - 6, d, 0, 0, 0)
break
case 12:
time = new Date(Y - 1, m, d, 0, 0, 0)
break
case 36:
time = new Date(Y - 3, m, d, 0, 0, 0)
break
}
return time.getTime()
}
_filter(n) {
if (n < 999) {
var time = this._getTime(n)
this.state.list = this.props.list.filter(it => it.x >= time)
} else {
this.state.list = this.props.list.concat()
}
}
draw() {
var { list, key } = this.state
var ctx = this.__CTX__
var x = 36
var max = 0
var min = Number.MAX_SAFE_INTEGER
var step = 0 // 纵坐标间隔
var dis = +(640 / list.length).toFixed(2) || 1 // 横坐标间隔
var point
var p1, p2, p3, p4
var format = key > 12 ? 'Y/m' : 'm/d'
for (let it of list) {
if (max < it.y) {
max = it.y
}
if (min > it.y) {
min = it.y
}
}
min = ~~(min / 100)
max = Math.ceil(max / 100)
step = ~~((max - min) / 3)
p1 = Math.floor(list.length / 4)
p2 = Math.floor(list.length / 2)
p3 = Math.floor((list.length * 3) / 4)
p4 = list.length - 1
ctx.clearRect(0, 0, 680, 230)
// 纵坐标数值
ctx.font = '12px Arial'
ctx.textAlign = 'right'
ctx.fillStyle = DARK
ctx.fillText(min / 100, 32, 205)
ctx.fillText((min + step) / 100, 32, 155)
ctx.fillText((min + step + step) / 100, 32, 105)
ctx.fillText((min + step + step + step) / 100, 32, 55)
ctx.font = '10px Arial'
ctx.textAlign = 'left'
ctx.fillText(new Date(list[0].x).format(format), x - 12, 225)
ctx.fillText(new Date(list[p1].x).format(format), x + dis * p1 - 12, 225)
ctx.fillText(new Date(list[p2].x).format(format), x + dis * p2 - 12, 225)
ctx.fillText(new Date(list[p3].x).format(format), x + dis * p3 - 12, 225)
ctx.fillText(
new Date(list[p4].x).format(format),
x + dis * p4 - 12 - (key > 12 ? 24 : 4),
225
)
// x轴参考线
ctx.fillStyle = PLAIN
ctx.fillRect(x, 50, 648, 1)
ctx.fillRect(x, 100, 648, 1)
ctx.fillRect(x, 150, 648, 1)
ctx.fillRect(x, 200, 648, 1)
// y轴参考 线
ctx.fillRect(x, 0, 1, 210)
ctx.fillRect(x + dis * p1, 0, 1, 210)
ctx.fillRect(x + dis * p2, 0, 1, 210)
ctx.fillRect(x + dis * p3, 0, 1, 210)
ctx.fillRect(x + dis * p4, 0, 1, 210)
point = list.shift()
// 曲线
ctx.beginPath()
ctx.strokeStyle = BLUE
ctx.lineWidth = 1
ctx.moveTo(x, 200 - (((point.y / 100 - min) / step) * 50).toFixed(0))
while (list.length) {
let y
point = list.shift()
y = 200 - (((point.y / 100 - min) / step) * 50).toFixed(0)
x += dis
ctx.lineTo(x, y)
}
ctx.stroke()
}
connectedCallback() {
$.bind(this.__FILTER__, 'click', ev => {
var el = ev.target
if (this.props.list.length < 1) {
return
}
if (el.tagName === 'WC-BUTTON') {
var k = +el.dataset.key
$.each(this.__FILTER__.children, function(it) {
it.removeAttribute('color')
})
el.setAttribute('color', 'blue')
this.state.key = k
this._filter(k)
this.draw()
}
})
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {
return
}
switch (name) {
case 'list':
try {
var list = JSON.parse(val)
list.forEach(it => (it.x = it.x * 1000))
this.props.list = list
this._filter(this.state.key)
this.removeAttribute('list')
this.draw()
} catch (e) {}
break
}
}
}
if (!customElements.get('wc-line')) {
customElements.define('wc-line', Line)
}

141
src/lib/chart/rank.js Normal file
View File

@ -0,0 +1,141 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
const RED = '#ff5061'
const GREEN = '#4caf50'
const BLUE = '#64b5f6'
const GREY = '#bdbdbd'
const PLAIN = '#f2f5fc'
const DARK = '#62778d'
export default class Rank extends HTMLElement {
static get observedAttributes() {
return ['stat']
}
props = {
stat: {}
}
constructor() {
super()
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
display: flex; }
canvas {
width: 680px;
height: 100px; }
</style>
<canvas></canvas>
`
this.__SCENE__ = this.root.children[1]
this.__CTX__ = this.__SCENE__.getContext('2d')
this.__SCENE__.width = 680
this.__SCENE__.height = 100
}
draw() {
var { rank, e1, e3, e6, e12, cm, cp } = this.props.stat
var ctx = this.__CTX__
var x = 32
while (rank.length < 60) {
rank.unshift(0)
}
ctx.clearRect(0, 0, 680, 101)
ctx.font = '10px Arial'
ctx.textAlign = 'right'
ctx.fillStyle = RED
ctx.fillText('10%', 28, 10)
ctx.fillText('5%', 28, 30)
ctx.fillStyle = GREEN
ctx.fillText('-5%', 28, 80)
ctx.fillText('-10%', 28, 100)
ctx.font = '10px menlo,Hiragino Sans GB'
ctx.textAlign = 'left'
ctx.fillStyle = DARK
ctx.fillText('60天红绿榜', 160, 10)
ctx.font = '12px menlo,Hiragino Sans GB'
ctx.fillText(`最近1个月收益: ${e1}%`, 360, 25)
ctx.fillText(`最近3个月收益: ${e3}%`, 360, 45)
ctx.fillText(`最近半年收益: ${e6}%`, 528, 25)
ctx.fillText(`最近一年收益: ${e12}%`, 528, 45)
ctx.fillStyle = cp > 0 ? RED : cp === 0 ? GREY : GREEN
ctx.fillRect(360, 65, 140, 20)
ctx.fillRect(526, 65, 140, 20)
ctx.fillStyle = '#fff'
ctx.font = 'bold 14px menlo,Hiragino Sans GB'
ctx.fillText(`实时净值: ¥${cm}`, 364, 80)
ctx.fillText(`实时涨跌: ${cp}%`, 532, 80)
ctx.fillStyle = PLAIN
ctx.fillRect(28, 25, 320, 1)
ctx.fillRect(28, 75, 320, 1)
ctx.fillStyle = GREY
ctx.fillRect(28, 0, 1, 140)
ctx.fillRect(0, 50, 348, 1)
while (rank.length) {
var n = rank.shift()
var y = Math.ceil(50 - (n / 10) * 50)
ctx.fillStyle = n > 0 ? RED : GREEN
if (y > 50) {
ctx.fillRect(x, 50, 3, y - 50)
} else {
ctx.fillRect(x, y, 3, 50 - y)
}
x += 5
}
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {
return
}
switch (name) {
case 'stat':
try {
var stat = JSON.parse(val)
this.props.stat = stat
this.removeAttribute('stat')
this.draw()
} catch (e) {}
break
}
}
}
if (!customElements.get('wc-rank')) {
customElements.define('wc-rank', Rank)
}

View File

@ -0,0 +1,78 @@
@charset "UTF-8";
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2014-10-10 00:45:09
*
* douiCSS
*
* ,
* do-st-*
* do-fn-*
* do-mod-modname
* UIdo-uiname, .do-uiname
* .do-layer .body { ... }
*
*
* 1 display float position overflow z-index /
* 2 width height margin padding border
* 3 line-height font-size vertical-align text-align user-select outline ....
* 4 color background opacity cursor ...
* 5 content list-style quotes ...
*
*/
* {margin: 0;padding: 0;vertical-align: baseline;box-sizing:border-box;}
::before,::after {box-sizing:border-box;}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section,content {display: block;}
img {border:0;display:inline-block;}
ol, ul {list-style: none;}
blockquote, q {quotes: none;}
blockquote::before, blockquote::after,
q::before, q::after {content: '';content: none;}
table {border-collapse: collapse;border-spacing: 0;}
a:focus,input,textarea,button:focus,input:focus,textarea:focus {outline:none;}
::-moz-focus-inner {
border:none;outline:none;
}
body {font-family:"Helvetica Neue", Arial,"WenQuanYi Micro Hei","PingFang SC","Hiragino Sans GB","Segoe UI", "Microsoft Yahei", sans-serif;-webkit-font-smoothing: antialiased;text-size-adjust: 100%;-webkit-tap-highlight-color: transparent;}
code,pre,samp {font-family:Menlo,Monaco,Consolas,"Courier New",monospace;}
[anot],[\:repeat],[\:if] {visibility:hidden;}
.noselect {-webkit-touch-callout: none;-webkit-user-select: none;-moz-user-select: none;user-select: none;}
.noselect img, .noselect a {-webkit-user-drag:none;}
.text-ell {overflow:hidden; white-space:nowrap; text-overflow:ellipsis }
.osx-thin {-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}
:root {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064;
}

1
src/lib/drag/core.js Normal file
View File

@ -0,0 +1 @@
"use strict";import $ from"../utils.js";const DEF_OPT={axis:"",limit:!1,overflow:!0};export default class Drag{constructor(t){this.$elem=t,this._init()}_init(){this.$elem.style.transform="";var{x:t,y:s}=this.$elem.getBoundingClientRect();this.pos={x:t,y:s,_x:0,_y:0}}by(t,s={}){return this.$drag=t,this.opt=Object.assign(Object.create(null),DEF_OPT,s),!1!==this.opt.limit&&(this.opt.overflow=!1),t.style.cursor="move",this._handleResize=$.bind(window,"resize",this._init.bind(this)),this._handleMousedown=$.bind(t,"mousedown",t=>{if(this.disabled)return;var s=this.$elem.getBoundingClientRect();s.x-this.pos._x!==this.pos.x&&(this.pos.x=s.x-this.pos._x),s.y-this.pos._y!==this.pos.y&&(this.pos.y=s.y-this.pos._y);let e=t.pageX,i=t.pageY,o=document.documentElement.clientWidth,n=document.documentElement.clientHeight,h=s.width,p=s.height,d=[0,o-h,n-p,0];if("parent"===this.opt.limit){let t=this.$elem.parentNode.getBoundingClientRect();d=[t.top,t.right-h,t.bottom-p,t.left]}let l=$.bind(document,"mousemove",t=>{t.preventDefault();let o=t.pageX-e+(s.x-this.pos.x),n=t.pageY-i+(s.y-this.pos.y);"x"===this.opt.axis&&(n=0),"y"===this.opt.axis&&(o=0),!1===this.opt.overflow&&(o<d[3]-this.pos.x?o=d[3]-this.pos.x:o>d[1]-this.pos.x&&(o=d[1]-this.pos.x),n<d[0]-this.pos.y?n=d[0]-this.pos.y:n>d[2]-this.pos.y&&(n=d[2]-this.pos.y)),this.pos._x=o,this.pos._y=n,this.$elem.dispatchEvent(new CustomEvent("dragging",{detail:{offset:{x:this.pos.x+o,y:this.pos.y+n},move:{x:o,y:n}}})),this.$elem.style.transform=`translate(${o}px, ${n}px)`}),m=$.bind(document,"mouseup",t=>{this.$elem.dispatchEvent(new CustomEvent("dragged",{detail:{offset:{x:this.pos.x+this.pos._x,y:this.pos.y+this.pos._y},move:{x:this.pos._x,y:this.pos._y}}})),$.unbind(document,"mousemove",l),$.unbind(document,"mouseup",m)})}),this}on(t,s){if(t&&"function"==typeof s)return $.bind(this,t,s)}off(t,s){$.unbind(this,t,s)}destroy(){$.unbind(window,"resize",this._handleResize),$.unbind(this.$drag,"mousedown",this._handleMousedown),delete this.$elem,delete this.$drag}};

1
src/lib/drag/index.js Normal file
View File

@ -0,0 +1 @@
"use strict";import Drag from"./core.js";Anot.directive("drag",{priority:1500,init:function(e){e.expr='"'+e.expr+'"',e.overflow=!0,e.axis="xy",e.element.dataset.axis&&(e.axis=e.element.dataset.axis,delete e.element.dataset.axis),e.limit=!1,e.element.dataset.limit&&(e.limit=e.element.dataset.limit,e.overflow=!1,delete e.element.dataset.limit)},update:function(e){var t=this.element;if(e)for(t=this.element.parentNode;t;){if(t.classList||Anot.error(`${this.name}=${this.expr}, 解析异常[元素不存在]`),"WC-LAYER"===t.tagName&&"layer"===e){t=t.root.children[1];break}if(t.classList.contains(e)||t.id===e)break;t=t.parentNode}new Drag(t).by(this.element,{limit:this.limit,axis:this.axis,overflow:this.overflow})}});

484
src/lib/form/button.js Normal file
View File

@ -0,0 +1,484 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import "../icon/index.js"
import $ from "../utils.js"
const IS_FIREFOX = !!window.sidebar
export default class Button extends HTMLElement {
static get observedAttributes() {
return ["icon","autofocus","loading","disabled","lazy"]
}
props = {
icon: '',
autofocus: '',
loading: false,
disabled: false,
lazy: 0 // 并发拦截时间, 单位毫秒
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
overflow: hidden;
display: inline-block;
min-width: 64px;
height: 32px;
border-radius: 2px;
user-select: none;
-moz-user-select: none;
color: var(--color-dark-2);
font-size: 14px;
cursor: pointer; }
:host button {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: inherit;
padding: 0 10px;
margin: auto;
line-height: 0;
border: 1px solid var(--color-plain-3);
border-radius: inherit;
white-space: nowrap;
background: #fff;
font-size: inherit;
font-family: inherit;
outline: none;
color: inherit;
cursor: inherit; }
:host button:hover {
background: var(--color-plain-1); }
:host button:active {
border-color: var(--color-grey-1); }
:host button::-moz-focus-inner {
border: none; }
:host .icon {
--size: 18px;
margin-right: 3px; }
:host([size='large']) {
min-width: 120px;
height: 42px;
font-size: 16px; }
:host([size='large']) .icon {
--size: 20px; }
:host([size='large'][circle]) {
min-width: 62px;
height: 62px; }
:host([size='large'][circle]) button {
padding: 0 20px; }
:host([size='medium']) {
min-width: 90px;
height: 36px; }
:host([size='medium']) button {
padding: 0 15px; }
:host([size='medium'][circle]) {
min-width: 36px; }
:host([size='mini']),
:host([text]) {
min-width: 24px;
height: 24px;
font-size: 12px; }
:host([size='mini']) button,
:host([text]) button {
padding: 0 5px; }
:host([size='mini']) .icon,
:host([text]) .icon {
--size: 14px; }
:host([size='mini'][circle]) {
min-width: 24px; }
:host([text]) {
height: 18px; }
:host([text]) button {
padding: 0;
border: 0; }
:host([text]) button:hover {
background: none;
text-decoration: underline; }
:host([round]) {
border-radius: 21px; }
:host([circle]) {
min-width: 32px;
border-radius: 50%; }
:host([circle]) button {
padding: 0; }
:host([circle]) .icon {
margin-right: 0; }
:host([loading]),
:host([disabled]) {
cursor: not-allowed;
color: var(--color-grey-1);
opacity: 0.6; }
:host([loading]) .icon,
:host([disabled]) .icon {
color: var(--color-grey-1); }
:host([loading]) button,
:host([disabled]) button {
background: #fff;
border-color: var(--color-plain-3); }
:host([color]) {
color: #fff; }
:host([color]) button {
border-color: transparent; }
:host([color]) .icon {
color: #fff; }
:host([color='red']) button {
background: var(--color-red-2); }
:host([color='red']) button:hover {
background: var(--color-red-1); }
:host([color='red']) button:active {
background: var(--color-red-3); }
:host([color='red'][text]) button {
background: transparent;
color: var(--color-red-2); }
:host([color='red'][text]) button:hover {
color: var(--color-red-1); }
:host([color='red'][text]) button:active {
color: var(--color-red-3); }
:host([color='red'][loading]) button,
:host([color='red'][disabled]) button {
background: var(--color-red-1); }
:host([color='blue']) button {
background: var(--color-blue-2); }
:host([color='blue']) button:hover {
background: var(--color-blue-1); }
:host([color='blue']) button:active {
background: var(--color-blue-3); }
:host([color='blue'][text]) button {
background: transparent;
color: var(--color-blue-2); }
:host([color='blue'][text]) button:hover {
color: var(--color-blue-1); }
:host([color='blue'][text]) button:active {
color: var(--color-blue-3); }
:host([color='blue'][loading]) button,
:host([color='blue'][disabled]) button {
background: var(--color-blue-1); }
:host([color='green']) button {
background: var(--color-green-2); }
:host([color='green']) button:hover {
background: var(--color-green-1); }
:host([color='green']) button:active {
background: var(--color-green-3); }
:host([color='green'][text]) button {
background: transparent;
color: var(--color-green-2); }
:host([color='green'][text]) button:hover {
color: var(--color-green-1); }
:host([color='green'][text]) button:active {
color: var(--color-green-3); }
:host([color='green'][loading]) button,
:host([color='green'][disabled]) button {
background: var(--color-green-1); }
:host([color='teal']) button {
background: var(--color-teal-2); }
:host([color='teal']) button:hover {
background: var(--color-teal-1); }
:host([color='teal']) button:active {
background: var(--color-teal-3); }
:host([color='teal'][text]) button {
background: transparent;
color: var(--color-teal-2); }
:host([color='teal'][text]) button:hover {
color: var(--color-teal-1); }
:host([color='teal'][text]) button:active {
color: var(--color-teal-3); }
:host([color='teal'][loading]) button,
:host([color='teal'][disabled]) button {
background: var(--color-teal-1); }
:host([color='orange']) button {
background: var(--color-orange-2); }
:host([color='orange']) button:hover {
background: var(--color-orange-1); }
:host([color='orange']) button:active {
background: var(--color-orange-3); }
:host([color='orange'][text]) button {
background: transparent;
color: var(--color-orange-2); }
:host([color='orange'][text]) button:hover {
color: var(--color-orange-1); }
:host([color='orange'][text]) button:active {
color: var(--color-orange-3); }
:host([color='orange'][loading]) button,
:host([color='orange'][disabled]) button {
background: var(--color-orange-1); }
:host([color='dark']) button {
background: var(--color-dark-2); }
:host([color='dark']) button:hover {
background: var(--color-dark-1); }
:host([color='dark']) button:active {
background: var(--color-dark-3); }
:host([color='dark'][text]) button {
background: transparent;
color: var(--color-dark-2); }
:host([color='dark'][text]) button:hover {
color: var(--color-dark-1); }
:host([color='dark'][text]) button:active {
color: var(--color-dark-3); }
:host([color='dark'][loading]) button,
:host([color='dark'][disabled]) button {
background: var(--color-dark-1); }
:host([color='purple']) button {
background: var(--color-purple-2); }
:host([color='purple']) button:hover {
background: var(--color-purple-1); }
:host([color='purple']) button:active {
background: var(--color-purple-3); }
:host([color='purple'][text]) button {
background: transparent;
color: var(--color-purple-2); }
:host([color='purple'][text]) button:hover {
color: var(--color-purple-1); }
:host([color='purple'][text]) button:active {
color: var(--color-purple-3); }
:host([color='purple'][loading]) button,
:host([color='purple'][disabled]) button {
background: var(--color-purple-1); }
:host([color='grey']) button {
background: var(--color-grey-2); }
:host([color='grey']) button:hover {
background: var(--color-grey-1); }
:host([color='grey']) button:active {
background: var(--color-grey-3); }
:host([color='grey'][text]) button {
background: transparent;
color: var(--color-grey-2); }
:host([color='grey'][text]) button:hover {
color: var(--color-grey-1); }
:host([color='grey'][text]) button:active {
color: var(--color-grey-3); }
:host([color='grey'][loading]) button,
:host([color='grey'][disabled]) button {
background: var(--color-grey-1); }
:host([no-border]) button {
border-color: transparent;
background: inherit; }
:host([text][loading]) button, :host([text][loading]) button:hover, :host([text][loading]) button:active,
:host([text][disabled]) button,
:host([text][disabled]) button:hover,
:host([text][disabled]) button:active {
text-decoration: none;
background: transparent; }
:host(:focus-within) {
box-shadow: 0 0 2px #88f7df; }
:host(:focus-within[disabled]),
:host(:focus-within[loading]) {
box-shadow: none; }
</style>
<button>
<wc-icon class="icon"></wc-icon>
<slot></slot>
</button>
`
// 圆形按钮不允许文字
if (this.hasAttribute('circle')) {
this.textContent = ''
}
this.__BTN__ = this.root.children[1]
this.__ICO__ = this.__BTN__.children[0]
}
get loading() {
return this.props.loading
}
set loading(val) {
var type = typeof val
if (val === this.props.loading) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.loading = true
this.__ICO__.setAttribute('is', 'loading')
this.setAttribute('loading', '')
} else {
this.props.loading = false
this.__ICO__.setAttribute('is', this.props.icon)
this.removeAttribute('loading')
}
}
get disabled() {
return this.props.disabled
}
set disabled(val) {
var type = typeof val
if (val === this.props.disabled) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.disabled = true
this.setAttribute('disabled', '')
} else {
this.props.disabled = false
this.removeAttribute('disabled')
}
}
connectedCallback() {
this.stamp = 0
// 阻止事件冒泡, 避免用户自己绑定click事件不受这2个值的限制
this._handleClick = $.bind(this.__BTN__, 'click', ev => {
var { loading, disabled, lazy } = this.props
var now = Date.now()
if (loading || disabled) {
return ev.stopPropagation()
}
// 并发拦截
if (lazy && now - this.stamp < lazy) {
return ev.stopPropagation()
}
this.stamp = now
})
}
disconnectedCallback() {
$.unbind(this.__BTN__, 'click', this._handleClick)
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'icon':
this.props.icon = val
if (val) {
if (!this.props.loading) {
this.__ICO__.setAttribute('is', val)
}
} else {
this.removeAttribute('icon')
this.__ICO__.removeAttribute('is')
}
break
case 'autofocus':
this.__BTN__.setAttribute('autofocus', '')
// 辣鸡火狐, 要触发一下focus, 才能聚焦
if (IS_FIREFOX) {
setTimeout(_ => {
this.__BTN__.focus()
}, 10)
}
break
case 'lazy':
this.props.lazy = val >> 0
break
case 'loading':
case 'disabled':
this[name] = true
break
}
}
}
if(!customElements.get('wc-button')){
customElements.define('wc-button', Button)
}

View File

@ -0,0 +1,311 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import "../icon/index.js"
import $ from "../utils.js"
export default class CheckboxItem extends HTMLElement {
static get observedAttributes() {
return ["color","value","checked","readonly","disabled"]
}
props = {
color: '',
value: '',
checked: false,
readonly: false,
disabled: false
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
display: inline-flex;
line-height: 1;
font-size: 14px; }
:host label {
display: flex;
justify-content: center;
align-items: center;
min-width: 32px;
height: 32px;
padding: 0 5px;
line-height: 0;
-moz-user-select: none;
user-select: none;
white-space: nowrap;
cursor: inherit;
color: var(--color-grey-3); }
:host .dot {
--size: 18px;
padding: 2px;
margin-right: 3px; }
:host([readonly]) {
opacity: 0.8; }
:host([disabled]) {
cursor: not-allowed;
opacity: 0.6; }
:host([size='large']) {
font-size: 16px; }
:host([size='large']) label {
height: 42px; }
:host([size='large']) .dot {
--size: 22px; }
:host([size='medium']) label {
height: 38px; }
:host([size='medium']) .dot {
--size: 20px; }
:host([size='mini']) {
font-size: 12px; }
:host([size='mini']) label {
height: 20px; }
:host([size='mini']) .dot {
--size: 14px; }
:host([color='red']) label.checked {
color: var(--color-red-1); }
:host([color='red']) label.checked .dot {
border-color: var(--color-red-1); }
:host([color='red']) label.checked .dot::after {
background: var(--color-red-1); }
:host([color='blue']) label.checked {
color: var(--color-blue-1); }
:host([color='blue']) label.checked .dot {
border-color: var(--color-blue-1); }
:host([color='blue']) label.checked .dot::after {
background: var(--color-blue-1); }
:host([color='green']) label.checked {
color: var(--color-green-1); }
:host([color='green']) label.checked .dot {
border-color: var(--color-green-1); }
:host([color='green']) label.checked .dot::after {
background: var(--color-green-1); }
:host([color='teal']) label.checked {
color: var(--color-teal-1); }
:host([color='teal']) label.checked .dot {
border-color: var(--color-teal-1); }
:host([color='teal']) label.checked .dot::after {
background: var(--color-teal-1); }
:host([color='orange']) label.checked {
color: var(--color-orange-1); }
:host([color='orange']) label.checked .dot {
border-color: var(--color-orange-1); }
:host([color='orange']) label.checked .dot::after {
background: var(--color-orange-1); }
:host([color='dark']) label.checked {
color: var(--color-dark-1); }
:host([color='dark']) label.checked .dot {
border-color: var(--color-dark-1); }
:host([color='dark']) label.checked .dot::after {
background: var(--color-dark-1); }
:host([color='purple']) label.checked {
color: var(--color-purple-1); }
:host([color='purple']) label.checked .dot {
border-color: var(--color-purple-1); }
:host([color='purple']) label.checked .dot::after {
background: var(--color-purple-1); }
</style>
<label>
<wc-icon class="dot" is="checkbox-off"></wc-icon>
<slot />
</label>
`
this.__SWITCH__ = this.root.lastElementChild
this.__ICO__ = this.__SWITCH__.children[0]
this._isInGroup = false
}
_checkGroup() {
this._isInGroup = this.parentNode.tagName === 'WC-CHECKBOX'
if (this._isInGroup && this.parentNode.root) {
if (this.parentNode.value.includes(this.value)) {
this.checked = true
}
}
}
get value() {
return this.props.value
}
set value(val) {
this.props.value = val
}
get checked() {
return this.props.checked
}
set checked(val) {
this.props.checked = !!val
var { checked, color } = this.props
this.__SWITCH__.classList.toggle('checked', checked)
this.__ICO__.setAttribute('is', 'checkbox-' + (checked ? 'on' : 'off'))
if (checked) {
this.__ICO__.setAttribute('color', color)
} else {
this.__ICO__.removeAttribute('color')
}
}
get readOnly() {
return this.props.readonly
}
set readOnly(val) {
var type = typeof val
if (val === this.props.readonly) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.readonly = true
this.setAttribute('readonly', '')
} else {
this.props.readonly = false
this.removeAttribute('readonly')
}
}
get disabled() {
return this.props.disabled
}
set disabled(val) {
var type = typeof val
if (val === this.props.disabled) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.disabled = true
this.setAttribute('disabled', '')
} else {
this.props.disabled = false
this.removeAttribute('disabled')
}
}
connectedCallback() {
this._checkGroup()
this._handlClick = $.bind(this, 'click', ev => {
ev.preventDefault()
if (this.disabled || this.readOnly) {
return
}
this.checked = !this.checked
if (this._isInGroup) {
this.parentNode.dispatchEvent(
new CustomEvent('child-picked', {
detail: { value: this.value, checked: this.checked }
})
)
} else {
this.dispatchEvent(new CustomEvent('input'))
}
})
}
disconnectedCallback() {
$.unbind(this, 'click', this._handlClick)
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'value':
case 'color':
this.props[name] = val
break
case 'checked':
case 'readonly':
case 'disabled':
var k = name
if (k === 'readonly') {
k = 'readOnly'
}
this[k] = true
break
}
}
}
if(!customElements.get('wc-checkbox-item')){
customElements.define('wc-checkbox-item', CheckboxItem)
}

142
src/lib/form/checkbox.js Normal file
View File

@ -0,0 +1,142 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import $ from "../utils.js"
import "./checkbox-item.js"
export default class Checkbox extends HTMLElement {
static get observedAttributes() {
return ["value"]
}
props = {
value: []
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
display: inline-flex; }
</style>
<slot />
`
}
_updateChildrenStat() {
Array.from(this.children).forEach(it => {
if (it.tagName === 'WC-CHECKBOX-ITEM' && it.root) {
if (this.value.includes(it.value)) {
it.checked = true
} else {
it.checked = false
}
}
})
}
get value() {
return this.props.value
}
set value(val) {
if (val === this.props.value) {
return
}
this.props.value = val
this._updateChildrenStat()
}
connectedCallback() {
this._pickedFn = $.bind(this, 'child-picked', ev => {
var tmp = [...this.props.value]
var idx = tmp.indexOf(ev.detail.value)
if (ev.detail.checked) {
if (idx < 0) {
tmp.push(ev.detail.value)
}
} else {
if (~idx) {
tmp.splice(idx, 1)
}
}
this.props.value = tmp
this.dispatchEvent(new CustomEvent('input'))
})
}
disconnectedCallback() {
$.unbind(this, 'child-picked', this._pickedFn)
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'value':
if (val) {
this.value = val.split(/,\s*?/)
}
break
}
}
}
if(!customElements.get('wc-checkbox')){
customElements.define('wc-checkbox', Checkbox)
}

617
src/lib/form/input.js Normal file
View File

@ -0,0 +1,617 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import "../scroll/index.js"
import "../icon/index.js"
import $ from "../utils.js"
const TYPES = ['text', 'textarea', 'password']
const INPUTS = {
text: '<input spellcheck="false">',
textarea: '<textarea spellcheck="false"></textarea>'
}
export default class Input extends HTMLElement {
static get observedAttributes() {
return ["value","icon","type","placeholder","maxlength","minlength","autofocus","readonly","disabled"]
}
props = {
value: '',
icon: '',
type: 'text',
placeholder: '',
maxlength: null,
minlength: null,
autofocus: false,
readonly: false,
disabled: false
}
state = {
mvidx: null //下拉列表光标的索引ID
}
constructor() {
super();
var type = this.getAttribute('type')
var input = ''
if (type !== 'textarea') {
type = 'text'
}
input = INPUTS[type]
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>@charset "UTF-8";
* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
ul,
li {
list-style: none; }
:host {
overflow: hidden;
display: inline-block;
user-select: none;
-moz-user-select: none;
color: var(--color-dark-1);
border-radius: 2px;
cursor: text; }
.label {
display: flex;
justify-content: center;
align-items: center;
min-width: 64px;
width: 100%;
height: 32px;
font-size: 14px;
border: 1px solid var(--color-plain-3);
border-radius: inherit;
background: var(--bg-color, #fff);
color: inherit;
cursor: inherit;
/* ----- */ }
.label input,
.label textarea {
flex: 1;
min-width: 32px;
width: 0;
height: 100%;
padding: 0 5px;
border: 0;
border-radius: inherit;
color: inherit;
font: inherit;
background: none;
outline: none;
box-shadow: none;
cursor: inherit; }
.label input::placeholder,
.label textarea::placeholder {
color: var(--color-grey-1); }
.label textarea {
padding: 5px 8px;
resize: none; }
.label .prepend,
.label .append {
display: none;
justify-content: center;
align-items: center;
width: auto;
height: 30px;
padding: 0 10px;
line-height: 0;
background: var(--bg-color, --color-plain-1);
white-space: nowrap; }
.label .prepend {
border-right: 1px solid var(--color-plain-3);
border-radius: 2px 0 0 2px; }
.label .append {
border-left: 1px solid var(--color-plain-3);
border-radius: 0 2px 2px 0; }
.label[prepend] .prepend,
.label[append] .append {
display: flex; }
.label[prepend] input,
.label[append] input {
min-width: 64px; }
.label .icon {
--size: 20px;
padding: 0 5px;
margin: 0 5px;
color: var(--color-grey-2); }
.suggestion {
display: none;
position: fixed;
z-index: 10240;
left: 0;
top: 0;
width: 200px;
height: auto;
max-height: 200px;
min-height: 46px;
padding: 8px 0;
border-radius: 2px;
background: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); }
.suggestion .list {
width: 100%; }
.suggestion::after {
position: absolute;
left: 30px;
top: -4px;
width: 8px;
height: 8px;
background: #fff;
box-shadow: -1px -1px 2px rgba(0, 0, 0, 0.1);
transform: rotate(45deg);
content: ''; }
.suggestion.show {
display: flex; }
.suggestion li {
overflow: hidden;
width: 100%;
height: 30px;
line-height: 30px;
padding: 0 8px;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer; }
.suggestion li:hover, .suggestion li[focus] {
background: var(--color-plain-1); }
/* --- */
:host([auto-border]) .label {
border-color: transparent; }
:host([disabled]) {
cursor: not-allowed; }
:host([disabled]) .label {
background: var(--color-plain-1);
opacity: 0.6; }
:host([readonly]) {
cursor: default; }
:host(:focus-within) {
box-shadow: 0 0 2px #88f7df; }
:host(:focus-within) .label {
border-color: var(--color-plain-3); }
:host(:focus-within[readonly]) {
box-shadow: 0 0 2px #f3be4d; }
:host([type='textarea']) {
display: flex;
height: 80px; }
:host([type='textarea']) .label {
width: 100%;
height: 100%; }
:host([type='textarea']) .icon,
:host([type='textarea']) .suggestion {
display: none; }
/* 额外样式 */
:host([round]) {
border-radius: 21px; }
:host([round]) .label input {
padding: 0 10px; }
:host([round]) .label[prepend] input,
:host([round]) .label[append] input {
padding: 0 5px; }
:host([round]) .prepend {
border-radius: 21px 0 0 21px; }
:host([round]) .append {
border-radius: 0 21px 21px 0; }
:host([size='large']) .label {
height: 42px;
font-size: 16px; }
:host([size='large']) .prepend,
:host([size='large']) .append {
height: 40px; }
:host([size='medium']) .label {
height: 36px; }
:host([size='medium']) .prepend,
:host([size='medium']) .append {
height: 34px; }
:host([size='mini']) .label {
height: 24px;
font-size: 12px; }
:host([size='mini']) .icon {
--size: 16px; }
:host([size='mini']) .prepend,
:host([size='mini']) .append {
height: 18px; }
:host([no-border]),
:host(:focus-within[no-border]) {
box-shadow: none; }
:host([no-border]) .label,
:host(:focus-within[no-border]) .label {
border: 0; }
:host([transparent]) .label {
background: transparent; }
</style>
<div class="label">
<slot class="prepend" name="prepend"></slot>
${input}
<wc-icon class="icon"></wc-icon>
<slot class="append" name="append"></slot>
<div class="suggestion">
<wc-scroll>
<ul class="list"></ul>
</wc-scroll>
</div>
</div>
`
this.props.type = type
this.__OUTER__ = this.root.children[1]
this.__PREPEND__ = this.__OUTER__.children[0]
this.__INPUT__ = this.__OUTER__.children[1]
this.__ICO__ = this.__OUTER__.children[2]
this.__APPEND__ = this.__OUTER__.children[3]
this.__LIST__ = this.__OUTER__.children[4]
}
get readOnly() {
return this.props.readonly
}
set readOnly(val) {
var type = typeof val
if (val === this.props.readonly) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.readonly = true
this.setAttribute('readonly', '')
this.__INPUT__.setAttribute('readonly', '')
} else {
this.props.readonly = false
this.removeAttribute('readonly')
this.__INPUT__.removeAttribute('readonly')
}
}
get disabled() {
return this.props.disabled
}
set disabled(val) {
var type = typeof val
if (val === this.props.disabled) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.disabled = true
this.setAttribute('disabled', '')
this.__INPUT__.setAttribute('disabled', '')
} else {
this.props.disabled = false
this.removeAttribute('disabled')
this.__INPUT__.removeAttribute('disabled')
}
}
get value() {
return this.__INPUT__.value
}
set value(val) {
this.__INPUT__.value = val
}
get type() {
return this.__INPUT__.type
}
set type(val) {
if (val !== 'textarea') {
this.__INPUT__.type = val
}
}
// 移动光标选择下拉选项
_moveSelect(ev) {
var { list } = this.props
if (list && list.length) {
ev.preventDefault()
var step = ev.keyCode === 38 ? -1 : 1
var items = Array.from(
this.__LIST__.firstElementChild.firstElementChild.children
)
if (this.state.mvidx === null) {
this.state.mvidx = 0
} else {
this.state.mvidx += step
}
if (this.state.mvidx < 0) {
this.state.mvidx = 0
} else if (this.state.mvidx > items.length - 1) {
this.state.mvidx = items.length - 1
}
items.forEach((it, i) => {
if (i === this.state.mvidx) {
this.__LIST__.firstElementChild.scrollTop = it.offsetTop - 150
it.setAttribute('focus', '')
} else {
it.removeAttribute('focus')
}
})
}
}
// 触发列表选择
_fetchSelect(idx, ev) {
var item = this.props.list[idx]
this.value = item.value
this.dispatchEvent(
new CustomEvent('select', {
detail: item
})
)
this._handleChange(ev)
this.__LIST__.classList.remove('show')
this.state.mvidx = null
}
_updateAttr() {
var { maxlength, minlength } = this.props
if (maxlength && maxlength > 0) {
this.__INPUT__.setAttribute('maxlength', maxlength)
} else {
this.__INPUT__.removeAttribute('maxlength')
}
if (minlength && minlength > 0) {
this.__INPUT__.setAttribute('minlength', minlength)
} else {
this.__INPUT__.removeAttribute('minlength')
}
}
connectedCallback() {
var prepend = this.__PREPEND__.assignedNodes()
var append = this.__APPEND__.assignedNodes()
var { type } = this.props
// 相同插槽, 只允许1个
while (prepend.length > 1) {
this.removeChild(prepend.pop())
}
while (append.length > 1) {
this.removeChild(append.pop())
}
if (prepend.length && type !== 'textarea') {
this.__OUTER__.setAttribute('prepend', '')
}
if (append.length && type !== 'textarea') {
this.__OUTER__.setAttribute('append', '')
}
this._updateAttr()
// 键盘事件
this._handleSubmit = $.catch(this.__INPUT__, 'keydown', ev => {
if (this.disabled || this.readOnly) {
return
}
// up: 38, down: 40
if (ev.keyCode === 38 || ev.keyCode === 40) {
// 仅普通文本表单, 密码和多行文本框不做响应
if (type === 'text') {
return this._moveSelect(ev)
}
}
// 回车触发submit事件
// textarea 要按Ctrl Or Cmd键, 才会触发
if (ev.keyCode === 13) {
// 如果是输入建议存在,则第1次回车的时候, 不触发提交
if (type === 'text' && this.state.mvidx !== null) {
return this._fetchSelect(this.state.mvidx, ev)
}
if (
type === 'text' ||
(type === 'textarea' && (ev.ctrlKey || ev.metaKey))
) {
this.dispatchEvent(
new CustomEvent('submit', {
detail: this.value
})
)
}
}
})
// 非textarea, 可做输入建议功能
if (type === 'text') {
// 输入状态事件
this._handleChange = $.bind(this.__INPUT__, 'input', ev => {
ev.preventDefault()
this.dispatchEvent(
new CustomEvent('fetch-suggest', {
detail: {
value: this.value,
send: list => {
this.props.list = list
this._parseSuggestion()
}
}
})
)
})
// 渲染建议列表
this._parseSuggestion = $.bind(this.__INPUT__, 'click', ev => {
var { list } = this.props
let { x, y, width } = this.getBoundingClientRect()
if (list && list.length) {
var html = list
.map((it, i) => `<li data-idx="${i}">${it.value}</li>`)
.join('')
this.__LIST__.firstElementChild.firstElementChild.innerHTML = html
this.__LIST__.classList.toggle('show', true)
this.__LIST__.style.cssText = `left:${x}px;top:${y +
50}px;width:${width}px;`
} else {
this.__LIST__.classList.toggle('show', false)
}
})
this._inactiveFn = $.outside(this, ev => {
this.__LIST__.classList.remove('show')
})
// 选择建议
this._handleSelect = $.bind(this.__LIST__, 'click', ev => {
if (ev.target.tagName === 'LI') {
this._fetchSelect(ev.target.dataset.idx, ev)
this.dispatchEvent(new CustomEvent('input'))
}
})
} else {
this._handleWheel = $.catch(this.__INPUT__, 'wheel')
}
}
disconnectedCallback() {
$.unbind(this.__INPUT__, 'wheel', this._handleWheel)
$.unbind(this.__INPUT__, 'keydown', this._handleSubmit)
$.unbind(this.__INPUT__, 'input', this._handleChange)
$.unbind(this.__LIST__, 'click', this._handleSelect)
$.clearOutside(this._inactiveFn)
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'icon':
this.props.icon = val
if (val) {
this.__ICO__.setAttribute('is', val)
} else {
this.removeAttribute('icon')
this.__ICO__.removeAttribute('is')
}
break
case 'autofocus':
this.__INPUT__.setAttribute('autofocus', '')
// 辣鸡火狐, 要触发一下focus, 才能聚焦
setTimeout(_ => {
this.__INPUT__.focus()
}, 10)
break
case 'placeholder':
this.__INPUT__.setAttribute('placeholder', val)
break
case 'type':
if (~TYPES.indexOf(val)) {
this.type = val
} else {
this.type = 'text'
}
break
case 'value':
this.value = val
break
case 'maxlength':
case 'minlength':
this.props[name] = val
this._updateAttr()
break
case 'readonly':
case 'disabled':
var k = name
if (k === 'readonly') {
k = 'readOnly'
}
this[k] = true
break
}
}
}
if(!customElements.get('wc-input')){
customElements.define('wc-input', Input)
}

413
src/lib/form/number.js Normal file
View File

@ -0,0 +1,413 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import "../scroll/index.js"
import "../icon/index.js"
import $ from "../utils.js"
export default class Number extends HTMLElement {
static get observedAttributes() {
return ["value","max","min","step","autofocus","readonly","disabled"]
}
props = {
value: 0,
max: null,
min: null,
step: 1,
autofocus: false,
readonly: false,
disabled: false
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>@charset "UTF-8";
* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
overflow: hidden;
display: inline-block;
width: 128px;
height: 32px;
user-select: none;
-moz-user-select: none;
color: var(--color-dark-2);
border-radius: 2px; }
.label {
display: flex;
width: 100%;
height: 100%;
margin: 0 auto;
line-height: 0;
font-size: 14px;
border: 1px solid var(--color-plain-3);
border-radius: inherit;
background: var(--bg-color, #fff);
color: inherit;
cursor: text;
/* ----- */ }
.label span {
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 100%;
background: var(--bg-color, --color-plain-1);
font-size: 18px;
cursor: pointer; }
.label span:first-child {
border-radius: 2px 0 0 2px;
border-right: 1px solid var(--color-plain-3); }
.label span:last-child {
border-radius: 0 2px 2px 0;
border-left: 1px solid var(--color-plain-3); }
.label span.disabled {
cursor: not-allowed;
opacity: 0.6; }
.label input {
flex: 1;
min-width: 0;
width: 0;
height: 100%;
padding: 0 5px;
border: 0;
border-radius: inherit;
color: inherit;
text-align: center;
font-size: inherit;
font-family: inherit;
background: none;
outline: none;
box-shadow: none;
cursor: inherit; }
.label input::placeholder {
color: var(--color-grey-1); }
.label .icon {
padding: 0 5px;
--size: 20px; }
/* --- */
:host([readonly]) .label {
cursor: default;
opacity: 0.8; }
:host([readonly]) .label span {
cursor: inherit; }
:host([disabled]) .label {
background: var(--color-plain-1);
cursor: not-allowed;
opacity: 0.6; }
:host([disabled]) .label span {
cursor: inherit; }
:host(:focus-within) {
box-shadow: 0 0 2px #88f7df; }
:host(:focus-within[readonly]) {
box-shadow: 0 0 2px #f3be4d; }
/* 额外样式 */
:host([round]) {
border-radius: 21px; }
:host([round]) .label span:first-child {
border-radius: 21px 0 0 21px; }
:host([round]) .label span:last-child {
border-radius: 0 21px 21px 0; }
:host([size='large']) {
width: 192px;
height: 42px; }
:host([size='large']) .label {
font-size: 16px; }
:host([size='large']) .label span {
width: 48px; }
:host([size='large']) .prepend,
:host([size='large']) .append {
height: 40px; }
:host([size='medium']) {
width: 144px;
height: 36px; }
:host([size='medium']) .label span {
width: 36px; }
:host([size='medium']) .prepend,
:host([size='medium']) .append {
height: 34px; }
:host([size='mini']) {
width: 96px;
height: 24px; }
:host([size='mini']) .label {
font-size: 12px; }
:host([size='mini']) .label span {
width: 28px; }
:host([size='mini']) .icon {
--size: 16px; }
:host([size='mini']) .prepend,
:host([size='mini']) .append {
height: 18px; }
</style>
<div class="label">
<span data-act="-">-</span>
<!-- <wc-icon class="icon" is="minus"></wc-icon> -->
<input value="0" maxlength="9" />
<span data-act="+">+</span>
<!-- <wc-icon class="icon" is="plus"></wc-icon> -->
</div>
`
this.__OUTER__ = this.root.children[1]
this.__INPUT__ = this.__OUTER__.children[1]
}
get readOnly() {
return this.props.readonly
}
set readOnly(val) {
var type = typeof val
if (val === this.props.readonly) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.readonly = true
this.setAttribute('readonly', '')
this.__INPUT__.setAttribute('readonly', '')
} else {
this.props.readonly = false
this.removeAttribute('readonly')
this.__INPUT__.removeAttribute('readonly')
}
}
get disabled() {
return this.props.disabled
}
set disabled(val) {
var type = typeof val
if (val === this.props.disabled) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.disabled = true
this.setAttribute('disabled', '')
this.__INPUT__.setAttribute('disabled', '')
} else {
this.props.disabled = false
this.removeAttribute('disabled')
this.__INPUT__.removeAttribute('disabled')
}
}
get value() {
return this.props.value
}
set value(val) {
var n = +val
if (n === n) {
val = n
} else {
val = 0
}
this.props.value = val
this.__INPUT__.value = val
this._checkActionEnable()
}
_checkActionEnable() {
var { max, min, value } = this.props
var n = value
if (min !== null) {
if (min > n) {
n = min
}
this.__OUTER__.children[0].classList.toggle('disabled', value <= min)
}
if (max !== null) {
if (max < n) {
n = max
}
this.__OUTER__.children[2].classList.toggle('disabled', value >= max)
}
if (n !== value) {
this.props.value = n
this.__INPUT__.value = n
this.dispatchEvent(new CustomEvent('input'))
}
}
_updateValue(act) {
var { max, min, value, step } = this.props
if (act === '+') {
if (max !== null && max < value + step) {
return
}
value += step
} else {
if (min !== null && min > value - step) {
return
}
value -= step
}
this.props.value = +value.toFixed(2)
this.__INPUT__.value = this.props.value
this._checkActionEnable()
this.dispatchEvent(new CustomEvent('input'))
}
connectedCallback() {
// 键盘事件
this._handleSubmit = $.catch(this.__INPUT__, 'keydown', ev => {
if (this.disabled || this.readOnly) {
return
}
// up: 38, down: 40
if (ev.keyCode === 38 || ev.keyCode === 40) {
ev.preventDefault()
return this._updateValue(ev.keyCode === 38 ? '+' : '-')
}
// 回车触发submit事件
if (ev.keyCode === 13) {
ev.preventDefault()
this.dispatchEvent(
new CustomEvent('submit', {
detail: this.value
})
)
}
})
this._handleChange = $.catch(this.__INPUT__, 'change', ev => {
if (isFinite(this.__INPUT__.value)) {
this.props.value = +this.__INPUT__.value
if (!this.__INPUT__.value.endsWith('.')) {
this.__INPUT__.value = this.props.value
}
} else {
this.__INPUT__.value = this.props.value = 0
}
this.dispatchEvent(new CustomEvent('input'))
})
this._handleAction = $.bind(this.__OUTER__, 'click', ev => {
if (this.disabled || this.readOnly) {
return
}
var target = ev.target
if (target.tagName === 'SPAN' || target.parentNode === 'SPAN') {
var act = target.dataset.act || target.parentNode.dataset.act
this._updateValue(act)
}
})
}
disconnectedCallback() {
$.unbind(this.__INPUT__, 'keydown', this._handleSubmit)
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'autofocus':
this.__INPUT__.setAttribute('autofocus', '')
// 辣鸡火狐, 要触发一下focus, 才能聚焦
setTimeout(_ => {
this.__INPUT__.focus()
}, 10)
break
case 'value':
this.value = val >> 0
break
case 'step':
case 'max':
case 'min':
var n = +val
if (n === n) {
this.props[name] = n
}
this._checkActionEnable()
break
case 'readonly':
case 'disabled':
var k = name
if (k === 'readonly') {
k = 'readOnly'
}
this[k] = true
break
}
}
}
if(!customElements.get('wc-number')){
customElements.define('wc-number', Number)
}

163
src/lib/form/progress.js Normal file
View File

@ -0,0 +1,163 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
export default class Progress extends HTMLElement {
static get observedAttributes() {
return ["value","max"]
}
props = {
value: 0,
max: 1
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
display: flex;
align-items: center; }
:host label {
flex: 1;
height: var(--size, 10px);
border-radius: 9px;
background: var(--color-plain-2); }
:host label span {
display: block;
width: 0;
height: 100%;
border-radius: 9px;
background: var(--color-teal-1); }
:host([size='large']) label {
height: 18px; }
:host([size='medium']) label {
height: 14px; }
:host([size='mini']) label {
height: 6px; }
:host([color='red']) label span {
background: var(--color-red-1); }
:host([color='blue']) label span {
background: var(--color-blue-1); }
:host([color='green']) label span {
background: var(--color-green-1); }
:host([color='orange']) label span {
background: var(--color-orange-1); }
:host([color='dark']) label span {
background: var(--color-dark-1); }
:host([color='purple']) label span {
background: var(--color-purple-1); }
</style>
<label><span></span></label>
`
this.__THUMB__ = this.root.children[1].lastElementChild
}
get value() {
return this.props.value
}
set value(val) {
this.props.value = +val
this.calculate()
}
calculate() {
var { max, value } = this.props
this.__THUMB__.style.width = `${(100 * value) / max}%`
}
connectedCallback() {
this.calculate()
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'max':
var max = +val
if (max !== max || max < 1) {
max = 1
}
this.props.max = max
this.calculate()
break
case 'value':
var v = +val
if (v === v) {
this.props.value = v
this.calculate()
}
break
}
}
}
if(!customElements.get('wc-progress')){
customElements.define('wc-progress', Progress)
}

308
src/lib/form/radio-item.js Normal file
View File

@ -0,0 +1,308 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import $ from "../utils.js"
export default class RadioItem extends HTMLElement {
static get observedAttributes() {
return ["value","checked","readonly","disabled"]
}
props = {
value: '',
checked: false,
readonly: false,
disabled: false
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
display: inline-flex;
line-height: 1;
font-size: 14px; }
:host label {
display: flex;
justify-content: center;
align-items: center;
min-width: 32px;
height: 32px;
padding: 0 5px;
line-height: 1;
-moz-user-select: none;
user-select: none;
white-space: nowrap;
cursor: inherit;
color: var(--color-grey-3); }
:host label.checked .dot::after {
visibility: visible; }
:host .dot {
display: flex;
justify-content: center;
align-items: center;
width: 18px;
height: 18px;
margin-right: 3px;
border: 1px solid var(--color-grey-1);
border-radius: 50%;
background: #fff; }
:host .dot::after {
display: block;
visibility: hidden;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--color-grey-1);
content: ''; }
:host([readonly]) {
opacity: 0.8; }
:host([disabled]) {
cursor: not-allowed;
opacity: 0.6; }
:host([size='large']) label {
min-width: 58px;
height: 32px; }
:host([size='large']) .dot {
width: 26px;
height: 26px; }
:host([size='large']) .dot::after {
width: 18px;
height: 18px; }
:host([size='medium']) label {
min-width: 50px;
height: 28px; }
:host([size='medium']) .dot {
width: 22px;
height: 22px; }
:host([size='medium']) .dot::after {
width: 14px;
height: 14px; }
:host([size='mini']) label {
height: 14px; }
:host([size='mini']) .dot {
width: 14px;
height: 14px; }
:host([size='mini']) .dot::after {
width: 8px;
height: 8px; }
:host([color='red']) label.checked {
color: var(--color-red-1); }
:host([color='red']) label.checked .dot {
border-color: var(--color-red-1); }
:host([color='red']) label.checked .dot::after {
background: var(--color-red-1); }
:host([color='blue']) label.checked {
color: var(--color-blue-1); }
:host([color='blue']) label.checked .dot {
border-color: var(--color-blue-1); }
:host([color='blue']) label.checked .dot::after {
background: var(--color-blue-1); }
:host([color='green']) label.checked {
color: var(--color-green-1); }
:host([color='green']) label.checked .dot {
border-color: var(--color-green-1); }
:host([color='green']) label.checked .dot::after {
background: var(--color-green-1); }
:host([color='teal']) label.checked {
color: var(--color-teal-1); }
:host([color='teal']) label.checked .dot {
border-color: var(--color-teal-1); }
:host([color='teal']) label.checked .dot::after {
background: var(--color-teal-1); }
:host([color='orange']) label.checked {
color: var(--color-orange-1); }
:host([color='orange']) label.checked .dot {
border-color: var(--color-orange-1); }
:host([color='orange']) label.checked .dot::after {
background: var(--color-orange-1); }
:host([color='dark']) label.checked {
color: var(--color-dark-1); }
:host([color='dark']) label.checked .dot {
border-color: var(--color-dark-1); }
:host([color='dark']) label.checked .dot::after {
background: var(--color-dark-1); }
:host([color='purple']) label.checked {
color: var(--color-purple-1); }
:host([color='purple']) label.checked .dot {
border-color: var(--color-purple-1); }
:host([color='purple']) label.checked .dot::after {
background: var(--color-purple-1); }
</style>
<label>
<span class="dot"></span>
<slot />
</label>
`
this.__SWITCH__ = this.root.lastElementChild
}
get value() {
return this.props.value
}
set value(val) {
this.props.value = val
}
get checked() {
return this.props.checked
}
set checked(val) {
this.props.checked = !!val
this.__SWITCH__.classList.toggle('checked', this.props.checked)
}
get readOnly() {
return this.props.readonly
}
set readOnly(val) {
var type = typeof val
if (val === this.props.readonly) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.readonly = true
this.setAttribute('readonly', '')
} else {
this.props.readonly = false
this.removeAttribute('readonly')
}
}
get disabled() {
return this.props.disabled
}
set disabled(val) {
var type = typeof val
if (val === this.props.disabled) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.disabled = true
this.setAttribute('disabled', '')
} else {
this.props.disabled = false
this.removeAttribute('disabled')
}
}
connectedCallback() {
if (this.value === this.parentNode.value) {
this.checked = true
}
this._handleClick = $.catch(this, 'click', ev => {
if (this.disabled || this.readOnly || this.checked) {
return
}
this.parentNode.dispatchEvent(
new CustomEvent('child-picked', { detail: this.value })
)
})
}
disconnectedCallback() {
$.unbind(this, 'click', this._handleClick)
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'value':
this.value = val
break
case 'checked':
case 'readonly':
case 'disabled':
var k = name
if (k === 'readonly') {
k = 'readOnly'
}
this[k] = true
break
}
}
}
if(!customElements.get('wc-radio-item')){
customElements.define('wc-radio-item', RadioItem)
}

130
src/lib/form/radio.js Normal file
View File

@ -0,0 +1,130 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import $ from "../utils.js"
import "./radio-item.js"
export default class Radio extends HTMLElement {
static get observedAttributes() {
return ["value"]
}
props = {
value: null
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
display: inline-flex; }
</style>
<slot />
`
}
_updateChildrenStat() {
Array.from(this.children).forEach(it => {
if (it.tagName === 'WC-RADIO-ITEM' && it.root) {
if (it.value === this.props.value) {
it.checked = true
} else {
it.checked = false
}
}
})
}
get value() {
return this.props.value
}
set value(val) {
if (val === this.props.value) {
return
}
this.props.value = val
this._updateChildrenStat()
}
connectedCallback() {
this._pickedFn = $.bind(this, 'child-picked', ev => {
log('radio picked: ', ev.detail)
this.value = ev.detail
this.dispatchEvent(new CustomEvent('input'))
})
}
disconnectedCallback() {
$.unbind(this, 'child-picked', this._pickedFn)
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'value':
this.value = val
break
}
}
}
if(!customElements.get('wc-radio')){
customElements.define('wc-radio', Radio)
}

559
src/lib/form/select.js Normal file
View File

@ -0,0 +1,559 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import "../scroll/index.js"
import "../icon/index.js"
import $ from "../utils.js"
function parseOptions(arr, props) {
let html = ''
for (let it of arr) {
if (it.list) {
html += `<dt>${it.name}</dt>`
for (let _ of it.list) {
props.DICT[_.value] = _
if (!_.disabled) {
props.LIST.push(_)
}
html += `<dd sub ${
_.disabled ? 'disabled' : `data-idx="${props.LIST.length - 1}"`
} ${_.value === props.value ? 'focus' : ''}>${_.label}</dd>`
}
} else {
if (!it.disabled) {
props.LIST.push(it)
}
props.DICT[it.value] = it
html += `<dd ${
it.disabled ? 'disabled' : `data-idx="${props.LIST.length - 1}"`
} ${it.value === props.value ? 'focus' : ''}>${it.label}</dd>`
}
}
return html
}
export default class Select extends HTMLElement {
static get observedAttributes() {
return ["label","placeholder","multi","value","options","mvidx","readonly","disabled"]
}
props = {
label: '',
placeholder: '',
multi: '',
value: '',
options: '',
mvidx: null, //下拉列表光标的索引ID
readonly: false,
disabled: false
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>@charset "UTF-8";
* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
overflow: hidden;
display: inline-block;
user-select: none;
-moz-user-select: none;
color: var(--color-dark-2);
border-radius: 2px; }
.label {
display: flex;
justify-content: center;
align-items: center;
min-width: 64px;
width: 100%;
height: 32px;
line-height: 0;
font-size: 14px;
border: 1px solid var(--color-plain-3);
border-radius: inherit;
background: #fff;
color: inherit;
cursor: default;
/* ----- */ }
.label input {
flex: 1;
width: 0;
min-width: 64px;
width: 0;
height: 100%;
padding: 0 5px;
border: 0;
border-radius: inherit;
color: inherit;
font-size: inherit;
background: none;
outline: none;
box-shadow: none;
cursor: inherit; }
.label input::placeholder {
color: var(--color-grey-1); }
.label .prepend,
.label .append {
display: none;
justify-content: center;
align-items: center;
width: auto;
height: 30px;
padding: 0 10px;
white-space: nowrap;
background: var(--color-plain-1); }
.label .prepend {
border-right: 1px solid var(--color-plain-3);
border-radius: 2px 0 0 2px; }
.label .append {
border-left: 1px solid var(--color-plain-3);
border-radius: 0 2px 2px 0; }
.label[prepend] .prepend,
.label[append] .append {
display: flex; }
.label .arrow {
padding: 0 5px;
--size: 14px;
color: #ddd;
transform: rotate(-90deg); }
.opt-box {
display: none;
position: fixed;
z-index: 10260;
left: 0;
top: 0;
width: 200px;
height: auto;
max-height: 200px;
padding: 8px 0;
border-radius: 2px;
background: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
cursor: default; }
.opt-box .list {
width: 100%; }
.opt-box::after {
position: absolute;
left: 30px;
top: -4px;
width: 8px;
height: 8px;
background: #fff;
box-shadow: -1px -1px 2px rgba(0, 0, 0, 0.1);
transform: rotate(45deg);
content: ''; }
.opt-box.show {
display: flex; }
.opt-box dt,
.opt-box dd {
overflow: hidden;
width: 100%;
height: 30px;
line-height: 30px;
padding: 0 8px;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap; }
.opt-box dt {
font-size: 12px;
color: var(--color-grey-1); }
.opt-box dd {
cursor: pointer; }
.opt-box dd:hover, .opt-box dd[focus] {
background: var(--color-plain-1); }
.opt-box dd[focus] {
color: var(--color-teal-1); }
.opt-box dd[sub] {
text-indent: 1em; }
.opt-box dd[disabled] {
color: var(--color-grey-1);
cursor: not-allowed;
background: none; }
/* --- */
:host([disabled]) .label {
background: var(--color-plain-1);
cursor: not-allowed;
opacity: 0.6; }
:host([readonly]) .label {
opacity: 0.8; }
:host(:focus-within) {
box-shadow: 0 0 2px #88f7df; }
:host(:focus-within[readonly]) {
box-shadow: 0 0 2px #f3be4d; }
/* 额外样式 */
:host([round]) {
border-radius: 21px; }
:host([round]) .label input {
padding: 0 10px; }
:host([round]) .prepend {
border-radius: 21px 0 0 21px; }
:host([round]) .append {
border-radius: 0 21px 21px 0; }
:host([size='large']) .label {
height: 42px;
font-size: 16px; }
:host([size='large']) .prepend,
:host([size='large']) .append {
height: 40px; }
:host([size='medium']) .label {
height: 36px; }
:host([size='medium']) .prepend,
:host([size='medium']) .append {
height: 34px; }
:host([size='mini']) .label {
height: 24px;
font-size: 12px; }
:host([size='mini']) .arrow {
--size: 12px; }
:host([size='mini']) .prepend,
:host([size='mini']) .append {
height: 18px; }
</style>
<div class="label">
<slot class="prepend" name="prepend"></slot>
<input readonly />
<wc-icon class="arrow" is="left"></wc-icon>
<slot class="append" name="append"></slot>
<div class="opt-box">
<wc-scroll>
<dl class="list"></dl>
</wc-scroll>
</div>
</div>
`
this.__OUTER__ = this.root.children[1]
this.__PREPEND__ = this.__OUTER__.children[0]
this.__INPUT__ = this.__OUTER__.children[1]
this.__APPEND__ = this.__OUTER__.children[3]
this.__OPTG__ = this.__OUTER__.children[4]
}
get readOnly() {
return this.props.readonly
}
set readOnly(val) {
var type = typeof val
if (val === this.props.readonly) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.readonly = true
this.setAttribute('readonly', '')
} else {
this.props.readonly = false
this.removeAttribute('readonly')
}
}
get disabled() {
return this.props.disabled
}
set disabled(val) {
var type = typeof val
if (val === this.props.disabled) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.disabled = true
this.setAttribute('disabled', '')
this.__INPUT__.setAttribute('disabled', '')
} else {
this.props.disabled = false
this.removeAttribute('disabled')
this.__INPUT__.removeAttribute('disabled')
}
}
get value() {
return this.props.value
}
set value(val) {
var { DICT, active } = this.props
this.props.value = val
this.__INPUT__.value = (DICT && DICT[val] && DICT[val].label) || val
if (!active) {
this._updateStyle()
}
}
_renderOptions(options) {
this.props.DICT = {}
this.props.LIST = []
var elem = this.__OPTG__.firstElementChild.firstElementChild
elem.innerHTML = parseOptions(options, this.props)
this.props.ITEMS = Array.from(elem.children).filter(it => {
return it.tagName === 'DD' && !it.hasAttribute('disabled')
})
this.value = this.props.value
}
// 移动光标选择下拉选项
_moveSelect(ev) {
var { LIST, DICT, ITEMS } = this.props
if (LIST && LIST.length) {
ev.preventDefault()
var step = ev.keyCode === 38 ? -1 : 1
if (this.props.mvidx === null) {
this.props.mvidx = 0
} else {
this.props.mvidx += step
}
if (this.props.mvidx < 0) {
this.props.mvidx = 0
} else if (this.props.mvidx > ITEMS.length - 1) {
this.props.mvidx = ITEMS.length - 1
}
ITEMS.forEach((it, i) => {
if (i === this.props.mvidx) {
this.__OPTG__.firstElementChild.scrollTop = it.offsetTop - 150
it.setAttribute('focus', '')
} else {
it.removeAttribute('focus')
}
})
}
}
_updateStyle(idx) {
var { LIST, ITEMS, value } = this.props
if (LIST && LIST.length) {
if (idx === undefined) {
for (let i = -1, it; (it = LIST[++i]); ) {
if (value === it.value) {
idx = i
break
}
}
}
this.props.mvidx = idx
ITEMS.forEach((it, i) => {
if (i === idx) {
it.setAttribute('focus', '')
} else {
it.removeAttribute('focus')
}
})
}
}
// 触发列表选择
_fetchSelect(idx, needUpdateStyle) {
var item = this.props.LIST[idx]
this.value = item.value
this.dispatchEvent(
new CustomEvent('select', {
detail: item
})
)
if (needUpdateStyle) {
this._updateStyle(idx)
}
this.props.active = false
this.__OPTG__.classList.remove('show')
}
connectedCallback() {
var prepend = this.__PREPEND__.assignedNodes()
var append = this.__APPEND__.assignedNodes()
// 相同插槽, 只允许1个
while (prepend.length > 1) {
this.removeChild(prepend.pop())
}
while (append.length > 1) {
this.removeChild(append.pop())
}
if (prepend.length && this.props.type !== 'textarea') {
this.__OUTER__.setAttribute('prepend', '')
}
if (append.length && this.props.type !== 'textarea') {
this.__OUTER__.setAttribute('append', '')
}
function initPos() {
var { x, y, width } = this.getBoundingClientRect()
var size = this.getAttribute('size')
this.props.active = true
if (size && size === 'mini') {
y += 32
} else {
y += 50
}
this.__OPTG__.style.cssText = `left:${x}px;top:${y}px;width:${width}px;`
}
/* ---------------------------------------------------- */
/* ----------------- 各种事件 ------------------ */
/* ---------------------------------------------------- */
// 键盘事件
this._handleKeydown = $.catch(this.__INPUT__, 'keydown', ev => {
if (this.disabled || this.readOnly) {
return
}
// up: 38, down: 40
if (ev.keyCode === 38 || ev.keyCode === 40) {
if (!this.props.active) {
initPos.call(this)
this.__OPTG__.classList.toggle('show', true)
return
}
return this._moveSelect(ev)
}
// 回车触发select事件
if (ev.keyCode === 13) {
if (this.props.mvidx !== null && this.props.active) {
return this._fetchSelect(this.props.mvidx)
}
}
})
// 渲染列表
this._activeFn = $.bind(this.__INPUT__, 'click', ev => {
var { options } = this.props
if (this.disabled || this.readOnly) {
return
}
initPos.call(this)
this.__OPTG__.classList.toggle('show')
})
// 选择选项
this._handleSelect = $.bind(this.__OPTG__, 'click', ev => {
if (ev.target.tagName === 'DD' && !ev.target.hasAttribute('disabled')) {
this._fetchSelect(+ev.target.dataset.idx, true)
this.dispatchEvent(new CustomEvent('input'))
}
})
this._inactiveFn = $.outside(this, ev => {
this.__OPTG__.classList.toggle('show', false)
this.props.active = false
})
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
// label和placeholder 功能相同
case 'label':
case 'placeholder':
this.__INPUT__.setAttribute('placeholder', val)
break
case 'options':
if (val) {
try {
this._renderOptions(JSON.parse(val))
} catch (err) {}
this.removeAttribute('options')
}
break
case 'value':
this.value = val
break
case 'readonly':
case 'disabled':
var k = name
if (k === 'readonly') {
k = 'readOnly'
}
this[k] = true
break
}
}
disconnectedCallback() {
$.unbind(this.__INPUT__, 'keydown', this._handleKeydown)
$.unbind(this.__INPUT__, 'click', this._activeFn)
$.unbind(this.__OPTG__, 'click', this._handleSelect)
$.clearOutside(this._inactiveFn)
}
}
if(!customElements.get('wc-select')){
customElements.define('wc-select', Select)
}

301
src/lib/form/star.js Normal file
View File

@ -0,0 +1,301 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import $ from "../utils.js"
export default class Star extends HTMLElement {
static get observedAttributes() {
return ["value","text","size","color","'allow-half'","'show-value'","starSize","disabled"]
}
props = {
value: 0,
text: [],
size: '',
color: '',
'allow-half': false,
'show-value': false,
starSize: 32, // 星星的宽度, 用于实现半星
disabled: false
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
display: flex;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
cursor: pointer;
font-size: 14px;
--size: 24px; }
label {
display: flex;
align-items: center;
line-height: 0;
cursor: inherit; }
label wc-icon {
margin: 0 3px;
transition: transform 0.1s easein-out; }
label wc-icon:hover {
transform: scale(1.05); }
label span {
padding: 0 8px;
margin: 0 3px; }
:host([size='large']) {
font-size: 16px;
--size: 36px; }
:host([size='medium']) {
--size: 30px; }
:host([size='mini']) {
font-size: 12px;
--size: 20px; }
:host([color='red']) label span {
color: var(--color-red-1); }
:host([color='teal']) label span {
color: var(--color-teal-1); }
:host([color='green']) label span {
color: var(--color-green-1); }
:host([color='grey']) label span {
color: var(--color-grey-1); }
:host([color='blue']) label span {
color: var(--color-blue-1); }
:host([color='purple']) label span {
color: var(--color-purple-1); }
:host([color='orange']) label span {
color: var(--color-orange-1); }
:host([disabled]) {
cursor: default;
opacity: 0.6; }
:host([disabled]) label wc-icon:hover {
transform: none; }
</style>
<label>
<wc-icon data-idx="0" is="star" color="grey"></wc-icon>
<wc-icon data-idx="1" is="star" color="grey"></wc-icon>
<wc-icon data-idx="2" is="star" color="grey"></wc-icon>
<wc-icon data-idx="3" is="star" color="grey"></wc-icon>
<wc-icon data-idx="4" is="star" color="grey"></wc-icon>
<span class="text"></span>
</label>
`
this.__BOX__ = this.root.children[1]
this.__STARS__ = Array.from(this.__BOX__.children)
this.__TEXT__ = this.__STARS__.pop()
}
get value() {
return this.props.value
}
set value(val) {
var v = +val
var tmp = val >> 0
if (v === v && v > 0) {
val = v
} else {
val = 0
}
if (val > 5) {
val = 5
}
this.props.value = val
this._updateDraw(-1)
}
/**
* 更新图标渲染
* i: int
* f: float
*/
_updateDraw(i, f = 0) {
var _last = 'star-half'
var { value, tmp = { i: 0, f: 0 } } = this.props
if (i === -1) {
i = Math.floor(value)
f = +(value % 1).toFixed(1)
if (i > 0 && i === value) {
i--
f = 1
}
}
if (!this.props['allow-half']) {
f = f > 0 ? 1 : 0
}
// 减少DOM操作
if (i === tmp.i && f === tmp.f) {
return
}
if (f > 0.5) {
_last = 'star-full'
}
this.__STARS__.forEach((it, k) => {
it.setAttribute('is', k < i ? 'star-full' : 'star')
it.setAttribute('color', k < i ? this.props.color : 'grey')
})
if (f > 0) {
this.__STARS__[i].setAttribute('is', _last)
this.__STARS__[i].setAttribute('color', this.props.color)
}
// 缓存结果
this.props.tmp = { i, f }
if (i === 0 && f === 0) {
this.__TEXT__.textContent = ''
} else {
if (this.props.text.length === 5) {
this.__TEXT__.textContent = this.props.text[i]
} else {
if (this.props['show-value']) {
this.__TEXT__.textContent = i + f
}
}
}
}
connectedCallback() {
$.catch(this.__BOX__, 'mousemove', ev => {
if (this.props.disabled) {
return
}
if (ev.target.tagName === 'WC-ICON') {
let idx = +ev.target.dataset.idx
this._updateDraw(idx, +(ev.offsetX / this.props.starSize).toFixed(1))
}
})
$.catch(this.__BOX__, 'click', ev => {
var { tmp, disabled } = this.props
if (disabled) {
return
}
if (ev.target.tagName === 'WC-ICON') {
this.props.value = tmp.i + tmp.f
this.dispatchEvent(new CustomEvent('input'))
}
})
$.catch(this.__BOX__, 'mouseleave', ev => {
if (this.props.disabled) {
return
}
this._updateDraw(-1)
})
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'size':
this.props.starSize = this.__STARS__[0].clientWidth
break
case 'allow-half':
case 'show-value':
case 'disabled':
this.props[name] = true
break
case 'color':
if (val) {
this.props.color = val
}
break
case 'text':
if (val) {
val = val.split('|')
if (val.length === 5) {
this.props.text = val.map(it => it.trim())
}
}
break
case 'value':
this.value = val
break
}
}
}
if(!customElements.get('wc-star')){
customElements.define('wc-star', Star)
}

241
src/lib/form/switch.js Normal file
View File

@ -0,0 +1,241 @@
/**
*
* @authors yutent (yutent.io@gmail.com)
* @date 2020-12-08 11:30:52
* @version v1.0.0
*
*/
import $ from "../utils.js"
export default class Switch extends HTMLElement {
static get observedAttributes() {
return ["'active-text'","'inactive-text'","checked","disabled"]
}
props = {
'active-text': null,
'inactive-text': null,
checked: false,
disabled: false
}
constructor() {
super();
Object.defineProperty(this, 'root', {
value: this.attachShadow({ mode: 'open' }),
writable: true,
enumerable: false,
configurable: true
})
this.root.innerHTML = `<style>* {
box-sizing: border-box;
margin: 0;
padding: 0; }
::before,
::after {
box-sizing: border-box; }
:host {
--color-teal-1: #4db6ac;
--color-teal-2: #26a69a;
--color-teal-3: #009688;
--color-green-1: #81c784;
--color-green-2: #66bb6a;
--color-green-3: #4caf50;
--color-purple-1: #9575cd;
--color-purple-2: #9575cd;
--color-purple-3: #673ab7;
--color-blue-1: #64b5f6;
--color-blue-2: #42a5f5;
--color-blue-3: #2196f3;
--color-red-1: #ff5061;
--color-red-2: #eb3b48;
--color-red-3: #ce3742;
--color-orange-1: #ffb618;
--color-orange-2: #f39c12;
--color-orange-3: #e67e22;
--color-plain-1: #f2f5fc;
--color-plain-2: #e8ebf4;
--color-plain-3: #dae1e9;
--color-grey-1: #bdbdbd;
--color-grey-2: #9e9e9e;
--color-grey-3: #757575;
--color-dark-1: #62778d;
--color-dark-2: #526273;
--color-dark-3: #425064; }
:host {
display: inline-block; }
:host section {
display: flex;
justify-content: center;
align-items: center;
line-height: 0;
white-space: nowrap; }
:host label {
display: flex;
align-items: center;
width: 32px;
height: 18px;
padding: 3px;
margin: 5px;
border-radius: 21px;
background: var(--color-plain-3);
cursor: inherit; }
:host label.checked {
flex-direction: row-reverse;
background: var(--color-grey-3); }
:host .dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff; }
:host([disabled]) {
cursor: not-allowed;
opacity: 0.6; }
:host([size='large']) label {
width: 46px;
height: 26px;
padding: 3px 5px; }
:host([size='large']) .dot {
width: 18px;
height: 18px; }
:host([size='medium']) label {
width: 38px;
height: 22px;
padding: 3px 4px; }
:host([size='medium']) .dot {
width: 16px;
height: 16px; }
:host([size='mini']) label {
width: 22px;
height: 14px;
padding: 2px; }
:host([size='mini']) .dot {
width: 10px;
height: 10px; }
:host([color='red']) label.checked {
background: var(--color-red-1); }
:host([color='blue']) label.checked {
background: var(--color-blue-1); }
:host([color='green']) label.checked {
background: var(--color-green-1); }
:host([color='teal']) label.checked {
background: var(--color-teal-1); }
:host([color='orange']) label.checked {
background: var(--color-orange-1); }
:host([color='dark']) label.checked {
background: var(--color-dark-1); }
:host([color='purple']) label.checked {
background: var(--color-purple-1); }
</style>
<section>
<label>
<span class="dot"></span>
</label>
<slot></slot>
</section>
`
this.__SWITCH__ = this.root.lastElementChild.firstElementChild
}
get value() {
return this.props.checked
}
set value(val) {
this.checked = val
}
get checked() {
return this.props.checked
}
set checked(val) {
this.props.checked = !!val
this.__SWITCH__.classList.toggle('checked', this.props.checked)
}
get disabled() {
return this.props.disabled
}
set disabled(val) {
var type = typeof val
if (val === this.props.disabled) {
return
}
if ((type === 'boolean' && val) || type !== 'boolean') {
this.props.disabled = true
this.setAttribute('disabled', '')
} else {
this.props.disabled = false
this.removeAttribute('disabled')
}
}
connectedCallback() {
this._handleClick = $.bind(this, 'click', ev => {
if (this.disabled) {
return
}
this.checked = !this.checked
if (this.checked) {
if (this.props['active-text'] !== null) {
this.textContent = this.props['active-text']
}
} else {
if (this.props['inactive-text'] !== null) {
this.textContent = this.props['inactive-text']
}
}
this.dispatchEvent(new CustomEvent('input'))
})
}
disconnectedCallback() {
$.unbind(this, 'click', this._handleClick)
}
attributeChangedCallback(name, old, val) {
if (val === null || old === val) {return}
switch (name) {
case 'checked':
case 'disabled':
this[name] = true
break
case 'active-text':
case 'inactive-text':
this.props[name] = val + ''
break
}
}
}
if(!customElements.get('wc-switch')){
customElements.define('wc-switch', Switch)
}

15
src/lib/icon/index.js Normal file
View File

@ -0,0 +1,15 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2020-07-07 16:27:17
* @version v2.0.1
*
*/
'use strict'
import SVG_DICT from"./svg.js";let dict=SVG_DICT;window.EXT_SVG_DICT&&Object.assign(dict,EXT_SVG_DICT);export default class Icon extends HTMLElement{static get observedAttributes(){return["is"]}constructor(){super(),Object.defineProperty(this,"root",{value:this.attachShadow({mode:"open"}),writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(this,"props",{value:{is:""},writable:!0,enumerable:!1,configurable:!0}),this.root.innerHTML="<style>*{box-sizing:border-box;margin:0;padding:0}::before,::after{box-sizing:border-box}:host{display:inline-block;color:inherit}:host(:not([is])){display:none}.icon{display:block;width:var(--size, 32px);height:var(--size, 32px);margin:var(--pad, auto);fill:currentColor}.icon.load{animation:load 1.5s linear infinite}.icon circle{stroke:currentColor;animation:circle 1.5s ease-in-out infinite}:host([size='large']) .icon{width:42px;height:42px}:host([size='medium']) .icon{width:38px;height:38px}:host([size='mini']) .icon{width:20px;height:20px}:host([color='red']){color:#ff5061}:host([color='blue']){color:#64b5f6}:host([color='green']){color:#81c784}:host([color='teal']){color:#4db6ac}:host([color='orange']){color:#ffb618}:host([color='dark']){color:#62778d}:host([color='purple']){color:#9575cd}:host([color='grey']){color:#bdbdbd}@keyframes circle{0%{stroke-dasharray:0, 3812px;stroke-dashoffset:0}50%{stroke-dasharray:1906px, 3812px;stroke-dashoffset:-287px}100%{stroke-dasharray:1906px, 3812px;stroke-dashoffset:-2393px}}@keyframes load{to{transform:rotate(360deg)}}</style> <svg class=\"icon\" viewBox=\"0 0 1024 1024\"></svg> ",this.__ICO__=this.root.lastElementChild,this.drawPath()}get is(){return this.props.is}set is(o){o&&this.setAttribute("is",o)}drawPath(){var{is:o}=this.props,t=dict[o];this.__ICO__&&o&&t&&(this.__ICO__.innerHTML="loading"===o?t:`<path d="${t}" />`,this.__ICO__.classList.toggle("load","loading"===o))}attributeChangedCallback(o,t,e){if(null!==e&&t!==e)switch(o){case"is":this.props.is=e,e?this.drawPath():this.removeAttribute("is")}}};
if(!customElements.get('wc-icon')){
customElements.define('wc-icon', Icon)
}

1
src/lib/icon/svg.js Normal file

File diff suppressed because one or more lines are too long

15
src/lib/layer/index.js Normal file

File diff suppressed because one or more lines are too long

15
src/lib/scroll/index.js Normal file

File diff suppressed because one or more lines are too long

25
src/lib/socket.js Normal file
View File

@ -0,0 +1,25 @@
/**
* 与主进程的通讯
* @author yutent<yutent.io@gmail.com>
* @date 2020/07/14 11:42:02
*/
const { ipcRenderer } = require('electron')
const EventEmitter = require('events')
const util = require('util')
class Socket {
constructor() {
ipcRenderer.on('app', (ev, conn) => {
this.emit(conn.type, conn.data)
})
}
dispatch(type = '', data = {}) {
return ipcRenderer.sendSync('app', { data, type })
}
}
util.inherits(Socket, EventEmitter)
export default new Socket()

1
src/lib/utils.js Normal file
View File

@ -0,0 +1 @@
function noop(){}export default{nextTick:function(){let t=[];let e=document.createTextNode("\x3c!-- --\x3e");new MutationObserver(function(){let e=t.length;for(let n=0;n<e;n++)t[n]();t=t.slice(e)}).observe(e,{characterData:!0});let n=!1;return function(i){t.push(i),n=!n,e.data=n}}(),each(t,e){if(t)if(Array.isArray(t))for(let n,i=0;(n=t[i++])&&!1!==e(n,i-1););else for(let n in t)if(t.hasOwnProperty(n)&&!1===e(t[n],n))break},bind(t,e,n=noop,i=!1){let o=e.split(",");return this.each(o,function(e){e=e.trim(),t.addEventListener(e,n,i)}),n},catch(t,e,n,i){return this.bind(t,e,function(t){t.stopPropagation(),n&&n(t)},i)},unbind(t,e,n=noop,i=!1){let o=e.split(",");this.each(o,function(e){e=e.trim(),t.removeEventListener(e,n,i)})},outside(t,e=noop){return this.bind(document,"mousedown",n=>{if(n)if(n.path){for(var i=n.path.concat();i.length>3;)if(i.shift()===t)return}else{var o=n.explicitOriginalTarget||n.target;if(t===o||t.contains(o)||t.root&&t.root.contains(o))return}e(n)})},clearOutside(t=noop){this.unbind(document,"mousedown",t)}};

153
src/main.js Normal file
View File

@ -0,0 +1,153 @@
/**
*
* @author yutent<yutent@doui.cc>
* @date 2019/09/16 20:51:19
*/
const {
app,
BrowserWindow,
protocol,
ipcMain,
net,
Notification
} = require('electron')
const path = require('path')
const fs = require('iofs')
const { createMainWindow, createFloatWindow } = require('./tools/window')
const createMenu = require('./tools/menu')
const createTay = require('./tools/tray')
const MIME_TYPES = {
'.js': 'text/javascript',
'.html': 'text/html',
'.htm': 'text/plain',
'.css': 'text/css',
'.jpg': 'image/jpg',
'.png': 'image/png',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/ico'
}
const ROOT = __dirname
var timer
function fetch(url) {
return new Promise((y, n) => {
var conn = net.request(url)
var r = []
conn.on('response', res => {
res.on('data', c => {
r.push(c)
})
res.on('end', _ => {
y(Buffer.concat(r).toString())
})
})
conn.on('error', e => {
n(e)
})
conn.end()
})
}
function ring() {
var n = 5
var t = setInterval(() => {
var notify = new Notification({
title: '搞基⏰',
subtitle: '神奇的2点半到啦',
body: '神奇的2点半到啦, 该加仓的加仓, 该卖的卖啦'
})
notify.show()
n--
if (n === 0) {
clearInterval(t)
}
}, 1000)
}
/* ----------------------------------------------------- */
app.commandLine.appendSwitch('--lang', 'zh-CN')
app.commandLine.appendSwitch('--autoplay-policy', 'no-user-gesture-required')
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
/* ----------------------------------------------------- */
app.dock.hide()
// 初始化应用
app.once('ready', () => {
// 注册协议
protocol.registerBufferProtocol('app', (req, cb) => {
let file = req.url.replace(/^app:\/\/local\//, '')
let ext = path.extname(req.url)
let buff = fs.cat(path.resolve(ROOT, file))
cb({ data: buff, mimeType: MIME_TYPES[ext] })
})
// 创建浏览器窗口
app.__main__ = createMainWindow(path.resolve(ROOT, './images/app.png'))
app.__float__ = createFloatWindow()
createMenu(app.__main__)
createTay(app.__float__, app.__main__)
app.__main__.on('closed', () => {
app.__main__ = null
app.__float__ = null
app.exit()
})
// mac专属事件,点击dock栏图标,可激活窗口
// app.on('activate', _ => {
// if (app.__main__) {
// app.__main__.restore()
// }
// })
})
ipcMain.on('app', (ev, conn) => {
switch (conn.type) {
case 'fetch':
fetch(conn.data).then(r => {
ev.returnValue = r
})
break
case 'notify':
clearTimeout(timer)
var t1 = Date.now()
var t2 = new Date()
t2.setHours(14)
t2.setMinutes(30)
t2.setSeconds(0)
if (t2.getTime() - t1 > 0) {
timer = setTimeout(ring, t2.getTime() - t1)
}
ev.returnValue = true
break
case 'data-reload':
app.__main__.webContents.send('app', { type: 'data-reload', data: null })
ev.returnValue = true
break
case 'devtools':
app.__main__.openDevTools()
ev.returnValue = true
break
}
})

36
src/tools/menu.js Normal file
View File

@ -0,0 +1,36 @@
/**
* 菜单项
* @author yutent<yutent.io@gmail.com>
* @date 2020/12/10 19:30:02
*/
const { Menu } = require('electron')
module.exports = function(win) {
var menuList = Menu.buildFromTemplate([
{
label: '搞基数据',
submenu: [
{ role: 'about', label: '关于搞基数据' },
{ type: 'separator' },
{
label: '退出',
accelerator: 'Command+Q',
click(a, b, ev) {
win.destroy()
}
}
]
},
{
label: '编辑',
submenu: [
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' },
{ role: 'selectall', label: '全选' }
]
}
])
Menu.setApplicationMenu(menuList)
}

42
src/tools/tray.js Normal file
View File

@ -0,0 +1,42 @@
/**
* 托盘
* @author yutent<yutent.io@gmail.com>
* @date 2020/12/10 19:30:20
*/
const { Tray, Menu } = require('electron')
const path = require('path')
const ROOT = __dirname
module.exports = function(mini, main) {
var menuList = Menu.buildFromTemplate([
{
label: '显示主窗口',
click(a, b, ev) {
main.restore()
}
},
{
label: '不搞基了',
accelerator: 'Command+Q',
click(a, b, ev) {
main.destroy()
}
}
])
var tray = new Tray(path.join(ROOT, '../images/tray.png'))
tray.on('click', _ => {
var b = tray.getBounds()
mini.setBounds({ x: b.x - 120, y: b.y + b.height })
mini.show()
mini.focus()
mini.webContents.send('app', { type: 'float-visible', data: null })
})
tray.on('right-click', _ => {
tray.popUpContextMenu(menuList)
})
main.__tray__ = tray
}

76
src/tools/window.js Normal file
View File

@ -0,0 +1,76 @@
/**
*
* @author yutent<yutent.io@gmail.com>
* @date 2020/12/10 14:57:49
*/
const { BrowserWindow } = require('electron')
/**
* 应用主窗口
*/
exports.createMainWindow = function(icon) {
var win = new BrowserWindow({
title: '搞基数据',
width: 1024,
height: 540,
frame: false,
titleBarStyle: 'hiddenInset',
resizable: false,
maximizable: false,
icon,
transparent: true,
vibrancy: 'hud',
visualEffectState: 'active',
webPreferences: {
// webSecurity: false,
experimentalFeatures: true,
nodeIntegration: true,
spellcheck: false
},
show: false
})
win.loadURL('app://local/index.html')
// win.on('ready-to-show', _ => {
// win.show()
// win.openDevTools()
// })
win.on('close', ev => {
ev.preventDefault()
win.hide()
})
return win
}
// 创建悬浮窗口
exports.createFloatWindow = function() {
var win = new BrowserWindow({
width: 280,
height: 360,
resizable: false,
maximizable: false,
frame: false,
show: false,
vibrancy: 'hud',
visualEffectState: 'active',
webPreferences: {
experimentalFeatures: true,
nodeIntegration: true,
spellcheck: false
}
})
// win.openDevTools()
win.on('blur', ev => {
win.hide()
})
win.loadURL('app://local/float.html')
return win
}

37
src/view.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<title></title>
<link href="/lib/css/reset-basic.css" rel="stylesheet">
<link href="/css/float.css" rel="stylesheet">
<script src="/js/float.js" type="module"></script>
</head>
<body class="noselect">
<div class="app" anot="app">
<wc-scroll class="list">
<section class="item" :for="it in list">
<div class="info">
<h3 class="text-ell" :text="it.name"></h3>
<cite :text="it.code"></cite>
</div>
<div class="today">
<span class="money" :text="'¥' + it.cm"></span>
<span
class="percent"
:class="{red: it.cp > 0, green: it.cp < 0}"
:text="it.cp + '%'">
</span>
</div>
</section>
<section :if="list.length === 0" class="item">啥基都没有...</section>
</wc-scroll>
</div>
</body>
</html>