添加了编译流程;

This commit is contained in:
袁涛
2025-08-13 12:33:49 +08:00
parent 3f9dbff91e
commit 9017753f56
6 changed files with 2809 additions and 162 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

25
gulpfile.js Normal file
View File

@@ -0,0 +1,25 @@
const gulp = require('gulp')
const chinese2unicode = require('gulp-chinese2unicode')
const gulpTerser = require('gulp-terser')
// 构建
gulp.task('minify', async () => {
console.log('开始构建...')
gulp
.src('index.js')
.pipe(
gulpTerser({
mangle: {
properties: true,
},
format: {
ascii_only: true,
},
})
)
.pipe(chinese2unicode())
.on('data', function () {
console.log('构建完成!')
})
.pipe(gulp.dest('./src/'))
})

378
index.js
View File

@@ -3,113 +3,139 @@ class API {
constructor(options = {}) {
// 接口基础地址
this.baseUrl = ''
// 请求对象
this.xhr = new XMLHttpRequest()
// 合并配置
Object.assign(this, options)
}
// 创建新的 XMLHttpRequest 实例
createXHR() {
return new XMLHttpRequest()
}
// 设置请求头
setHeaders(headers = {}) {
const { xhr } = this
setHeaders(xhr, headers = {}) {
for (let key in headers) {
xhr.setRequestHeader(key, headers[key])
}
}
// 格式化数据
formatData(data) {
let parseData = null
if (typeof data !== 'string') return data
try {
parseData = JSON.parse(data)
} catch {
parseData = data
return JSON.parse(data)
} catch (e) {
return data
}
return parseData
}
// 中断请求
abort(callback = null) {
this.xhr.abort()
callback && callback()
abort(xhr, callback = null) {
if (xhr) {
xhr.abort()
callback && callback()
}
}
// 处理响应
handleResponse(xhr, callback, success, fail, error) {
try {
const response = this.formatData(xhr.responseText)
if (xhr.status >= 200 && xhr.status < 300) {
callback && callback(response)
success && success(response)
} else {
const err = new Error(`HTTP Error ${xhr.status}`)
error && error(err)
fail && fail(err)
}
} catch (e) {
error && error(e)
fail && fail(e)
}
}
// get请求
get(options = {}) {
const { baseUrl, xhr, formatData } = this
// 默认配置
const defaultOption = {
// 请求地址
url: '',
// 请求头
headers: null,
// 成功回调
callback: null,
success: null,
// 失败回调
fail: null,
error: null,
// 是否异步
async: true,
}
Object.assign(defaultOption, options)
const { url, callback, success, fail, error, async, headers } = defaultOption
xhr.open('GET', `${baseUrl}${url}`, async)
headers && this.setHeaders(headers)
const config = Object.assign({}, defaultOption, options)
const { url, callback, success, fail, error, async, headers } = config
const xhr = this.createXHR()
xhr.addEventListener('error', function (err) {
xhr.open('GET', `${this.baseUrl}${url}`, async)
if (headers) {
this.setHeaders(xhr, headers)
}
xhr.addEventListener('error', err => {
error && error(err)
fail && fail(err)
})
xhr.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
callback && callback(formatData(this.responseText))
success && success(formatData(this.responseText))
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
this.handleResponse(xhr, callback, success, fail, error)
}
}
xhr.send()
return xhr
}
// post请求
post(options = {}) {
const { baseUrl, xhr, formatData } = this
// 默认配置
const defaultOption = {
// 请求地址
url: '',
// 请求数据
data: null,
// 请求头
headers: null,
// 进度回调
progress: null,
// 成功回调
callback: null,
success: null,
// 失败回调
fail: null,
error: null,
// 是否异步
async: true,
}
Object.assign(defaultOption, options)
const { url, data, progress, callback, success, error, fail, async, headers } = defaultOption
xhr.open('POST', `${baseUrl}${url}`, async)
headers && this.setHeaders(headers)
const config = Object.assign({}, defaultOption, options)
const { url, data, progress, callback, success, error, fail, async, headers } = config
const xhr = this.createXHR()
xhr.addEventListener('progress', function (evt) {
const { response } = evt.target
progress && progress(response)
})
xhr.open('POST', `${this.baseUrl}${url}`, async)
xhr.addEventListener('error', function (err) {
if (headers) {
this.setHeaders(xhr, headers)
}
if (progress) {
xhr.upload.addEventListener('progress', evt => {
progress(evt)
})
}
xhr.addEventListener('error', err => {
error && error(err)
fail && fail(err)
})
xhr.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
callback && callback(formatData(this.responseText))
success && success(formatData(this.responseText))
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
this.handleResponse(xhr, callback, success, fail, error)
}
}
xhr.send(data)
return xhr
}
}
@@ -117,16 +143,21 @@ class API {
class UI {
constructor(registry = null) {
const that = this
// 属性转换方法
const parseBoolean = value => {
return value !== null && value !== 'false'
}
// 搜索框
this.Search = class Input extends HTMLElement {
// 可用属性
static observedAttributes = ['value', 'placeholder', 'disabled', 'max', 'wrap']
static get observedAttributes() {
return ['value', 'disabled']
return ['value', 'placeholder', 'disabled', 'max', 'wrap']
}
// 构造函数
constructor() {
super()
this.attachShadow({ mode: 'open' })
// 输入框
this.input = null
// 输入框值
@@ -147,38 +178,37 @@ class UI {
}
// 属性变化
attributeChangedCallback(name, oldValue, newValue) {
this[name] = newValue
if (name == 'disabled') {
this.input.querySelector('.search-input').setAttribute('disabled', eval(this.disabled))
this.input.querySelector('.search-input').setAttribute('contenteditable', !eval(this.disabled))
}
if (this[name] === newValue) return
if (name == 'value') {
this.input.querySelector('.search-input').value = newValue
this[name] = name === 'disabled' || name === 'wrap' ? parseBoolean(newValue) : newValue
if (!this.input) return
switch (name) {
case 'disabled':
const inputEl = this.shadowRoot.querySelector('.search-input')
if (inputEl) {
inputEl.toggleAttribute('disabled', this.disabled)
inputEl.contentEditable = !this.disabled
const clearBtn = this.shadowRoot.querySelector('.search-clear')
if (clearBtn) {
clearBtn.classList.toggle('visible')
}
}
break
case 'value':
const searchInput = this.shadowRoot.querySelector('.search-input')
if (searchInput) {
searchInput.textContent = newValue
}
break
}
}
// 初始化
init() {
const { value, placeholder, disabled, wrap } = this
const shadow = this.attachShadow({ mode: 'open' })
const template = document.createElement('template')
const style = document.createElement('style')
const inputBox = document.createElement('div')
inputBox.className = inputBox.part = 'search'
const input = document.createElement('div')
input.className = input.part = 'search-input'
input.setAttribute('placeholder', placeholder)
input.setAttribute('disabled', disabled)
input.contentEditable = true
const prefix = document.createElement('slot')
prefix.name = 'prefix'
const append = document.createElement('slot')
append.name = 'append'
const clear = document.createElement('button')
clear.className = clear.part = 'search-clear'
clear.innerHTML = that.Icon.clear
style.innerHTML = `
const shadow = this.shadowRoot
shadow.innerHTML = `
<style>
.search {
--background-color: #fff;
--border-color: #ccc;
@@ -190,57 +220,62 @@ class UI {
--min-width: 150px;
--gap: .5em;
--scrollbar-color: var(--border-color);
display: flex;
align-items: center;
font-size: 1em;
position: relative;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--padding);
box-sizing: border-box;
width: 100%;
min-width: var(--min-width);
gap: var(--gap);
cursor: text;
display: flex;
align-items: center;
font-size: 1em;
position: relative;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--padding);
box-sizing: border-box;
width: 100%;
min-width: var(--min-width);
gap: var(--gap);
cursor: text;
}
.search-input {
width: 100%;
font-size: 1em;
outline: none;
transition: all .2s;
resize: none;
width: 100%;
font-size: 1em;
outline: none;
transition: all .2s;
resize: none;
min-height: 1em;
overflow: hidden;
}
.search-input::-webkit-scrollbar {
width: 0;
height: .1em;
width: 0;
height: .1em;
}
.search-input::-webkit-scrollbar-thumb {
background: var(--scrollbar-color);
background: var(--scrollbar-color);
}
.search-input:empty::before {
content: attr(placeholder);
color: var(--placeholder-color);
content: attr(placeholder);
color: var(--placeholder-color);
}
.search-input:focus::before {
display: none;
display: none;
}
.search:focus-within {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.search-input:focus-within + .search-clear {
opacity: 1;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.search-clear {
cursor: pointer;
color: var(--clear-color);
width: 20px;
height: 20px;
background: none;
border: none;
opacity: 0;
transition: all .3s;
position: relative;
cursor: pointer;
color: var(--clear-color);
width: 20px;
height: 20px;
background: none;
border: none;
opacity: 0;
transition: all .3s;
position: relative;
flex-shrink: 0;
pointer-events: none;
}
.search-clear.visible {
opacity: 1;
pointer-events: auto;
}
.search-clear svg {
fill: var(--clear-color);
@@ -249,80 +284,99 @@ class UI {
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
width: 2em;
height: 2em;
width: 100%;
height: 100%;
}
`
</style>
<div class="search" part="search">
<slot name="prefix"></slot>
<div class="search-input" part="search-input"
placeholder="${this.placeholder}"
${this.disabled ? 'disabled' : ''}
contenteditable="${!this.disabled}">
${this.value}
</div>
<button class="search-clear" part="search-clear">
${that.Icon.clear}
</button>
<slot name="append"></slot>
</div>
`
inputBox.appendChild(prefix)
inputBox.appendChild(input)
inputBox.appendChild(clear)
inputBox.appendChild(append)
this.input = shadow.querySelector('.search-input')
this.input.textContent = ''
this.value = ''
// 初始清除按钮状态
this.updateClearButton()
if (wrap || wrap === '') {
input.style.whiteSpace = 'pre-wrap'
input.style.overflowY = 'auto'
// 换行处理
if (this.wrap) {
this.input.style.whiteSpace = 'pre-wrap'
this.input.style.overflowY = 'auto'
} else {
input.style.whiteSpace = 'nowrap'
input.style.overflowX = 'auto'
this.input.style.whiteSpace = 'nowrap'
this.input.style.overflowX = 'auto'
}
input.innerText = value.trim()
template.content.appendChild(style)
template.content.appendChild(inputBox)
shadow.appendChild(template.content.cloneNode(true))
this.input = shadow
}
// 清空输入框
updateClearButton() {
const clearBtn = this.shadowRoot.querySelector('.search-clear')
if (clearBtn) {
clearBtn.classList.toggle('visible', this.value.length > 0)
}
}
clear() {
this.input.querySelector('.search-input').innerText = this.value = ''
this.input.textContent = ''
this.value = ''
this.updateClearButton()
this.dispatchEvent(new CustomEvent('change', { detail: this.value }))
}
// 绑定事件
bindEvent() {
const { input, max } = this
// 清空输入框
input.querySelector('.search-clear').addEventListener('click', e => {
const clearBtn = this.shadowRoot.querySelector('.search-clear')
const inputEl = this.input
clearBtn.addEventListener('click', e => {
e.preventDefault()
input.querySelector('.search-input').innerText = this.value = ''
this.clear()
})
// 输入框输入事件
input.querySelector('.search-input').addEventListener('input', e => {
e.preventDefault()
// 过滤换行符
this.value = e.target.innerText.replace(/[\n]/g, '')
inputEl.addEventListener('input', e => {
let newValue = inputEl.textContent.replace(/[\n]/g, '')
if (this.value.length < max) {
e.target.innerText = this.value
} else {
e.target.innerText = this.value = this.value.slice(0, max)
if (newValue.length > this.max) {
newValue = newValue.substring(0, this.max)
inputEl.textContent = newValue
}
// 移动光标到最后
const range = document.createRange()
const selection = window.getSelection()
range.selectNodeContents(e.target)
range.collapse(false)
selection.removeAllRanges()
this.value = newValue
this.updateClearButton()
// 移动光标到末尾
const range = document.createRange()
const sel = window.getSelection()
range.selectNodeContents(inputEl)
range.collapse(false)
sel.removeAllRanges()
sel.addRange(range)
selection.addRange(range)
e.target.focus()
this.dispatchEvent(new CustomEvent('change', { detail: this.value }))
})
// 输入框回车事件
input.querySelector('.search-input').addEventListener('keydown', e => {
inputEl.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault()
this.dispatchEvent(new CustomEvent('enter', { detail: this.value }))
}
})
// 输入框失焦事件
input.querySelector('.search-input').addEventListener('blur', () => this.dispatchEvent(new CustomEvent('blur')))
inputEl.addEventListener('blur', () => {
this.dispatchEvent(new CustomEvent('blur'))
})
// 输入框聚焦事件
input.querySelector('.search-input').addEventListener('focus', () => this.dispatchEvent(new CustomEvent('focus')))
inputEl.addEventListener('focus', () => {
this.dispatchEvent(new CustomEvent('focus'))
})
}
}
// 下拉框
@@ -330,7 +384,7 @@ class UI {
// 可用属性
static observedAttributes = ['placeholder', 'list', 'value', 'current', 'disabled', 'focus']
static get observedAttributes() {
return ['value', 'current', 'disabled', 'focus']
return ['placeholder', 'list', 'value', 'current', 'disabled', 'focus']
}
// 构造函数
constructor() {

2548
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "pure-component",
"version": "1.0.0",
"description": "一个功能强大的UI组件库和一个轻量级的API请求工具可以帮助开发者快速构建现代化的Web应用界面。",
"main": "index.js",
"scripts": {
"build": "gulp minify"
},
"author": "",
"license": "ISC",
"dependencies": {
"gulp": "^5.0.1",
"gulp-chinese2unicode": "^1.0.1",
"gulp-terser": "^2.1.0",
"gulp-uglify": "^3.0.2",
"terser": "^5.43.1"
}
}

1
src/index.js Normal file

File diff suppressed because one or more lines are too long