feat: 权限细化、商业认证跳转、UI改进
权限: - viewer 管理配方页只显示我的配方,隐藏公共配方库和工具栏 - 高级编辑者可看到精油价目信息不全的红色提示 - 商业核算删除按钮仅管理员可见 - 搜索未收录通知只发管理员和高级编辑者 Tab 可见性: - 所有用户可见:配方查询、管理配方、个人库存、精油价目、商业核算 - 需登录的 tab 点击弹登录框,登录后跳转 - 操作日志/Bug/用户管理仅管理员可见 商业核算: - 未认证用户可看项目列表,点详情提示去认证 - 跳转到我的账户页商业认证区域并自动滚动 其他: - 我的配方和收藏配方默认折叠 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -324,7 +324,7 @@ def symptom_search(body: dict, user=Depends(get_current_user)):
|
||||
# If user reports no match, notify editors
|
||||
if body.get("report_missing"):
|
||||
who = user.get("display_name") or user.get("username") or "用户"
|
||||
for role in ("admin", "senior_editor", "editor"):
|
||||
for role in ("admin", "senior_editor"):
|
||||
conn.execute(
|
||||
"INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)",
|
||||
(role, "🔍 用户需求:" + query,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div v-for="tab in visibleTabs" :key="tab.key"
|
||||
class="nav-tab"
|
||||
:class="{ active: ui.currentSection === tab.key }"
|
||||
@click="goSection(tab.key)"
|
||||
@click="handleTabClick(tab)"
|
||||
>{{ tab.icon }} {{ tab.label }}</div>
|
||||
</div>
|
||||
|
||||
@@ -72,29 +72,24 @@ const route = useRoute()
|
||||
const showUserMenu = ref(false)
|
||||
const navTabsRef = ref(null)
|
||||
|
||||
// Tab 定义,顺序固定:配方查询 → 管理配方 → 个人库存 → 精油价目 → 商业核算 → 操作日志 → Bug → 用户管理
|
||||
// require: 'login' = 需要登录, 'business' = 需要商业认证, 'admin' = 需要管理员
|
||||
// Tab 定义,顺序固定
|
||||
// require: 点击时需要的条件,不满足则提示
|
||||
// hide: 完全隐藏(只有满足条件才显示)
|
||||
const allTabs = [
|
||||
{ key: 'search', icon: '🔍', label: '配方查询' },
|
||||
{ key: 'manage', icon: '📋', label: '管理配方', require: 'login' },
|
||||
{ key: 'inventory', icon: '📦', label: '个人库存', require: 'login' },
|
||||
{ key: 'oils', icon: '💧', label: '精油价目' },
|
||||
{ key: 'projects', icon: '💼', label: '商业核算', require: 'business' },
|
||||
{ key: 'audit', icon: '📜', label: '操作日志', require: 'admin' },
|
||||
{ key: 'bugs', icon: '🐛', label: 'Bug', require: 'admin' },
|
||||
{ key: 'users', icon: '👥', label: '用户管理', require: 'admin' },
|
||||
{ key: 'projects', icon: '💼', label: '商业核算', require: 'login' },
|
||||
{ key: 'audit', icon: '📜', label: '操作日志', hide: 'admin' },
|
||||
{ key: 'bugs', icon: '🐛', label: 'Bug', hide: 'admin' },
|
||||
{ key: 'users', icon: '👥', label: '用户管理', hide: 'admin' },
|
||||
]
|
||||
|
||||
// 根据当前用户角色,过滤出可见的 tab
|
||||
// 未登录: 配方查询, 精油价目
|
||||
// 普通登录: 配方查询, 管理配方, 个人库存, 精油价目
|
||||
// 商业用户: + 商业核算
|
||||
// 管理员: + 操作日志, Bug, 用户管理
|
||||
// 所有人都能看到大部分 tab,bug 和用户管理只有 admin 可见
|
||||
const visibleTabs = computed(() => allTabs.filter(t => {
|
||||
if (!t.require) return true
|
||||
if (t.require === 'login') return auth.isLoggedIn
|
||||
if (t.require === 'business') return auth.isBusiness
|
||||
if (t.require === 'admin') return auth.isAdmin
|
||||
if (!t.hide) return true
|
||||
if (t.hide === 'admin') return auth.isAdmin
|
||||
return true
|
||||
}))
|
||||
const unreadNotifCount = ref(0)
|
||||
@@ -124,6 +119,22 @@ const prMatch = hostname.match(/^pr-(\d+)\./)
|
||||
const isPreview = !!prMatch
|
||||
const prId = prMatch ? prMatch[1] : ''
|
||||
|
||||
function handleTabClick(tab) {
|
||||
if (tab.require === 'login' && !auth.isLoggedIn) {
|
||||
ui.openLogin(() => goSection(tab.key))
|
||||
return
|
||||
}
|
||||
if (tab.require === 'business' && !auth.isBusiness) {
|
||||
if (!auth.isLoggedIn) {
|
||||
ui.openLogin(() => goSection(tab.key))
|
||||
} else {
|
||||
ui.showToast('需要商业认证才能使用此功能')
|
||||
}
|
||||
return
|
||||
}
|
||||
goSection(tab.key)
|
||||
}
|
||||
|
||||
function goSection(name) {
|
||||
ui.showSection(name)
|
||||
router.push('/' + (name === 'search' ? '' : name))
|
||||
@@ -178,12 +189,12 @@ function onSwipeEnd(e) {
|
||||
const currentIdx = tabs.indexOf(ui.currentSection)
|
||||
if (currentIdx < 0) return
|
||||
|
||||
if (dx < 0 && currentIdx < tabs.length - 1) {
|
||||
// 左滑 → 下一个 tab
|
||||
goSection(tabs[currentIdx + 1])
|
||||
} else if (dx > 0 && currentIdx > 0) {
|
||||
// 右滑 → 上一个 tab
|
||||
goSection(tabs[currentIdx - 1])
|
||||
let nextIdx = -1
|
||||
if (dx < 0 && currentIdx < tabs.length - 1) nextIdx = currentIdx + 1
|
||||
else if (dx > 0 && currentIdx > 0) nextIdx = currentIdx - 1
|
||||
if (nextIdx >= 0) {
|
||||
const tab = visibleTabs.value[nextIdx]
|
||||
handleTabClick(tab)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Business Verification -->
|
||||
<div v-if="!auth.isBusiness" class="section-card">
|
||||
<div v-if="!auth.isBusiness" ref="bizCertRef" class="section-card">
|
||||
<h4>💼 商业认证</h4>
|
||||
<p class="hint-text">申请商业认证后可使用商业核算功能。</p>
|
||||
<div class="form-group">
|
||||
@@ -259,8 +259,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, nextTick, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useOilsStore } from '../stores/oils'
|
||||
import { useDiaryStore } from '../stores/diary'
|
||||
@@ -274,8 +274,10 @@ const oils = useOilsStore()
|
||||
const diaryStore = useDiaryStore()
|
||||
const ui = useUiStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const bizCertRef = ref(null)
|
||||
|
||||
const activeTab = ref('brand')
|
||||
const activeTab = ref(route.query.tab || 'brand')
|
||||
const pasteText = ref('')
|
||||
const selectedDiaryId = ref(null)
|
||||
const returnRecipeId = ref(null)
|
||||
@@ -305,6 +307,11 @@ onMounted(async () => {
|
||||
displayName.value = auth.user.display_name || ''
|
||||
await loadBrandSettings()
|
||||
returnRecipeId.value = localStorage.getItem('oil_return_recipe_id') || null
|
||||
// 从商业核算跳转过来,滚到商业认证区域
|
||||
if (route.query.section === 'biz-cert') {
|
||||
await nextTick()
|
||||
bizCertRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
})
|
||||
|
||||
function goBackToRecipe() {
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
v-for="name in filteredOilNames"
|
||||
:key="name + '-' + cardVersion"
|
||||
class="oil-chip"
|
||||
:class="{ 'oil-chip--inactive': getMeta(name)?.isActive === false, 'oil-chip--incomplete': auth.isAdmin && isIncomplete(name) }"
|
||||
:class="{ 'oil-chip--inactive': getMeta(name)?.isActive === false, 'oil-chip--incomplete': auth.canManage && isIncomplete(name) }"
|
||||
:style="chipStyle(name)"
|
||||
@click="openOilDetail(name)"
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Project List -->
|
||||
<div class="toolbar">
|
||||
<h3 class="page-title">💼 商业核算</h3>
|
||||
<button class="btn-primary" @click="createProject">+ 新建项目</button>
|
||||
<button v-if="auth.isBusiness" class="btn-primary" @click="createProject">+ 新建项目</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!selectedProject" class="project-list">
|
||||
@@ -23,7 +23,7 @@
|
||||
成本 {{ oils.fmtPrice(oils.calcCost(p.ingredients)) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="proj-actions" @click.stop>
|
||||
<div v-if="auth.isAdmin" class="proj-actions" @click.stop>
|
||||
<button class="btn-icon-sm" @click="deleteProject(p)" title="删除">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,6 +177,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useOilsStore } from '../stores/oils'
|
||||
import { useRecipesStore } from '../stores/recipes'
|
||||
@@ -188,6 +189,14 @@ const auth = useAuthStore()
|
||||
const oils = useOilsStore()
|
||||
const recipeStore = useRecipesStore()
|
||||
const ui = useUiStore()
|
||||
const router = useRouter()
|
||||
|
||||
async function showCertPrompt() {
|
||||
const ok = await showConfirm('此功能需要商业认证,是否前往申请认证?', { okText: '去认证', cancelText: '取消' })
|
||||
if (ok) {
|
||||
router.push('/mydiary?tab=account§ion=biz-cert')
|
||||
}
|
||||
}
|
||||
|
||||
const projects = ref([])
|
||||
const selectedProject = ref(null)
|
||||
@@ -237,6 +246,10 @@ async function createProject() {
|
||||
}
|
||||
|
||||
function selectProject(p) {
|
||||
if (!auth.isBusiness) {
|
||||
showCertPrompt()
|
||||
return
|
||||
}
|
||||
selectedProject.value = {
|
||||
...p,
|
||||
ingredients: (p.ingredients || []).map(i => ({ ...i })),
|
||||
|
||||
@@ -189,8 +189,8 @@ const selectedCategory = ref(null)
|
||||
const categories = ref([])
|
||||
const selectedRecipeIndex = ref(null)
|
||||
const selectedDiaryRecipe = ref(null)
|
||||
const showMyRecipes = ref(true)
|
||||
const showFavorites = ref(true)
|
||||
const showMyRecipes = ref(false)
|
||||
const showFavorites = ref(false)
|
||||
const catIdx = ref(0)
|
||||
const sharedCount = ref(0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user