diff --git a/code/base-project/base-vue/.vscode/vue3.code-snippets b/code/base-project/base-vue/.vscode/vue3.code-snippets index 8a6c6f5c..2e706203 100644 --- a/code/base-project/base-vue/.vscode/vue3.code-snippets +++ b/code/base-project/base-vue/.vscode/vue3.code-snippets @@ -1,39 +1,59 @@ { - "Print AoDesignComponent TSX": { - "scope": "typescriptreact", - "prefix": "atsx", - "body": [ - "import { defineComponent } from 'vue';", - "import { getPrefixCls } from '../_utils/getPrefixCls';", - "", - "export default defineComponent({", - " name: '${1:AoComponent}',", - " props: {", - " },", - " emits: [],", - " setup(props, { slots, emit }) {", - " const prefixCls = getPrefixCls('${2:component}');", - " return () => (", - "
", - " {slots.default?.()}", - "
", - " );", - " }", - "})" - ] - }, - "Print AoDesignComponent SCSS": { - "scope": "scss", - "prefix": "ascss", - "body": [ - "@use '../_styles/_variables.scss' as *;", - "@use 'sass:color';", - "", - "$${1:component}-prefix: #{$prefix}-${1:component};", - "", - ".#{${1:component}-prefix} {", - "}" - ] - } + "Vue3 SFC with SCSS": { + "prefix": "v3", + "scope": "vue", + "body": [ + "", + "", + "", + "", + "" + ], + "description": "Vue 3 SFC template with SCSS (script setup, TypeScript)" + }, + "Print AoDesignComponent TSX": { + "scope": "typescriptreact", + "prefix": "atsx", + "body": [ + "import { defineComponent } from 'vue';", + "import { getPrefixCls } from '../_utils/getPrefixCls';", + "", + "export default defineComponent({", + " name: '${1:AoComponent}',", + " props: {", + " },", + " emits: [],", + " setup(props, { slots, emit }) {", + " const prefixCls = getPrefixCls('${2:component}');", + " return () => (", + "
", + " {slots.default?.()}", + "
", + " );", + " }", + "})" + ] + }, + "Print AoDesignComponent SCSS": { + "scope": "scss", + "prefix": "ascss", + "body": [ + "@use '../_styles/_variables.scss' as *;", + "@use 'sass:color';", + "", + "$${1:component}-prefix: #{$prefix}-${1:component};", + "", + ".#{${1:component}-prefix} {", + "}" + ] } +} \ No newline at end of file diff --git a/code/base-project/base-vue/packages/ao-design/package.json b/code/base-project/base-vue/packages/ao-design/package.json index 7ce4eb17..7cdd455a 100644 --- a/code/base-project/base-vue/packages/ao-design/package.json +++ b/code/base-project/base-vue/packages/ao-design/package.json @@ -10,6 +10,10 @@ "*": ["./src/*"] } }, + "files": [ + "src/**/*.scss", + "src/**/*.ts" + ], "peerDependencies": { "@iconify/vue": "5.0.0", "vue": "^3.3.0" diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/_variables.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/_variables.scss index d5ed3ea6..eec18171 100644 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/_variables.scss +++ b/code/base-project/base-vue/packages/ao-design/src/_styles/_variables.scss @@ -84,3 +84,9 @@ $shadow-surface: var(--ao-shadow-surface); $shadow-popup: var(--ao-shadow-popup); // 上层投影(警示/弹窗)使用的组件包括:全局提示 / 消息通知 $shadow-overlay: var(--ao-shadow-overlay); + +// 内投影 +$shadow-inset-top: var(--ao-shadow-inset-top); +$shadow-inset-right: var(--ao-shadow-inset-right); +$shadow-inset-bottom: var(--ao-shadow-inset-bottom); +$shadow-inset-left: var(--ao-shadow-inset-left); diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/index.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/index.scss index 55e3b730..e81ec056 100644 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/index.scss +++ b/code/base-project/base-vue/packages/ao-design/src/_styles/index.scss @@ -1 +1,2 @@ @use './theme/index.scss' as *; + diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_dark.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_dark.scss index 4e7ab70c..46ebd5f4 100644 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_dark.scss +++ b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_dark.scss @@ -63,4 +63,10 @@ --ao-shadow-popup: 0 8px 10px rgba(0, 0, 0, 12%), 0 3px 14px rgba(0, 0, 0, 10%), 0 5px 5px rgba(0, 0, 0, 16%); // 上层投影(警示/弹窗)使用的组件包括:全局提示 / 消息通知 --ao-shadow-overlay: 0 16px 24px rgba(0, 0, 0, 14%), 0 6px 30px rgba(0, 0, 0, 12%), 0 8px 10px rgba(0, 0, 0, 20%); + + // 内投影 + --ao-shadow-inset-top: inset 0 0.5px 0 #5e5e5e; + --ao-shadow-inset-right: inset 0.5px 0 0 #5e5e5e; + --ao-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e; + --ao-shadow-inset-left: inset -0.5px 0 0 #5e5e5e; } diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_light.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_light.scss index 227ae15d..1167b2b1 100644 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_light.scss +++ b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_light.scss @@ -66,4 +66,10 @@ // 上层投影(警示/弹窗)使用的组件包括:全局提示 / 消息通知 --ao-shadow-overlay: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%); + + // 内投影 + --ao-shadow-inset-top: inset 0 0.5px 0 #dcdcdc; + --ao-shadow-inset-right: inset 0.5px 0 0 #dcdcdc; + --ao-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc; + --ao-shadow-inset-left: inset -0.5px 0 0 #dcdcdc; } diff --git a/code/base-project/base-vue/packages/ao-design/src/button/index.tsx b/code/base-project/base-vue/packages/ao-design/src/button/index.tsx index 0039d13e..6614d149 100644 --- a/code/base-project/base-vue/packages/ao-design/src/button/index.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/button/index.tsx @@ -1,7 +1,6 @@ import { computed, defineComponent } from 'vue' import { getPrefixCls } from '../_utils/getPrefixCls' import Icon from '../icon' -import './index.scss' export default defineComponent({ name: 'Button', diff --git a/code/base-project/base-vue/packages/ao-design/src/icon/index.scss b/code/base-project/base-vue/packages/ao-design/src/icon/index.scss index a2d46b96..f93bdc4f 100644 --- a/code/base-project/base-vue/packages/ao-design/src/icon/index.scss +++ b/code/base-project/base-vue/packages/ao-design/src/icon/index.scss @@ -6,6 +6,12 @@ $icon-prefix: #{$prefix}-icon; display: inline-flex; align-items: center; justify-content: center; + width: 1em; + height: 1em; + svg { + width: 1em; + height: 1em; + } &-spin { animation: spin 1s linear infinite; --rotate-angle: 360deg; diff --git a/code/base-project/base-vue/packages/ao-design/src/icon/index.tsx b/code/base-project/base-vue/packages/ao-design/src/icon/index.tsx index 87880a45..dca629d8 100644 --- a/code/base-project/base-vue/packages/ao-design/src/icon/index.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/icon/index.tsx @@ -2,30 +2,39 @@ import type { PropType } from 'vue' import { Icon } from '@iconify/vue' import { defineComponent } from 'vue' import { getPrefixCls } from '../_utils/getPrefixCls' -import './index.scss' export default defineComponent({ name: 'Icon', props: { icon: { type: String as PropType, required: true }, - size: { type: Number, default: 16 }, + size: { type: Number }, color: { type: String, default: 'currentColor' }, spin: { type: Boolean, default: false }, }, - setup(props) { + setup(props, { slots }) { const prefixCls = getPrefixCls('icon') - + const fontStyle = { + ...(props.size ? { fontSize: `${props.size}px` } : {}), // size 存在才设置 + verticalAlign: 'middle', + } return () => ( - + {slots.default + ? ( + slots.default() + ) + : props.icon + ? ( + + ) + : null} ) }, diff --git a/code/base-project/base-vue/packages/ao-design/src/index.scss b/code/base-project/base-vue/packages/ao-design/src/index.scss index 09272773..5c9248fc 100644 --- a/code/base-project/base-vue/packages/ao-design/src/index.scss +++ b/code/base-project/base-vue/packages/ao-design/src/index.scss @@ -4,5 +4,6 @@ @use './input/index.scss' as *; @use './textarea/index.scss' as *; @use './group/index.scss' as *; -@use "./popup/index.scss" as *; - +@use './popup/index.scss' as *; +@use './icon/index.scss' as *; +@use './message/index.scss' as *; diff --git a/code/base-project/base-vue/packages/ao-design/src/index.ts b/code/base-project/base-vue/packages/ao-design/src/index.ts index fc64c948..2c20afb3 100644 --- a/code/base-project/base-vue/packages/ao-design/src/index.ts +++ b/code/base-project/base-vue/packages/ao-design/src/index.ts @@ -3,6 +3,8 @@ import type { App, Component } from 'vue' import * as comps from './components' +import { useMessage } from './message' + // 按需导出组件(可单独 import) export * from './components' @@ -13,7 +15,7 @@ const allComponents = Object.values(comps).filter( export interface AoDesignOptions { prefix?: string - components?: Component[] // 按需注册时传入需要注册的组件 + components?: Component[] } const AoDesign = { @@ -38,4 +40,4 @@ const AoDesign = { }, } -export default AoDesign +export { AoDesign, useMessage } diff --git a/code/base-project/base-vue/packages/ao-design/src/message/index.scss b/code/base-project/base-vue/packages/ao-design/src/message/index.scss new file mode 100644 index 00000000..6fea7059 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/message/index.scss @@ -0,0 +1,128 @@ +@use '../_styles/_variables.scss' as *; +@use 'sass:color'; + +$message-prefix: #{$prefix}-message; + +.#{$message-prefix} { + &-wrapper { + position: fixed; + z-index: 6000; + display: flex; + align-items: center; + gap: 8px; + } + &-item { + display: inline-flex; + align-items: center; + outline: 0; + width: fit-content; + vertical-align: middle; + border-radius: $border-radius; + padding: 6px 12px; + background-color: $bg-color-container; + color: $text-color-base; + box-shadow: $shadow-overlay; + box-sizing: border-box; + font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; + } + &-content { + font-size: 14px; + line-height: 1; + } + &-icon { + flex-shrink: 0; + margin-right: 8px; + } + + &-close { + flex-shrink: 0; + margin-left: 6px; + color: $text-color-assist; + cursor: pointer; + border-radius: 50%; + width: 16px; + height: 16px; + &:hover { + background-color: $default-color-hover; + } + } + + &-info { + color: $primary-color; + } + &-success { + color: $success-color; + } + &-warning { + color: $warning-color; + } + &-error { + color: $danger-color; + } + &-loading { + color: $primary-color; + } + &-placement { + &-top { + top: 20px; + left: 50%; + transform: translateX(-50%); + flex-direction: column; + align-items: center; + } + &-bottom { + bottom: 20px; + left: 50%; + transform: translateX(-50%); + flex-direction: column-reverse; + align-items: center; + } + &-left { + top: 50%; + left: 20px; + transform: translateY(-50%); + flex-direction: column; + align-items: flex-start; + } + &-right { + top: 50%; + right: 20px; + transform: translateY(-50%); + flex-direction: column; + align-items: flex-end; + } + + // 组合方向 + &-top-start { + top: 20px; + left: 20px; + flex-direction: column; + align-items: flex-start; + } + &-top-end { + top: 20px; + right: 20px; + flex-direction: column; + align-items: flex-end; + } + &-bottom-start { + bottom: 20px; + left: 20px; + flex-direction: column-reverse; + align-items: flex-start; + } + &-bottom-end { + bottom: 20px; + right: 20px; + flex-direction: column-reverse; + align-items: flex-end; + } + &-center { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + flex-direction: column; + align-items: center; + } + } +} diff --git a/code/base-project/base-vue/packages/ao-design/src/message/index.ts b/code/base-project/base-vue/packages/ao-design/src/message/index.ts new file mode 100644 index 00000000..64b3c4bf --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/message/index.ts @@ -0,0 +1,176 @@ +import { createApp, h } from 'vue' +import { getPrefixCls } from '../_utils/getPrefixCls' +import Icon from '../icon/index' + +type Placement + = | 'top' + | 'bottom' + | 'left' + | 'right' + | 'top-start' + | 'top-end' + | 'bottom-start' + | 'bottom-end' + | 'center' + +interface MessageOptions { + message: string + type?: 'info' | 'success' | 'warning' | 'error' | 'loading' + placement?: Placement + duration?: number + showClose?: boolean +} + +interface MessageHandle { + close: () => void +} + +interface MessageFn { + (msg: string | MessageOptions): MessageHandle + info: (msg: string | MessageOptions) => MessageHandle + success: (msg: string | MessageOptions) => MessageHandle + warning: (msg: string | MessageOptions) => MessageHandle + error: (msg: string | MessageOptions) => MessageHandle + closeAll: () => void +} + +const prefixCls = getPrefixCls('message') +const containers: Partial> = {} +const instances: MessageHandle[] = [] + +function getContainer(placement: Placement = 'top') { + if (!containers[placement]) { + const wrapper = document.createElement('div') + wrapper.className = `${prefixCls}-wrapper ${prefixCls}-placement-${placement}` + document.body.appendChild(wrapper) + containers[placement] = wrapper + } + return containers[placement]! +} + +function getTransform(placement: Placement, hidden: boolean) { + if (placement.startsWith('top')) + return hidden ? 'translateY(-10px)' : 'translateY(0)' + if (placement.startsWith('bottom')) + return hidden ? 'translateY(10px)' : 'translateY(0)' + if (placement.startsWith('left')) + return hidden ? 'translateX(-10px)' : 'translateX(0)' + if (placement.startsWith('right')) + return hidden ? 'translateX(10px)' : 'translateX(0)' + if (placement === 'center') + return hidden ? 'scale(0.8)' : 'scale(1)' + return hidden ? 'translateY(-10px)' : 'translateY(0)' +} + +export function useMessage(): MessageFn { + const createMessage = ( + msg: string | MessageOptions, + defaultType: 'info' | 'success' | 'warning' | 'error' | 'loading' = 'info', + ): MessageHandle => { + const opts: MessageOptions = typeof msg === 'string' + ? { message: msg, type: defaultType } + : { ...msg, type: msg.type || defaultType } + + const { message, type, placement = 'top', duration = 3000 } = opts + const container = getContainer(placement) + + let timer: number | undefined + + const iconMap: Record = { + success: 'ix:success-filled', + error: 'ix:error-filled', + warning: 'ix:warning-filled', + info: 'ix:info-filled', + loading: 'mingcute:loading-line', + } + + let handle: MessageHandle + + const app = createApp({ + render() { + return h( + 'div', + { + class: [`${prefixCls}-item`], + style: { + opacity: 0, + transform: getTransform(placement, true), + transition: 'all 0.3s ease-in-out', + }, + }, + [ + h(Icon, { + icon: iconMap[type ?? 'info'], + size: 20, + class: `${prefixCls}-icon ${prefixCls}-${type ?? 'info'}`, + spin: type === 'loading', + }), + h('span', { class: `${prefixCls}-content` }, message), + opts.showClose + ? h(Icon, { + icon: 'ix:close', + size: 14, + class: `${prefixCls}-close`, + onClick: () => handle?.close?.(), + }) + : null, + ], + ) + }, + }) + + const vm = app.mount(document.createElement('div')) + container.appendChild(vm.$el) + + // 出现动画 + requestAnimationFrame(() => { + Object.assign(vm.$el.style, { + opacity: '1', + transform: getTransform(placement, false), + }) + }) + + handle = { + close: () => { + if (timer) + clearTimeout(timer) + + Object.assign(vm.$el.style, { + opacity: '0', + transform: getTransform(placement, true), + }) + + setTimeout(() => { + if (container.contains(vm.$el)) + container.removeChild(vm.$el) + app.unmount() + const index = instances.indexOf(handle) + if (index !== -1) + instances.splice(index, 1) + }, 300) + }, + } + + instances.push(handle) + + // 自动关闭 + if (duration > 0) { + timer = window.setTimeout(() => { + handle.close() + }, duration) + } + + return handle + } + + const api = ((msg: string | MessageOptions) => createMessage(msg)) as MessageFn + api.info = msg => createMessage(msg, 'info') + api.success = msg => createMessage(msg, 'success') + api.warning = msg => createMessage(msg, 'warning') + api.error = msg => createMessage(msg, 'error') + api.closeAll = () => { + instances.slice().forEach(instance => instance.close()) + } + + return api +} diff --git a/code/base-project/base-vue/packages/ao-design/src/popup/index.scss b/code/base-project/base-vue/packages/ao-design/src/popup/index.scss index 72d2302a..bd90277c 100644 --- a/code/base-project/base-vue/packages/ao-design/src/popup/index.scss +++ b/code/base-project/base-vue/packages/ao-design/src/popup/index.scss @@ -9,10 +9,11 @@ $popup-prefix: '#{$prefix}-popup'; color: $text-color-base; padding: 4px 8px; font-size: 13px; - border-radius: 2px; + border-radius: $border-radius; z-index: 1000; box-sizing: border-box; text-align: center; + box-shadow: $shadow-popup, $shadow-inset-top, $shadow-inset-right, $shadow-inset-bottom, $shadow-inset-left; &-arrow { position: absolute; width: 8px; @@ -28,53 +29,38 @@ $popup-prefix: '#{$prefix}-popup'; transform-origin: center; } } - // 上方弹出,阴影主方向靠下(轻微),边缘明显 - &[data-popup-placement^='top'] { - box-shadow: - 0 4px 8px rgba(0, 0, 0, 0.06), - 0 0 4px rgba(0, 0, 0, 0.12); // - } - - // 下方弹出,阴影主方向靠上,边缘明显 - &[data-popup-placement^='bottom'] { - box-shadow: - 0 -4px 8px rgba(0, 0, 0, 0.06), - 0 0 4px rgba(0, 0, 0, 0.12); - } - - // 左侧弹出,阴影主方向靠右,边缘明显 - &[data-popup-placement^='left'] { - box-shadow: - 4px 0 8px rgba(0, 0, 0, 0.06), - 0 0 4px rgba(0, 0, 0, 0.12); - } - - // 右侧弹出,阴影主方向靠左,边缘明显 - &[data-popup-placement^='right'] { - box-shadow: - -4px 0 8px rgba(0, 0, 0, 0.06), - 0 0 4px rgba(0, 0, 0, 0.12); - } // 上方弹出,箭头在底部 &[data-popup-placement^='top'] > &-arrow { bottom: -4px; } + &[data-popup-placement^='top'] > &-arrow::before { + box-shadow: $shadow-inset-left, $shadow-inset-bottom; + } // 下方弹出,箭头在顶部 &[data-popup-placement^='bottom'] > &-arrow { top: -4px; } + &[data-popup-placement^='bottom'] > &-arrow::before { + box-shadow: $shadow-inset-top, $shadow-inset-right; + } // 左侧弹出,箭头在右侧 &[data-popup-placement^='left'] > &-arrow { right: -4px; } + &[data-popup-placement^='left'] > &-arrow::before { + box-shadow: $shadow-inset-left, $shadow-inset-top; + } // 右侧弹出,箭头在左侧 &[data-popup-placement^='right'] > &-arrow { left: -4px; } + &[data-popup-placement^='right'] > &-arrow::before { + box-shadow: $shadow-inset-right, $shadow-inset-bottom; + } } /* 普通淡入淡出动画 */ diff --git a/code/base-project/base-vue/packages/ao-design/src/popup/index.tsx b/code/base-project/base-vue/packages/ao-design/src/popup/index.tsx index deb5a60d..d0e5ed09 100644 --- a/code/base-project/base-vue/packages/ao-design/src/popup/index.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/popup/index.tsx @@ -1,4 +1,4 @@ -import type { PropType, VNode } from 'vue' +import type { PropType } from 'vue' import { arrow, autoUpdate, flip, offset, useFloating } from '@floating-ui/vue' import { diff --git a/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx b/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx index ef252c71..eaafbdb5 100644 --- a/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx @@ -2,7 +2,6 @@ import type { PropType } from 'vue' import { defineComponent } from 'vue' import { getPrefixCls } from '../_utils/getPrefixCls' import Icon from '../icon' -import './index.scss' export default defineComponent({ name: 'Spin', diff --git a/code/base-project/base-vue/src/App.vue b/code/base-project/base-vue/src/App.vue index 5f1d4fec..79b14f18 100644 --- a/code/base-project/base-vue/src/App.vue +++ b/code/base-project/base-vue/src/App.vue @@ -1,145 +1,111 @@ - diff --git a/code/base-project/base-vue/src/components.d.ts b/code/base-project/base-vue/src/components.d.ts index 2ea6b193..bb59f05f 100644 --- a/code/base-project/base-vue/src/components.d.ts +++ b/code/base-project/base-vue/src/components.d.ts @@ -8,19 +8,15 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { - A: typeof import('@ao/ao-design')['default'] - AB: typeof import('@ao/ao-design')['B'] AButton: typeof import('@ao/ao-design')['Button'] AConfigProvider: typeof import('@ao/ao-design')['ConfigProvider'] AGroup: typeof import('@ao/ao-design')['Group'] AIcon: typeof import('@ao/ao-design')['Icon'] AInput: typeof import('@ao/ao-design')['Input'] ALoading: typeof import('@ao/ao-design')['Loading'] - ALoding: typeof import('@ao/ao-design')['Loding'] + AMessage: typeof import('@ao/ao-design')['Message'] APopup: typeof import('@ao/ao-design')['Popup'] ASpin: typeof import('@ao/ao-design')['Spin'] - AT: typeof import('@ao/ao-design')['T'] - ATex: typeof import('@ao/ao-design')['Tex'] ATextarea: typeof import('@ao/ao-design')['Textarea'] } }