feat: 精油排序+容量显示+共享状态+注册日志+防误关
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 15s
Test / e2e-test (push) Successful in 51s

- 配方卡片精油按字母排序
- 配方卡片显示容量(单次/Xml)
- 管理员共享直接显示已共享
- 编辑overlay不会误关(去掉backdrop点击关闭)
- 注册记入活动日志
- 轮播分类已按tag_name匹配

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 18:04:03 +00:00
parent e605da786a
commit de89ccebac
4 changed files with 26 additions and 7 deletions

View File

@@ -355,6 +355,8 @@ def register(body: dict):
"INSERT INTO users (username, token, role, display_name, password) VALUES (?, ?, ?, ?, ?)", "INSERT INTO users (username, token, role, display_name, password) VALUES (?, ?, ?, ?, ?)",
(username, token, "viewer", display_name or username, hash_password(password)) (username, token, "viewer", display_name or username, hash_password(password))
) )
uid = conn.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone()
log_audit(conn, uid["id"] if uid else None, "register", "user", username, display_name or username, None)
conn.commit() conn.commit()
except Exception: except Exception:
conn.close() conn.close()

View File

@@ -5,6 +5,7 @@
<span v-for="tag in visibleTags" :key="tag" class="tag" :class="{ 'tag-reviewed': tag === '已审核' }">{{ tag }}</span> <span v-for="tag in visibleTags" :key="tag" class="tag" :class="{ 'tag-reviewed': tag === '已审核' }">{{ tag }}</span>
</div> </div>
<div class="recipe-card-oils">{{ oilNames }}</div> <div class="recipe-card-oils">{{ oilNames }}</div>
<span v-if="volumeLabel" class="recipe-card-volume">{{ volumeLabel }}</span>
<div class="recipe-card-bottom"> <div class="recipe-card-bottom">
<div class="recipe-card-price">💰 {{ priceInfo.cost }}</div> <div class="recipe-card-price">💰 {{ priceInfo.cost }}</div>
<button <button
@@ -41,10 +42,20 @@ const visibleTags = computed(() => {
}) })
const oilNames = computed(() => const oilNames = computed(() =>
props.recipe.ingredients.map(i => i.oil).join('、') [...props.recipe.ingredients].sort((a, b) => a.oil.localeCompare(b.oil, 'zh')).map(i => i.oil).join('、')
) )
const priceInfo = computed(() => oilsStore.fmtCostWithRetail(props.recipe.ingredients)) const priceInfo = computed(() => oilsStore.fmtCostWithRetail(props.recipe.ingredients))
const isFav = computed(() => recipesStore.isFavorite(props.recipe)) const isFav = computed(() => recipesStore.isFavorite(props.recipe))
const volumeLabel = computed(() => {
const ings = props.recipe.ingredients || []
const coco = ings.find(i => i.oil === '椰子油')
if (!coco || !coco.drops) return ''
const totalDrops = ings.reduce((s, i) => s + (i.drops || 0), 0)
const ml = totalDrops / 18.6
if (ml <= 2) return '单次'
return `${Math.round(ml)}ml`
})
</script> </script>
<style scoped> <style scoped>
@@ -98,6 +109,13 @@ const isFav = computed(() => recipesStore.isFavorite(props.recipe))
line-height: 1.7; line-height: 1.7;
} }
.recipe-card-volume {
font-size: 10px;
color: #b0aab5;
display: block;
margin-bottom: 4px;
}
.recipe-card-bottom { .recipe-card-bottom {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -96,6 +96,7 @@ const ACTION_MAP = {
reject_business: '拒绝商业认证', reject_business: '拒绝商业认证',
grant_business: '开通商业认证', grant_business: '开通商业认证',
revoke_business: '撤销商业认证', revoke_business: '撤销商业认证',
register: '用户注册',
} }
const actionTypes = [ const actionTypes = [

View File

@@ -178,7 +178,7 @@
</div> </div>
<!-- Add/Edit Recipe Overlay --> <!-- Add/Edit Recipe Overlay -->
<div v-if="showAddOverlay" class="overlay" @click.self="closeOverlay"> <div v-if="showAddOverlay" class="overlay">
<div class="overlay-panel"> <div class="overlay-panel">
<div class="overlay-header"> <div class="overlay-header">
<h3>{{ editingRecipe ? '编辑配方' : '添加配方' }}</h3> <h3>{{ editingRecipe ? '编辑配方' : '添加配方' }}</h3>
@@ -1312,12 +1312,10 @@ function diaryMatchesPublic(d) {
} }
function getDiaryShareStatus(d) { function getDiaryShareStatus(d) {
// Check pending (owned by user in public library, not yet adopted) // Admin/senior_editor share directly — check public match first
if (sharedCount.value.pendingNames.includes(d.name)) return 'pending'
// Check if public library has same recipe with same content
if (diaryMatchesPublic(d)) return 'shared' if (diaryMatchesPublic(d)) return 'shared'
// Check adopted names from audit log // Non-admin: check pending (owned by user, not yet adopted)
if (sharedCount.value.adoptedNames.includes(d.name) && diaryMatchesPublic(d)) return 'shared' if (!auth.isAdmin && !auth.canManage && sharedCount.value.pendingNames.includes(d.name)) return 'pending'
return null return null
} }