Files
ctms/frontend/src/components/FaqCategoryPanel.vue
T
2026-06-10 15:20:06 +08:00

199 lines
4.8 KiB
Vue

<template>
<aside class="panel unified-shell faq-category-panel">
<div class="header">
<span>{{ TEXT.common.labels.category }}</span>
<div v-if="canManageCategories">
<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="">
<span class="cat-icon"><FaqIcon icon="fa-th-large" /></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">
<FaqIcon v-if="c.icon" :icon="c.icon" />
<span v-else>{{ c.name.slice(0, 1) }}</span>
</span>
<span class="name" :title="c.name">{{ c.name }}</span>
<span class="cat-count">{{ categoryCounts[c.id] || 0 }}</span>
</el-menu-item>
</el-menu>
<FaqCategoryManager
v-model="showForm"
:categories="props.categories"
:faqs="props.faqs"
:can-create="canCreateCategory"
:can-update="canUpdateCategory"
:can-delete="canDeleteCategory"
@success="emit('refresh')"
/>
</aside>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import FaqIcon from "./FaqIcon.vue";
import FaqCategoryManager from "./FaqCategoryManager.vue";
import type { FaqCategory } from "../api/faqs";
import { usePermission } from "../utils/permission";
import { TEXT } from "../locales";
const props = defineProps<{
categories: FaqCategory[];
faqs?: any[];
modelValue: string;
}>();
const emit = defineEmits(["update:modelValue", "refresh"]);
const showForm = ref(false);
const { can } = usePermission();
const canCreateCategory = computed(() => can("faq.category.create"));
const canUpdateCategory = computed(() => can("faq.category.update"));
const canDeleteCategory = computed(() => can("faq.category.delete"));
const canManageCategories = computed(() => canCreateCategory.value || canUpdateCategory.value || canDeleteCategory.value);
const sortedCategories = computed(() =>
[...props.categories].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0))
);
const activeCategory = computed(() => props.modelValue || "");
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 openManager = () => {
showForm.value = true;
};
</script>
<style scoped>
.panel {
border: 0;
border-radius: 0;
box-shadow: none;
overflow: hidden;
padding: 24px 14px;
background: transparent;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
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;
}
.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;
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 :deep(svg) {
font-size: 13px;
width: 1em;
height: 1em;
}
.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;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.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>