ConfigDrawer.vue 3.74 KB
<script setup lang="ts">
defineProps<{
  open: boolean
  groups: Array<{
    title: string
    fields: Array<{
      key: string
      label: string
      type: string
      value: string
    }>
  }>
  loading: boolean
  saving: boolean
}>()

const emit = defineEmits<{
  'update:open': [value: boolean]
  updateField: [key: string, value: string]
  refresh: []
  save: []
}>()
</script>

<template>
  <transition name="fade">
    <div v-if="open" class="drawer-overlay" @click.self="$emit('update:open', false)">
      <aside class="drawer">
        <div class="drawer-head">
          <div>
            <p class="drawer-kicker">Configuration</p>
            <h2>配置中心</h2>
          </div>
          <div class="drawer-actions">
            <button class="ghost-button" type="button" :disabled="loading" @click="$emit('refresh')">刷新</button>
            <button class="ghost-button" type="button" @click="$emit('update:open', false)">关闭</button>
          </div>
        </div>

        <div class="drawer-body">
          <section v-for="group in groups" :key="group.title" class="config-group">
            <header class="config-group-head">
              <h3>{{ group.title }}</h3>
            </header>
            <div class="config-grid">
              <label v-for="field in group.fields" :key="field.key" class="field">
                <span class="field-label">{{ field.label }}</span>
                <input
                  class="field-control"
                  :type="field.type"
                  :value="field.value"
                  @input="$emit('updateField', field.key, ($event.target as HTMLInputElement).value)"
                />
              </label>
            </div>
          </section>
        </div>

        <div class="drawer-footer">
          <button class="solid-button" type="button" :disabled="saving" @click="$emit('save')">
            {{ saving ? '保存中…' : '保存配置' }}
          </button>
        </div>
      </aside>
    </div>
  </transition>
</template>

<style scoped>
.drawer-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  justify-content: flex-end;
  background: rgba(15, 18, 16, 0.28);
  backdrop-filter: blur(10px);
}

.drawer {
  width: min(720px, 100vw);
  height: 100vh;
  display: grid;
  grid-template-rows: auto 1fr auto;
  background: rgba(250, 245, 237, 0.98);
  border-left: 1px solid var(--border-strong);
  box-shadow: -24px 0 80px rgba(23, 27, 24, 0.14);
}

.drawer-head,
.drawer-actions {
  display: flex;
}

.drawer-head {
  justify-content: space-between;
  gap: 18px;
  padding: 24px 28px;
  border-bottom: 1px solid var(--border-soft);
}

.drawer-kicker,
.field-label {
  margin: 0 0 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.drawer-head h2,
.config-group-head h3 {
  margin: 0;
}

.drawer-actions {
  gap: 12px;
}

.drawer-body {
  overflow: auto;
  padding: 24px 28px;
  display: grid;
  gap: 22px;
}

.config-group {
  padding: 20px;
  border-radius: 24px;
  border: 1px solid var(--border-soft);
  background: rgba(255, 252, 246, 0.74);
}

.config-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  margin-top: 16px;
}

.field {
  display: grid;
  gap: 10px;
}

.field-control {
  min-height: 50px;
  padding: 14px 16px;
  border-radius: 16px;
  border: 1px solid var(--border-soft);
  background: rgba(255, 255, 255, 0.74);
  font: inherit;
}

.drawer-footer {
  padding: 20px 28px 28px;
  border-top: 1px solid var(--border-soft);
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.18s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

@media (max-width: 720px) {
  .config-grid {
    grid-template-columns: 1fr;
  }
}
</style>