feat(faq): refine category icons and project faq flows
This commit is contained in:
@@ -1,45 +1,42 @@
|
||||
<template>
|
||||
<div class="panel unified-shell">
|
||||
<aside class="panel unified-shell faq-category-panel">
|
||||
<div class="header">
|
||||
<span>{{ TEXT.common.labels.category }}</span>
|
||||
<div v-if="canCreateCategory">
|
||||
<PermissionAction action="faq.category.create">
|
||||
<el-button type="primary" size="small" @click="openForm()">{{ TEXT.modules.knowledgeMedicalConsult.newCategory }}</el-button>
|
||||
</PermissionAction>
|
||||
<el-button type="primary" size="small" class="category-create-btn" @click="openManager()">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu :default-active="activeCategory" @select="onSelect" class="menu">
|
||||
<el-menu-item index="">{{ TEXT.common.labels.all }}</el-menu-item>
|
||||
<el-menu-item index="">
|
||||
<span class="cat-icon"><i class="fas fa-th-large"></i></span>
|
||||
<span class="name">{{ TEXT.common.labels.all }}</span>
|
||||
<span class="cat-count">{{ totalFaqCount }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-for="c in sortedCategories" :key="c.id" :index="c.id">
|
||||
<span class="cat-icon">
|
||||
<i v-if="c.icon" :class="['fas', c.icon]"></i>
|
||||
<span v-else>{{ c.name.slice(0, 1) }}</span>
|
||||
</span>
|
||||
<span class="name" :title="c.name">{{ c.name }}</span>
|
||||
<div v-if="canEdit(c) || canDeleteCategory" class="actions">
|
||||
<PermissionAction v-if="canEdit(c)" action="faq.category.update">
|
||||
<el-button type="text" size="small" @click.stop="openForm(c)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</PermissionAction>
|
||||
<PermissionAction v-if="canDeleteCategory" action="faq.category.delete">
|
||||
<el-button type="text" size="small" class="danger" @click.stop="onDelete(c)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</PermissionAction>
|
||||
</div>
|
||||
<span class="cat-count">{{ categoryCounts[c.id] || 0 }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<FaqCategoryForm v-model="showForm" :category="editing" :is-admin="isAdmin" @success="emit('refresh')" />
|
||||
</div>
|
||||
<FaqCategoryManager v-model="showForm" :categories="props.categories" :faqs="props.faqs" @success="emit('refresh')" />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import FaqCategoryForm from "./FaqCategoryForm.vue";
|
||||
import FaqCategoryManager from "./FaqCategoryManager.vue";
|
||||
import type { FaqCategory } from "../api/faqs";
|
||||
import { deleteFaqCategory } from "../api/faqs";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import PermissionAction from "./PermissionAction.vue";
|
||||
import { usePermission } from "../utils/permission";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
categories: FaqCategory[];
|
||||
faqs?: any[];
|
||||
modelValue: string;
|
||||
}>();
|
||||
const emit = defineEmits(["update:modelValue", "refresh"]);
|
||||
@@ -48,52 +45,43 @@ const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
|
||||
const showForm = ref(false);
|
||||
const editing = ref<FaqCategory | null>(null);
|
||||
|
||||
const { can } = usePermission();
|
||||
const isAdmin = computed(() => !!auth.user?.is_admin);
|
||||
const canCreateCategory = computed(() => can("faq.category.create"));
|
||||
const canUpdateCategory = computed(() => can("faq.category.update"));
|
||||
const canDeleteCategory = computed(() => can("faq.category.delete"));
|
||||
|
||||
const sortedCategories = computed(() =>
|
||||
[...props.categories].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0))
|
||||
);
|
||||
|
||||
const activeCategory = computed(() => props.modelValue || "");
|
||||
|
||||
const canEdit = (c: FaqCategory) => {
|
||||
if (!canUpdateCategory.value) return false;
|
||||
if (isAdmin.value) return true;
|
||||
return c.study_id === study.currentStudy?.id;
|
||||
};
|
||||
const categoryCounts = computed(() => {
|
||||
const map: Record<string, number> = {};
|
||||
(props.faqs || []).forEach((f: any) => {
|
||||
if (f.category_id) {
|
||||
map[f.category_id] = (map[f.category_id] || 0) + 1;
|
||||
}
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const totalFaqCount = computed(() => (props.faqs || []).length);
|
||||
|
||||
const onSelect = (id: string) => {
|
||||
if (id && id === activeCategory.value) {
|
||||
openManager();
|
||||
return;
|
||||
}
|
||||
emit("update:modelValue", id);
|
||||
};
|
||||
|
||||
const openForm = (cat?: FaqCategory) => {
|
||||
editing.value = cat || null;
|
||||
showForm.value = true;
|
||||
};
|
||||
const activeIcons = (excludeId?: string) =>
|
||||
sortedCategories.value
|
||||
.filter((c) => c.id !== excludeId && c.icon)
|
||||
.map((c) => c.icon as string);
|
||||
|
||||
const onDelete = async (cat: FaqCategory) => {
|
||||
if (!canDeleteCategory.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
const ok = await ElMessageBox.confirm(
|
||||
TEXT.modules.knowledgeMedicalConsult.confirmDeleteCategory.replace("{name}", cat.name),
|
||||
TEXT.common.labels.tips
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteFaqCategory(cat.id, study.currentStudy?.id);
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
emit("refresh");
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
const openManager = () => {
|
||||
showForm.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -103,26 +91,79 @@ const onDelete = async (cat: FaqCategory) => {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
padding: 24px 14px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
gap: 10px;
|
||||
padding: 0 8px 16px;
|
||||
border-bottom: 1px solid #edf2f8;
|
||||
color: #8b98aa;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.menu {
|
||||
max-height: calc(100vh - 240px);
|
||||
overflow: auto;
|
||||
border-right: 0;
|
||||
padding: 14px 0 0;
|
||||
background: transparent;
|
||||
}
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
.menu :deep(.el-menu-item) {
|
||||
height: 44px;
|
||||
margin: 4px 0;
|
||||
padding: 0 10px !important;
|
||||
border-radius: 10px;
|
||||
color: #5f6f7a;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 44px;
|
||||
}
|
||||
.menu :deep(.el-menu-item:hover) {
|
||||
background: rgba(31, 95, 184, 0.06);
|
||||
color: #1f5fb8;
|
||||
}
|
||||
.menu :deep(.el-menu-item.is-active) {
|
||||
background: #e7f4ff;
|
||||
color: #1f5fb8;
|
||||
}
|
||||
.cat-icon {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 10px;
|
||||
border-radius: 7px;
|
||||
background: rgba(95, 111, 122, 0.08);
|
||||
color: #697982;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.cat-icon i {
|
||||
font-size: 13px;
|
||||
}
|
||||
.menu :deep(.el-menu-item.is-active) .cat-icon {
|
||||
background: #d7ebff;
|
||||
color: #1f5fb8;
|
||||
}
|
||||
.category-create-btn {
|
||||
height: 34px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: #415a77;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 8px 18px rgba(65, 90, 119, 0.16);
|
||||
}
|
||||
.category-create-btn:hover,
|
||||
.category-create-btn:focus {
|
||||
background: #344960;
|
||||
}
|
||||
.name {
|
||||
flex: 1;
|
||||
@@ -131,7 +172,24 @@ const onDelete = async (cat: FaqCategory) => {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.danger {
|
||||
color: #f56c6c;
|
||||
|
||||
.cat-count {
|
||||
flex-shrink: 0;
|
||||
margin-left: 6px;
|
||||
min-width: 20px;
|
||||
height: 18px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(95, 111, 122, 0.08);
|
||||
color: #697982;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu :deep(.el-menu-item.is-active) .cat-count {
|
||||
background: #d7ebff;
|
||||
color: #1f5fb8;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user