fix(faq): persist active defaults for medical consults

This commit is contained in:
Cheng Zhou
2026-06-09 09:01:34 +08:00
parent d472b8410e
commit 1fef12cd86
5 changed files with 195 additions and 48 deletions
+2 -1
View File
@@ -4,7 +4,7 @@ from typing import Optional
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint, func
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
@@ -20,6 +20,7 @@ class FaqCategory(Base):
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
sort_order: Mapped[int] = mapped_column(nullable=False, default=0)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
icon: Mapped[Optional[str]] = mapped_column(String(30), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
+1
View File
@@ -26,6 +26,7 @@ class FaqItem(Base):
answer: Mapped[str] = mapped_column(Text, nullable=False)
keywords: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
+39
View File
@@ -0,0 +1,39 @@
import uuid
from sqlalchemy import text
import pytest
from app.crud.faq_category import create_category
from app.crud.faq_item import create_item
from app.schemas.faq import CategoryCreate, FaqCreate
@pytest.mark.asyncio
async def test_create_category_defaults_to_active(db_session):
result = await db_session.execute(text("SELECT id FROM studies LIMIT 1"))
study_id = result.scalar_one()
category = await create_category(
db_session,
CategoryCreate(study_id=study_id, name="AE", sort_order=0),
)
assert category.is_active is True
@pytest.mark.asyncio
async def test_create_item_defaults_to_active(db_session):
result = await db_session.execute(text("SELECT id FROM studies LIMIT 1"))
study_id = result.scalar_one()
category = await create_category(
db_session,
CategoryCreate(study_id=study_id, name=f"AE-{uuid.uuid4().hex[:8]}", sort_order=0),
)
item = await create_item(
db_session,
FaqCreate(study_id=study_id, category_id=category.id, question="AE 如何记录?"),
created_by=uuid.uuid4(),
)
assert item.is_active is True
@@ -49,7 +49,7 @@
<template #reference>
<div class="row-icon" :class="{ 'row-icon--selected': cat.icon }" title="选择图标">
<FaqIcon v-if="cat.icon" :icon="cat.icon" />
<span v-else class="row-icon-placeholder"><Grid /></span>
<span v-else class="row-icon-placeholder"><FaqIcon icon="fa-icons" /></span>
</div>
</template>
<div class="icon-popover-grid">
@@ -96,7 +96,7 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { ArrowDown, ArrowUp, Delete, Grid, Plus } from "@element-plus/icons-vue";
import { ArrowDown, ArrowUp, Delete, Plus } from "@element-plus/icons-vue";
import FaqIcon from "./FaqIcon.vue";
import { createFaqCategory, updateFaqCategory, deleteFaqCategory } from "../api/faqs";
import type { FaqCategory } from "../api/faqs";
+151 -45
View File
@@ -1,58 +1,164 @@
<template>
<component :is="iconComponent" />
<svg
class="faq-icon"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
v-for="path in iconPaths"
:key="path"
:d="path"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
<script setup lang="ts">
import { computed } from "vue";
import type { Component } from "vue";
import {
Calendar,
Camera,
Collection,
DocumentChecked,
Filter,
FirstAidKit,
Flag,
Grid,
Help,
List,
Management,
Memo,
More,
OfficeBuilding,
Reading,
ScaleToOriginal,
Sell,
SetUp,
User,
} from "@element-plus/icons-vue";
const props = defineProps<{
icon?: string | null;
}>();
const iconMap: Record<string, Component> = {
"fa-th-large": Grid,
"fa-ruler-combined": ScaleToOriginal,
"fa-calendar-check": Calendar,
"fa-filter": Filter,
"fa-flag-checkered": Flag,
"fa-capsules": FirstAidKit,
"fa-prescription-bottle": Sell,
"fa-vial": SetUp,
"fa-heartbeat": Help,
"fa-pills": FirstAidKit,
"fa-file-medical": DocumentChecked,
"fa-camera": Camera,
"fa-notes-medical": Memo,
"fa-clipboard-list": List,
"fa-syringe": FirstAidKit,
"fa-flask": Management,
"fa-book-medical": Reading,
"fa-user-md": User,
"fa-hospital": OfficeBuilding,
"fa-ellipsis-h": More,
const fallbackIcon = "fa-th-large";
const icons: Record<string, string[]> = {
"fa-th-large": [
"M4 4h7v7H4z",
"M13 4h7v7h-7z",
"M4 13h7v7H4z",
"M13 13h7v7h-7z",
],
"fa-ruler-combined": [
"M4 17 17 4l3 3L7 20H4z",
"M14 7l3 3",
"M11 10l2 2",
"M8 13l3 3",
],
"fa-calendar-check": [
"M5 5h14v15H5z",
"M8 3v4",
"M16 3v4",
"M5 10h14",
"M8 15l2 2 5-5",
],
"fa-filter": [
"M4 5h16l-6 7v5l-4 2v-7z",
],
"fa-flag-checkered": [
"M5 21V4",
"M5 5h12l-2 4 2 4H5",
"M9 5v8",
"M13 5v8",
"M7 9h10",
],
"fa-capsules": [
"M10 8 6 12a4 4 0 0 0 6 6l4-4a4 4 0 0 0-6-6z",
"M8 14l4 4",
"M15 5a4 4 0 0 1 6 6l-3 3-6-6z",
],
"fa-prescription-bottle": [
"M8 3h8",
"M9 6h6l1 3v12H8V9z",
"M10 13h4",
"M12 11v4",
],
"fa-vial": [
"M10 3h8",
"M13 4v6l-7 7a3 3 0 0 0 4 4l7-7h4",
"M9 16l5 5",
],
"fa-heartbeat": [
"M20 8c0-3-4-5-8-1-4-4-8-2-8 1 0 6 8 11 8 11s3-2 5-5",
"M3 13h4l2-4 4 8 2-4h6",
],
"fa-pills": [
"M9 7 6 10a4 4 0 0 0 6 6l3-3a4 4 0 0 0-6-6z",
"M8 14l4 4",
"M16 8h5",
"M18.5 5.5v5",
],
"fa-file-medical": [
"M6 3h9l3 3v18H6z",
"M14 3v5h5",
"M10 15h6",
"M13 12v6",
],
"fa-camera": [
"M4 8h4l2-3h4l2 3h4v13H4z",
"M9 14a3 3 0 1 0 6 0 3 3 0 0 0-6 0z",
],
"fa-notes-medical": [
"M7 3h10v4H7z",
"M5 5h14v19H5z",
"M9 14h6",
"M12 11v6",
],
"fa-clipboard-list": [
"M8 4h8v4H8z",
"M6 6h12v18H6z",
"M9 12h1",
"M13 12h4",
"M9 16h1",
"M13 16h4",
],
"fa-syringe": [
"M14 4l6 6",
"M16 2l-3 3",
"M22 8l-3 3",
"M4 20l7-7",
"M8 10l6 6",
"M10 8l6 6-7 7H4v-5z",
],
"fa-flask": [
"M9 3h6",
"M10 3v6l-5 9a3 3 0 0 0 3 4h8a3 3 0 0 0 3-4l-5-9V3",
"M8 16h8",
],
"fa-book-medical": [
"M6 4h13v17H7a3 3 0 0 1 0-6h12",
"M10 10h6",
"M13 7v6",
],
"fa-user-md": [
"M9 8a4 4 0 1 0 8 0 4 4 0 0 0-8 0z",
"M5 21a8 8 0 0 1 16 0",
"M8 18l4 3 4-3",
],
"fa-hospital": [
"M5 21V5h14v16",
"M3 21h18",
"M10 10h4",
"M12 8v6",
"M8 17h2",
"M14 17h2",
],
"fa-ellipsis-h": [
"M5 12h.01",
"M12 12h.01",
"M19 12h.01",
],
"fa-icons": [
"M4 5h7v7H4z",
"M14 4l6 6",
"M17 4v6h6",
"M6 19a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
"M15 15h6v6h-6z",
],
};
const iconComponent = computed(() => iconMap[props.icon || ""] || Collection);
const iconPaths = computed(() => icons[props.icon || fallbackIcon] || icons[fallbackIcon]);
</script>
<style scoped>
.faq-icon {
display: block;
width: 1em;
height: 1em;
}
</style>