feat: 写组件

1. 全局config
2. logo组件
3. 实时预览
4. scss复用
未来
1. 继续开发组件
This commit is contained in:
HeXiaoLong:Suanier 2025-05-20 17:56:25 +08:00
parent db1c5c4522
commit 9170d7efd6
23 changed files with 545 additions and 3101 deletions

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="zh">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />

12
index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ni UI Preview</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/preview/main.ts"></script>
</body>
</html>

12
indexg.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ni UI Preview: Global</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/preview/globalMain.ts"></script>
</body>
</html>

3099
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,12 @@
"name": "ni", "name": "ni",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",
"keywords": ["Vue3", "Vue UI", "Ni UI", "Ni"], "keywords": [
"Vue3",
"Vue UI",
"Ni UI",
"Ni"
],
"license": "MIT", "license": "MIT",
"homepage": "http://uair.cc", "homepage": "http://uair.cc",
"author": "星撰玉衡", "author": "星撰玉衡",
@ -15,30 +20,30 @@
"main": "./dist/ni.umd.js", "main": "./dist/ni.umd.js",
"module": "./dist/ni.es.js", "module": "./dist/ni.es.js",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"sideEffects": [ "sideEffects": [
"**/*.css", "**/*.css",
"**/*.scss" "**/*.scss"
], ],
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:g": "vite --open indexg.html",
"build": "vite build --config vite.config.ts && tsc -p tsconfig.build.json", "build": "vite build --config vite.config.ts && tsc -p tsconfig.build.json",
"docs:dev": "vitepress dev docs", "docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs", "docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs" "docs:preview": "vitepress preview docs"
}, },
"dependencies": { "dependencies": {
"vue": "^3.3.4" "vue": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.5.9",
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^4.2.3",
"@vue/tsconfig": "^0.4.0", "@vue/tsconfig": "^0.4.0",
"sass": "^1.89.0",
"typescript": "~5.0.4", "typescript": "~5.0.4",
"vite": "^4.4.9", "vite": "^4.4.9",
"vue-tsc": "^1.8.8",
"sass": "^1.66.1",
"vitepress": "^1.0.0-rc.44", "vitepress": "^1.0.0-rc.44",
"@types/node": "^20.5.9" "vue-tsc": "^1.8.8"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.0.0" "vue": "^3.0.0"

12
preview/App.vue Normal file
View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import {NiButton, NiLogo} from 'ni'
</script>
<template>
<NiButton>按钮</NiButton>
<NiLogo type="primary" size="large" />
</template>
<style scoped>
</style>

16
preview/globalApp.vue Normal file
View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import { useNiConfig, setNiConfig } from 'ni'
setNiConfig({
})
</script>
<template>
<NiConfigProvider>
<NiButton>按钮</NiButton>
<NiLogo type="primary" size="large" />
</NiConfigProvider>
</template>
<style scoped>
</style>

7
preview/globalMain.ts Normal file
View File

@ -0,0 +1,7 @@
import { createApp } from 'vue'
import ni from 'ni'
import App from './globalApp.vue'
ni.setNiConfig({
logoName: ['星', '撰']
})
createApp(App).use(ni).mount('#app')

3
preview/main.ts Normal file
View File

@ -0,0 +1,3 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View File

@ -0,0 +1,23 @@
<template>
<slot></slot>
</template>
<script lang="ts" setup>
import { provide, watch } from 'vue'
import type { NiConfig } from './types'
import defaultConfig from './defaultConfig'
const props = defineProps<{
config?: Partial<NiConfig>
}>()
defaultConfig
//
watch(() => props.config, (newConfig) => {
Object.assign(defaultConfig, newConfig)
})
//
provide('ni-config', defaultConfig)
</script>

View File

@ -0,0 +1,17 @@
import { reactive } from "vue"
import { NiConfig } from "./types"
const defaultConfig = reactive<NiConfig>({
logoName: ['星', '撰'],
size: 'default',
zIndex: 2000,
namespace: 'ni',
})
export default defaultConfig
function setNiConfig(config: Partial<NiConfig>) {
Object.assign(defaultConfig, config)
}
export { setNiConfig }

View File

@ -0,0 +1,5 @@
import NiConfigProvider from './ConfigProvider.vue'
export * from './types'
export default NiConfigProvider
export { useNiConfig } from './useConfig'
export { setNiConfig } from './defaultConfig'

View File

@ -0,0 +1,10 @@
export interface NiConfig {
logoName?: [string, string]
name?: string
size?: 'small' | 'default' | 'large'
zIndex?: number
namespace?: string
// 可以添加更多全局配置项
}
export type NiConfigKey = keyof NiConfig

View File

@ -0,0 +1,18 @@
import { inject, computed, type Ref, Reactive } from 'vue'
import type { NiConfig } from './types'
export function useNiConfig() {
const config = inject<Reactive<NiConfig>>('ni-config')
if (!config) {
throw new Error('useConfig must be used within NiConfigProvider')
}
return {
config: computed(() => config),
get: (key: keyof NiConfig) => config[key],
set: (key: keyof NiConfig, value: any) => {
config[key] = value
}
}
}

View File

@ -1,9 +1,19 @@
import NiButton from './button' import NiButton from './button'
import NiLogo from './logo'
import NiConfigProvider, { useNiConfig } from './config'
import { setNiConfig } from './config/defaultConfig'
export { export {
NiButton, NiButton,
NiLogo,
NiConfigProvider,
useNiConfig,
setNiConfig,
} }
export default { export default {
NiButton, NiButton,
NiLogo,
NiConfigProvider,
useNiConfig,
setNiConfig,
} }

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import { computed } from 'vue';
import { logoProps } from './types';
import { type NiConfig, useNiConfig } from '~/config';
const niConfig = useNiConfig()
const props = defineProps(logoProps);
// * logo
const logoContent = computed<string[]>(() => {
const name = props.logoName || niConfig.get('logoName') || ['N', 'i']
return Array.isArray(name) ? name : [String(name)[0], String(name)[1]]
})
console.log(props, niConfig, logoContent);
</script>
<template>
<div class="ni-logo">
<div class="content">
<div class="first">{{ logoContent[0] }}</div>
<div class="second">{{ logoContent[1] }}</div>
</div>
<div class="mask">
<div class="first"><div class="maskContainer"><div class="maskContent">{{ logoContent[0] }}</div></div></div>
<div class="second"><div class="maskContainer"><div class="maskContent">{{ logoContent[1] }}</div></div></div>
</div>
</div>
</template>
<style scoped lang="scss">
@use '@/styles/style.scss' as niStyle;
.ni-logo{
// logo
// * logo
--ni-logo-width: 6em;
// * logo
--ni-logo-height: 3em;
// * logo
--ni-logo-mask-background-color: #111;
// * logo
--ni-logo-mask-font-color: #fff;
// = logo
position: relative;
width: var(--ni-logo-width);
height: var(--ni-logo-height);
user-select: none;
.content,.mask{
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
&.content > div{
flex: 1;
@extend .AllCenter;
}
}
.content{
position: relative;
}
.mask{
position: absolute;
top: 0;
left: 0;
& > div.first, & > div.second{
position: relative;
width: 50%;
overflow: hidden;
height: 100%;
&.first{
}
&.second{
& > div.maskContainer{
background-color: var(--ni-logo-mask-background-color);
color: var(--ni-logo-mask-font-color);
@extend .LogoAnimation;
}
}
& > div.maskContainer{
width: 0;
height: 100%;
overflow: hidden;
& > div.maskContent{
width: calc(var(--ni-logo-width) / 2);
height: 100%;
@extend .AllCenter;
}
}
}
}
}
</style>

View File

@ -0,0 +1,3 @@
import NiLogo from './Logo.vue'
export { NiLogo }
export default NiLogo

View File

@ -0,0 +1,27 @@
import { ExtractPropTypes, PropType } from 'vue'
export const logoProps = {
logoName: {
type: Array as PropType<string[]>,
},
type: {
type: String as PropType<'default' | 'primary'>,
default: 'default',
validator: (value: string) => {
return ['default', 'primary'].includes(value)
}
},
size: {
type: String as PropType<'small' | 'medium' | 'large'>,
default: 'medium',
validator: (value: string) => {
return ['small', 'medium', 'large'].includes(value)
}
},
disabled: {
type: Boolean,
default: false
}
} as const
export type LogoProps = ExtractPropTypes<typeof logoProps>

View File

@ -9,13 +9,18 @@ export const version = '0.0.1'
// 导出安装方法 // 导出安装方法
export const install = (app: App) => { export const install = (app: App) => {
Object.entries(components).forEach(([path, module]: [string, any]) => { Object.entries(components).forEach(([name, module]: [string, any]) => {
app.component(path, module) if(name === 'default' || name === 'useConfig') return
// 检查是否为Vue组件具有render函数或template属性
if (module && (typeof module.render === 'function' || module.template)) {
app.component(name, module)
}
}) })
} }
// 默认导出 // 默认导出
export default { export default {
version, version,
install install,
setNiConfig: components.setNiConfig
} }

185
src/styles/style.scss Normal file
View File

@ -0,0 +1,185 @@
@forward 'variables';
// MIXINS
// 弹性布局
@mixin flex($direction: row, $justify: center, $align: center) {
display: flex;
flex-direction: $direction;
justify-content: $justify;
align-items: $align;
}
// 绝对定位
@mixin absolute($top: 0, $right: 0, $bottom: 0, $left: 0) {
position: absolute;
top: $top;
right: $right;
bottom: $bottom;
left: $left;
}
// 文本溢出省略
@mixin text-ellipsis($lines: 1) {
@if $lines == 1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} @else {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
// 清除浮动
@mixin clearfix {
&::after {
content: '';
display: table;
clear: both;
}
}
// 响应式断点
@mixin respond-to($breakpoint) {
@if $breakpoint == 'sm' {
@media (min-width: 576px) { @content; }
} @else if $breakpoint == 'md' {
@media (min-width: 768px) { @content; }
} @else if $breakpoint == 'lg' {
@media (min-width: 992px) { @content; }
} @else if $breakpoint == 'xl' {
@media (min-width: 1200px) { @content; }
}
}
// 动画相关
@mixin animation($name, $duration: 1s, $timing: ease, $delay: 0s, $iteration: 1, $direction: normal, $fill: forwards) {
animation: $name $duration $timing $delay $iteration $direction $fill;
/*
* 1. $name必需参数
* 动画名称
* 必须与 @keyframes 定义的名称匹配
* 例如fade-in, slide-in-right
* 2. $duration可选参数默认值1s
* 动画持续时间
* 可选值
* 时间值0.5s, 1s, 2s
* 毫秒值500ms, 1000ms
* 3. $timing可选参数默认值ease
* 动画的时间函数
* 可选值
* linear匀速
* ease缓入缓出默认
* ease-in缓入
* ease-out缓出
* ease-in-out缓入缓出
* cubic-bezier(n,n,n,n)自定义贝塞尔曲线
* steps(n)分步动画
* 4. $delay可选参数默认值0s
* 动画开始前的延迟时间
* 可选值
* 时间值0.5s, 1s
* 毫秒值500ms, 1000ms
* 负值提前开始动画
* 5. $iteration可选参数默认值1
* 动画重复次数
* 可选值
* 数字1, 2, 3
* infinite无限循环
* 6. $direction可选参数默认值normal
* 动画方向
* 可选值
* normal正常播放
* reverse反向播放
* alternate交替播放正向->反向
* alternate-reverse交替反向播放反向->正向
* 7. $fill可选参数默认值forwards
* 动画填充模式
* 可选值
* forwards保持最后一帧
* backwards应用第一帧
* both同时应用 forwards backwards
* none不应用任何填充
*/
}
@mixin keyframes($name) {
@keyframes #{$name} {
@content;
}
}
// 常用动画关键帧
@include keyframes(fade-in) {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@include keyframes(fade-out) {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@include keyframes(slide-in-right) {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@include keyframes(slide-out-right) {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
// 动画类
.fade-in {
@include animation(fade-in);
}
.fade-out {
@include animation(fade-out);
}
.slide-in-right {
@include animation(slide-in-right);
}
.slide-out-right {
@include animation(slide-out-right);
}
// PUBLIC
// * 全屏居中
.AllCenter{
@include flex(row, center, center);
}
// * Logo动画
@keyframes stepLogo {
0%{
width: 0;
}
100%{
width: 100%;
}
}
.LogoAnimation{
animation: stepLogo .5s linear forwards;
}

43
src/styles/variables.scss Normal file
View File

@ -0,0 +1,43 @@
// 颜色变量
$primary-color: #409eff;
$success-color: #67c23a;
$warning-color: #e6a23c;
$danger-color: #f56c6c;
$info-color: #909399;
// 文字颜色
$text-primary: #303133;
$text-regular: #606266;
$text-secondary: #909399;
$text-placeholder: #c0c4cc;
// 边框颜色
$border-color-base: #dcdfe6;
$border-color-light: #e4e7ed;
$border-color-lighter: #ebeef5;
$border-color-extra-light: #f2f6fc;
// 背景颜色
$background-color-base: #f5f7fa;
// 断点
$breakpoints: (
'sm': 576px,
'md': 768px,
'lg': 992px,
'xl': 1200px
);
// 字体大小
$font-size-extra-large: 20px;
$font-size-large: 18px;
$font-size-medium: 16px;
$font-size-base: 14px;
$font-size-small: 13px;
$font-size-extra-small: 12px;
// 圆角
$border-radius-base: 4px;
$border-radius-small: 2px;
$border-radius-large: 8px;
$border-radius-circle: 50%;

View File

@ -18,9 +18,11 @@
"types": ["node", "vite/client", "vue"], "types": ["node", "vite/client", "vue"],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"],
"ni": ["src/index.ts"],
"~/*": ["src/components/*"],
} }
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "preview/**/*.ts", "preview/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -21,5 +21,20 @@ export default defineConfig({
} }
} }
} }
},
css: {
preprocessorOptions: {
scss: {
// api: "modern-compiler", // Element Plus 中的解决办法
silenceDeprecations: ['legacy-js-api']
},
},
},
resolve: {
alias: {
'ni': resolve(__dirname, 'src/index.ts'),
'~': resolve(__dirname, 'src/components'),
'@': resolve(__dirname, 'src')
}
} }
}) })