Files
oil-formula-calculator/frontend/src/views/RecipeManager.vue
Hera Zhao 3a65cb7209
Some checks failed
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / teardown-preview (pull_request) Has been skipped
Test / e2e-test (push) Failing after 54s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 15s
feat: header重排、共享配方、待审核、权限优化
Header:
- 登录按钮固定右侧,flex布局自适应所有屏幕
- 登录后不显示版本号,用户名在右侧
- 商业认证用户显示🏢标识
- 手机端响应式适配

配方共享:
- 个人配方卡片加📤共享按钮
- 提交到公共库,非管理员需审核

管理配方:
- 待审核栏从recipes动态计算(不依赖不存在的API)
- 采纳用/adopt端点,拒绝=确认删除
- senior_editor可编辑精油和公共配方

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 08:24:28 +00:00

980 lines
23 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="recipe-manager">
<!-- Review Bar (admin only) -->
<div v-if="auth.isAdmin && pendingCount > 0" class="review-bar" @click="showPending = !showPending">
📝 待审核配方: {{ pendingCount }}
<span class="toggle-icon">{{ showPending ? '▾' : '▸' }}</span>
</div>
<div v-if="showPending && pendingRecipes.length" class="pending-list">
<div v-for="r in pendingRecipes" :key="r._id" class="pending-item">
<span class="pending-name">{{ r.name }}</span>
<span class="pending-owner">{{ r._owner_name }}</span>
<button class="btn-sm btn-approve" @click="approveRecipe(r)">通过</button>
<button class="btn-sm btn-reject" @click="rejectRecipe(r)">拒绝</button>
</div>
</div>
<!-- Search & Actions Bar -->
<div class="manage-toolbar">
<div class="search-box">
<input
class="search-input"
v-model="manageSearch"
placeholder="搜索配方..."
/>
<button v-if="manageSearch" class="search-clear-btn" @click="manageSearch = ''"></button>
</div>
<button class="btn-primary" @click="showAddOverlay = true">+ 添加配方</button>
<button class="btn-outline" @click="exportExcel">📊 导出Excel</button>
</div>
<!-- Tag Filter Bar -->
<div class="tag-filter-bar">
<button class="tag-toggle-btn" @click="showTagFilter = !showTagFilter">
🏷 标签筛选 {{ showTagFilter ? '' : '' }}
</button>
<div v-if="showTagFilter" class="tag-list">
<span
v-for="tag in recipeStore.allTags"
:key="tag"
class="tag-chip"
:class="{ active: selectedTags.includes(tag) }"
@click="toggleTag(tag)"
>{{ tag }}</span>
</div>
</div>
<!-- Batch Operations -->
<div v-if="selectedIds.size > 0" class="batch-bar">
<span>已选 {{ selectedIds.size }} </span>
<select v-model="batchAction" class="batch-select">
<option value="">批量操作...</option>
<option value="tag">添加标签</option>
<option value="share">分享</option>
<option value="export">导出卡片</option>
<option value="delete">删除</option>
</select>
<button class="btn-sm btn-primary" @click="executeBatch" :disabled="!batchAction">执行</button>
<button class="btn-sm btn-outline" @click="clearSelection">取消选择</button>
</div>
<!-- My Recipes Section (from diary) -->
<div class="recipe-section">
<h3 class="section-title">📖 我的配方 ({{ myRecipes.length }})</h3>
<div class="recipe-list">
<div
v-for="d in myFilteredRecipes"
:key="'diary-' + d.id"
class="recipe-row diary-row"
>
<div class="row-info" @click="editDiaryRecipe(d)">
<span class="row-name">{{ d.name }}</span>
<span class="row-tags">
<span v-for="t in (d.tags || [])" :key="t" class="mini-tag">{{ t }}</span>
</span>
<span class="row-cost">{{ oils.fmtPrice(oils.calcCost(d.ingredients || [])) }}</span>
</div>
<div class="row-actions">
<button class="btn-icon" @click="editDiaryRecipe(d)" title="编辑"></button>
<button class="btn-icon" @click="removeDiaryRecipe(d)" title="删除">🗑</button>
</div>
</div>
<div v-if="myFilteredRecipes.length === 0" class="empty-hint">暂无个人配方</div>
</div>
</div>
<!-- Public Recipes Section -->
<div class="recipe-section">
<h3 class="section-title">🌿 公共配方库 ({{ publicRecipes.length }})</h3>
<div class="recipe-list">
<div
v-for="r in publicFilteredRecipes"
:key="r._id"
class="recipe-row"
:class="{ selected: selectedIds.has(r._id) }"
>
<input
type="checkbox"
:checked="selectedIds.has(r._id)"
@change="toggleSelect(r._id)"
class="row-check"
/>
<div class="row-info" @click="editRecipe(r)">
<span class="row-name">{{ r.name }}</span>
<span class="row-owner">{{ r._owner_name }}</span>
<span class="row-tags">
<span v-for="t in r.tags" :key="t" class="mini-tag">{{ t }}</span>
</span>
<span class="row-cost">{{ oils.fmtPrice(oils.calcCost(r.ingredients)) }}</span>
</div>
<div class="row-actions" v-if="auth.canEditRecipe(r)">
<button class="btn-icon" @click="editRecipe(r)" title="编辑"></button>
<button class="btn-icon" @click="removeRecipe(r)" title="删除">🗑</button>
</div>
</div>
<div v-if="publicFilteredRecipes.length === 0" class="empty-hint">暂无公共配方</div>
</div>
</div>
<!-- Add/Edit Recipe Overlay -->
<div v-if="showAddOverlay" class="overlay" @click.self="closeOverlay">
<div class="overlay-panel">
<div class="overlay-header">
<h3>{{ editingRecipe ? '编辑配方' : '添加配方' }}</h3>
<button class="btn-close" @click="closeOverlay"></button>
</div>
<!-- Smart Paste Section -->
<div class="paste-section">
<textarea
v-model="smartPasteText"
class="paste-input"
placeholder="粘贴配方文本,支持智能识别...&#10;例如: 薰衣草3滴 茶树2滴"
rows="4"
></textarea>
<button class="btn-primary" @click="handleSmartPaste" :disabled="!smartPasteText.trim()">
智能识别
</button>
</div>
<div class="divider-text">或手动输入</div>
<!-- Manual Form -->
<div class="form-group">
<label>配方名称</label>
<input v-model="formName" class="form-input" placeholder="配方名称" />
</div>
<div class="form-group">
<label>成分</label>
<div v-for="(ing, i) in formIngredients" :key="i" class="ing-row">
<select v-model="ing.oil" class="form-select">
<option value="">选择精油</option>
<option v-for="name in oils.oilNames" :key="name" :value="name">{{ name }}</option>
</select>
<input v-model.number="ing.drops" type="number" min="0" class="form-input-sm" placeholder="滴数" />
<button class="btn-icon-sm" @click="formIngredients.splice(i, 1)"></button>
</div>
<button class="btn-outline btn-sm" @click="formIngredients.push({ oil: '', drops: 1 })">+ 添加成分</button>
</div>
<div class="form-group">
<label>备注</label>
<textarea v-model="formNote" class="form-input" rows="2" placeholder="配方备注"></textarea>
</div>
<div class="form-group">
<label>标签</label>
<div class="tag-list">
<span
v-for="tag in recipeStore.allTags"
:key="tag"
class="tag-chip"
:class="{ active: formTags.includes(tag) }"
@click="toggleFormTag(tag)"
>{{ tag }}</span>
</div>
</div>
<div class="overlay-footer">
<button class="btn-outline" @click="closeOverlay">取消</button>
<button class="btn-primary" @click="saveCurrentRecipe">保存</button>
</div>
</div>
</div>
<!-- Tag Picker Overlay -->
<TagPicker
v-if="showTagPicker"
:name="tagPickerName"
:currentTags="tagPickerTags"
:allTags="recipeStore.allTags"
@save="onTagPickerSave"
@close="showTagPicker = false"
/>
</div>
</template>
<script setup>
import { ref, computed, reactive, onMounted, watch } from 'vue'
import { useAuthStore } from '../stores/auth'
import { useOilsStore } from '../stores/oils'
import { useRecipesStore } from '../stores/recipes'
import { useDiaryStore } from '../stores/diary'
import { useUiStore } from '../stores/ui'
import { api } from '../composables/useApi'
import { showConfirm, showPrompt } from '../composables/useDialog'
import { parseSingleBlock } from '../composables/useSmartPaste'
import RecipeCard from '../components/RecipeCard.vue'
import TagPicker from '../components/TagPicker.vue'
const auth = useAuthStore()
const oils = useOilsStore()
const recipeStore = useRecipesStore()
const diaryStore = useDiaryStore()
const ui = useUiStore()
const manageSearch = ref('')
const selectedTags = ref([])
const showTagFilter = ref(false)
const selectedIds = reactive(new Set())
const batchAction = ref('')
const showAddOverlay = ref(false)
const editingRecipe = ref(null)
const showPending = ref(false)
const pendingRecipes = ref([])
const pendingCount = ref(0)
// Form state
const formName = ref('')
const formIngredients = ref([{ oil: '', drops: 1 }])
const formNote = ref('')
const formTags = ref([])
const smartPasteText = ref('')
// Tag picker state
const showTagPicker = ref(false)
const tagPickerName = ref('')
const tagPickerTags = ref([])
// Computed lists
// "我的配方" = diary (user_diary table), personal recipes
const myRecipes = computed(() => diaryStore.userDiary)
// "公共配方库" = all recipes in public library (recipes table)
const publicRecipes = computed(() => recipeStore.recipes)
function filterBySearchAndTags(list) {
let result = list
const q = manageSearch.value.trim().toLowerCase()
if (q) {
result = result.filter(r =>
r.name.toLowerCase().includes(q) ||
(r.ingredients || []).some(ing => (ing.oil || '').toLowerCase().includes(q)) ||
(r.tags && r.tags.some(t => t.toLowerCase().includes(q)))
)
}
if (selectedTags.value.length > 0) {
result = result.filter(r =>
r.tags && selectedTags.value.every(t => r.tags.includes(t))
)
}
return result
}
const myFilteredRecipes = computed(() => filterBySearchAndTags(myRecipes.value))
const publicFilteredRecipes = computed(() => filterBySearchAndTags(publicRecipes.value))
function toggleTag(tag) {
const idx = selectedTags.value.indexOf(tag)
if (idx >= 0) selectedTags.value.splice(idx, 1)
else selectedTags.value.push(tag)
}
function toggleSelect(id) {
if (selectedIds.has(id)) selectedIds.delete(id)
else selectedIds.add(id)
}
function clearSelection() {
selectedIds.clear()
batchAction.value = ''
}
async function executeBatch() {
const ids = [...selectedIds]
if (!ids.length || !batchAction.value) return
if (batchAction.value === 'delete') {
const ok = await showConfirm(`确定删除 ${ids.length} 个配方?`)
if (!ok) return
for (const id of ids) {
await recipeStore.deleteRecipe(id)
}
ui.showToast(`已删除 ${ids.length} 个配方`)
} else if (batchAction.value === 'tag') {
const tagName = await showPrompt('输入要添加的标签:')
if (!tagName) return
for (const id of ids) {
const recipe = recipeStore.recipes.find(r => r._id === id)
if (recipe && !recipe.tags.includes(tagName)) {
recipe.tags.push(tagName)
await recipeStore.saveRecipe(recipe)
}
}
ui.showToast(`已为 ${ids.length} 个配方添加标签`)
} else if (batchAction.value === 'share') {
const text = ids.map(id => {
const r = recipeStore.recipes.find(rec => rec._id === id)
if (!r) return ''
const ings = r.ingredients.map(ing => `${ing.oil} ${ing.drops}`).join('')
return `${r.name}${ings}`
}).filter(Boolean).join('\n\n')
try {
await navigator.clipboard.writeText(text)
ui.showToast('已复制到剪贴板')
} catch {
ui.showToast('复制失败')
}
} else if (batchAction.value === 'export') {
ui.showToast('导出卡片功能开发中')
}
clearSelection()
}
function editRecipe(recipe) {
editingRecipe.value = recipe
formName.value = recipe.name
formIngredients.value = recipe.ingredients.map(i => ({ ...i }))
formNote.value = recipe.note || ''
formTags.value = [...(recipe.tags || [])]
showAddOverlay.value = true
}
function closeOverlay() {
showAddOverlay.value = false
editingRecipe.value = null
resetForm()
}
function resetForm() {
formName.value = ''
formIngredients.value = [{ oil: '', drops: 1 }]
formNote.value = ''
formTags.value = []
smartPasteText.value = ''
}
function handleSmartPaste() {
const result = parseSingleBlock(smartPasteText.value, oils.oilNames)
formName.value = result.name
formIngredients.value = result.ingredients.length > 0
? result.ingredients
: [{ oil: '', drops: 1 }]
if (result.notFound.length > 0) {
ui.showToast(`未识别: ${result.notFound.join(', ')}`)
}
}
function toggleFormTag(tag) {
const idx = formTags.value.indexOf(tag)
if (idx >= 0) formTags.value.splice(idx, 1)
else formTags.value.push(tag)
}
async function saveCurrentRecipe() {
const validIngs = formIngredients.value.filter(i => i.oil && i.drops > 0)
if (!formName.value.trim()) {
ui.showToast('请输入配方名称')
return
}
if (validIngs.length === 0) {
ui.showToast('请至少添加一个成分')
return
}
const payload = {
name: formName.value.trim(),
ingredients: validIngs,
note: formNote.value,
tags: formTags.value,
}
if (editingRecipe.value) {
payload._id = editingRecipe.value._id
payload._version = editingRecipe.value._version
}
try {
await recipeStore.saveRecipe(payload)
ui.showToast(editingRecipe.value ? '配方已更新' : '配方已添加')
closeOverlay()
} catch (e) {
ui.showToast('保存失败: ' + (e.message || '未知错误'))
}
}
// Load diary on mount
onMounted(async () => {
if (auth.isLoggedIn) {
await diaryStore.loadDiary()
}
})
function editDiaryRecipe(diary) {
// For now, navigate to MyDiary page to edit
// TODO: inline editing
ui.showToast('请到「我的」页面编辑个人配方')
}
async function removeDiaryRecipe(diary) {
const ok = await showConfirm(`确定删除个人配方 "${diary.name}"`)
if (!ok) return
try {
await diaryStore.deleteDiary(diary.id)
ui.showToast('已删除')
} catch {
ui.showToast('删除失败')
}
}
async function removeRecipe(recipe) {
const ok = await showConfirm(`确定删除配方 "${recipe.name}"`)
if (!ok) return
try {
await recipeStore.deleteRecipe(recipe._id)
ui.showToast('已删除')
} catch (e) {
ui.showToast('删除失败')
}
}
async function approveRecipe(recipe) {
try {
await api('/api/recipes/' + recipe._id + '/adopt', { method: 'POST' })
ui.showToast('已采纳')
await recipeStore.loadRecipes()
} catch {
ui.showToast('操作失败')
}
}
async function rejectRecipe(recipe) {
const ok = await showConfirm(`确定删除「${recipe.name}」?`)
if (!ok) return
try {
await recipeStore.deleteRecipe(recipe._id)
ui.showToast('已删除')
} catch {
ui.showToast('操作失败')
}
}
async function exportExcel() {
try {
const res = await api('/api/recipes/export-excel')
if (!res.ok) throw new Error('Export failed')
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '配方导出.xlsx'
a.click()
URL.revokeObjectURL(url)
ui.showToast('导出成功')
} catch {
ui.showToast('导出失败')
}
}
function onTagPickerSave(tags) {
formTags.value = tags
showTagPicker.value = false
}
watch(() => recipeStore.recipes, () => {
if (auth.isAdmin) {
const pending = recipeStore.recipes.filter(r => r._owner_id && r._owner_id !== auth.user.id)
pendingRecipes.value = pending
pendingCount.value = pending.length
}
}, { immediate: true })
</script>
<style scoped>
.recipe-manager {
padding: 0 12px 24px;
}
.review-bar {
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
padding: 12px 16px;
border-radius: 10px;
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
color: #e65100;
}
.pending-list {
margin-bottom: 12px;
}
.pending-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: #fffde7;
border-radius: 8px;
margin-bottom: 6px;
font-size: 13px;
}
.pending-name {
font-weight: 600;
flex: 1;
}
.pending-owner {
color: #999;
font-size: 12px;
}
.manage-toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.search-box {
display: flex;
align-items: center;
background: #f8f7f5;
border-radius: 10px;
padding: 2px 8px;
border: 1.5px solid #e5e4e7;
flex: 1;
min-width: 160px;
}
.search-input {
flex: 1;
border: none;
background: transparent;
padding: 8px 6px;
font-size: 14px;
outline: none;
font-family: inherit;
}
.search-clear-btn {
border: none;
background: transparent;
cursor: pointer;
color: #999;
padding: 4px;
}
.tag-filter-bar {
margin-bottom: 12px;
}
.tag-toggle-btn {
background: #f8f7f5;
border: 1.5px solid #e5e4e7;
border-radius: 10px;
padding: 8px 16px;
font-size: 13px;
cursor: pointer;
font-family: inherit;
color: #3e3a44;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.tag-chip {
padding: 4px 12px;
border-radius: 16px;
background: #f0eeeb;
font-size: 12px;
cursor: pointer;
color: #6b6375;
border: 1.5px solid transparent;
transition: all 0.15s;
}
.tag-chip.active {
background: #e8f5e9;
border-color: #7ec6a4;
color: #2e7d5a;
font-weight: 600;
}
.batch-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: #e8f5e9;
border-radius: 10px;
margin-bottom: 12px;
font-size: 13px;
flex-wrap: wrap;
}
.batch-select {
padding: 6px 10px;
border-radius: 8px;
border: 1.5px solid #d4cfc7;
font-size: 13px;
font-family: inherit;
background: #fff;
}
.recipe-section {
margin-bottom: 20px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #3e3a44;
margin: 0 0 10px;
}
.recipe-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.recipe-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #fff;
border: 1.5px solid #e5e4e7;
border-radius: 10px;
transition: all 0.15s;
}
.recipe-row:hover {
border-color: #d4cfc7;
background: #fafaf8;
}
.recipe-row.selected {
border-color: #7ec6a4;
background: #f0faf5;
}
.row-check {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: #4a9d7e;
}
.row-info {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
min-width: 0;
flex-wrap: wrap;
}
.row-name {
font-weight: 600;
font-size: 14px;
color: #3e3a44;
}
.row-owner {
font-size: 11px;
color: #b0aab5;
}
.row-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.mini-tag {
padding: 2px 8px;
border-radius: 10px;
background: #f0eeeb;
font-size: 11px;
color: #6b6375;
}
.row-cost {
font-size: 13px;
color: #4a9d7e;
font-weight: 500;
margin-left: auto;
white-space: nowrap;
}
.row-actions {
display: flex;
gap: 4px;
}
.btn-icon {
border: none;
background: transparent;
cursor: pointer;
font-size: 16px;
padding: 4px;
border-radius: 6px;
}
.btn-icon:hover {
background: #f0eeeb;
}
/* Overlay */
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.overlay-panel {
background: #fff;
border-radius: 16px;
padding: 24px;
width: 100%;
max-width: 520px;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
}
.overlay-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.overlay-header h3 {
margin: 0;
font-size: 17px;
color: #3e3a44;
}
.btn-close {
border: none;
background: #f0eeeb;
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
color: #6b6375;
}
.paste-section {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.paste-input {
width: 100%;
border: 1.5px solid #d4cfc7;
border-radius: 10px;
padding: 10px 14px;
font-size: 13px;
font-family: inherit;
resize: vertical;
outline: none;
box-sizing: border-box;
}
.paste-input:focus {
border-color: #7ec6a4;
}
.divider-text {
text-align: center;
color: #b0aab5;
font-size: 12px;
margin: 12px 0;
position: relative;
}
.divider-text::before,
.divider-text::after {
content: '';
position: absolute;
top: 50%;
width: 35%;
height: 1px;
background: #e5e4e7;
}
.divider-text::before {
left: 0;
}
.divider-text::after {
right: 0;
}
.form-group {
margin-bottom: 14px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
color: #3e3a44;
margin-bottom: 6px;
}
.form-input {
width: 100%;
padding: 10px 14px;
border: 1.5px solid #d4cfc7;
border-radius: 10px;
font-size: 14px;
font-family: inherit;
outline: none;
box-sizing: border-box;
}
.form-input:focus {
border-color: #7ec6a4;
}
.ing-row {
display: flex;
gap: 6px;
align-items: center;
margin-bottom: 6px;
}
.form-select {
flex: 1;
padding: 8px 10px;
border: 1.5px solid #d4cfc7;
border-radius: 8px;
font-size: 13px;
font-family: inherit;
background: #fff;
}
.form-input-sm {
width: 70px;
padding: 8px 10px;
border: 1.5px solid #d4cfc7;
border-radius: 8px;
font-size: 13px;
font-family: inherit;
outline: none;
text-align: center;
}
.btn-icon-sm {
border: none;
background: transparent;
cursor: pointer;
font-size: 14px;
color: #999;
padding: 4px;
}
.overlay-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 18px;
}
/* Buttons */
.btn-primary {
background: linear-gradient(135deg, #7ec6a4 0%, #4a9d7e 100%);
color: #fff;
border: none;
border-radius: 10px;
padding: 9px 20px;
font-size: 13px;
cursor: pointer;
font-family: inherit;
font-weight: 500;
white-space: nowrap;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-primary:disabled {
opacity: 0.5;
cursor: default;
}
.btn-outline {
background: #fff;
color: #6b6375;
border: 1.5px solid #d4cfc7;
border-radius: 10px;
padding: 9px 20px;
font-size: 13px;
cursor: pointer;
font-family: inherit;
font-weight: 500;
white-space: nowrap;
}
.btn-outline:hover {
background: #f8f7f5;
}
.btn-sm {
padding: 6px 14px;
font-size: 12px;
border-radius: 8px;
}
.btn-approve {
background: #4a9d7e;
color: #fff;
border: none;
cursor: pointer;
font-family: inherit;
}
.btn-reject {
background: #ef5350;
color: #fff;
border: none;
cursor: pointer;
font-family: inherit;
}
.empty-hint {
text-align: center;
color: #b0aab5;
font-size: 13px;
padding: 24px 0;
}
.toggle-icon {
font-size: 12px;
}
@media (max-width: 600px) {
.manage-toolbar {
flex-direction: column;
}
}
</style>