|
|
@@ -0,0 +1,573 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed } from 'vue'
|
|
|
+import type { Category } from '../api/types'
|
|
|
+import { categoryApiObj } from '../api/service'
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ serviceCounts: Record<string, number>
|
|
|
+}>()
|
|
|
+
|
|
|
+const emit = defineEmits<{
|
|
|
+ select: [categoryId: string | null]
|
|
|
+}>()
|
|
|
+
|
|
|
+const categories = ref<Category[]>([])
|
|
|
+const selectedId = ref<string | null>(null)
|
|
|
+const expandedIds = ref<Set<string>>(new Set())
|
|
|
+
|
|
|
+// 内联编辑状态
|
|
|
+const editingId = ref<string | null>(null)
|
|
|
+const editingName = ref('')
|
|
|
+const addingParentId = ref<string | null | 'ROOT'>('NONE') // 'NONE' = 不在添加, 'ROOT' = 根级, 其他 = parentId
|
|
|
+const newName = ref('')
|
|
|
+
|
|
|
+// 右键菜单
|
|
|
+const contextMenu = ref<{ x: number; y: number; categoryId: string } | null>(null)
|
|
|
+
|
|
|
+/** 树节点 */
|
|
|
+interface TreeNode {
|
|
|
+ category: Category
|
|
|
+ children: TreeNode[]
|
|
|
+}
|
|
|
+
|
|
|
+/** 扁平化渲染行 */
|
|
|
+interface FlatRow {
|
|
|
+ category: Category
|
|
|
+ depth: number
|
|
|
+ hasChildren: boolean
|
|
|
+}
|
|
|
+
|
|
|
+/** 将树扁平化为行列表(根据展开状态) */
|
|
|
+const flatRows = computed<FlatRow[]>(() => {
|
|
|
+ const map = new Map<string, TreeNode>()
|
|
|
+ const roots: TreeNode[] = []
|
|
|
+
|
|
|
+ for (const cat of categories.value) {
|
|
|
+ map.set(cat.id, { category: cat, children: [] })
|
|
|
+ }
|
|
|
+ for (const cat of categories.value) {
|
|
|
+ const node = map.get(cat.id)!
|
|
|
+ if (cat.parentId && map.has(cat.parentId)) {
|
|
|
+ map.get(cat.parentId)!.children.push(node)
|
|
|
+ } else {
|
|
|
+ roots.push(node)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const sortNodes = (nodes: TreeNode[]) => {
|
|
|
+ nodes.sort((a, b) => (a.category.sortOrder || 0) - (b.category.sortOrder || 0))
|
|
|
+ nodes.forEach(n => sortNodes(n.children))
|
|
|
+ }
|
|
|
+ sortNodes(roots)
|
|
|
+
|
|
|
+ const result: FlatRow[] = []
|
|
|
+ const flatten = (nodes: TreeNode[], depth: number) => {
|
|
|
+ for (const node of nodes) {
|
|
|
+ result.push({
|
|
|
+ category: node.category,
|
|
|
+ depth,
|
|
|
+ hasChildren: node.children.length > 0,
|
|
|
+ })
|
|
|
+ if (expandedIds.value.has(node.category.id)) {
|
|
|
+ flatten(node.children, depth + 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ flatten(roots, 0)
|
|
|
+ return result
|
|
|
+})
|
|
|
+
|
|
|
+const totalCount = computed(() =>
|
|
|
+ Object.values(props.serviceCounts).reduce((a, b) => a + b, 0)
|
|
|
+)
|
|
|
+
|
|
|
+/** 递归统计分类及子分类下的服务数 */
|
|
|
+const getCategoryCount = (id: string): number => {
|
|
|
+ let count = props.serviceCounts[id] || 0
|
|
|
+ for (const cat of categories.value) {
|
|
|
+ if (cat.parentId === id) count += getCategoryCount(cat.id)
|
|
|
+ }
|
|
|
+ return count
|
|
|
+}
|
|
|
+
|
|
|
+/** 加载分类列表 */
|
|
|
+const fetchCategories = async () => {
|
|
|
+ try {
|
|
|
+ categories.value = await categoryApiObj.list()
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载分类失败', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 选中分类 */
|
|
|
+const selectCategory = (id: string | null) => {
|
|
|
+ selectedId.value = id
|
|
|
+ emit('select', id)
|
|
|
+}
|
|
|
+
|
|
|
+/** 切换展开/折叠 */
|
|
|
+const toggleExpand = (id: string) => {
|
|
|
+ if (expandedIds.value.has(id)) {
|
|
|
+ expandedIds.value.delete(id)
|
|
|
+ } else {
|
|
|
+ expandedIds.value.add(id)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 展开所有父节点(给外部用) */
|
|
|
+const expandParents = (catId: string) => {
|
|
|
+ let current = categories.value.find(c => c.id === catId)
|
|
|
+ while (current?.parentId) {
|
|
|
+ expandedIds.value.add(current.parentId)
|
|
|
+ current = categories.value.find(c => c.id === current!.parentId)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 开始添加 */
|
|
|
+const startAdd = (parentId: string | null) => {
|
|
|
+ addingParentId.value = parentId ?? 'ROOT'
|
|
|
+ newName.value = ''
|
|
|
+ if (parentId) expandedIds.value.add(parentId)
|
|
|
+}
|
|
|
+
|
|
|
+/** 确认添加 */
|
|
|
+const confirmAdd = async () => {
|
|
|
+ const name = newName.value.trim()
|
|
|
+ if (!name) return
|
|
|
+ const parentId = addingParentId.value === 'ROOT' ? null : addingParentId.value
|
|
|
+ try {
|
|
|
+ await categoryApiObj.create({ id: '', name, parentId: parentId ?? undefined, sortOrder: 0 })
|
|
|
+ await fetchCategories()
|
|
|
+ } catch (e) {
|
|
|
+ console.error('添加分类失败', e)
|
|
|
+ }
|
|
|
+ cancelAdd()
|
|
|
+}
|
|
|
+
|
|
|
+const cancelAdd = () => {
|
|
|
+ addingParentId.value = 'NONE'
|
|
|
+ newName.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+/** 开始重命名 */
|
|
|
+const startRename = (cat: Category) => {
|
|
|
+ editingId.value = cat.id
|
|
|
+ editingName.value = cat.name
|
|
|
+ contextMenu.value = null
|
|
|
+}
|
|
|
+
|
|
|
+/** 确认重命名 */
|
|
|
+const confirmRename = async () => {
|
|
|
+ const name = editingName.value.trim()
|
|
|
+ if (!name || !editingId.value) return
|
|
|
+ try {
|
|
|
+ const cat = categories.value.find(c => c.id === editingId.value)
|
|
|
+ if (cat) {
|
|
|
+ await categoryApiObj.update(cat.id, { ...cat, name })
|
|
|
+ await fetchCategories()
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('重命名失败', e)
|
|
|
+ }
|
|
|
+ cancelRename()
|
|
|
+}
|
|
|
+
|
|
|
+const cancelRename = () => {
|
|
|
+ editingId.value = null
|
|
|
+ editingName.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+/** 删除分类 */
|
|
|
+const deleteCategory = async (id: string) => {
|
|
|
+ contextMenu.value = null
|
|
|
+ const cat = categories.value.find(c => c.id === id)
|
|
|
+ if (!cat) return
|
|
|
+ const childCount = categories.value.filter(c => c.parentId === id).length
|
|
|
+ const msg = childCount > 0
|
|
|
+ ? `分类"${cat.name}"下还有 ${childCount} 个子分类,删除后子分类将变为根分类。确定删除?`
|
|
|
+ : `确定删除分类"${cat.name}"?`
|
|
|
+ if (!confirm(msg)) return
|
|
|
+ try {
|
|
|
+ await categoryApiObj.delete(id)
|
|
|
+ await fetchCategories()
|
|
|
+ if (selectedId.value === id) selectCategory(null)
|
|
|
+ } catch (e) {
|
|
|
+ console.error('删除分类失败', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 右键菜单 */
|
|
|
+const showContextMenu = (e: MouseEvent, cat: Category) => {
|
|
|
+ e.preventDefault()
|
|
|
+ e.stopPropagation()
|
|
|
+ contextMenu.value = { x: e.clientX, y: e.clientY, categoryId: cat.id }
|
|
|
+}
|
|
|
+
|
|
|
+const closeContextMenu = () => {
|
|
|
+ contextMenu.value = null
|
|
|
+}
|
|
|
+
|
|
|
+/** 获取某行之后插入输入框的判断 */
|
|
|
+const shouldShowAddInput = (row: FlatRow): boolean => {
|
|
|
+ if (addingParentId.value === 'NONE') return false
|
|
|
+ if (addingParentId.value === 'ROOT') return false // 根级在末尾显示
|
|
|
+ return addingParentId.value === row.category.id
|
|
|
+}
|
|
|
+
|
|
|
+const showRootAddInput = computed(() => addingParentId.value === 'ROOT')
|
|
|
+
|
|
|
+defineExpose({ fetchCategories, selectedId, expandParents, categories })
|
|
|
+
|
|
|
+fetchCategories()
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="category-sidebar" @click="closeContextMenu">
|
|
|
+ <div class="sidebar-header">
|
|
|
+ <span class="sidebar-title">分类</span>
|
|
|
+ <button class="add-root-btn" @click.stop="startAdd(null)" title="添加根分类">
|
|
|
+ <i class="pi pi-plus"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="tree-container">
|
|
|
+ <!-- 全部服务 -->
|
|
|
+ <div
|
|
|
+ class="tree-item all-item"
|
|
|
+ :class="{ active: selectedId === null }"
|
|
|
+ @click.stop="selectCategory(null)"
|
|
|
+ >
|
|
|
+ <i class="pi pi-th-large tree-icon"></i>
|
|
|
+ <span class="tree-label">全部服务</span>
|
|
|
+ <span class="tree-count">{{ totalCount }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 扁平化渲染树 -->
|
|
|
+ <template v-for="row in flatRows" :key="row.category.id">
|
|
|
+ <!-- 添加子分类输入框(在目标分类下方) -->
|
|
|
+ <div
|
|
|
+ v-if="shouldShowAddInput(row)"
|
|
|
+ class="add-input-row"
|
|
|
+ :style="{ paddingLeft: (12 + (row.depth + 1) * 20) + 'px' }"
|
|
|
+ @click.stop
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ v-model="newName"
|
|
|
+ type="text"
|
|
|
+ placeholder="分类名称"
|
|
|
+ class="add-input"
|
|
|
+ @keyup.enter="confirmAdd"
|
|
|
+ @keyup.escape="cancelAdd"
|
|
|
+ ref="addInputRef"
|
|
|
+ />
|
|
|
+ <button class="inline-btn confirm" @click.stop="confirmAdd"><i class="pi pi-check"></i></button>
|
|
|
+ <button class="inline-btn cancel" @click.stop="cancelAdd"><i class="pi pi-times"></i></button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分类节点 -->
|
|
|
+ <div
|
|
|
+ class="tree-item"
|
|
|
+ :class="{ active: selectedId === row.category.id }"
|
|
|
+ :style="{ paddingLeft: (12 + row.depth * 20) + 'px' }"
|
|
|
+ @click.stop="selectCategory(row.category.id)"
|
|
|
+ @contextmenu.stop="showContextMenu($event, row.category)"
|
|
|
+ >
|
|
|
+ <span class="tree-toggle" @click.stop="toggleExpand(row.category.id)">
|
|
|
+ <i v-if="row.hasChildren" :class="['pi', expandedIds.has(row.category.id) ? 'pi-chevron-down' : 'pi-chevron-right']"></i>
|
|
|
+ <span v-else class="toggle-placeholder"></span>
|
|
|
+ </span>
|
|
|
+ <i class="pi pi-folder tree-icon"></i>
|
|
|
+
|
|
|
+ <!-- 重命名模式 -->
|
|
|
+ <template v-if="editingId === row.category.id">
|
|
|
+ <input
|
|
|
+ v-model="editingName"
|
|
|
+ type="text"
|
|
|
+ class="add-input"
|
|
|
+ @keyup.enter="confirmRename"
|
|
|
+ @keyup.escape="cancelRename"
|
|
|
+ @click.stop
|
|
|
+ />
|
|
|
+ <button class="inline-btn confirm" @click.stop="confirmRename"><i class="pi pi-check"></i></button>
|
|
|
+ <button class="inline-btn cancel" @click.stop="cancelRename"><i class="pi pi-times"></i></button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 显示模式 -->
|
|
|
+ <template v-else>
|
|
|
+ <span class="tree-label">{{ row.category.name }}</span>
|
|
|
+ <span class="tree-count">{{ getCategoryCount(row.category.id) }}</span>
|
|
|
+ <button class="tree-add-btn" @click.stop="startAdd(row.category.id)" title="添加子分类">
|
|
|
+ <i class="pi pi-plus"></i>
|
|
|
+ </button>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 根级添加输入框(在末尾) -->
|
|
|
+ <div v-if="showRootAddInput" class="add-input-row" :style="{ paddingLeft: '32px' }" @click.stop>
|
|
|
+ <input
|
|
|
+ v-model="newName"
|
|
|
+ type="text"
|
|
|
+ placeholder="分类名称"
|
|
|
+ class="add-input"
|
|
|
+ @keyup.enter="confirmAdd"
|
|
|
+ @keyup.escape="cancelAdd"
|
|
|
+ />
|
|
|
+ <button class="inline-btn confirm" @click.stop="confirmAdd"><i class="pi pi-check"></i></button>
|
|
|
+ <button class="inline-btn cancel" @click.stop="cancelAdd"><i class="pi pi-times"></i></button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 空状态 -->
|
|
|
+ <div v-if="flatRows.length === 0 && !showRootAddInput" class="empty-tip">
|
|
|
+ 点击 <i class="pi pi-plus" style="font-size: 11px"></i> 添加分类
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右键菜单 -->
|
|
|
+ <div
|
|
|
+ v-if="contextMenu"
|
|
|
+ class="context-menu"
|
|
|
+ :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
|
|
+ @click.stop
|
|
|
+ >
|
|
|
+ <button @click="startRename(categories.find(c => c.id === contextMenu?.categoryId)!)">
|
|
|
+ <i class="pi pi-pencil"></i> 重命名
|
|
|
+ </button>
|
|
|
+ <button @click="startAdd(contextMenu!.categoryId)">
|
|
|
+ <i class="pi pi-plus"></i> 添加子分类
|
|
|
+ </button>
|
|
|
+ <button class="danger" @click="deleteCategory(contextMenu!.categoryId)">
|
|
|
+ <i class="pi pi-trash"></i> 删除
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.category-sidebar {
|
|
|
+ width: 240px;
|
|
|
+ min-width: 240px;
|
|
|
+ background: #fff;
|
|
|
+ border-right: 1px solid #e5e7eb;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100vh;
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.sidebar-header {
|
|
|
+ padding: 14px 16px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ border-bottom: 1px solid #f3f4f6;
|
|
|
+}
|
|
|
+
|
|
|
+.sidebar-title {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #6b7280;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+}
|
|
|
+
|
|
|
+.add-root-btn {
|
|
|
+ background: none;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 2px 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 12px;
|
|
|
+ transition: all 0.15s;
|
|
|
+}
|
|
|
+
|
|
|
+.add-root-btn:hover {
|
|
|
+ background: #f3f4f6;
|
|
|
+ color: #4f46e5;
|
|
|
+ border-color: #4f46e5;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-container {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 7px 12px;
|
|
|
+ padding-right: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ gap: 6px;
|
|
|
+ transition: background 0.1s;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #374151;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-item:hover {
|
|
|
+ background: #f9fafb;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-item.active {
|
|
|
+ background: #eef2ff;
|
|
|
+ color: #4f46e5;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-item.active .tree-icon,
|
|
|
+.tree-item.active .tree-count {
|
|
|
+ color: #4f46e5;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-item.active .tree-count {
|
|
|
+ background: rgba(79, 70, 229, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.all-item {
|
|
|
+ margin-bottom: 4px;
|
|
|
+ border-bottom: 1px solid #f3f4f6;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ padding-left: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-toggle {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 10px;
|
|
|
+ color: #9ca3af;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.toggle-placeholder {
|
|
|
+ display: inline-block;
|
|
|
+ width: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-icon {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #9ca3af;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-label {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-count {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #9ca3af;
|
|
|
+ background: #f3f4f6;
|
|
|
+ padding: 1px 6px;
|
|
|
+ border-radius: 8px;
|
|
|
+ min-width: 20px;
|
|
|
+ text-align: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-add-btn {
|
|
|
+ opacity: 0;
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ padding: 2px;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #9ca3af;
|
|
|
+ font-size: 11px;
|
|
|
+ border-radius: 3px;
|
|
|
+ transition: all 0.15s;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-item:hover .tree-add-btn {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-add-btn:hover {
|
|
|
+ color: #4f46e5;
|
|
|
+ background: #eef2ff;
|
|
|
+}
|
|
|
+
|
|
|
+.add-input-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 4px 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.add-input {
|
|
|
+ flex: 1;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border: 1px solid #d1d5db;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ outline: none;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.add-input:focus {
|
|
|
+ border-color: #4f46e5;
|
|
|
+}
|
|
|
+
|
|
|
+.inline-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ padding: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 3px;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.inline-btn.confirm { color: #10b981; }
|
|
|
+.inline-btn.confirm:hover { background: #ecfdf5; }
|
|
|
+.inline-btn.cancel { color: #ef4444; }
|
|
|
+.inline-btn.cancel:hover { background: #fef2f2; }
|
|
|
+
|
|
|
+.context-menu {
|
|
|
+ position: fixed;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
|
+ z-index: 2000;
|
|
|
+ padding: 4px;
|
|
|
+ min-width: 140px;
|
|
|
+}
|
|
|
+
|
|
|
+.context-menu button {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ width: 100%;
|
|
|
+ padding: 7px 12px;
|
|
|
+ border: none;
|
|
|
+ background: none;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #374151;
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: background 0.1s;
|
|
|
+}
|
|
|
+
|
|
|
+.context-menu button:hover {
|
|
|
+ background: #f3f4f6;
|
|
|
+}
|
|
|
+
|
|
|
+.context-menu button.danger { color: #ef4444; }
|
|
|
+.context-menu button.danger:hover { background: #fef2f2; }
|
|
|
+
|
|
|
+.empty-tip {
|
|
|
+ padding: 12px 16px;
|
|
|
+ color: #9ca3af;
|
|
|
+ font-size: 12px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+</style>
|