20261007220117

This commit is contained in:
oneao committed 2026-10-07 22:01:17 +08:00
1 parent 9217b3fa42
commit 8b1ec67609
275 files changed
+20026 -49299

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
# @g3soft/ui
G3Soft 通用 UI 组件库(Vue 3 + TypeScript)。
## 安装
```bash
pnpm add @g3soft/ui @g3soft/tokens
```
`vue` 通过 `peerDependencies` 声明,由宿主项目提供,请确保宿主已安装 Vue 3。
## 使用
样式变量来自 `@g3soft/tokens`,在应用入口引入一次:
```ts
import '@g3soft/tokens'
```
按需引入组件:
```vue
<script setup lang="ts">
import { G3Button } from '@g3soft/ui'
</script>
<template>
<G3Button type="primary" @click="handleClick">主要按钮</G3Button>
</template>
```
也可以全量注册:
```ts
import { G3UI } from '@g3soft/ui'
import { createApp } from 'vue'
createApp(App).use(G3UI).mount('#app')
```
## 构建
```bash
pnpm build
```
产物在 `dist/`,包含 ESM 入口与类型声明。发布前会由 `prepublishOnly` 执行 `publint` 校验包结构。
## 约定
- 样式**不写 `scoped`**,统一用 `g3-` 前缀 + BEM 命名,便于消费方覆盖。
- 每个 CSS 变量都带兜底值,保证未引入 tokens 时组件仍可正常显示。
- 组件不写死颜色与尺寸,一律引用 `--g3-*` 变量。
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@g3soft/ui",
"version": "0.0.1",
"description": "G3Soft 通用 UI 组件库",
"license": "UNLICENSED",
"type": "module",
"files": [
"dist",
"README.md"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./style.css": "./dist/style.css",
"./package.json": "./package.json"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "publint"
},
"peerDependencies": {
"vue": "^3.5.0"
},
"dependencies": {
"@g3soft/tokens": "workspace:*"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.0",
"publint": "latest",
"vite": "^8.0.0",
"vite-plugin-dts": "latest",
"vue": "catalog:",
"vue-tsc": "^3.0.0"
},
"publishConfig": {
"access": "restricted"
}
}
@@ -0,0 +1,213 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { ButtonProps } from './types'
/**
* 常用的操作按钮。
*
* 支持 7 种视觉类型、3 种尺寸,以及加载、禁用、撑满、纯图标等状态。
*/
defineOptions({ name: 'G3Button' })
const props = withDefaults(defineProps<ButtonProps>(), {
type: 'default',
size: 'medium',
htmlType: 'button',
disabled: false,
loading: false,
block: false,
iconOnly: false,
})
/**
* 点击按钮时触发。
* 禁用或加载中不会触发。
*/
const emit = defineEmits<{ click: [ev: MouseEvent] }>()
const classes = computed(() => [
'g3-button',
`g3-button--${props.type}`,
`g3-button--${props.size}`,
{
'is-disabled': props.disabled,
'is-loading': props.loading,
'is-block': props.block,
'is-icon-only': props.iconOnly,
},
])
function handleClick(ev: MouseEvent) {
if (props.disabled || props.loading) {
ev.preventDefault()
ev.stopPropagation()
return
}
emit('click', ev)
}
</script>
<template>
<button
:class="classes"
:type="htmlType"
:disabled="disabled || loading"
@click="handleClick"
>
<span v-if="loading" class="g3-button__spinner" aria-hidden="true" />
<span v-if="$slots.icon" class="g3-button__icon"><slot name="icon" /></span>
<span v-if="$slots.default" class="g3-button__content"><slot /></span>
</button>
</template>
<style>
/**
* 不写 scoped:组件库用 g3- 前缀 + BEM 做隔离,便于消费方覆盖样式。
* 每个变量都带兜底值,保证未引入 @g3soft/tokens 时组件仍可正常显示。
*/
.g3-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
box-sizing: border-box;
height: var(--g3-control-height, 32px);
padding: 0 15px;
border: 1px solid var(--g3-border-color, #d9d9d9);
border-radius: var(--g3-radius, 6px);
background: var(--g3-bg-container, #fff);
color: var(--g3-text-1, rgba(0, 0, 0, 0.88));
font-family: inherit;
font-size: var(--g3-font-size, 14px);
line-height: 1;
white-space: nowrap;
cursor: pointer;
user-select: none;
transition: all var(--g3-duration, 0.2s) var(--g3-ease, ease);
}
.g3-button:hover:not(:disabled) {
border-color: var(--g3-color-primary, #1677ff);
color: var(--g3-color-primary, #1677ff);
}
.g3-button:focus-visible {
outline: 2px solid var(--g3-color-primary, #1677ff);
outline-offset: 1px;
}
.g3-button:disabled {
background: var(--g3-bg-disabled, rgba(0, 0, 0, 0.04));
border-color: var(--g3-border-color, #d9d9d9);
color: var(--g3-text-disabled, rgba(0, 0, 0, 0.25));
cursor: not-allowed;
}
/* ---------- 尺寸 ---------- */
.g3-button--small {
height: var(--g3-control-height-sm, 24px);
padding: 0 7px;
font-size: var(--g3-font-size-sm, 12px);
}
.g3-button--large {
height: var(--g3-control-height-lg, 40px);
padding: 0 19px;
font-size: var(--g3-font-size-lg, 16px);
}
.g3-button.is-block {
display: flex;
width: 100%;
}
.g3-button.is-icon-only {
width: var(--g3-control-height, 32px);
padding: 0;
}
.g3-button.is-icon-only.g3-button--small {
width: var(--g3-control-height-sm, 24px);
}
.g3-button.is-icon-only.g3-button--large {
width: var(--g3-control-height-lg, 40px);
}
/* ---------- primary ---------- */
.g3-button--primary {
background: var(--g3-color-primary, #1677ff);
border-color: var(--g3-color-primary, #1677ff);
color: var(--g3-text-inverse, #fff);
}
.g3-button--primary:hover:not(:disabled) {
background: var(--g3-color-primary-hover, #4096ff);
border-color: var(--g3-color-primary-hover, #4096ff);
color: var(--g3-text-inverse, #fff);
}
.g3-button--primary:active:not(:disabled) {
background: var(--g3-color-primary-active, #0958d9);
border-color: var(--g3-color-primary-active, #0958d9);
}
.g3-button--primary:disabled {
background: var(--g3-bg-disabled, rgba(0, 0, 0, 0.04));
border-color: var(--g3-border-color, #d9d9d9);
color: var(--g3-text-disabled, rgba(0, 0, 0, 0.25));
}
/* ---------- success / warning / danger ---------- */
.g3-button--success {
background: var(--g3-color-success, #52c41a);
border-color: var(--g3-color-success, #52c41a);
color: var(--g3-text-inverse, #fff);
}
.g3-button--warning {
background: var(--g3-color-warning, #faad14);
border-color: var(--g3-color-warning, #faad14);
color: var(--g3-text-inverse, #fff);
}
.g3-button--danger {
background: var(--g3-color-danger, #ff4d4f);
border-color: var(--g3-color-danger, #ff4d4f);
color: var(--g3-text-inverse, #fff);
}
/* ---------- text / link ---------- */
.g3-button--text,
.g3-button--link {
height: auto;
padding: 0 4px;
border-color: transparent;
background: transparent;
}
.g3-button--link {
color: var(--g3-color-primary, #1677ff);
}
/* ---------- loading ---------- */
.g3-button.is-loading {
cursor: default;
opacity: 0.65;
}
.g3-button__spinner {
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: g3-button-spin 0.8s linear infinite;
}
@keyframes g3-button-spin {
to {
transform: rotate(360deg);
}
}
</style>
@@ -0,0 +1,2 @@
export { default as G3Button } from './Button.vue'
export type { ButtonProps, ButtonSize, ButtonType } from './types'
@@ -0,0 +1,29 @@
/** 按钮视觉类型 */
export type ButtonType =
| 'default'
| 'primary'
| 'success'
| 'warning'
| 'danger'
| 'text'
| 'link'
/** 按钮尺寸 */
export type ButtonSize = 'small' | 'medium' | 'large'
export interface ButtonProps {
/** 视觉类型 */
type?: ButtonType
/** 尺寸 */
size?: ButtonSize
/** 原生 button 的 type 属性 */
htmlType?: 'button' | 'submit' | 'reset'
/** 是否禁用 */
disabled?: boolean
/** 是否加载中:同时禁止点击并展示加载图标 */
loading?: boolean
/** 是否撑满父容器宽度 */
block?: boolean
/** 是否仅图标:渲染为正方形按钮 */
iconOnly?: boolean
}
+21
View File
@@ -0,0 +1,21 @@
import type { App, Plugin } from 'vue'
import G3Button from './button/Button.vue'
export { G3Button }
export type { ButtonProps, ButtonSize, ButtonType } from './button/types'
/**
* 可选的全量注册插件。
* 按需 import 是主路径;这里只是为不熟悉按需引入的团队提供便利入口。
*/
const components = { G3Button }
export const G3UI: Plugin = {
install(app: App) {
Object.entries(components).forEach(([name, component]) => {
app.component(name, component)
})
},
}
export default G3UI
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"strict": true,
"noImplicitOverride": true,
"esModuleInterop": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
}
@@ -0,0 +1,40 @@
import { fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
/**
* 组件库构建:产出 ESM + 类型声明。
*
* 关键点:
* - vue 必须外置(external),否则包里会打进一份 Vue,消费方出现双实例
* - preserveModules 保留源码结构,消费方能按需 tree-shaking
* - vite-plugin-dts 生成 dist/index.d.ts,配合 package.json 的 exports.types
*/
export default defineConfig({
plugins: [
vue(),
dts({
// 只产出声明文件,JS 交给 vite 本体
include: ['src'],
outDir: 'dist',
rollupTypes: true,
}),
],
build: {
lib: {
entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)),
formats: ['es'],
fileName: () => 'index.js',
},
rollupOptions: {
external: ['vue'],
output: {
preserveModules: true,
preserveModulesRoot: 'src',
entryFileNames: '[name].js',
assetFileNames: 'style.css',
},
},
},
})