Преглед изворни кода

feat(@six/smart-pharmacy): 智慧药事系统第二版-点评项分类接口对接

cmj пре 2 недеља
родитељ
комит
3eb7f17531

+ 187 - 96
apps/smart-pharmacy/src/api/method/prescription-review.ts

@@ -28,6 +28,8 @@ export namespace PrescriptionReviewModel {
     source: ReviewIndicatorSource;
     /** 分类下点评项数量 */
     indicatorCount?: number;
+    sortNo?: number;
+    remark?: string;
   }
 
   export interface ReviewIndicator extends TransformRecord {
@@ -274,6 +276,72 @@ export function updateReviewCostLimitMethod(dictValue: string) {
   });
 }
 
+const COMMENT_CATEGORY_BASE = '/manager/tcmp-pc/commentCategory';
+
+function toCommentCategoryRecordId(id?: unknown): string {
+  if (id === undefined || id === null) return '';
+  return String(id);
+}
+
+function normalizeCommentList(data: unknown): TransformData[] {
+  if (Array.isArray(data)) return data;
+  if (data && typeof data === 'object') {
+    const record = data as TransformData;
+    if (Array.isArray(record.rows)) return record.rows;
+    if (Array.isArray(record.list)) return record.list;
+    if (Array.isArray(record.items)) return record.items;
+    if (Array.isArray(record.data)) return record.data;
+  }
+  return [];
+}
+
+function fromCommentIndicator(
+  data: TransformData,
+  categoryId: string,
+  categoryName = '',
+): PrescriptionReviewModel.ReviewIndicator {
+  const id = toCommentCategoryRecordId(data?.id);
+  const isSystem = String(data?.isSystem ?? '');
+  const statusValue = data?.status;
+  return {
+    id,
+    categoryId,
+    categoryName,
+    name: String(data?.classifyName ?? data?.commentName ?? data?.name ?? ''),
+    source: isSystem === '0' ? 'system' : 'custom',
+    associatedChineseMedicine: String(data?.associatedChineseMedicine ?? '0') === '1',
+    remark: data?.remark ?? undefined,
+    status:
+      statusValue === undefined || statusValue === null
+        ? 1
+        : Number(statusValue) === 0
+          ? 0
+          : 1,
+    createUser: data?.createBy ?? undefined,
+    createTime: data?.createTime ?? undefined,
+    updateUser: data?.updateBy ?? undefined,
+    updateTime: data?.updateTime ?? undefined,
+  };
+}
+
+function fromCommentCategory(
+  data: TransformData,
+): PrescriptionReviewModel.ReviewIndicatorCategory {
+  const id = toCommentCategoryRecordId(data?.id);
+  const isSystem = String(data?.isSystem ?? '');
+  return {
+    id,
+    name: String(data?.classifyName ?? ''),
+    source: isSystem === '0' ? 'system' : 'custom',
+    sortNo: Number(data?.sortNo ?? 0),
+    remark: data?.remark ?? undefined,
+    createUser: data?.createBy ?? undefined,
+    createTime: data?.createTime ?? undefined,
+    updateUser: data?.updateBy ?? undefined,
+    updateTime: data?.updateTime ?? undefined,
+  };
+}
+
 const MOCK_INDICATOR_CATEGORIES: PrescriptionReviewModel.ReviewIndicatorCategory[] =
   [
     { id: 'cat-1', name: '适应症', source: 'system' },
@@ -511,15 +579,28 @@ function withCategoryIndicatorCount(
   }));
 }
 
