This commit is contained in:
Wyle.Gong-巩文昕 2025-04-23 11:22:46 +08:00
parent 7a1aae1e2f
commit 0b29e4e856
20 changed files with 1301 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit f1c7dedc1e6c2bceec011c4da59caa35e104f10f

21
vyes-ui/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 veypi
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.

9
vyes-ui/Makefile Normal file
View File

@ -0,0 +1,9 @@
#
# Makefile
# Copyright (C) 2024 veypi <i@veypi.com>
# 2025-03-04 14:03:56
# Distributed under terms of the GPL license.
#
run:
@go run ./cli/*.go -f ./cfg/dev.yaml

1
vyes-ui/README.md Normal file
View File

@ -0,0 +1 @@
# vyes-ui

24
vyes-ui/go.mod Normal file
View File

@ -0,0 +1,24 @@
module vyesui
go 1.23.2
replace github.com/veypi/OneBD => ../../../OneBD/
replace github.com/veypi/utils => ../../../utils/
require (
github.com/google/uuid v1.6.0
github.com/veypi/OneBD v0.0.0-00010101000000-000000000000
github.com/veypi/utils v0.3.7
gorm.io/gorm v1.25.12
)
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/rs/zerolog v1.17.2 // indirect
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

34
vyes-ui/go.sum Normal file
View File

@ -0,0 +1,34 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

25
vyes-ui/init.go Normal file
View File

@ -0,0 +1,25 @@
//
// Copyright (C) 2025 veypi <i@veypi.com>
// 2025-03-04 14:03:56
// Distributed under terms of the MIT license.
//
package vyesui
import (
"embed"
"github.com/veypi/OneBD/rest"
"github.com/veypi/OneBD/rest/middlewares/vyes"
)
var Router = rest.NewRouter()
//go:embed ui/*
var uifs embed.FS
//go:embed ui/root.html
var rootFile []byte
func init() {
vyes.WrapUI(Router, uifs)
}

136
vyes-ui/ui/btn.html Normal file
View File

@ -0,0 +1,136 @@
<!DOCTYPE html>
<html>
<style>
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #000;
border-radius: 50%;
width: var(--size);
height: var(--size);
}
/* 基础样式 */
body {
--color: #3498db;
--size: 1rem;
border: none;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
background-color: var(--color);
select: none;
line-height: 1;
user-select: none;
font-size: var(--size);
padding: calc(var(--size) / 2) var(--size);
}
body[size=lg] {
--size: 1.5rem;
}
body[size=md] {
--size: 1rem;
}
body[size=sm] {
--size: 0.75rem;
}
body[size=xs] {
--size: 0.5rem;
}
body:hover {
background-color: color-mix(in srgb, var(--color), white 20%);
color: black;
}
/* 经典按钮 */
body[typ='classic'] {
color: white;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
body[typ='classic']:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* 圆角按钮 */
body[typ=rounded] {
color: white;
border-radius: 25px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
body[typ=rounded]:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* 扁平按钮 */
body[typ=flat] {
color: white;
border-radius: 0;
}
body[typ=flat]:hover {
transform: translateY(-2px);
}
/* 边框按钮 */
body[typ=outline] {
background-color: transparent;
color: var(--color);
border: 2px solid var(--color);
border-radius: 4px;
}
body[typ=outline]:hover {
background-color: transparent;
color: color-mix(in srgb, var(--color), white 40%);
border: 2px solid color-mix(in srgb, var(--color), white 40%);
}
body.disable {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
</style>
<body :typ='typ' :class="{disable}" :style='`--color:`+color' :size='size'>
<vslot @click='wrap' class="w-full h-full" v-if='ok'>ok</vslot>
<div v-else class="animate-spin loading-spinner"></div>
</body>
<script setup>
typ = 'classic'
color = ''
ok = true
size = 'md'
disable = false
click = async () => { }
wrap = async () => {
if (!ok || disable) {
return
}
ok = false
let fc = click
if (fc instanceof Promise) {
fc = await fc
} else if (fc instanceof Function) {
fc = fc()
} else {
console.log('click is not a function', fc)
}
if (fc instanceof Promise) {
await fc
}
ok = true
}
</script>
</html>

58
vyes-ui/ui/dialog.html Normal file
View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<body style="display:none;">
<vslot>
<div @click='show=false'>
blank dialog
</div>
</vslot>
</body>
<script setup>
show = true
</script>
<script>
let dom = document.createElement('div')
Object.assign(dom.style, {
position: 'fixed',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
'z-index': 1000,
transition: 'all 0.3s ease',
})
dom.classList.add('animate__animated')
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
$data.show = false
}
})
document.addEventListener('click', (event) => {
if (dom.contains(event.target)) {
return
}
$data.show = false
})
let nodes = Array.from($node.childNodes)
setTimeout(() => {
dom.innerHTML = ''
dom.append(...nodes)
$vyes.vproxy.Watch(() => {
dom.classList.remove('animate__fadeIn')
dom.classList.remove('animate__fadeOut')
if ($data.show) {
dom.classList.add('animate__fadeIn')
document.body.appendChild(dom)
} else {
dom.classList.add('animate__fadeOut')
setTimeout(() => {
// dom.style.display = 'none'
// $node.appendChild(...nodes)
dom.remove()
}, 300)
}
})
}, 100)
</script>
</html>

