20260817225227
This commit is contained in:
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 },
|
||||
|
||||
Reference in new issue
Block a user