feat: 审核流程完善 + 共享状态提示 + 贡献统计含拒绝
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 4s
PR Preview / deploy-preview (pull_request) Successful in 16s
Test / e2e-test (push) Failing after 55s

审核流程:
- 高级编辑者可看到待审核配方,点击推荐通过→通知管理员
- 高级编辑者可直接拒绝(和管理员相同逻辑)
- 管理员收到推荐通知后最终决定
- 去审核通知点击自动展开待审核列表
- 新增 /api/recipes/{id}/recommend 端点

共享:
- 已共享配方再点共享→提示"已共享,感谢贡献"
- 审核中配方再点共享→提示"正在审核中,请耐心等待"

贡献统计:
- 被拒绝的配方也计入总申请数(0/1不会变回0/0)
- reject_recipe日志记录from_user

其他:
- 配方卡片去掉编辑按钮

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 18:28:05 +00:00
parent 97c53bb3c3
commit 866950c2f6
4 changed files with 90 additions and 24 deletions

View File

@@ -14,11 +14,6 @@
</button>
</div>
<div style="flex:1"></div>
<button
v-if="canEditThisRecipe"
class="action-btn action-btn-sm"
@click="goEditInManager"
>编辑</button>
<button class="detail-close-btn" @click="handleClose"></button>
</div>

View File

@@ -130,8 +130,14 @@ function isSearchMissing(n) {
}
function isReviewable(n) {
if (!auth.isAdmin || !n.title) return false
return n.title.includes('待审核') || n.title.includes('商业认证') || n.title.includes('申请')
if (!n.title) return false
// Admin: review recipe/business/applications
if (auth.isAdmin) {
return n.title.includes('待审核') || n.title.includes('商业认证') || n.title.includes('申请') || n.title.includes('推荐通过')
}
// Senior editor: assigned reviews
if (auth.canManage && n.title.includes('请审核')) return true
return false
}
async function markAdded(n) {
@@ -141,7 +147,8 @@ async function markAdded(n) {
function goReview(n) {
markOneRead(n)
emit('close')
if (n.title.includes('配方')) {
if (n.title.includes('配方') || n.title.includes('审核') || n.title.includes('推荐')) {
localStorage.setItem('oil_open_pending', '1')
router.push('/manage')
} else if (n.title.includes('商业认证') || n.title.includes('申请')) {
router.push('/users')

View File

@@ -1,7 +1,7 @@
<template>
<div class="recipe-manager">
<!-- Review Bar (admin only) -->
<div v-if="auth.isAdmin && pendingCount > 0" class="review-bar" @click="showPending = !showPending" >
<!-- Review Bar (admin + senior_editor with assigned reviews) -->
<div v-if="(auth.isAdmin || auth.canManage) && pendingCount > 0" class="review-bar" @click="showPending = !showPending" >
📝 待审核配方: {{ pendingCount }}
<span class="toggle-icon">{{ showPending ? '▾' : '▸' }}</span>
</div>
@@ -9,16 +9,22 @@
<div v-for="r in pendingRecipes" :key="r._id" class="pending-item">
<span class="pending-name clickable" @click="openRecipeDetail(r)">{{ 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>
<button class="btn-sm btn-outline" @click="r._showAssign = !r._showAssign">指派</button>
<div v-if="r._showAssign" class="assign-row">
<select v-model="r._assignTo" class="assign-select">
<option value="">选择审核人...</option>
<option v-for="u in seniorEditors" :key="u.id" :value="u.id">{{ u.display_name || u.username }}</option>
</select>
<button class="btn-sm btn-primary" @click="assignReview(r)" :disabled="!r._assignTo">发送</button>
</div>
<template v-if="auth.isAdmin">
<button class="btn-sm btn-approve" @click="approveRecipe(r)">通过</button>
<button class="btn-sm btn-reject" @click="rejectRecipe(r)">拒绝</button>
<button class="btn-sm btn-outline" @click="r._showAssign = !r._showAssign">指派</button>
<div v-if="r._showAssign" class="assign-row">
<select v-model="r._assignTo" class="assign-select">
<option value="">选择审核人...</option>
<option v-for="u in seniorEditors" :key="u.id" :value="u.id">{{ u.display_name || u.username }}</option>
</select>
<button class="btn-sm btn-primary" @click="assignReview(r)" :disabled="!r._assignTo">发送</button>
</div>
</template>
<template v-else>
<button class="btn-sm btn-approve" @click="recommendApprove(r)">推荐通过</button>
<button class="btn-sm btn-reject" @click="rejectRecipe(r)">拒绝</button>
</template>
</div>
</div>
@@ -127,7 +133,7 @@
<span v-else-if="getDiaryShareStatus(d) === 'pending'" class="share-tag pending">等待审核</span>
</div>
<div class="row-actions">
<button v-if="getDiaryShareStatus(d) !== 'shared'" class="btn-icon" @click="shareDiaryToPublic(d)" title="共享到公共配方库">📤</button>
<button class="btn-icon" @click="handleShare(d)" title="共享到公共配方库">📤</button>
<button class="btn-icon" @click="removeDiaryRecipe(d)" title="删除">🗑</button>
</div>
</div>
@@ -1110,6 +1116,11 @@ onMounted(async () => {
}
} catch {}
}
// Auto-expand pending if navigated from notification
if (localStorage.getItem('oil_open_pending')) {
localStorage.removeItem('oil_open_pending')
showPending.value = true
}
// Open recipe editor if redirected from card view
const editId = localStorage.getItem('oil_edit_recipe_id')
if (editId) {
@@ -1161,6 +1172,31 @@ function getDiaryShareStatus(d) {
return null
}
function handleShare(d) {
const status = getDiaryShareStatus(d)
if (status === 'shared') {
ui.showToast('该配方已共享到公共配方库,感谢你的贡献!')
return
}
if (status === 'pending') {
ui.showToast('该配方正在审核中,请耐心等待')
return
}
shareDiaryToPublic(d)
}
async function recommendApprove(recipe) {
try {
await api('/api/recipes/' + recipe._id + '/recommend', {
method: 'POST',
body: JSON.stringify({ recommendation: 'approve' }),
})
ui.showToast('已推荐通过,等待管理员最终审核')
} catch {
ui.showToast('操作失败')
}
}
async function shareDiaryToPublic(diary) {
const ok = await showConfirm(`将「${diary.name}」共享到公共配方库?`)
if (!ok) return