-function getNextCustomCategoryName() {
-  const prefix = '新加分类';
-  let index = 1;
-  while (
-    MOCK_INDICATOR_CATEGORIES.some((item) => item.name === `${prefix}${index}`)
-  ) {
-    index += 1;
-  }
-  return `${prefix}${index}`;
+function toCommentCategoryAddPayload(
+  classifyName: string,
+  sortNo = 0,
+): TransformData {
+  return {
+    classifyName,
+    isSystem: '1',
+    sortNo,
+    del: '0',
+  };
+}
+
+function toCommentCategoryUpdatePayload(
+  id: string,
+  classifyName: string,
+  sortNo = 0,
+): TransformData {
+  return {
+    id: toCommentCategoryRecordId(id),
+    classifyName,
+    sortNo,
+  };
 }
 
 function syncIndicatorCategoryName(
@@ -548,121 +629,131 @@ function filterIndicators(
   return result;
 }
 
-/** 点评指标分类列表(当前为本地 mock) */
-export function listReviewIndicatorCategoriesMethod(): Promise<
+/** 点评指标分类列表 */
+export async function listReviewIndicatorCategoriesMethod(): Promise<
   PrescriptionReviewModel.ReviewIndicatorCategory[]
 > {
-  return Promise.resolve(withCategoryIndicatorCount([...MOCK_INDICATOR_CATEGORIES]));
+  const result = await http.get<
+    TransformList<PrescriptionReviewModel.ReviewIndicatorCategory>,
+    TransformList
+  >(`${COMMENT_CATEGORY_BASE}/pageList`, {
+    params: {
+      pageNum: 1,
+      pageSize: 999,
+    },
+    cacheFor: 0,
+    transform({ items, ...data }) {
+      const rows = items ?? [];
+      return {
+        ...data,
+        items: rows
+          .map((item) => fromCommentCategory(item))
+          .sort((a, b) => (a.sortNo ?? 0) - (b.sortNo ?? 0)),
+      };
+    },
+  });
+  return result.items;
 }
 
-/** 新增点评指标分类(当前为本地 mock) */
-export function createReviewIndicatorCategoryMethod(name?: string) {
-  const trimmed = (name ?? getNextCustomCategoryName()).trim();
+/** 新增点评指标分类 */
+export async function createReviewIndicatorCategoryMethod(name: string) {
+  const trimmed = name.trim();
   if (!trimmed) {
-    return Promise.reject(new Error('分类名称不能为空'));
+    throw new Error('分类名称不能为空');
   }
-  if (MOCK_INDICATOR_CATEGORIES.some((item) => item.name === trimmed)) {
-    return Promise.reject(new Error('分类名称已存在'));
+  const categories = await listReviewIndicatorCategoriesMethod();
+  if (categories.some((item) => item.name === trimmed)) {
+    throw new Error('分类名称已存在');
   }
-  const category: PrescriptionReviewModel.ReviewIndicatorCategory = {
-    id: `cat-${Date.now()}`,
-    name: trimmed,
-    source: 'custom',
-    indicatorCount: 0,
-  };
-  MOCK_INDICATOR_CATEGORIES.push(category);
-  return Promise.resolve(category);
+  const sortNo =
+    categories.reduce((max, item) => Math.max(max, item.sortNo ?? 0), -1) + 1;
+  const result = await http.post<TransformData, TransformData>(
+    `${COMMENT_CATEGORY_BASE}/add`,
+    toCommentCategoryAddPayload(trimmed, sortNo),
+    { cacheFor: 0 },
+  );
+  return fromCommentCategory({
+    ...toCommentCategoryAddPayload(trimmed, sortNo),
+    ...(result ?? {}),
+    classifyName: trimmed,
+  });
 }
 
-/** 更新点评指标分类名称(当前为本地 mock,系统内置不可改) */
-export function updateReviewIndicatorCategoryMethod(
+/** 更新点评指标分类 */
+export async function updateReviewIndicatorCategoryMethod(
   categoryId: string,
-  { name }: { name: string },
+  { name, sortNo = 0 }: { name: string; sortNo?: number },
 ) {
-  const category = MOCK_INDICATOR_CATEGORIES.find((item) => item.id === categoryId);
-  if (!category) {
-    return Promise.reject(new Error('分类不存在'));
-  }
-  if (category.source === 'system') {
-    return Promise.reject(new Error('系统内置分类不可修改'));
-  }
   const trimmed = name.trim();
   if (!trimmed) {
-    return Promise.reject(new Error('分类名称不能为空'));
-  }
-  if (
-    MOCK_INDICATOR_CATEGORIES.some(
-      (item) => item.id !== categoryId && item.name === trimmed,
-    )
-  ) {
-    return Promise.reject(new Error('分类名称已存在'));
+    throw new Error('分类名称不能为空');
   }
-  category.name = trimmed;
-  MOCK_INDICATORS.forEach((item) => {
-    if (item.categoryId === categoryId) {
-      item.categoryName = trimmed;
-    }
-  });
-  return Promise.resolve({
-    ...category,
-    indicatorCount: getCategoryIndicatorCount(categoryId),
+  const payload = toCommentCategoryUpdatePayload(categoryId, trimmed, sortNo);
+  const result = await http.post<TransformData, TransformData>(
+    `${COMMENT_CATEGORY_BASE}/update`,
+    payload,
+    { cacheFor: 0 },
+  );
+  return fromCommentCategory({
+    ...payload,
+    ...(result ?? {}),
+    classifyName: trimmed,
   });
 }
 
-/** 点评指标分类排序(当前为本地 mock) */
-export function sortReviewIndicatorCategoriesMethod(orderedIds: string[]) {
-  if (orderedIds.length !== MOCK_INDICATOR_CATEGORIES.length) {
-    return Promise.reject(new Error('分类排序数据不完整'));
+/** 点评指标分类排序(交换两条记录的 sortNo) */
+export async function sortReviewIndicatorCategoriesMethod(
+  updates: Array<{ id: string; sortNo: number }>,
+) {
+  if (!updates.length) {
+    return;
   }
-  const categoryMap = new Map(
-    MOCK_INDICATOR_CATEGORIES.map((item) => [item.id, item]),
+  await Promise.all(
+    updates.map(({ id, sortNo }) =>
+      http.post<void, unknown>(
+        `${COMMENT_CATEGORY_BASE}/update`,
+        {
+          id: toCommentCategoryRecordId(id),
+          sortNo,
+        },
+        { cacheFor: 0 },
+      ),
+    ),
   );
-  const sorted = orderedIds.map((id) => categoryMap.get(id)).filter(Boolean) as
-    PrescriptionReviewModel.ReviewIndicatorCategory[];
-  if (sorted.length !== MOCK_INDICATOR_CATEGORIES.length) {
-    return Promise.reject(new Error('分类排序数据无效'));
-  }
-  MOCK_INDICATOR_CATEGORIES.splice(0, MOCK_INDICATOR_CATEGORIES.length, ...sorted);
-  return Promise.resolve(withCategoryIndicatorCount([...MOCK_INDICATOR_CATEGORIES]));
 }
 
-/** 删除点评指标分类(当前为本地 mock,系统内置不可删) */
+/** 删除点评指标分类 */
 export function deleteReviewIndicatorCategoryMethod(categoryId: string) {
-  const index = MOCK_INDICATOR_CATEGORIES.findIndex(
-    (item) => item.id === categoryId,
-  );
-  if (index === -1) {
-    return Promise.reject(new Error('分类不存在'));
-  }
-  const category = MOCK_INDICATOR_CATEGORIES[index];
-  if (!category) {
-    return Promise.reject(new Error('分类不存在'));
-  }
-  if (category.source === 'system') {
-    return Promise.reject(new Error('系统内置分类不可删除'));
-  }
-  const inUse = MOCK_INDICATORS.some((item) => item.categoryId === categoryId);
-  if (inUse) {
-    return Promise.reject(new Error('该分类下存在点评项,无法删除'));
-  }
-  MOCK_INDICATOR_CATEGORIES.splice(index, 1);
-  return Promise.resolve(true);
+  return http.post<void, unknown>(`${COMMENT_CATEGORY_BASE}/delete`, void 0, {
+    params: { id: toCommentCategoryRecordId(categoryId) },
+    cacheFor: 0,
+  });
 }
 
-/** 按分类获取点评指标列表(当前为本地 mock) */
-export function listReviewIndicatorsByCategoryMethod(
+/** 按分类获取点评指标列表 */
+export async function listReviewIndicatorsByCategoryMethod(
   categoryId: string,
-  options?: { enabledOnly?: boolean },
+  options?: { enabledOnly?: boolean; categoryName?: string },
 ) {
-  const category = MOCK_INDICATOR_CATEGORIES.find((item) => item.id === categoryId);
-  if (!category) {
-    return Promise.reject(new Error('分类不存在'));
-  }
-  let items = MOCK_INDICATORS.filter((item) => item.categoryId === categoryId);
+  const items = await http.get<
+    PrescriptionReviewModel.ReviewIndicator[],
+    TransformData[]
+  >(`${COMMENT_CATEGORY_BASE}/list`, {
+    params: { categoryId: toCommentCategoryRecordId(categoryId) },
+    cacheFor: 0,
+    transform(data) {
+      return normalizeCommentList(data)
+        .sort((a, b) => Number(a.sortNo ?? 0) - Number(b.sortNo ?? 0))
+        .map((item) =>
+          fromCommentIndicator(item, categoryId, options?.categoryName ?? ''),
+        );
+    },
+  });
+
   if (options?.enabledOnly) {
-    items = items.filter((item) => item.status === 1);
+    return items.filter((item) => item.status === 1);
   }
-  return Promise.resolve(items);
+  return items;
 }
 
 /** 点评指标排序(当前为本地 mock,按分类内顺序) */

+ 98 - 19
apps/smart-pharmacy/src/views/prescription-review/indicator-library/modules/category-modal.vue

@@ -4,7 +4,7 @@ import type { PrescriptionReviewModel } from '#/api';
 import { computed, ref, watch } from 'vue';
 
 import { useVbenModal } from '@vben/common-ui';
-import { IconifyIcon } from '@vben/icons';
+import { GripVertical, Plus, SquareMinus } from '@vben/icons';
 
 import { Input, message } from 'ant-design-vue';
 
@@ -30,6 +30,29 @@ const loading = ref(false);
 const dragIndex = ref(-1);
 const editingNames = ref<Record<string, string>>({});
 
+const DRAFT_CATEGORY_ID_PREFIX = 'draft-';
+
+function isDraftCategory(category: { id: string }) {
+  return category.id.startsWith(DRAFT_CATEGORY_ID_PREFIX);
+}
+
+function getNextCustomCategoryName() {
+  const prefix = '新加分类';
+  let index = 1;
+  const existingNames = new Set(
+    categories.value.map((item) => editingNames.value[item.id] ?? item.name),
+  );
+  while (existingNames.has(`${prefix}${index}`)) {
+    index += 1;
+  }
+  return `${prefix}${index}`;
+}
+
+function removeDraftCategory(categoryId: string) {
+  categories.value = categories.value.filter((item) => item.id !== categoryId);
+  delete editingNames.value[categoryId];
+}
+
 const modalTitle = '点评项分类';
 
 const sortableList = computed(() =>
@@ -61,33 +84,56 @@ async function loadCategories() {
 async function loadIndicators(categoryId: string) {
   loading.value = true;
   try {
-    indicators.value = await listReviewIndicatorsByCategoryMethod(categoryId);
+    indicators.value = await listReviewIndicatorsByCategoryMethod(categoryId, {
+      categoryName: currentCategory.value?.name,
+    });
   } finally {
     loading.value = false;
   }
 }
 
-async function onAddCategory() {
-  try {
-    const category = await createReviewIndicatorCategoryMethod();
-    editingNames.value[category.id] = category.name;
-    await loadCategories();
-    emit('success');
-  } catch (error: any) {
-    message.error(error.message || '新增分类失败');
-  }
+function onAddCategory() {
+  const draftId = `${DRAFT_CATEGORY_ID_PREFIX}${Date.now()}`;
+  const defaultName = getNextCustomCategoryName();
+  categories.value.push({
+    id: draftId,
+    name: defaultName,
+    source: 'custom',
+  });
+  editingNames.value[draftId] = defaultName;
 }
 
 async function onSaveCategoryName(
   category: PrescriptionReviewModel.ReviewIndicatorCategory,
 ) {
   const newName = editingNames.value[category.id]?.trim();
+
+  if (isDraftCategory(category)) {
+    if (!newName) {
+      removeDraftCategory(category.id);
+      return;
+    }
+    try {
+      await createReviewIndicatorCategoryMethod(newName);
+      message.success('新增分类成功');
+      await loadCategories();
+      emit('success');
+    } catch (error: any) {
+      message.error(error.message || '新增分类失败');
+      editingNames.value[category.id] = newName;
+    }
+    return;
+  }
+
   if (!newName || newName === category.name) {
     editingNames.value[category.id] = category.name;
     return;
   }
   try {
-    await updateReviewIndicatorCategoryMethod(category.id, { name: newName });
+    await updateReviewIndicatorCategoryMethod(category.id, {
+      name: newName,
+      sortNo: category.sortNo ?? 0,
+    });
     message.success('修改分类成功');
     await loadCategories();
     emit('success');
@@ -100,6 +146,10 @@ async function onSaveCategoryName(
 async function onDeleteCategory(
   category: PrescriptionReviewModel.ReviewIndicatorCategory,
 ) {
+  if (isDraftCategory(category)) {
+    removeDraftCategory(category.id);
+    return;
+  }
   if (category.indicatorCount && category.indicatorCount > 0) {
     message.warning('该分类下存在点评项,无法删除');
     return;
@@ -148,12 +198,25 @@ async function onDrop(targetIndex: number) {
 
   if (viewMode.value === 'categories') {
     const list = [...categories.value];
-    const [moved] = list.splice(sourceIndex, 1);
-    if (!moved) return;
-    list.splice(targetIndex, 0, moved);
+    const moved = list[sourceIndex];
+    const target = list[targetIndex];
+    if (!moved || !target) return;
+    if (isDraftCategory(moved) || isDraftCategory(target)) {
+      return;
+    }
+
+    const movedSortNo = moved.sortNo ?? sourceIndex;
+    const targetSortNo = target.sortNo ?? targetIndex;
+
+    list[sourceIndex] = { ...target, sortNo: movedSortNo };
+    list[targetIndex] = { ...moved, sortNo: targetSortNo };
     categories.value = list;
+
     try {
-      await sortReviewIndicatorCategoriesMethod(list.map((item) => item.id));
+      await sortReviewIndicatorCategoriesMethod([
+        { id: moved.id, sortNo: targetSortNo },
+        { id: target.id, sortNo: movedSortNo },
+      ]);
       emit('success');
     } catch (error: any) {
       message.error(error.message || '排序失败');
@@ -212,7 +275,7 @@ watch(viewMode, () => {
       <div v-if="viewMode === 'categories'" class="list-header">
         <span class="list-header-title">点评项分类</span>
         <button class="icon-btn" type="button" @click="onAddCategory">
-          <IconifyIcon class="size-4" icon="lucide:plus" />
+          <Plus class="size-4" />
         </button>
       </div>
 
@@ -234,7 +297,7 @@ watch(viewMode, () => {
             @dragend="onDragEnd"
             @dragstart="onDragStart(index, $event)"
           >
-            <IconifyIcon class="size-4" icon="lucide:grip-vertical" />
+            <GripVertical class="size-4" />
           </span>
 
           <template v-if="viewMode === 'categories'">
@@ -261,16 +324,23 @@ watch(viewMode, () => {
                 type="button"
                 @click="onDeleteCategory(item as PrescriptionReviewModel.ReviewIndicatorCategory)"
               >
-                <IconifyIcon class="size-4" icon="lucide:minus" />
+                <SquareMinus class="size-4" />
               </button>
             </div>
             <button
+              v-if="!isDraftCategory(item as PrescriptionReviewModel.ReviewIndicatorCategory)"
               class="review-link"
               type="button"
               @click="onOpenIndicators(item as PrescriptionReviewModel.ReviewIndicatorCategory)"
             >
               点评项
             </button>
+            <span
+              v-else
+              class="review-link review-link-static"
+            >
+              点评项
+            </span>
           </template>
 
           <template v-else>
@@ -396,6 +466,15 @@ watch(viewMode, () => {
   color: #4096ff;
 }
 
+.review-link-static {
+  cursor: default;
+  pointer-events: none;
+}
+
+.review-link-static:hover {
+  color: #1677ff;
+}
+
 .icon-btn {
   display: inline-flex;
   align-items: center;

+ 1 - 0
apps/smart-pharmacy/src/views/prescription-review/task/review.vue

@@ -133,6 +133,7 @@ async function loadIndicators() {
       categoryName: category.name,
       indicators: await listReviewIndicatorsByCategoryMethod(category.id, {
         enabledOnly: true,
+        categoryName: category.name,
       }),
     })),
   );