20261007220117
This commit is contained in:
1 parent
9217b3fa42
commit
8b1ec67609
275 files changed
+20026
-49299
No files matched your search
@@ -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-*` 变量。
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user