mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 14:06:28 +00:00
style: use united style
This commit is contained in:
@@ -325,191 +325,174 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* === Material Design 3 样式重写 === */
|
||||
|
||||
.crontab-editor {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background-color: var(--md-sys-color-surface-container-lowest);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dark .crontab-editor {
|
||||
border-color: #3a3a3c;
|
||||
background: #2c2c2e;
|
||||
.crontab-editor:focus-within {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
box-shadow: 0 0 0 1px var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
/* 模式选择标签 */
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 2px solid #ebeef5;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.dark .mode-tabs {
|
||||
border-bottom-color: #3a3a3c;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.mode-tab {
|
||||
padding: 8px 16px;
|
||||
padding: 10px 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1px;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: -1px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dark .mode-tab {
|
||||
color: #a0a0a3;
|
||||
.mode-tab:hover {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
background-color: rgba(76, 175, 80, 0.04);
|
||||
}
|
||||
|
||||
.mode-tab.active {
|
||||
color: #409eff;
|
||||
border-bottom-color: #409eff;
|
||||
}
|
||||
|
||||
.dark .mode-tab.active {
|
||||
color: #81c784;
|
||||
border-bottom-color: #81c784;
|
||||
color: var(--md-sys-color-primary);
|
||||
border-bottom-color: var(--md-sys-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 模式内容区域 */
|
||||
.mode-content {
|
||||
margin: 16px 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* 快速选项 */
|
||||
.quick-option {
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
padding: 16px;
|
||||
background-color: var(--md-sys-color-surface);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dark .quick-option {
|
||||
background: #1c1c1e;
|
||||
.quick-option:hover {
|
||||
border-color: var(--md-sys-color-outline);
|
||||
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.3),
|
||||
0px 1px 3px 1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-weight: 600;
|
||||
color: #1c1b1f;
|
||||
}
|
||||
|
||||
.dark .option-label {
|
||||
color: #e5e5e7;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
margin-left: 12px;
|
||||
color: #909399;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.dark .option-desc {
|
||||
color: #a0a0a3;
|
||||
}
|
||||
|
||||
/* 表达式输入 */
|
||||
.expression-input {
|
||||
margin: 12px 0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: 8px;
|
||||
color: #909399;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dark .help-text {
|
||||
color: #a0a0a3;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.help-text a {
|
||||
color: #409eff;
|
||||
color: var(--md-sys-color-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dark .help-text a {
|
||||
color: #81c784;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.help-text a:hover {
|
||||
color: var(--md-sys-color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 预览区域 */
|
||||
.preview-section {
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.dark .preview-section {
|
||||
background: #1c1c1e;
|
||||
padding: 16px;
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.preview-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
color: #1c1b1f;
|
||||
}
|
||||
|
||||
.dark .preview-section h4 {
|
||||
color: #e5e5e7;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.execution-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
padding-left: 24px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
.dark .execution-list {
|
||||
color: #a0a0a3;
|
||||
.execution-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 验证消息 */
|
||||
.validation-message {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
border: 1px solid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.validation-message.success {
|
||||
background: #f0f9ff;
|
||||
color: #67c23a;
|
||||
border: 1px solid #c6e2ff;
|
||||
}
|
||||
|
||||
.dark .validation-message.success {
|
||||
background: rgba(129, 199, 132, 0.1);
|
||||
color: #81c784;
|
||||
border-color: rgba(129, 199, 132, 0.3);
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.validation-message.error {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
border: 1px solid #fde7e7;
|
||||
}
|
||||
|
||||
.dark .validation-message.error {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
color: #ef5350;
|
||||
border-color: rgba(244, 67, 54, 0.3);
|
||||
background-color: var(--md-sys-color-error-container);
|
||||
color: var(--md-sys-color-on-error-container);
|
||||
border-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
.validation-message.info {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.dark .validation-message.info {
|
||||
background: #2c2c2e;
|
||||
color: #a0a0a3;
|
||||
border-color: #3a3a3c;
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
border-color: var(--md-sys-color-outline-variant);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
placeholder="在表单中显示的名称"
|
||||
allow-clear
|
||||
/>
|
||||
<span class="text-xs text-gray-500 mt-1">显示名称</span>
|
||||
<span class="text-xs text-on-surface-variant mt-1">显示名称</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="字段类型" class="mb-0">
|
||||
@@ -24,7 +24,7 @@
|
||||
<a-select-option label="🔢 数字输入" value="number" />
|
||||
<a-select-option label="📋 下拉选择" value="select" />
|
||||
</a-select>
|
||||
<span class="text-xs text-gray-500 mt-1">用户填写时使用的输入控件</span>
|
||||
<span class="text-xs text-on-surface-variant mt-1">用户填写时使用的输入控件</span>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
@@ -38,22 +38,22 @@
|
||||
class="w-full"
|
||||
>
|
||||
<a-select-option label="字符串 (string)" value="string">
|
||||
<span class="text-xs text-gray-500">字符串 (string)</span>
|
||||
<span class="text-xs text-on-surface-variant">字符串 (string)</span>
|
||||
</a-select-option>
|
||||
<a-select-option label="整数 (int)" value="int">
|
||||
<span class="text-xs text-gray-500">整数 (int)</span>
|
||||
<span class="text-xs text-on-surface-variant">整数 (int)</span>
|
||||
</a-select-option>
|
||||
<a-select-option label="浮点数 (double)" value="double">
|
||||
<span class="text-xs text-gray-500">浮点数 (double)</span>
|
||||
<span class="text-xs text-on-surface-variant">浮点数 (double)</span>
|
||||
</a-select-option>
|
||||
<a-select-option label="布尔值 (bool)" value="bool">
|
||||
<span class="text-xs text-gray-500">布尔值 (bool)</span>
|
||||
<span class="text-xs text-on-surface-variant">布尔值 (bool)</span>
|
||||
</a-select-option>
|
||||
<a-select-option label="JSON对象 (json)" value="json">
|
||||
<span class="text-xs text-gray-500">JSON对象 (json) - 用于Values字段</span>
|
||||
<span class="text-xs text-on-surface-variant">JSON对象 (json) - 用于Values字段</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<span class="text-xs text-gray-500 mt-1">数据存储时的类型</span>
|
||||
<span class="text-xs text-on-surface-variant mt-1">数据存储时的类型</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="默认值" class="mb-0">
|
||||
@@ -72,7 +72,7 @@
|
||||
:rows="3"
|
||||
allow-clear
|
||||
/>
|
||||
<span class="text-xs text-gray-500 mt-1">
|
||||
<span class="text-xs text-on-surface-variant mt-1">
|
||||
<template v-if="modelValue.value_type === 'json'">
|
||||
<p>输入JSON对象,会自动序列化为字符串</p>
|
||||
<p>如:{"key1":value1,"key2":value2}</p>
|
||||
@@ -92,15 +92,15 @@
|
||||
placeholder="输入框的灰色提示文本"
|
||||
allow-clear
|
||||
/>
|
||||
<span class="text-xs text-gray-500 mt-1">占位符</span>
|
||||
<span class="text-xs text-on-surface-variant mt-1">占位符</span>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Row 4: Switches -->
|
||||
<div class="grid grid-cols-2 gap-4 p-3 bg-blue-50 rounded-lg">
|
||||
<div class="grid grid-cols-2 gap-4 p-3 bg-surface-container-low rounded-md3 border border-outline-variant">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700">是否必填</label>
|
||||
<p class="text-xs text-gray-500">用户必须填写此字段</p>
|
||||
<label class="text-sm font-medium text-on-surface">是否必填</label>
|
||||
<p class="text-xs text-on-surface-variant">用户必须填写此字段</p>
|
||||
</div>
|
||||
<a-switch
|
||||
:checked="modelValue.required"
|
||||
@@ -111,8 +111,8 @@
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700">是否隐藏</label>
|
||||
<p class="text-xs text-gray-500">直接使用默认值,不在表单中显示</p>
|
||||
<label class="text-sm font-medium text-on-surface">是否隐藏</label>
|
||||
<p class="text-xs text-on-surface-variant">直接使用默认值,不在表单中显示</p>
|
||||
</div>
|
||||
<a-switch
|
||||
:checked="modelValue.hidden"
|
||||
@@ -142,9 +142,9 @@
|
||||
<div
|
||||
v-for="(option, index) in modelValue.options || []"
|
||||
:key="index"
|
||||
class="flex items-center gap-2 p-2 bg-gray-50 rounded"
|
||||
class="flex items-center gap-2 p-2 bg-surface-container rounded-md3"
|
||||
>
|
||||
<span class="text-xs text-gray-500 w-8">{{ index + 1 }}.</span>
|
||||
<span class="text-xs text-on-surface-variant w-8">{{ index + 1 }}.</span>
|
||||
<a-input
|
||||
:value="option.label"
|
||||
@change="e => updateOption(index, 'label', e.target.value)"
|
||||
@@ -175,8 +175,8 @@
|
||||
添加选项
|
||||
</a-button>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
💡 提示:显示文本是用户看到的内容,选项值是实际保存的数据
|
||||
<p class="text-xs text-on-surface-variant mt-2">
|
||||
💡 提示:显示文本是用户看到的内容,选项值是实际保存的数据
|
||||
</p>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="field-tree-node border-2 rounded-lg p-4 bg-white shadow-sm hover:shadow-md transition-shadow">
|
||||
<div class="field-tree-node border border-outline-variant rounded-md3 p-4 bg-surface shadow-md3-1 hover:shadow-md3-2 transition-shadow">
|
||||
<!-- 普通字段 -->
|
||||
<div v-if="isFieldConfig" class="field-config">
|
||||
<div class="flex items-center justify-between mb-3 pb-2 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between mb-3 pb-2 border-b border-outline-variant">
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="isCollapsed = !isCollapsed"
|
||||
class="hover:bg-gray-100 rounded p-1 transition-colors"
|
||||
class="hover:bg-surface-container rounded-md3 p-1 transition-colors"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-600 transition-transform"
|
||||
class="w-4 h-4 text-on-surface-variant transition-transform"
|
||||
:class="{ 'rotate-180': !isCollapsed }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -19,10 +19,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
<span class="font-mono text-base font-bold text-blue-700">{{ fieldKey }}</span>
|
||||
<span class="font-mono text-base font-bold text-primary">{{ fieldKey }}</span>
|
||||
<a-tag type="primary" size="small">普通字段</a-tag>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -45,22 +45,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!isCollapsed" class="bg-gray-50 rounded-lg p-3">
|
||||
<div v-show="!isCollapsed" class="bg-surface-container-low rounded-md3 p-3">
|
||||
<FieldConfigEditor v-model="localFieldConfig" :field-key="fieldKey" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数组字段 -->
|
||||
<div v-else-if="isArray" class="array-field">
|
||||
<div class="flex items-center justify-between mb-3 pb-2 border-b border-purple-200">
|
||||
<div class="flex items-center justify-between mb-3 pb-2 border-b border-outline-variant">
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="isCollapsed = !isCollapsed"
|
||||
class="hover:bg-gray-100 rounded p-1 transition-colors"
|
||||
class="hover:bg-surface-container rounded-md3 p-1 transition-colors"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-600 transition-transform"
|
||||
class="w-4 h-4 text-on-surface-variant transition-transform"
|
||||
:class="{ 'rotate-180': !isCollapsed }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -69,10 +69,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>
|
||||
<span class="font-mono text-base font-bold text-purple-700">{{ fieldKey }}</span>
|
||||
<span class="font-mono text-base font-bold text-secondary">{{ fieldKey }}</span>
|
||||
<a-tag type="warning" size="small">数组字段</a-tag>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -102,8 +102,8 @@
|
||||
</div>
|
||||
|
||||
<div v-show="!isCollapsed">
|
||||
<div v-if="localFieldConfig.length === 0" class="text-center py-6 bg-purple-50 rounded-lg border border-dashed border-purple-300">
|
||||
<p class="text-sm text-gray-500 mb-2">数组为空</p>
|
||||
<div v-if="localFieldConfig.length === 0" class="text-center py-6 bg-surface-container-low rounded-md3 border border-dashed border-outline">
|
||||
<p class="text-sm text-on-surface-variant mb-2">数组为空</p>
|
||||
<a-button size="small" type="primary" @click="addArrayItem">添加第一个元素</a-button>
|
||||
</div>
|
||||
|
||||
@@ -111,17 +111,17 @@
|
||||
<div
|
||||
v-for="(item, index) in localFieldConfig"
|
||||
:key="index"
|
||||
class="border-2 border-purple-200 rounded-lg p-3 bg-purple-50"
|
||||
class="border border-outline-variant rounded-md3 p-3 bg-surface-container"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-semibold text-purple-700">元素 #{{ index + 1 }}</span>
|
||||
<span class="text-sm font-semibold text-secondary">元素 #{{ index + 1 }}</span>
|
||||
<a-button size="small" type="danger" plain @click="removeArrayItem(index)">
|
||||
删除元素
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 如果数组元素是字段配置对象,直接渲染为字段编辑器 -->
|
||||
<div v-if="typeof item === 'object' && !Array.isArray(item) && 'display_name' in item" class="bg-white rounded-lg p-3">
|
||||
<div v-if="typeof item === 'object' && !Array.isArray(item) && 'display_name' in item" class="bg-surface rounded-md3 p-3">
|
||||
<FieldConfigEditor :model-value="item" @update:model-value="updateArrayItemField(index, $event)" :field-key="`元素${index + 1}`" />
|
||||
</div>
|
||||
|
||||
@@ -164,15 +164,15 @@
|
||||
|
||||
<!-- 对象字段 -->
|
||||
<div v-else-if="isObject" class="object-field">
|
||||
<div class="flex items-center justify-between mb-3 pb-2 border-b border-green-200">
|
||||
<div class="flex items-center justify-between mb-3 pb-2 border-b border-outline-variant">
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="isCollapsed = !isCollapsed"
|
||||
class="hover:bg-gray-100 rounded p-1 transition-colors"
|
||||
class="hover:bg-surface-container rounded-md3 p-1 transition-colors"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-600 transition-transform"
|
||||
class="w-4 h-4 text-on-surface-variant transition-transform"
|
||||
:class="{ 'rotate-180': !isCollapsed }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -181,10 +181,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span class="font-mono text-base font-bold text-green-700">{{ fieldKey }}</span>
|
||||
<span class="font-mono text-base font-bold text-accent">{{ fieldKey }}</span>
|
||||
<a-tag type="success" size="small">对象字段</a-tag>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -214,12 +214,12 @@
|
||||
</div>
|
||||
|
||||
<div v-show="!isCollapsed">
|
||||
<div v-if="Object.keys(localFieldConfig).length === 0" class="text-center py-6 bg-green-50 rounded-lg border border-dashed border-green-300">
|
||||
<p class="text-sm text-gray-500 mb-2">对象为空</p>
|
||||
<div v-if="Object.keys(localFieldConfig).length === 0" class="text-center py-6 bg-surface-container-low rounded-md3 border border-dashed border-outline">
|
||||
<p class="text-sm text-on-surface-variant mb-2">对象为空</p>
|
||||
<a-button size="small" type="primary" @click="addFieldToObject">添加第一个子字段</a-button>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3 mt-3 pl-4 border-l-4 border-green-300">
|
||||
<div v-else class="space-y-3 mt-3 pl-4 border-l-4 border-accent">
|
||||
<!-- 递归渲染对象中的字段 -->
|
||||
<FieldTreeNode
|
||||
v-for="(subConfig, subKey) in localFieldConfig"
|
||||
|
||||
@@ -26,11 +26,7 @@ onMounted(() => {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%);
|
||||
}
|
||||
|
||||
.dark .layout-container {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
|
||||
background: linear-gradient(135deg, var(--md-sys-color-surface-container-lowest) 0%, var(--md-sys-color-surface-container-low) 100%);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="sticky top-0 z-50 glass-effect border-b border-gray-200/50 dark:border-gray-700/50 shadow-md3-2">
|
||||
<div class="sticky top-0 z-50 md3-surface elevation-2 border-b border-outline-variant">
|
||||
<nav class="max-w-7xl mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo and Brand -->
|
||||
@@ -24,7 +24,7 @@
|
||||
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
isActive
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
: 'text-on-surface hover:bg-surface-container'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
@@ -45,7 +45,7 @@
|
||||
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
isActive
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
: 'text-on-surface hover:bg-surface-container'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
@@ -66,7 +66,7 @@
|
||||
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
isActive
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
: 'text-on-surface hover:bg-surface-container'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
@@ -81,7 +81,7 @@
|
||||
<a
|
||||
:class="[
|
||||
'px-4 py-2 rounded-full font-medium transition-all flex items-center space-x-2 cursor-pointer',
|
||||
isAdminPath ? 'bg-secondary-100 dark:bg-secondary-900/30 text-secondary-700 dark:text-secondary-400' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
isAdminPath ? 'bg-secondary-100 dark:bg-secondary-900/30 text-secondary-700 dark:text-secondary-400' : 'text-on-surface hover:bg-surface-container'
|
||||
]"
|
||||
>
|
||||
<SettingOutlined />
|
||||
@@ -121,7 +121,7 @@
|
||||
<!-- Token Status Indicator (Desktop & Mobile) -->
|
||||
<a-tooltip v-if="showTokenStatus" :title="tokenStatusTooltip">
|
||||
<div
|
||||
class="px-2 md:px-3 py-1.5 rounded-full cursor-pointer transition-all hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center space-x-1 md:space-x-2"
|
||||
class="px-2 md:px-3 py-1.5 rounded-full cursor-pointer transition-all hover:bg-surface-container flex items-center space-x-1 md:space-x-2"
|
||||
@click="handleTokenStatusClick"
|
||||
>
|
||||
<a-badge :status="tokenBadgeStatus" />
|
||||
@@ -142,32 +142,32 @@
|
||||
</a-tooltip>
|
||||
|
||||
<!-- Theme Toggle Button -->
|
||||
<a-tooltip :title="isDark ? '切换到亮色模式' : '切换到暗色模式'">
|
||||
<a-button
|
||||
type="text"
|
||||
class="!p-2 !flex !items-center !justify-center hover:!bg-gray-100 dark:hover:!bg-gray-700 transition-all"
|
||||
<a-tooltip :title="isDark ? '切换到亮色模式' : '切换到暗色模式'" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-all"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<BulbFilled v-if="isDark" class="text-lg text-yellow-400" />
|
||||
<BulbOutlined v-else class="text-lg text-gray-700 dark:text-gray-300" />
|
||||
</a-button>
|
||||
<BulbFilled v-if="isDark" class="text-xl text-yellow-400" />
|
||||
<BulbOutlined v-else class="text-xl text-on-surface" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- Desktop User Menu -->
|
||||
<a-dropdown v-if="!isMobile" :trigger="['hover']">
|
||||
<a class="flex items-center space-x-3 px-4 py-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all cursor-pointer">
|
||||
<a class="flex items-center space-x-3 px-4 py-2 rounded-full hover:bg-surface-container transition-all cursor-pointer">
|
||||
<a-avatar :style="{ backgroundColor: '#f56a00' }">
|
||||
{{ userInitial }}
|
||||
</a-avatar>
|
||||
<span class="hidden md:block font-medium text-gray-700 dark:text-gray-200">{{ authStore.user?.alias || '用户' }}</span>
|
||||
<DownOutlined class="text-xs text-gray-500 dark:text-gray-400" />
|
||||
<span class="hidden md:block font-medium text-on-surface">{{ authStore.user?.alias || '用户' }}</span>
|
||||
<DownOutlined class="text-xs text-on-surface-variant" />
|
||||
</a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="info" disabled>
|
||||
<div class="px-2 py-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ authStore.user?.alias }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
|
||||
<p class="text-sm font-medium text-on-surface">{{ authStore.user?.alias }}</p>
|
||||
<p class="text-xs text-on-surface-variant mt-1">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
|
||||
</div>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
@@ -184,14 +184,14 @@
|
||||
</a-dropdown>
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<a-button
|
||||
<button
|
||||
v-if="isMobile"
|
||||
type="text"
|
||||
type="button"
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-all"
|
||||
@click="drawerVisible = true"
|
||||
class="!p-2"
|
||||
>
|
||||
<MenuOutlined class="text-xl text-gray-700 dark:text-gray-300" />
|
||||
</a-button>
|
||||
<MenuOutlined class="text-xl text-on-surface" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -204,14 +204,14 @@
|
||||
title="菜单"
|
||||
>
|
||||
<!-- User Info in Drawer -->
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
<div class="mb-6 pb-4 border-b border-outline-variant">
|
||||
<div class="flex items-center space-x-3">
|
||||
<a-avatar :size="48" :style="{ backgroundColor: '#f56a00' }">
|
||||
{{ userInitial }}
|
||||
</a-avatar>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900">{{ authStore.user?.alias || '用户' }}</p>
|
||||
<p class="text-xs text-gray-500">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
|
||||
<p class="font-medium text-on-surface">{{ authStore.user?.alias || '用户' }}</p>
|
||||
<p class="text-xs text-on-surface-variant">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,11 +364,11 @@ const tokenBadgeText = computed(() => {
|
||||
|
||||
const tokenIconClass = computed(() => {
|
||||
const mins = remainingMinutes.value
|
||||
if (mins === null) return 'text-gray-500'
|
||||
if (mins < 0) return 'text-red-500' // 已过期
|
||||
if (mins <= 10) return 'text-red-500 animate-pulse' // 10分钟内,闪烁
|
||||
if (mins <= 30) return 'text-orange-500' // 30分钟内
|
||||
return 'text-blue-500' // 正常
|
||||
if (mins === null) return 'text-on-surface-variant'
|
||||
if (mins < 0) return 'text-red-500 dark:text-red-400' // 已过期
|
||||
if (mins <= 10) return 'text-red-500 dark:text-red-400 animate-pulse' // 10分钟内,闪烁
|
||||
if (mins <= 30) return 'text-orange-500 dark:text-orange-400' // 30分钟内
|
||||
return 'text-blue-500 dark:text-blue-400' // 正常
|
||||
})
|
||||
|
||||
const tokenStatusTooltip = computed(() => {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<a-card class="md3-card text-center" style="padding: 48px 20px;">
|
||||
<!-- 图标 -->
|
||||
<div v-if="icon" class="mb-6">
|
||||
<component
|
||||
:is="icon"
|
||||
class="text-8xl mx-auto"
|
||||
:class="iconColorClass"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<h3 class="md3-title-large text-on-surface mb-2">
|
||||
{{ title || '暂无数据' }}
|
||||
</h3>
|
||||
|
||||
<!-- 描述 -->
|
||||
<p class="md3-body-medium text-on-surface-variant mb-6">
|
||||
{{ description || '当前没有内容可显示' }}
|
||||
</p>
|
||||
|
||||
<!-- 操作按钮(可选) -->
|
||||
<div v-if="$slots.action || actionText">
|
||||
<slot name="action">
|
||||
<a-button
|
||||
v-if="actionText"
|
||||
type="primary"
|
||||
@click="handleAction"
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-if="actionIcon" #icon>
|
||||
<component :is="actionIcon" />
|
||||
</template>
|
||||
{{ actionText }}
|
||||
</a-button>
|
||||
</slot>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 图标组件
|
||||
*/
|
||||
icon: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 标题文本
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 描述文本
|
||||
*/
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 操作按钮文本
|
||||
*/
|
||||
actionText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 操作按钮图标
|
||||
*/
|
||||
actionIcon: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载状态
|
||||
*/
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
/**
|
||||
* 图标颜色
|
||||
*/
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: 'neutral',
|
||||
validator: (v) => ['primary', 'neutral', 'success', 'warning', 'error'].includes(v)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['action'])
|
||||
|
||||
const handleAction = () => {
|
||||
emit('action')
|
||||
}
|
||||
|
||||
const iconColorClass = computed(() => {
|
||||
const colors = {
|
||||
primary: 'text-primary',
|
||||
neutral: 'text-on-surface-variant',
|
||||
success: 'text-green-500',
|
||||
warning: 'text-orange-500',
|
||||
error: 'text-error'
|
||||
}
|
||||
return colors[props.iconColor]
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div v-if="loading" class="loading-state">
|
||||
<!-- 卡片骨架屏 -->
|
||||
<div v-if="type === 'card'" class="grid grid-cols-1 gap-4">
|
||||
<a-card
|
||||
v-for="i in count"
|
||||
:key="i"
|
||||
class="md3-card"
|
||||
>
|
||||
<a-skeleton
|
||||
:active="true"
|
||||
:paragraph="{ rows: paragraphRows }"
|
||||
:avatar="showAvatar"
|
||||
/>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 列表骨架屏 -->
|
||||
<div v-else-if="type === 'list'" class="space-y-4">
|
||||
<a-card
|
||||
v-for="i in count"
|
||||
:key="i"
|
||||
class="md3-card"
|
||||
>
|
||||
<a-skeleton
|
||||
:active="true"
|
||||
:paragraph="{ rows: 1 }"
|
||||
:avatar="showAvatar"
|
||||
/>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 表格骨架屏 -->
|
||||
<a-card v-else-if="type === 'table'" class="md3-card">
|
||||
<a-skeleton
|
||||
:active="true"
|
||||
:paragraph="{ rows: count * 2 }"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<!-- 默认骨架屏 -->
|
||||
<a-card v-else class="md3-card">
|
||||
<a-skeleton
|
||||
:active="true"
|
||||
:paragraph="{ rows: paragraphRows }"
|
||||
:avatar="showAvatar"
|
||||
/>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 是否显示加载状态
|
||||
*/
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
/**
|
||||
* 骨架屏类型
|
||||
*/
|
||||
type: {
|
||||
type: String,
|
||||
default: 'card',
|
||||
validator: (v) => ['card', 'list', 'table', 'default'].includes(v)
|
||||
},
|
||||
|
||||
/**
|
||||
* 骨架屏数量
|
||||
*/
|
||||
count: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
|
||||
/**
|
||||
* 段落行数
|
||||
*/
|
||||
paragraphRows: {
|
||||
type: Number,
|
||||
default: 4
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否显示头像
|
||||
*/
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-state {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<a-card
|
||||
class="md3-card animate-slide-up transition-standard hover:elevation-3"
|
||||
:style="{ animationDelay }"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- 数值和标签 -->
|
||||
<div class="flex-1">
|
||||
<p class="md3-label-medium text-on-surface-variant mb-1">{{ label }}</p>
|
||||
<p class="md3-headline-medium" :class="valueColorClass">
|
||||
{{ formattedValue }}
|
||||
</p>
|
||||
<p v-if="subtitle" class="md3-body-small text-on-surface-variant mt-1">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 图标 -->
|
||||
<div
|
||||
v-if="icon"
|
||||
class="w-12 h-12 rounded-md3 flex items-center justify-center flex-shrink-0 ml-4"
|
||||
:class="iconBgClass"
|
||||
>
|
||||
<component :is="icon" :class="iconColorClass" class="text-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 趋势指示器(可选) -->
|
||||
<div v-if="trend !== undefined" class="mt-3 pt-3 border-t border-outline-variant">
|
||||
<div class="flex items-center text-sm">
|
||||
<component
|
||||
:is="trendIcon"
|
||||
:class="trendColorClass"
|
||||
class="mr-1"
|
||||
/>
|
||||
<span :class="trendColorClass" class="md3-label-small">
|
||||
{{ trendText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
MinusOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 卡片标签
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示的数值
|
||||
*/
|
||||
value: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
|
||||
/**
|
||||
* 副标题/描述
|
||||
*/
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 图标组件
|
||||
*/
|
||||
icon: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 颜色主题
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: (v) => ['primary', 'success', 'warning', 'error', 'info', 'neutral'].includes(v)
|
||||
},
|
||||
|
||||
/**
|
||||
* 动画延迟(秒)
|
||||
*/
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化函数
|
||||
*/
|
||||
formatter: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 趋势值(正数上升,负数下降,0持平)
|
||||
*/
|
||||
trend: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
|
||||
/**
|
||||
* 趋势文本
|
||||
*/
|
||||
trendText: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 动画延迟
|
||||
const animationDelay = computed(() => `${props.delay}s`)
|
||||
|
||||
// 格式化数值
|
||||
const formattedValue = computed(() => {
|
||||
if (props.formatter) {
|
||||
return props.formatter(props.value)
|
||||
}
|
||||
return props.value
|
||||
})
|
||||
|
||||
// 颜色映射
|
||||
const colorClasses = {
|
||||
primary: {
|
||||
value: 'text-primary',
|
||||
iconBg: 'bg-primary-100 dark:bg-primary-900/30',
|
||||
icon: 'text-primary'
|
||||
},
|
||||
success: {
|
||||
value: 'text-green-600 dark:text-green-400',
|
||||
iconBg: 'bg-green-100 dark:bg-green-900/30',
|
||||
icon: 'text-green-600 dark:text-green-400'
|
||||
},
|
||||
warning: {
|
||||
value: 'text-orange-600 dark:text-orange-400',
|
||||
iconBg: 'bg-orange-100 dark:bg-orange-900/30',
|
||||
icon: 'text-orange-600 dark:text-orange-400'
|
||||
},
|
||||
error: {
|
||||
value: 'text-error',
|
||||
iconBg: 'bg-red-100 dark:bg-red-900/30',
|
||||
icon: 'text-error'
|
||||
},
|
||||
info: {
|
||||
value: 'text-secondary',
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900/30',
|
||||
icon: 'text-secondary'
|
||||
},
|
||||
neutral: {
|
||||
value: 'text-on-surface',
|
||||
iconBg: 'bg-surface-container',
|
||||
icon: 'text-on-surface-variant'
|
||||
}
|
||||
}
|
||||
|
||||
const valueColorClass = computed(() => colorClasses[props.color].value)
|
||||
const iconBgClass = computed(() => colorClasses[props.color].iconBg)
|
||||
const iconColorClass = computed(() => colorClasses[props.color].icon)
|
||||
|
||||
// 趋势图标和颜色
|
||||
const trendIcon = computed(() => {
|
||||
if (props.trend === undefined) return null
|
||||
if (props.trend > 0) return ArrowUpOutlined
|
||||
if (props.trend < 0) return ArrowDownOutlined
|
||||
return MinusOutlined
|
||||
})
|
||||
|
||||
const trendColorClass = computed(() => {
|
||||
if (props.trend === undefined) return ''
|
||||
if (props.trend > 0) return 'text-green-600 dark:text-green-400'
|
||||
if (props.trend < 0) return 'text-red-600 dark:text-red-400'
|
||||
return 'text-on-surface-variant'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.md3-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user