20260817225227

This commit is contained in:
oneao committed 2026-08-17 22:52:27 +08:00
1 parent d3acc7d28f
commit b90ae89b0e
39 files changed
+3861 -353

No files matched your search

@@ -1,98 +0,0 @@
<script setup>
import { computed } from 'vue'
import { PRESETS } from '@/theme/presets'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
const activeColor = computed(() => appStore.primaryColor)
function pick(color) {
appStore.setPrimary(color)
}
</script>
<template>
<div class="color-picker" role="group" aria-label="主题色">
<button
v-for="preset in PRESETS"
:key="preset.color"
class="swatch"
type="button"
:style="{ background: preset.color }"
:aria-label="preset.name"
:title="preset.name"
:aria-pressed="activeColor === preset.color"
@click="pick(preset.color)"
>
<svg
v-if="activeColor === preset.color"
class="check"
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="#fff"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 6 9 17l-5-5" />
</svg>
</button>
</div>
</template>
<style scoped>
.color-picker {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.swatch {
position: relative;
width: 24px;
height: 24px;
padding: 0;
border: none;
border-radius: 50%;
cursor: pointer;
box-shadow: inset 0 0 0 2px transparent;
transition:
transform 150ms ease,
box-shadow 150ms ease;
}
.swatch:hover {
transform: scale(1.15);
box-shadow:
inset 0 0 0 2px var(--fms-border),
var(--fms-shadow);
}
.swatch[aria-pressed='true'] {
box-shadow:
inset 0 0 0 2px var(--fms-card),
0 0 0 2px var(--fms-text);
}
.check {
position: absolute;
inset: 0;
margin: auto;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.35));
}
.swatch:focus-visible {
outline: 2px solid var(--fms-primary);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.swatch {
transition: none;
}
}
</style>
@@ -1,61 +0,0 @@
<script setup>
import { Moon, Sun } from '@lucide/vue'
import { computed } from 'vue'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
const isDark = computed(() => appStore.darkMode)
function toggle() {
appStore.toggleDark()
}
</script>
<template>
<button
class="theme-toggle"
type="button"
:aria-label="isDark ? '切换到浅色模式' : '切换到深色模式'"
:title="isDark ? '浅色模式' : '深色模式'"
@click="toggle"
>
<Sun v-if="!isDark" class="theme-toggle-icon" aria-hidden="true" />
<Moon v-else class="theme-toggle-icon" aria-hidden="true" />
</button>
</template>
<style scoped>
.theme-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
border: none;
border-radius: var(--fms-control-radius);
background: transparent;
color: var(--fms-text);
cursor: pointer;
transition: background 150ms ease;
}
.theme-toggle:hover {
background: var(--fms-secondary-hover);
}
.theme-toggle:focus-visible {
outline: 2px solid var(--fms-primary);
outline-offset: 2px;
}
.theme-toggle-icon {
width: 16px;
height: 16px;
}
@media (prefers-reduced-motion: reduce) {
.theme-toggle {
transition: none;
}
}
</style>
@@ -52,7 +52,12 @@ const classes = computed(() => [
</script>
<template>
<button :type="htmlType" :class="classes" :disabled="isDisabled" :aria-busy="loading || undefined">
<button
:type="htmlType"
:class="classes"
:disabled="isDisabled"
:aria-busy="loading || undefined"
>
<Loader v-if="loading" class="button-spinner" aria-hidden="true" />
<slot />
</button>
@@ -22,8 +22,8 @@ const multiFormat = ['YYYY年MM月DD日', 'YYYY-MM-DD']
<p class="eyebrow">Components</p>
<h2 id="date-title">DatePicker 日期选择</h2>
<p>
format 同时决定选择面板、选择精度和展示格式;valueFormat 仅控制绑定值格式,
未设置时自动跟随 format。
format 同时决定选择面板、选择精度和展示格式;valueFormat 仅控制绑定值格式, 未设置时自动跟随
format。
</p>
</div>
@@ -46,7 +46,10 @@ function openDrawer(placement) {
:placement="drawerPlacement"
:width="360"
>
<p>抽屉从<strong>{{ drawerPlacement }}</strong>侧滑入,拖动边缘可调整尺寸;点击遮罩、关闭按钮或按 Esc 均可关闭。</p>
<p>
抽屉从<strong>{{ drawerPlacement }}</strong
>侧滑入,拖动边缘可调整尺寸;点击遮罩、关闭按钮或按 Esc 均可关闭。
</p>
<p style="margin-top: 12px">适用于表单填写、详情查看等需要更大空间的场景。</p>
</Drawer>
@@ -54,7 +57,10 @@ function openDrawer(placement) {
<template #extra>
<Button type="link">帮助</Button>
</template>
<p>头部左侧通过 <code>#extra</code> 插槽放入附加操作,底部通过 <code>#footer</code> 插槽放置按钮。</p>
<p>
头部左侧通过 <code>#extra</code> 插槽放入附加操作,底部通过
<code>#footer</code> 插槽放置按钮。
</p>
<template #footer>
<Button type="outline" @click="drawerCustomOpen = false">取消</Button>
<Button type="default" @click="drawerCustomOpen = false">确定</Button>
@@ -122,7 +122,12 @@ const contextItems = [
<p class="demo-label">右键触发</p>
<div class="demo-panel">
<Dropdown :items="contextItems" trigger="contextmenu" placement="bottom-start" @click="onSelect">
<Dropdown
:items="contextItems"
trigger="contextmenu"
placement="bottom-start"
@click="onSelect"
>
<template #trigger>
<div class="dropdown-demo-context">在此区域右键</div>
</template>
@@ -155,7 +155,12 @@ function onPickerReset() {
@reset="onCustomReset"
>
<FormItem name="phone" label="手机号">
<Input v-model="customModel.phone" placeholder="请输入 11 位手机号" maxlength="11" allow-clear />
<Input
v-model="customModel.phone"
placeholder="请输入 11 位手机号"
maxlength="11"
allow-clear
/>
</FormItem>
<FormItem name="code" label="验证码" help="输入短信中的 6 位数字">
<Input v-model="customModel.code" placeholder="6 位数字" maxlength="6" allow-clear />
@@ -63,7 +63,10 @@ function saveWithFeedback() {
</div>
<Modal v-model:open="modalBasicOpen" title="基础模态框" :width="440">
<p>这是一个基础模态框。拖动标题栏可移动位置,拖动右下角可调整尺寸;右上角可切换全屏;点击遮罩、关闭按钮或按 Esc 均可关闭。</p>
<p>
这是一个基础模态框。拖动标题栏可移动位置,拖动右下角可调整尺寸;右上角可切换全屏;点击遮罩、关闭按钮或按
Esc 均可关闭。
</p>
</Modal>
<Modal v-model:open="modalCenteredOpen" title="垂直居中" :width="440" centered>
@@ -134,12 +134,7 @@ onBeforeUnmount(() => {
<p class="demo-label">自定义字段名(labelKey / valueKey)</p>
<div class="demo-panel demo-panel-grid">
<Select
v-model="regionValue"
:options="regionOptions"
label-key="name"
value-key="code"
/>
<Select v-model="regionValue" :options="regionOptions" label-key="name" value-key="code" />
</div>
<p class="demo-label">插槽自定义选项</p>
@@ -13,7 +13,6 @@ const tableRows = [
{ name: '组件库', version: '1.2.0', status: '运行中' },
{ name: '主题系统', version: '2.0.0', status: '已停用' },
]
</script>
<template>
@@ -111,7 +110,9 @@ const tableRows = [
</Spin>
</div>
<p class="spin-hint">同一卡片在未包裹与包裹 Spin(含加载中)两种状态下尺寸一致,内部元素位置不偏移。</p>
<p class="spin-hint">
同一卡片在未包裹与包裹 Spin(含加载中)两种状态下尺寸一致,内部元素位置不偏移。
</p>
</div>
</section>
</template>
@@ -15,7 +15,9 @@ function onSplitterResize(sizes) {
<div class="section-heading">
<p class="eyebrow">Components</p>
<h2 id="splitter-title">Splitter 分割面板</h2>
<p>水平或垂直分割容器,拖动分隔条调整各面板尺寸,支持固定尺寸、最小/最大约束与键盘方向键操作。</p>
<p>
水平或垂直分割容器,拖动分隔条调整各面板尺寸,支持固定尺寸、最小/最大约束与键盘方向键操作。
</p>
</div>
<p class="demo-label">水平分割(首尾固定,中间自适应)</p>
@@ -19,7 +19,9 @@ const tabsItems = [
<div class="section-heading">
<p class="eyebrow">Components</p>
<h2 id="tabs-title">Tabs 标签页</h2>
<p>line 线型标签页,支持 TabPane 插槽与 items 数组两种数据源、左右侧边布局与键盘方向键导航。</p>
<p>
line 线型标签页,支持 TabPane 插槽与 items 数组两种数据源、左右侧边布局与键盘方向键导航。
</p>
</div>
<p class="demo-label">基础用法(TabPane 插槽)</p>
@@ -157,7 +157,12 @@ onBeforeUnmount(() => {
<template>
<Teleport to="body">
<Transition name="fms-drawer-mask">
<div v-if="isOpen && mask" class="fms-drawer-mask" aria-hidden="true" @click="onMaskClick"></div>
<div
v-if="isOpen && mask"
class="fms-drawer-mask"
aria-hidden="true"
@click="onMaskClick"
></div>
</Transition>
<Transition name="fms-drawer">
<div v-if="isOpen" class="fms-drawer-wrap" :class="`fms-drawer-${placement}`">
@@ -6,7 +6,7 @@
* 从而复用现有的键盘导航 / 高亮 / 子菜单体系。
*
* 支持的属性与 items 数组项一一对应:
* key 标识,点击时回传 @click 的 { key, item }
* itemKey 标识,点击时回传 @click 的 { key, item }
* icon 图标组件(Lucide)
* label 菜单文本(也可直接用默认插槽写富内容,由父级读取 children)
* danger 危险项(红色)
@@ -15,7 +15,7 @@
* onClick 项自带点击回调,命中时优先执行
*/
defineProps({
key: { type: [String, Number], default: undefined },
itemKey: { type: [String, Number], default: undefined },
icon: { type: [Object, Function], default: undefined },
label: { type: String, default: '' },
danger: { type: Boolean, default: false },
@@ -50,7 +50,7 @@ function collectDropdownItems() {
continue
}
const item = {}
if (p.key != null) item.key = p.key
if (p.itemKey != null) item.key = p.itemKey
if (p.icon != null) item.icon = p.icon
if (p.label != null) item.label = p.label
if (p.danger) item.danger = true
@@ -188,13 +188,7 @@ provide('dropdownActiveIndex', activeIndex)
</template>
<template #content>
<div
ref="menuEl"
class="dropdown-menu"
role="menu"
tabindex="-1"
@keydown="onMenuKeydown"
>
<div ref="menuEl" class="dropdown-menu" role="menu" tabindex="-1" @keydown="onMenuKeydown">
<!-- 富头部插槽:菜单项列表上方的自定义区域(头像+名字+组织等),有内容时才渲染 -->
<div v-if="$slots.header" class="dropdown-header">
<slot name="header" />
@@ -4,9 +4,15 @@ import { messageState } from './message-store.js'
/* 9 个方位:分组容器始终存在,仅消息项进出,保证 enter/leave 动画稳定触发 */
const PLACEMENTS = [
'top-left', 'top-center', 'top-right',
'center-left', 'center', 'center-right',
'bottom-left', 'bottom-center', 'bottom-right',
'top-left',
'top-center',
'top-right',
'center-left',
'center',
'center-right',
'bottom-left',
'bottom-center',
'bottom-right',
]
/* ---------- 关闭某条 ---------- */
@@ -258,10 +258,26 @@ onBeforeUnmount(() => {
</div>
<!-- 四边缩放手柄:拖动调整面板宽高,全屏时不可用 -->
<template v-if="resizable && !isFullscreen">
<div ref="resizeERef" class="fms-modal-resizer fms-modal-resizer-e" aria-hidden="true"></div>
<div ref="resizeWRef" class="fms-modal-resizer fms-modal-resizer-w" aria-hidden="true"></div>
<div ref="resizeSRef" class="fms-modal-resizer fms-modal-resizer-s" aria-hidden="true"></div>
<div ref="resizeNRef" class="fms-modal-resizer fms-modal-resizer-n" aria-hidden="true"></div>
<div
ref="resizeERef"
class="fms-modal-resizer fms-modal-resizer-e"
aria-hidden="true"
></div>
<div
ref="resizeWRef"
class="fms-modal-resizer fms-modal-resizer-w"
aria-hidden="true"
></div>
<div
ref="resizeSRef"
class="fms-modal-resizer fms-modal-resizer-s"
aria-hidden="true"
></div>
<div
ref="resizeNRef"
class="fms-modal-resizer fms-modal-resizer-n"
aria-hidden="true"
></div>
</template>
</div>
</div>
@@ -5,9 +5,15 @@ import { notificationState } from './notification-store.js'
/* 9 个方位:分组容器始终存在,仅通知项进出,保证 enter/leave 动画稳定触发 */
const PLACEMENTS = [
'top-left', 'top-center', 'top-right',
'center-left', 'center', 'center-right',
'bottom-left', 'bottom-center', 'bottom-right',
'top-left',
'top-center',
'top-right',
'center-left',
'center',
'center-right',
'bottom-left',
'bottom-center',
'bottom-right',
]
/* ---------- 关闭某条 ---------- */
@@ -48,9 +48,7 @@ const pageSizeValue = computed(() =>
// 每页条数选项统一转为数字,保证与 pageSize 值类型匹配
const sizeOptions = computed(() => props.pageSizeOptions.map((item) => Number(item)))
const totalPages = computed(() =>
Math.max(1, Math.ceil((props.total || 0) / pageSizeValue.value)),
)
const totalPages = computed(() => Math.max(1, Math.ceil((props.total || 0) / pageSizeValue.value)))
// 显示层当前页:受控取 props,非受控取内部值;越界时收敛到 [1, totalPages],
// 受控模式不写回 props,保持受控语义由调用方同步
@@ -157,7 +157,14 @@ function onContextMenu(event) {
event.preventDefault()
const { clientX, clientY } = event
// 右键在鼠标位置弹出:构造零尺寸锚点矩形,computePosition 的 flip/shift 会兜底防溢出
pointAnchor = { left: clientX, top: clientY, width: 0, height: 0, right: clientX, bottom: clientY }
pointAnchor = {
left: clientX,
top: clientY,
width: 0,
height: 0,
right: clientX,
bottom: clientY,
}
if (isOpen.value) {
// 已打开时右键到新位置,仅重新定位不重置状态
updatePosition()
@@ -48,9 +48,7 @@ function hasDefaultSlot() {
return !!(slots.default && slots.default().length > 0)
}
const hasDescription = computed(
() => !!props.description || !!slots.description,
)
const hasDescription = computed(() => !!props.description || !!slots.description)
</script>
<template>
@@ -45,8 +45,8 @@
.switch-checked .switch-thumb {
transform: translateX(
calc(
var(--fms-switch-width) - var(--fms-switch-thumb-size) -
var(--fms-switch-thumb-offset) - var(--fms-switch-thumb-offset)
var(--fms-switch-width) - var(--fms-switch-thumb-size) - var(--fms-switch-thumb-offset) -
var(--fms-switch-thumb-offset)
)
);
}
@@ -81,8 +81,7 @@ const slotPanes = computed(() => {
const paneProps = node.props ?? {}
const { content, label } = extractChildren(node.children)
// key 解析顺序:tabKey prop > vnode.key > 出现顺序索引
const rawKey =
paneProps.tabKey != null ? paneProps.tabKey : node.key != null ? node.key : index
const rawKey = paneProps.tabKey != null ? paneProps.tabKey : node.key != null ? node.key : index
panes.push({
key: String(rawKey),
rawKey,
@@ -14,8 +14,7 @@ const props = defineProps({
/** @type {TagVariant} */
type: String,
default: 'default',
validator: (value) =>
['default', 'secondary', 'outline', 'danger', 'ghost'].includes(value),
validator: (value) => ['default', 'secondary', 'outline', 'danger', 'ghost'].includes(value),
},
/** 是否可关闭:显示关闭按钮,点击后触发 close 并隐藏自身 */
closable: { type: Boolean, default: false },