UI: 管理配方界面按角色调整
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 12s
Test / e2e-test (push) Has been cancelled

- 管理员: 搜索 + 导出Excel | 添加 + 全选 + 标签 + 批量
- 编辑者: 搜索 | 添加 + 全选 + 标签 + 批量(无导出)
- 普通用户: 全选 + 标签(无添加无导出)
- 批量操作改为下拉选择,内联在工具栏
- 我的配方和公共配方库默认折叠

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 10:57:03 +00:00
parent a81f7788c0
commit e78a446abe

View File

@@ -14,35 +14,39 @@
</div> </div>
</div> </div>
<!-- Search & Actions Bar (editor+) --> <!-- Row 1: Search (editor+) + Export (admin only) -->
<template v-if="auth.canEdit"> <template v-if="auth.canEdit">
<div class="manage-toolbar"> <div class="manage-toolbar">
<div class="search-box"> <div class="search-box">
<input <input class="search-input" v-model="manageSearch" placeholder="搜索配方..." />
class="search-input"
v-model="manageSearch"
placeholder="搜索配方..."
/>
<button v-if="manageSearch" class="search-clear-btn" @click="manageSearch = ''"></button> <button v-if="manageSearch" class="search-clear-btn" @click="manageSearch = ''"></button>
</div> </div>
<div class="toolbar-actions"> <button v-if="auth.isAdmin" class="btn-outline btn-sm" @click="exportExcel">📥 导出Excel</button>
<button class="btn-outline btn-sm" @click="exportExcel">📥 导出Excel</button>
</div>
</div> </div>
</template> </template>
<!-- Tag Filter & Select All (visible to all) --> <!-- Row 2: Add + Select + Tags + Batch -->
<div class="tag-filter-bar"> <div class="tag-filter-bar">
<button v-if="auth.canEdit" class="btn-outline btn-sm" @click="showAddOverlay = true">+ 添加配方</button>
<button <button
class="btn-sm" class="btn-sm"
:class="selectedDiaryIds.size > 0 ? 'btn-select-active' : 'btn-outline'" :class="selectedDiaryIds.size > 0 ? 'btn-select-active' : 'btn-outline'"
@click="toggleSelectAllDiary" @click="toggleSelectAllDiary"
>全选</button> >全选</button>
<button class="btn-outline btn-sm" @click="showAddOverlay = true">+ 添加配方</button>
<button class="tag-toggle-btn" @click="showTagFilter = !showTagFilter"> <button class="tag-toggle-btn" @click="showTagFilter = !showTagFilter">
🏷 标签筛选 {{ showTagFilter ? '' : '' }} 🏷 标签筛选 {{ showTagFilter ? '' : '' }}
</button> </button>
<!-- Batch (inline when selected) -->
<template v-if="selectedIds.size > 0 || selectedDiaryIds.size > 0">
<span class="batch-count">{{ selectedIds.size + selectedDiaryIds.size }}</span>
<select v-model="batchAction" class="batch-select" @change="onBatchSelect">
<option value="">批量操作...</option>
<option value="tag">打标签</option>
<option v-if="selectedDiaryIds.size > 0" value="share_public">分享到公共库</option>
<option value="delete">删除</option>
</select>
<button class="btn-sm btn-outline" @click="clearSelection">取消</button>
</template>
<div v-if="showTagFilter" class="tag-list"> <div v-if="showTagFilter" class="tag-list">
<span <span
v-for="tag in recipeStore.allTags" v-for="tag in recipeStore.allTags"
@@ -54,16 +58,6 @@
</div> </div>
</div> </div>
<!-- Batch Operations -->
<div v-if="selectedIds.size > 0 || selectedDiaryIds.size > 0" class="batch-bar">
<span>已选 {{ selectedIds.size + selectedDiaryIds.size }} </span>
<button class="btn-sm btn-outline" @click="executeBatchAction('tag')">🏷 打标签</button>
<button class="btn-sm btn-outline" @click="executeBatchAction('share_public')" v-if="selectedDiaryIds.size > 0">📤 分享到公共库</button>
<button class="btn-sm btn-outline" @click="executeBatchAction('export')">📷 导出卡片</button>
<button class="btn-sm btn-danger-outline" @click="executeBatchAction('delete')">🗑 删除</button>
<button class="btn-sm btn-outline" @click="clearSelection">取消</button>
</div>
<!-- My Recipes Section (from diary) --> <!-- My Recipes Section (from diary) -->
<div class="recipe-section"> <div class="recipe-section">
<h3 class="section-title clickable" @click="showMyRecipes = !showMyRecipes"> <h3 class="section-title clickable" @click="showMyRecipes = !showMyRecipes">
@@ -452,6 +446,14 @@ function toggleDiarySelect(id) {
function clearSelection() { function clearSelection() {
selectedIds.clear() selectedIds.clear()
selectedDiaryIds.clear() selectedDiaryIds.clear()
batchAction.value = ''
}
function onBatchSelect() {
if (batchAction.value) {
executeBatchAction(batchAction.value)
batchAction.value = ''
}
} }
function toggleSelectAllDiary() { function toggleSelectAllDiary() {
@@ -797,7 +799,8 @@ async function saveAllParsed() {
const sharedCount = ref({ adopted: 0, total: 0 }) const sharedCount = ref({ adopted: 0, total: 0 })
const previewRecipeIndex = ref(null) const previewRecipeIndex = ref(null)
const showMyRecipes = ref(true) const batchAction = ref('')
const showMyRecipes = ref(false)
const showPublicRecipes = ref(false) const showPublicRecipes = ref(false)
const showReviewHistory = ref(false) const showReviewHistory = ref(false)
const reviewHistory = ref([]) const reviewHistory = ref([])
@@ -1399,6 +1402,8 @@ watch(() => recipeStore.recipes, () => {
.drops-sm:focus { border-color: #7ec6a4; } .drops-sm:focus { border-color: #7ec6a4; }
.select-sm { padding: 4px 6px; border: 1.5px solid #d4cfc7; border-radius: 6px; font-size: 12px; font-family: inherit; background: #fff; width: auto; } .select-sm { padding: 4px 6px; border: 1.5px solid #d4cfc7; border-radius: 6px; font-size: 12px; font-family: inherit; background: #fff; width: auto; }
.btn-select-active { background: #e8f5e9; color: #2e7d5a; border: 1.5px solid #7ec6a4; border-radius: 10px; padding: 7px 14px; font-size: 13px; cursor: pointer; font-family: inherit; font-weight: 600; } .btn-select-active { background: #e8f5e9; color: #2e7d5a; border: 1.5px solid #7ec6a4; border-radius: 10px; padding: 7px 14px; font-size: 13px; cursor: pointer; font-family: inherit; font-weight: 600; }
.batch-count { font-size: 12px; color: #4a9d7e; font-weight: 600; white-space: nowrap; }
.batch-select { padding: 5px 8px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 12px; font-family: inherit; background: #fff; }
.divider-text { .divider-text {
text-align: center; text-align: center;