81
vyes-ui/ui/dropdown.html Normal file
View File

@ -0,0 +1,81 @@
<!doctype html>
<html>
<head>
<title>Dropdown Menu</title>
</head>
<style>
.dropdown {
width: 100%;
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
}
.dropdown-btn {
transition: all 0.3s;
cursor: pointer;
}
.dropdown-btn:hover {
opacity: 0.8;
}
.dropdown-content {
position: absolute;
top: 100%;
left: 0;
transform: translateZ(0) translateY(-10px);
width: 100%;
background: white;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
cursor: pointer;
background: var(--background-color, #fff);
color: var(--color, #000);
overflow: hidden;
}
.dropdown:hover .dropdown-content {
opacity: 1;
visibility: visible;
transform: translateZ(0) translateY(0);
}
body .dropdown-item {
display: block;
text-decoration: none;
transition: background 0.2s;
font-size: 1rem;
line-height: 1.5rem;
padding: 0.5rem 1rem;
}
body .dropdown-item:hover {
background-color: color-mix(in srgb, var(--background-color, #fff), #888 20%);
}
body {
display: inline-block;
}
</style>
<body>
<div class="dropdown">
<vslot class="dropdown-btn">
<button>Dropdown Menu</button>
</vslot>
<vslot name='menu' class="dropdown-content">
<a href="#" class="dropdown-item">Option 1</a>
<a href="#" class="dropdown-item">Option 2</a>
</vslot>
</div>
</body>
</html>

46
vyes-ui/ui/form.html Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<style>
</style>
<body>
<form vdom='vform' class="flex flex-col gap-4">
<div class="my-2" v-if='!key.disabled' :style="'height:'+key.height" v-for='key in keys'>
<div class="flex gap-2 h-full">
<div class="truncate" :style="'width:'+label_width">{{key.label || key.name}}</div>
<div class="flex-grow">
<div refu='input' :required='key.required' v:value='data[key.name]' :type='key.type' :validate='key.validate'
:opts='key.opts'></div>
</div>
</div>
</div>
<div style="color: red;">{{errmsg}}</div>
<div class="flex justify-center mt-10">
<div size='lg' refu='btn' :click='onOk' style="width: 8rem;">Ok</div>
</div>
</form>
</body>
<script setup>
label_width = '6rem'
keys = []
data = {}
vform = null
errmsg = ''
onsave = async () => { }
onOk = async () => {
let valid = $data.vform.checkValidity()
if (!valid) {
$data.vform.reportValidity()
return
}
try {
await $data.onsave($data.data)
} catch (error) {
errmsg = error.message || error || '未知错误'
console.error(error)
}
}
</script>
</html>

55
vyes-ui/ui/icon.html Normal file
View File

@ -0,0 +1,55 @@
<style>
body {
--icon-size: var(--icon-size, 2rem);
--icon-color: var(--icon-color, #000);
height: var(--icon-size);
width: var(--icon-size);
font-size: calc(var(--icon-size) - 0.5rem);
line-height: var(--icon-size);
display: inline-flex;
justify-content: center;
align-items: center;
transition: all 0.3s;
cursor: pointer;
color: var(--icon-color);
}
body i {
font-family: "iconfont" !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
}
body:hover {
opacity: 0.8;
}
body[type=round] {
border-radius: 50%;
}
body[type=round]:hover {
background-color: color-mix(in srgb, var(--icon-color), #999 40%);
color: #fff;
}
body[type=rect]:hover {
background-color: color-mix(in srgb, var(--icon-color), #999 40%);
color: #fff;
}
body[type=outline] {
border: 1px solid var(--icon-color);
background-color: transparent;
}
</style>
<body :style="`--icon-size:${size};--icon-color:${color}`" :type>
<i :class="'icon-'+name"></i>
</body>
<script setup>
size = '2rem'
color = '#000'
name = ''
type = 'round' // rect, round, outline, none
</script>

134
vyes-ui/ui/input.html Normal file
View File

@ -0,0 +1,134 @@
<!DOCTYPE html>
<html>
<style>
.clear {
background: none;
outline: none;
}
.clear:invalid+hr {
border: #f00 solid 1px !important;
}
body {
position: relative;
width: 100%;
}
hr {
margin: auto;
position: absolute;
bottom: -1px;
border: rgba(128, 128, 128, 0.2) solid 1px;
transition: all 0.2s linear;
}
body:hover hr {
border: rgba(128, 128, 128, 9) solid 1px;
width: 100% !important;
left: 0rem !important;
}
.vinput-core {
width: calc(100% - 1rem);
padding-left: 1rem;
min-width: 3rem;
}
input[type="text"]:focus {}
</style>
<body class='relative'>
<div class="flex relative">
<div v-if='label' class="flex-shrink-0" :style="{width: label_width}">{{label}}</div>
<textarea vdom='vref' :rows='opts?.rows' v-if="type==='textarea'" class="clear vinput-core grow" @input='sync_evt'
!value :placeholder !required>
</textarea>
<div v-else-if='type==="bool"'>
<div @click='value=!value'>{{value?'true':'false'}}</div>
</div>
<input vdom='vref' v-else-if='type==="number"' class="clear vinput-core grow" type="number" @input='sync_evt'
!value='value||0' :placeholder !required />
<input-select v:value v-else-if='type==="select"' class="vinput-core grow" :input='sync' :options='opts.options'
:placeholder></input-select>
<input vdom='vref' v-else class="clear vinput-core grow" type="text" @input='sync_evt' !value="value||''"
:placeholder !required />
<hr v-if='type!="select"'
:style="label?`width:calc(100% - 2rem - ${label_width});left: calc(1rem + ${label_width})`:'width:calc(100% - 2rem);left:1rem'">
</div>
</body>
<script setup>
vref = null
type = 'text' // text textarea
required = false
value = ''
label = ''
label_width = '6rem'
placeholder = ''
opts = {
rows: 5,
options: []
}
validate = (v) => true
sync_evt = (e) => {
sync(e.target.value)
}
sync = (v) => {
if (!$data.check(v)) {
$data.vref?.setCustomValidity("invalid");
return
}
$data.vref?.setCustomValidity('');
if ($data.type === 'textarea') {
$data.value = v
} else if ($data.type === 'number') {
$data.value = Number(v)
} else if ($data.type === 'bool') {
$data.value = v === 'true'
} else if ($data.type === 'select') {
$data.value = v
} else {
// type text
$data.value = v
}
}
check = (v) => {
if (!v) {
return !$data.required
}
if (!$data.validate) {
return true
} else if ($data.validate instanceof RegExp) {
if ($data.validate.test(v)) {
return true
}
} else if (typeof $data.validate === 'function') {
if ($data.validate(v)) {
return true
}
}
return false
}
</script>
<script>
if (!$data.value) {
switch ($data.type) {
case 'number':
sync(0)
break
case 'bool':
sync(false)
break
case 'select':
sync('')
break
default:
sync('')
}
}
</script>
</html>

View File

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<style>
.option-item {
padding: 8px;
cursor: pointer;
font-size: 14px;
}
.option-item:hover {
background-color: #f0f0f0;
}
::-webkit-scrollbar,
::-webkit-scrollbar-track {
width: 0.25rem;
height: 0.25rem;
background: none;
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 5px;
transform: translateX(10px);
}
body {
position: relative;
display: inline-flex;
user-select: none;
cursor: pointer;
justify-content: center;
}
.option-list {
position: absolute;
top: 100%;
left: 0;
width: 100%;
background: white;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
border-radius: 4px;
z-index: 1;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s;
max-height: 40vh;
overflow-y: auto;
}
body:hover .option-list {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
</style>
<body>
<div @click='onclick' class="select-none cursor-pointer">
{{selectLabel()||placeholder||'请选择'}}
</div>
<div @click='onclick'>🔻</div>
<div class="option-list">
<input class="clear grow border-gray-400 border mx-auto my-2 p-1 block" style="width: 90%" @input='filter'
placeholder='筛选' />
<div v-if='filterOptions.length===0' class="option-item">
Not Found Options
</div>
<div @click='onselect(v)' class="option-item border-b border-gray-100" v-for='v in filterOptions'>
{{v.label || v }}
</div>
</div>
</body>
<script setup>
placeholder = ''
options = [
{value: '1', label: 'Label 1'},
{value: '2', label: 'Label 2'},
{value: '3', label: 'Label 3'},
]
filterOptions = []
value = ''
input = (v) => {
value = v
}
filter = (event) => {
filterOptions = options.filter((o) => {
let tag = o.label || o.value || o
if (typeof tag !== 'string') {
tag = tag.toString()
}
return tag.includes(event.target.value || '')
})
}
onselect = (v) => {
input(v.value || v)
show = false
}
selectLabel = () => {
let res = value
options.forEach((o) => {
if (typeof o === 'string') {
return
}
if (o.value === value) {
res = o.label
}
})
return res
}
</script>
<script>
filterOptions = [...options]
</script>
</html>

11
vyes-ui/ui/preview.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<body>
<div>value</div>
</body>
<script setup>
value = '2'
</script>
</html>

65
vyes-ui/ui/root.html Normal file
View File

@ -0,0 +1,65 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VyesUI - 高效前端组件库</title>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.features {
display: flex;
flex-wrap: wrap;
gap: 2rem;
justify-content: center;
}
.feature-card {
background-color: #f9fafb;
border-radius: 8px;
padding: 1.5rem;
width: 300px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.feature-card:hover {
transform: translateY(-5px);
}
</style>
</head>
<body class="h-full w-full bg-gray-100">
<div class="container">
<header class="header">
<h1 class="text-4xl font-bold">欢迎使用 VyesUI</h1>
<p class="mt-2 text-gray-700">一个基于 Vyes 框架构建的高效前端组件库</p>
</header>
<section class="features">
<div class="feature-card">
<h2 class="text-xl font-semibold">简洁易用</h2>
<p>遵循 HTML5 规范,提供直观的 API 设计,让开发者快速上手。</p>
</div>
<div class="feature-card">
<h2 class="text-xl font-semibold">响应式设计</h2>
<p>内置移动端优先的设计理念,确保在各种设备上都能获得良好的用户体验。</p>
</div>
<div class="feature-card">
<h2 class="text-xl font-semibold">灵活定制</h2>
<p>支持 Tailwind CSS 样式系统,方便开发者根据需求定制样式。</p>
</div>
<div class="feature-card">
<h2 class="text-xl font-semibold">丰富的组件</h2>
<p>包含多种常用组件,满足不同场景下的开发需求。</p>
</div>
</section>
</div>
<script setup>
// 可以在这里定义页面所需的响应式数据或方法
// 当前页面为静态展示,无需定义额外逻辑
</script>
</html>

317
vyes-ui/ui/table.html Normal file
View File

@ -0,0 +1,317 @@
<!DOCTYPE html>
<html>
<style>
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #000;
border-radius: 50%;
width: 24px;
height: 24px;
}
.table {}
.table-column {
flex-grow: 1;
max-width: 30%;
border-bottom: 2px solid black;
background-color: var(--background-color, #fff);
}
.sticky-column {
position: sticky;
z-index: 10;
}
.header-key {
border-bottom: 2px solid black;
padding: 0 0.5rem;
font-size: 1.2rem;
height: 3rem;
line-height: 3rem;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
box-sizing: border-box;
}
.table-value {
display: block;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0.5rem;
height: 3rem;
line-height: 2rem;
}
.table-value[odd='1'] {
/* background-color: color-mix(in srgb, var(--background-color, #fff), #888 20%); */
}
.table-btn {}
.dialog {
min-height: 50vh;
max-height: 80vh;
overflow: auto;
width: 50vw;
background-color: var(--background-color, #fff);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 2rem;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
}
::-webkit-scrollbar,
::-webkit-scrollbar-track {
width: 0.25rem;
height: 0.25rem;
background: none;
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 5px;
transform: translateX(10px);
}
.keysearch {
outline: none;
border: 1px solid var(--color, #000);
}
</style>
<body>
<div class="flex justify-evenly overflow-x-auto">
<div v-if='!key.hidden' class="table-column" :class="{'sticky-column':index===0}" style="min-height: 21rem;left:0"
v-for='(key,index) in keys'>
<div class="header-key">{{key.label||key.name}}</div>
<div class="table-value" :odd='index%2' v-for='(row, index) in data'>
<vslot :name='key.name' v='row,index' :style="key.style">
<div refu='input' v:value='row[key.name]' :type="key.type==='textarea'?'text':key.type"
:required='key.required' :validate='key.validate' :opts='key.opts'
v-if='editable && !key.disabled && row._enable'>
</div>
<div v-else>
{{ (key.field?key.field(row):row[key.name]) || '&nbsp;'}}
</div>
</vslot>
</div>
</div>
<div class="table-column sticky-column" style="right:0;min-width: 8rem;">
<vslot name='_key' class="header-key">
<div refu='dropdown' class="w-full">
<div refu='icon' class="text-2xl" name='ecs'></div>
<div vslot='menu'>
<div class="dropdown-item" @click='show(0)'>创建</div>
<div class="dropdown-item" @click='show(1)'>高级检索</div>
<div class="dropdown-item" @click='show(2)'>智能导入</div>
</div>
</div>
</vslot>
<div class="table-value" :odd='index%2' v-for='(row, index) in data'>
<vslot class="w-full flex justify-center gap-2 text-xl" name='_addon' v='row,index'>
<div refu='icon' name='edit-square' color='#78c' v-if='!row._enable' @click='row._enable=true'></div>
<div refu='icon' name='save' color='#ff3300' v-else @click='wrap(1, row)'></div>
<div refu='icon' name='delete' color='#f66' v-if='!row._enable' @click='wrap(3, row)'></div>
<div refu='icon' name='close' color='#aaa' v-else @click='delete row._enable'></div>
</vslot>
</div>
</div>
</div>
<div class="flex items-center gap-2 px-4 select-none">
<input !value='listOpts.keyword' @input.delay1s='search' class="keysearch" placeholder="简单检索" />
<div refu='icon' name='left' class="ml-auto" @click='wrap(0,-1)'></div>
<div>{{listOpts.page}}</div>
<div refu='icon' name='right' class="mr-auto" @click='wrap(0,1)'></div>
<div class="">总计{{total}}条数据</div>
</div>
<div refu='dialog' v:show='showFlag'>
<div class="dialog">
<div v-if='showMode==0' refu='form' :keys="keys" :data='{}' :onsave='async (d)=> await wrap(2,d)'>
</div>
<table-setting :keys='keys' :opts='listOpts' v-else-if='showMode==1' :apply='()=>wrap(0)'>
</table-setting>
<div v-else-if class="w-full flex flex-col flex-grow items-center gap-4" style=" height: calc(100% - 0px);">
<textarea class="w-full bg-gray-200 flex-grow p-4" placeholder="请输入文本内容或者拖入文件" !value='ai_content'
@input='ai_content=$event.target.value' style="resize:vertical;"></textarea>
<div refu='btn' class="mx-auto" size='lg' :click='ai'>智能识别</div>
</div>
</div>
</div>
</body>
<script setup>
showFlag = false
showMode = 0
loading = false
keys = []
data = []
host = window.location.origin
api = ''
editable = false
ai_content = ''
ai = async () => {
let keyData = []
keys.forEach((k) => {
if (k.disabled) {
return
}
keyData.push({key: k.name, label: k.label, type: k.type || 'text'})
})
let body = JSON.stringify({
system: "",
model: 'qwen-max-latest',
messages: [{
role: 'user', content: `
接下来我将给你一个结构体描述信息和一段文本,结构体参数默认为字符串类型,如果参数 请直接以json方式返回提取的结构信息,如果提取失败就返回空列表对象[],如果用户提出生成随机数据或者生成数据等请求请你根据字段描述生成mock数据
结构体:
${JSON.stringify(keyData)}
描述信息:
${ai_content}
`}]
})
ai_content = ''
let json_res = ''
let checked = false
await $env.api.SSE(`${window.location.origin}/_/api/ai/gen`, {method: "POST", body: body}, (line, idx) => {
if (idx === -1) {
try {
console.log(json_res)
let data = JSON.parse(ai_content.slice(7, -3))
console.log(data)
let fc = async () => {
let c = 0
for (d of data) {
try {
create(d)
c++
} catch (e) {
console.log(e)
}
}
alert(`成功导入${c}条数据`)
next()
}
fc()
} catch (e) {
console.log(e)
}
console.log('done')
}
if (line.indexOf('```json') != -1) {
json_res = line.slice(line.indexOf('```json'))
checked = true
} else if (checked && line.indexOf('```') != -1) {
json_res += line.slice(0, line.indexOf('```'))
checked = false
} else if (checked) {
json_res += line
}
ai_content += line
})
}
show = (m) => {
showMode = m
showFlag = true
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
update = async (row) => {
if (api) {
return await $env.api.Patch(host + api + '/' + row.id, row)
}
}
total = 0
max = () => {
if (total > 0) {
return Math.ceil(total / listOpts.page_size)
}
return 0
}
listOpts = {
page: 1,
page_size: 10,
keyword: '',
keywords: {},
sort_by: 'created_at',
order: 'desc',
}
next = async (opts) => {
if (api) {
opts = Object.assign({}, opts)
if (opts.keywords && Object.keys(opts.keywords).length > 0) {
opts.keywords = JSON.stringify(opts.keywords)
} else {
delete opts.keywords
}
return await $env.api.Get(host + api, opts)
}
return []
}
create = async (data) => {
if (api) {
return await $env.api.Post(host + api, data)
}
return
}
del = async (row) => {
if (api) {
return await $env.api.Delete(host + api + '/' + row.id, row)
}
}
search = (e) => {
listOpts.keyword = e.target.value
wrap(0)
}
wrap = async (mode, props) => {
showFlag = false
if (mode === 0) {
if (typeof props === 'number') {
listOpts.page += props
}
if (listOpts.page < 1) {
listOpts.page = 1
return
}
let data = await $data.next(listOpts)
if (Array.isArray(data)) {
$data.data = data
} else {
$data.data = data.items
$data.total = data.total
}
} else if (mode === 1) {
await update(props)
props._enable = false
} else if (mode === 2) {
let res = await create(props)
showFlag = false
$data.data.push(res)
return res
} else if (mode == 3) {
await del(props)
$data.data = $data.data.filter((d) => d.id !== data.id)
return
}
}
onsave = () => { }
onerr = () => { }
</script>
<script>
if ($data.data.length === 0 && $data.api) {
try {
wrap(0)
} catch (e) {
$data.onerr(e)
}
}
</script>
</html>

View File

@ -0,0 +1,96 @@
<!doctype html>
<html>
<style>
.config-item {
margin-bottom: 1rem;
}
.field-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.field-item {
border: 1px solid #ddd;
padding: 0.25rem 0.5rem;
cursor: pointer;
}
</style>
<body class="container flex flex-col h-full gap-8">
<!-- 字段选择 -->
<h2 class="text-2xl">高级检索设置</h2>
<!-- 行数设置 -->
<div class="config-item">
<label>行数限制:</label>
<input type="number" !value="opts.page_size" @input='opts.page_size=Number($event.target.value)' min="1"
max="100" />
</div>
<!-- 排序设置 -->
<div class="config-item flex">
<label>排序字段:</label>
<input-select class="flex-grow" v:value='opts.sort_by' :options='options()'></input-select>
<label style="margin-left:1rem">排序方向:</label>
<input-select class="flex-grow" v:value='opts.order' :options='directionOpts'></input-select>
</div>
<div class="config-item">
<h4>选择显示字段:</h4>
<div class="field-list mt-4">
<span v-for="field in keys" :class="['field-item', field.hidden?'':'bg-blue-100' ]"
@click="field.hidden=!field.hidden">
{{ field.label || field.name }}
</span>
</div>
</div>
<!-- 筛选设置 -->
<div class="config-item">
<h4 class="mb-4">字段筛选:</h4>
<div class="flex flex-wrap justify-between">
<div v-for="field in keys" class="mb-2">
<div refu='input' label_width='8rem' :label='field.label' v:value='filters[field.name]' type='text'></div>
</div>
</div>
</div>
<div class="flex-grow"></div>
<button class="float-right bg-blue-400 rounded px-4 py-2" @click="wrap_apply">应用配置</button>
</body>
<script setup>
keys = [
{name: 'id', label: 'ID', type: 'number'},
{name: 'name', label: '姓名', type: 'string'},
]
opts = {
page: 1,
page_size: 10,
keyword: '',
keywords: {},
sort_by: 'created_at',
order: 'desc',
}
filters = {}
options = () => keys.map(f => {
return {value: f.name, label: f.label}
})
directionOpts = [
{value: 'asc', label: '升序'},
{value: 'desc', label: '降序'}
]
apply = () => {
console.log('当前配置:', opts)
}
wrap_apply = () => {
let tmp = {}
for (let key in filters) {
if (filters[key] !== undefined && filters[key] !== '' && filters[key] !== null) {
tmp[key] = filters[key]
}
}
opts.keywords = tmp
console.log('当前配置:', opts)
apply()
}
</script>
</html>

View File

@ -0,0 +1,28 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
"*.html",
],
theme: {
extend: {
colors: {
vprimary: '#2196f3',
vsecondary: '#ecc94b',
vaccents: '#ff9800',
verror: '#f44336',
vwaring: '#ff5722',
vinfo: '#ffc107',
vsuccess: '#53de58',
vignore: '#d1d5db',
}
},
},
plugins: [],
}

41
vyes-ui/ui/tree.html Normal file
View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<body>
<div class="tree select-none">
<div class="node">
<div @click='onclick'>
<vslot name='name' v='row,depth'>
{{row.name}}
</vslot>
</div>
<div v-if='row.children' class="children transition-all overflow-hidden"
:style="{'max-height':row.expand?'100%':'0'}">
<vslot name='child' v='row,k,depth' v-for="row in row.children">
<div>
{{row.name}}
</div>
</vslot>
</div>
</div>
</div>
</body>
<script setup>
depth = 0
row = {
name: 'demo-dir',
expand: false,
children: [
{name: 'file1'}
]
}
onexpand = () => { }
onclick = () => {
$data.row.expand = !$data.row.expand
if ($data.row.expand) {
$data.onexpand($data.row)
}
}
</script>
</html>