feat: VIP功能绑定、VIP病历模块、字典模块、传方按钮移动到顶部导航栏
This commit is contained in:
27
apps/web-antd/src/views/system/dict/disease/api/index.ts
Normal file
27
apps/web-antd/src/views/system/dict/disease/api/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 疾病诊断字典 API(yii_disease)
|
||||
*/
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'disease-dict/';
|
||||
|
||||
export async function getDiseaseDictList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
export async function createDiseaseDict(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
export async function updateDiseaseDict(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
export async function deleteDiseaseDict(data: { ids: number[] }) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
|
||||
/** 同步空拼音首拼/全拼 */
|
||||
export async function syncDiseaseDictPinyin() {
|
||||
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
|
||||
}
|
||||
269
apps/web-antd/src/views/system/dict/disease/index.vue
Normal file
269
apps/web-antd/src/views/system/dict/disease/index.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 疾病诊断字典(yii_disease)
|
||||
* 挂在字典管理下,维护接诊诊断库
|
||||
*/
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, message, Modal as AntModal } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import {
|
||||
createDiseaseDict,
|
||||
deleteDiseaseDict,
|
||||
getDiseaseDictList,
|
||||
syncDiseaseDictPinyin,
|
||||
updateDiseaseDict,
|
||||
} from './api';
|
||||
|
||||
defineOptions({ name: 'DiseaseDict' });
|
||||
|
||||
const hasTopTableDropDownActions = ref(false);
|
||||
const syncing = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
|
||||
const formOptions = {
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'keyword',
|
||||
label: '关键字',
|
||||
componentProps: { placeholder: '名称/代码/首拼/全拼' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions = {
|
||||
checkboxConfig: { highlight: true, labelField: '' },
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'diagnose_code', title: '诊断代码', width: 120 },
|
||||
{ field: 'name', title: '诊断名称', minWidth: 180 },
|
||||
{ field: 'pinyin', title: '首拼', width: 120 },
|
||||
{ field: 'pinyin_full', title: '全拼', minWidth: 160 },
|
||||
{ field: 'major_number', title: '主编号', width: 120 },
|
||||
{ field: 'ref_number', title: '次编号', width: 120 },
|
||||
{ field: 'created_at', title: '创建时间', width: 170 },
|
||||
{ title: '操作', slots: { default: 'action' }, width: 140, fixed: 'right' },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }: any, formValues: any) => {
|
||||
return await getDiseaseDictList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
search: true,
|
||||
refresh: true,
|
||||
slots: { buttons: 'toolbar-buttons' },
|
||||
},
|
||||
};
|
||||
|
||||
const gridEvents: VxeGridListeners<any> = {
|
||||
checkboxChange() {
|
||||
hasTopTableDropDownActions.value =
|
||||
gridApi.grid.getCheckboxRecords().length > 0;
|
||||
},
|
||||
checkboxAll() {
|
||||
hasTopTableDropDownActions.value =
|
||||
gridApi.grid.getCheckboxRecords().length > 0;
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions: gridOptions as any,
|
||||
gridEvents,
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-12',
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
componentProps: { class: 'w-full' },
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'id',
|
||||
label: 'ID',
|
||||
dependencies: { show: false, triggerFields: ['id'] },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'name',
|
||||
label: '诊断名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'diagnose_code',
|
||||
label: '诊断代码',
|
||||
formItemClass: 'col-span-6',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'major_number',
|
||||
label: '主编号',
|
||||
formItemClass: 'col-span-6',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'ref_number',
|
||||
label: '次编号',
|
||||
formItemClass: 'col-span-6',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'pinyin',
|
||||
label: '拼音首拼',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: { placeholder: '可留空,保存时按名称自动生成' },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'pinyin_full',
|
||||
label: '拼音全拼',
|
||||
componentProps: { placeholder: '可留空,保存时按名称自动生成' },
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
class: 'w-[640px]',
|
||||
draggable: true,
|
||||
onConfirm: async () => {
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values = await formApi.getValues();
|
||||
formModalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
if (isUpdate.value) {
|
||||
await updateDiseaseDict(values);
|
||||
} else {
|
||||
await createDiseaseDict(values);
|
||||
}
|
||||
message.success('保存成功');
|
||||
gridApi.query();
|
||||
formModalApi.close();
|
||||
} finally {
|
||||
formModalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formApi.resetForm();
|
||||
return;
|
||||
}
|
||||
const data = formModalApi.getData<{ values?: any; update?: boolean }>();
|
||||
isUpdate.value = !!data?.update;
|
||||
if (data?.values && data.update) {
|
||||
formApi.setValues({ ...data.values });
|
||||
} else {
|
||||
formApi.resetForm();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function showModal(row: any = {}, update = false) {
|
||||
formModalApi.setData({ values: row, update });
|
||||
formModalApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(ids: number[]) {
|
||||
await deleteDiseaseDict({ ids });
|
||||
message.success('删除成功');
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 手动同步空拼音(疾病库量大,可能稍慢) */
|
||||
function handleSyncPinyin() {
|
||||
AntModal.confirm({
|
||||
title: '同步拼音',
|
||||
content:
|
||||
'将扫描疾病诊断中首拼或全拼为空的记录并自动生成,已有值不会覆盖。确认执行?',
|
||||
onOk: async () => {
|
||||
syncing.value = true;
|
||||
try {
|
||||
const res = await syncDiseaseDictPinyin();
|
||||
message.success(
|
||||
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)} 条`,
|
||||
);
|
||||
gridApi.query();
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height title="疾病诊断">
|
||||
<FormModal :title="`${isUpdate ? '编辑' : '新增'}疾病诊断`">
|
||||
<Form />
|
||||
</FormModal>
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增',
|
||||
type: 'primary',
|
||||
onClick: () => showModal({}, false),
|
||||
},
|
||||
{
|
||||
label: '批量删除',
|
||||
danger: true,
|
||||
ifShow: () => hasTopTableDropDownActions,
|
||||
popConfirm: {
|
||||
title: '确认删除所选?',
|
||||
confirm: () => {
|
||||
const ids = gridApi.grid
|
||||
.getCheckboxRecords()
|
||||
.map((r: any) => r.id);
|
||||
handleDelete(ids);
|
||||
},
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
|
||||
同步拼音
|
||||
</Button>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{ label: '编辑', onClick: () => showModal(row, true) },
|
||||
{
|
||||
label: '删除',
|
||||
danger: true,
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
confirm: () => handleDelete([row.id]),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 公共医嘱字典 API(xk_doctor_order)
|
||||
*/
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'doctor-order/';
|
||||
|
||||
export async function getDoctorOrderDictList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
export async function createDoctorOrderDict(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
export async function updateDoctorOrderDict(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
export async function deleteDoctorOrderDict(data: { ids?: number[]; id?: number }) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
|
||||
/** 同步空拼音首拼/全拼 */
|
||||
export async function syncDoctorOrderDictPinyin() {
|
||||
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
|
||||
}
|
||||
235
apps/web-antd/src/views/system/dict/doctor-order/index.vue
Normal file
235
apps/web-antd/src/views/system/dict/doctor-order/index.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 公共医嘱字典(xk_doctor_order)
|
||||
* 挂在字典管理下,维护接诊公共医嘱
|
||||
*/
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, message, Modal as AntModal } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import {
|
||||
createDoctorOrderDict,
|
||||
deleteDoctorOrderDict,
|
||||
getDoctorOrderDictList,
|
||||
syncDoctorOrderDictPinyin,
|
||||
updateDoctorOrderDict,
|
||||
} from './api';
|
||||
|
||||
defineOptions({ name: 'DoctorOrderDict' });
|
||||
|
||||
const hasTopTableDropDownActions = ref(false);
|
||||
const syncing = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
|
||||
const formOptions = {
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'keyword',
|
||||
label: '关键字',
|
||||
componentProps: { placeholder: '内容/首拼/全拼' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions = {
|
||||
checkboxConfig: { highlight: true, labelField: '' },
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'name', title: '医嘱内容', minWidth: 220 },
|
||||
{ field: 'pinyin_initials', title: '首拼', width: 120 },
|
||||
{ field: 'pinyin_full', title: '全拼', minWidth: 160 },
|
||||
{ field: 'created_at', title: '创建时间', width: 170 },
|
||||
{ title: '操作', slots: { default: 'action' }, width: 140, fixed: 'right' },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }: any, formValues: any) => {
|
||||
return await getDoctorOrderDictList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
search: true,
|
||||
refresh: true,
|
||||
slots: { buttons: 'toolbar-buttons' },
|
||||
},
|
||||
};
|
||||
|
||||
const gridEvents: VxeGridListeners<any> = {
|
||||
checkboxChange() {
|
||||
hasTopTableDropDownActions.value =
|
||||
gridApi.grid.getCheckboxRecords().length > 0;
|
||||
},
|
||||
checkboxAll() {
|
||||
hasTopTableDropDownActions.value =
|
||||
gridApi.grid.getCheckboxRecords().length > 0;
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions: gridOptions as any,
|
||||
gridEvents,
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-12',
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
componentProps: { class: 'w-full' },
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'id',
|
||||
label: 'ID',
|
||||
dependencies: { show: false, triggerFields: ['id'] },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'name',
|
||||
label: '医嘱内容',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '请输入医嘱内容' },
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
class: 'w-[560px]',
|
||||
draggable: true,
|
||||
onConfirm: async () => {
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values = await formApi.getValues();
|
||||
formModalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
if (isUpdate.value) {
|
||||
await updateDoctorOrderDict(values);
|
||||
} else {
|
||||
await createDoctorOrderDict(values);
|
||||
}
|
||||
message.success('保存成功');
|
||||
gridApi.query();
|
||||
formModalApi.close();
|
||||
} finally {
|
||||
formModalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formApi.resetForm();
|
||||
return;
|
||||
}
|
||||
const data = formModalApi.getData<{ values?: any; update?: boolean }>();
|
||||
isUpdate.value = !!data?.update;
|
||||
if (data?.values && data.update) {
|
||||
formApi.setValues({ ...data.values });
|
||||
} else {
|
||||
formApi.resetForm();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function showModal(row: any = {}, update = false) {
|
||||
formModalApi.setData({ values: row, update });
|
||||
formModalApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(ids: number[]) {
|
||||
await deleteDoctorOrderDict({ ids });
|
||||
message.success('删除成功');
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
function handleSyncPinyin() {
|
||||
AntModal.confirm({
|
||||
title: '同步拼音',
|
||||
content:
|
||||
'将扫描公共医嘱中首拼或全拼为空的记录并自动生成,已有值不会覆盖。确认执行?',
|
||||
onOk: async () => {
|
||||
syncing.value = true;
|
||||
try {
|
||||
const res = await syncDoctorOrderDictPinyin();
|
||||
message.success(
|
||||
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)} 条`,
|
||||
);
|
||||
gridApi.query();
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height title="公共医嘱">
|
||||
<FormModal :title="`${isUpdate ? '编辑' : '新增'}公共医嘱`">
|
||||
<Form />
|
||||
</FormModal>
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增',
|
||||
type: 'primary',
|
||||
onClick: () => showModal({}, false),
|
||||
},
|
||||
{
|
||||
label: '批量删除',
|
||||
danger: true,
|
||||
ifShow: () => hasTopTableDropDownActions,
|
||||
popConfirm: {
|
||||
title: '确认删除所选?',
|
||||
confirm: () => {
|
||||
const ids = gridApi.grid
|
||||
.getCheckboxRecords()
|
||||
.map((r: any) => r.id);
|
||||
handleDelete(ids);
|
||||
},
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
|
||||
同步拼音
|
||||
</Button>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{ label: '编辑', onClick: () => showModal(row, true) },
|
||||
{
|
||||
label: '删除',
|
||||
danger: true,
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
confirm: () => handleDelete([row.id]),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
34
apps/web-antd/src/views/system/dict/tcm/api/index.ts
Normal file
34
apps/web-antd/src/views/system/dict/tcm/api/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 中医字典 API(疾病 / 治法 / 证候)
|
||||
*/
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'tcm-dict/';
|
||||
|
||||
export type TcmDictType = 'diseases' | 'method' | 'syndrome';
|
||||
|
||||
export async function getTcmDictList(data: {
|
||||
type: TcmDictType;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
}) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
export async function createTcmDict(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
export async function updateTcmDict(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
export async function deleteTcmDict(data: { type: TcmDictType; ids: number[] }) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
|
||||
/** 手动同步空拼音首拼 */
|
||||
export async function syncTcmDictPinyin(type: TcmDictType) {
|
||||
return requestClient.post<any>(`${prefix}sync-pinyin`, { type });
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 中医字典列表(疾病 / 治法 / 证候共用)
|
||||
* 支持 CRUD + 手动「同步拼音首拼」(仅补空值)
|
||||
*/
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, message, Modal as AntModal } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import {
|
||||
createTcmDict,
|
||||
deleteTcmDict,
|
||||
getTcmDictList,
|
||||
syncTcmDictPinyin,
|
||||
updateTcmDict,
|
||||
type TcmDictType,
|
||||
} from '../api';
|
||||
|
||||
const props = defineProps<{
|
||||
dictType: TcmDictType;
|
||||
pageTitle: string;
|
||||
pageName: string;
|
||||
}>();
|
||||
|
||||
defineOptions({ name: 'TcmDictListPage' });
|
||||
|
||||
const hasTopTableDropDownActions = ref(false);
|
||||
const syncing = ref(false);
|
||||
|
||||
const formOptions = {
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'keyword',
|
||||
label: '关键字',
|
||||
componentProps: { placeholder: '名称/编码/首拼' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions = computed(() => ({
|
||||
checkboxConfig: { highlight: true, labelField: '' },
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'code', title: '代码', width: 120 },
|
||||
{ field: 'number', title: '编号', width: 120 },
|
||||
{ field: 'name', title: '名称', minWidth: 160 },
|
||||
{ field: 'alias', title: '别名', minWidth: 120 },
|
||||
{ field: 'initials_of_pinyin', title: '拼音首拼', width: 120 },
|
||||
{ field: 'pinyin_full', title: '拼音全拼', minWidth: 160 },
|
||||
{ field: 'created_at', title: '创建时间', width: 170 },
|
||||
{ title: '操作', slots: { default: 'action' }, width: 140, fixed: 'right' },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }: any, formValues: any) => {
|
||||
return await getTcmDictList({
|
||||
type: props.dictType,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
search: true,
|
||||
refresh: true,
|
||||
slots: { buttons: 'toolbar-buttons' },
|
||||
},
|
||||
}));
|
||||
|
||||
const gridEvents: VxeGridListeners<any> = {
|
||||
checkboxChange() {
|
||||
hasTopTableDropDownActions.value =
|
||||
gridApi.grid.getCheckboxRecords().length > 0;
|
||||
},
|
||||
checkboxAll() {
|
||||
hasTopTableDropDownActions.value =
|
||||
gridApi.grid.getCheckboxRecords().length > 0;
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions: gridOptions.value as any,
|
||||
gridEvents,
|
||||
});
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const [Form, formApi] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-12',
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
componentProps: { class: 'w-full' },
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'id',
|
||||
label: 'ID',
|
||||
dependencies: { show: false, triggerFields: ['id'] },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'name',
|
||||
label: '名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'code',
|
||||
label: '代码',
|
||||
formItemClass: 'col-span-6',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'number',
|
||||
label: '编号',
|
||||
formItemClass: 'col-span-6',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'alias',
|
||||
label: '别名',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'initials_of_pinyin',
|
||||
label: '拼音首拼',
|
||||
componentProps: { placeholder: '保存时按名称自动生成,可留空' },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'pinyin_full',
|
||||
label: '拼音全拼',
|
||||
componentProps: { placeholder: '保存时按名称自动生成,可留空' },
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
class: 'w-[640px]',
|
||||
draggable: true,
|
||||
onConfirm: async () => {
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values = await formApi.getValues();
|
||||
formModalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
const payload = { ...values, type: props.dictType };
|
||||
if (isUpdate.value) {
|
||||
await updateTcmDict(payload);
|
||||
} else {
|
||||
await createTcmDict(payload);
|
||||
}
|
||||
message.success('保存成功');
|
||||
gridApi.query();
|
||||
formModalApi.close();
|
||||
} finally {
|
||||
formModalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formApi.resetFields();
|
||||
return;
|
||||
}
|
||||
const data = formModalApi.getData<{ values?: any; update?: boolean }>();
|
||||
isUpdate.value = !!data?.update;
|
||||
if (data?.values && data.update) {
|
||||
formApi.setValues({ ...data.values });
|
||||
} else {
|
||||
formApi.resetFields();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function showModal(row: any = {}, update = false) {
|
||||
formModalApi.setData({ values: row, update });
|
||||
formModalApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(ids: number[]) {
|
||||
await deleteTcmDict({ type: props.dictType, ids });
|
||||
message.success('删除成功');
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 手动同步:补全空首拼/全拼,并纠正名称含数字但旧拼音丢数字的脏数据 */
|
||||
function handleSyncPinyin() {
|
||||
AntModal.confirm({
|
||||
title: '同步拼音',
|
||||
content: `将扫描「${props.pageTitle}」中首拼/全拼为空(或含数字名称需纠正)的记录并自动生成。确认执行?`,
|
||||
onOk: async () => {
|
||||
syncing.value = true;
|
||||
try {
|
||||
const res = await syncTcmDictPinyin(props.dictType);
|
||||
message.success(
|
||||
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)} 条`,
|
||||
);
|
||||
gridApi.query();
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height :title="pageTitle">
|
||||
<FormModal :title="`${isUpdate ? '编辑' : '新增'}${pageTitle}`">
|
||||
<Form />
|
||||
</FormModal>
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增',
|
||||
type: 'primary',
|
||||
onClick: () => showModal({}, false),
|
||||
},
|
||||
{
|
||||
label: '批量删除',
|
||||
danger: true,
|
||||
ifShow: () => hasTopTableDropDownActions,
|
||||
popConfirm: {
|
||||
title: '确认删除所选?',
|
||||
confirm: () => {
|
||||
const ids = gridApi.grid
|
||||
.getCheckboxRecords()
|
||||
.map((r: any) => r.id);
|
||||
handleDelete(ids);
|
||||
},
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
|
||||
同步拼音
|
||||
</Button>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{ label: '编辑', onClick: () => showModal(row, true) },
|
||||
{
|
||||
label: '删除',
|
||||
danger: true,
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
confirm: () => handleDelete([row.id]),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
15
apps/web-antd/src/views/system/dict/tcm/diseases/index.vue
Normal file
15
apps/web-antd/src/views/system/dict/tcm/diseases/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 中医疾病字典(xk_traditional_chinese_medicine_diseases)
|
||||
*/
|
||||
import TcmDictListPage from '../components/TcmDictListPage.vue';
|
||||
|
||||
defineOptions({ name: 'TcmDiseasesDict' });
|
||||
</script>
|
||||
<template>
|
||||
<TcmDictListPage
|
||||
dict-type="diseases"
|
||||
page-title="中医疾病"
|
||||
page-name="TcmDiseasesDict"
|
||||
/>
|
||||
</template>
|
||||
15
apps/web-antd/src/views/system/dict/tcm/method/index.vue
Normal file
15
apps/web-antd/src/views/system/dict/tcm/method/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 中医治法字典(xk_traditional_chinese_medicine_method)
|
||||
*/
|
||||
import TcmDictListPage from '../components/TcmDictListPage.vue';
|
||||
|
||||
defineOptions({ name: 'TcmMethodDict' });
|
||||
</script>
|
||||
<template>
|
||||
<TcmDictListPage
|
||||
dict-type="method"
|
||||
page-title="中医治法"
|
||||
page-name="TcmMethodDict"
|
||||
/>
|
||||
</template>
|
||||
15
apps/web-antd/src/views/system/dict/tcm/syndrome/index.vue
Normal file
15
apps/web-antd/src/views/system/dict/tcm/syndrome/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 中医证候字典(xk_traditional_chinese_medicine_syndrome)
|
||||
*/
|
||||
import TcmDictListPage from '../components/TcmDictListPage.vue';
|
||||
|
||||
defineOptions({ name: 'TcmSyndromeDict' });
|
||||
</script>
|
||||
<template>
|
||||
<TcmDictListPage
|
||||
dict-type="syndrome"
|
||||
page-title="中医证候"
|
||||
page-name="TcmSyndromeDict"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 病历公共词条-按字段拆分页(从路由 path 末段解析 field_code)
|
||||
* 例:/system/medical-record/entry-public/chief-complaint → chief_complaint
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import EntryListPage from '#/views/doctor/medical-record/components/EntryListPage.vue';
|
||||
import { MEDICAL_RECORD_FIELD_OPTIONS } from '#/views/doctor/medical-record/config/constants';
|
||||
|
||||
defineOptions({ name: 'MedicalRecordEntryPublicField' });
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const fieldCode = computed(() => {
|
||||
const seg = (route.path.split('/').filter(Boolean).pop() || '').trim();
|
||||
return seg.replace(/-/g, '_');
|
||||
});
|
||||
|
||||
const fieldLabel = computed(() => {
|
||||
const hit = MEDICAL_RECORD_FIELD_OPTIONS.find((o) => o.value === fieldCode.value);
|
||||
return hit?.label || fieldCode.value;
|
||||
});
|
||||
|
||||
const pageTitle = computed(() => `公共词条·${fieldLabel.value}`);
|
||||
</script>
|
||||
<template>
|
||||
<EntryListPage
|
||||
:key="fieldCode"
|
||||
scope="public"
|
||||
:fixed-field-code="fieldCode"
|
||||
:page-title="pageTitle"
|
||||
:page-name="String(route.name || 'MedicalRecordEntryPublicField')"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 病历公共词条管理(平台)
|
||||
*/
|
||||
import EntryListPage from '#/views/doctor/medical-record/components/EntryListPage.vue';
|
||||
|
||||
defineOptions({ name: 'MedicalRecordEntryPublic' });
|
||||
</script>
|
||||
<template>
|
||||
<EntryListPage
|
||||
scope="public"
|
||||
page-title="病历公共词条"
|
||||
page-name="MedicalRecordEntryPublic"
|
||||
/>
|
||||
</template>
|
||||
@@ -15,3 +15,10 @@ export async function getPriceAdjustConfig(storeId?: number) {
|
||||
params: storeId ? { store_id: storeId } : {},
|
||||
});
|
||||
}
|
||||
|
||||
/** 按 key 批量读取系统配置(如 dict_search_rank_mode) */
|
||||
export async function getSystemConfigByKeys(keys: string[]) {
|
||||
return requestClient.get<any>(`${prefix}get-by-keys`, {
|
||||
params: { keys: keys.join(',') },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,6 +40,13 @@ const logisticsShowWhNameClinic = ref(true);
|
||||
const salespersonStoreEditEnabled = ref(false);
|
||||
/** 业务端单机登录:医生/药师/诊所管理员等同一账号仅 1 手机 + 1 网页(默认否) */
|
||||
const doctorSingleDeviceLogin = ref(false);
|
||||
/** 字典搜索匹配度:backend 服务端 / frontend 前端 */
|
||||
const dictSearchRankMode = ref<'backend' | 'frontend'>('backend');
|
||||
/**
|
||||
* 病历诊断/医嘱「常用|我的」展示位置
|
||||
* tags=文本域下标签(默认) bubble=气泡内展示 both=两边
|
||||
*/
|
||||
const mrCommonDisplayMode = ref<'tags' | 'bubble' | 'both'>('tags');
|
||||
/** 开票通知渠道:subscribe / sms,默认仅订阅消息 */
|
||||
const invoiceNoticeChannels = ref<string[]>(['subscribe']);
|
||||
|
||||
@@ -53,6 +60,8 @@ function readStoredTab() {
|
||||
stored === 'salesperson' ||
|
||||
stored === 'invoice_notice' ||
|
||||
stored === 'doctor_login' ||
|
||||
stored === 'dict_search' ||
|
||||
stored === 'medical_record' ||
|
||||
stored === 'oa_notify'
|
||||
) {
|
||||
activeKey.value = stored;
|
||||
@@ -121,6 +130,15 @@ async function load() {
|
||||
if (row.config_key === 'doctor_single_device_login') {
|
||||
doctorSingleDeviceLogin.value = parseBoolConfig(row.config_value, false);
|
||||
}
|
||||
if (row.config_key === 'dict_search_rank_mode') {
|
||||
dictSearchRankMode.value =
|
||||
String(row.config_value || '') === 'frontend' ? 'frontend' : 'backend';
|
||||
}
|
||||
if (row.config_key === 'mr_common_display_mode') {
|
||||
const mode = String(row.config_value || 'tags');
|
||||
mrCommonDisplayMode.value =
|
||||
mode === 'bubble' || mode === 'both' ? mode : 'tags';
|
||||
}
|
||||
if (row.config_key === 'invoice_notice_channels') {
|
||||
let channels: unknown = row.config_value;
|
||||
if (typeof channels === 'string') {
|
||||
@@ -202,6 +220,22 @@ async function handleSave() {
|
||||
description: '业务端单机登录(医生/药师/诊所管理员等:1手机+1网页)',
|
||||
sort: 310,
|
||||
},
|
||||
{
|
||||
config_key: 'dict_search_rank_mode',
|
||||
config_value: dictSearchRankMode.value,
|
||||
value_type: 'string',
|
||||
config_group: 'search',
|
||||
description: '字典搜索匹配度:backend服务端 / frontend前端',
|
||||
sort: 320,
|
||||
},
|
||||
{
|
||||
config_key: 'mr_common_display_mode',
|
||||
config_value: mrCommonDisplayMode.value,
|
||||
value_type: 'string',
|
||||
config_group: 'medical_record',
|
||||
description: '病历诊断医嘱常用展示:tags标签 / bubble气泡 / both两边',
|
||||
sort: 330,
|
||||
},
|
||||
{
|
||||
config_key: 'invoice_notice_channels',
|
||||
config_value: JSON.stringify(
|
||||
@@ -331,6 +365,33 @@ onMounted(() => {
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="dict_search" tab="字典搜索">
|
||||
<div class="py-4">
|
||||
<div class="mb-2 font-medium">诊断 / 医嘱 / 病历词条匹配度处理端</div>
|
||||
<div class="mb-2 text-sm text-gray-500">
|
||||
后端:服务端计算匹配度并排序(适合疾病库等大数据量);前端:接口只返回候选,由端上打分排序(适合调试或减轻服务端 CPU)。关键字高亮始终在端上渲染。
|
||||
</div>
|
||||
<Radio.Group v-model:value="dictSearchRankMode">
|
||||
<Radio value="backend">后端处理(推荐)</Radio>
|
||||
<Radio value="frontend">前端处理</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="medical_record" tab="词条">
|
||||
<div class="py-4">
|
||||
<div class="mb-2 font-medium">诊断 / 医嘱「常用|我的」展示位置</div>
|
||||
<div class="mb-2 text-sm text-gray-500">
|
||||
控制接诊病历里诊断、医嘱的常用快捷出现在哪里。默认「标签」即文本域下方的快捷标签;「气泡卡片」则在关键字搜索气泡内默认展示常用;「两边」同时展示。
|
||||
</div>
|
||||
<Radio.Group v-model:value="mrCommonDisplayMode">
|
||||
<Radio value="tags">标签(默认)</Radio>
|
||||
<Radio value="bubble">气泡卡片默认展示</Radio>
|
||||
<Radio value="both">两边都展示</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="invoice_notice" tab="开票通知">
|
||||
<div class="py-4">
|
||||
<div class="mb-2 font-medium">患者开票通知渠道</div>
|
||||
|
||||
24
apps/web-antd/src/views/system/vip/feature/api/index.ts
Normal file
24
apps/web-antd/src/views/system/vip/feature/api/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** VIP 功能字典 API */
|
||||
const prefix = 'vip-feature/';
|
||||
|
||||
export async function getVipFeatureList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
export async function getVipFeatureOption(data?: any) {
|
||||
return requestClient.get<any>(`${prefix}option`, { params: data });
|
||||
}
|
||||
|
||||
export async function createVipFeature(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
export async function updateVipFeature(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
export async function deleteVipFeature(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* VIP 功能字典新增/编辑弹窗
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { createVipFeature, updateVipFeature } from '../api';
|
||||
import { modalFormProps } from '../config/form';
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const gridApi = ref();
|
||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
const api = isUpdate.value ? updateVipFeature : createVipFeature;
|
||||
await api(values);
|
||||
message.success('保存成功');
|
||||
gridApi.value?.query();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (!isOpen) {
|
||||
formApi.resetFields();
|
||||
return;
|
||||
}
|
||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||
isUpdate.value = !!update;
|
||||
if (values && update) {
|
||||
formApi.setValues({ ...values });
|
||||
} else {
|
||||
formApi.resetFields();
|
||||
formApi.setValues({ sort: 0, status: 1, description: '' });
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal :title="`${isUpdate ? '编辑' : '新增'}VIP功能`" class="w-[560px]">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
/** VIP 功能字典常量 */
|
||||
export const STATUS_OPTIONS = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
];
|
||||
59
apps/web-antd/src/views/system/vip/feature/config/form.ts
Normal file
59
apps/web-antd/src/views/system/vip/feature/config/form.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import { STATUS_OPTIONS } from './constants';
|
||||
|
||||
/** VIP 功能字典表单 */
|
||||
export const modalFormProps: VbenFormProps = {
|
||||
wrapperClass: 'grid-cols-12',
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
componentProps: { class: 'w-full' },
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'id',
|
||||
label: 'ID',
|
||||
dependencies: { show: false, triggerFields: ['id'] },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'code',
|
||||
label: '功能编码',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: 'snake_case,如 medical_record' },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'name',
|
||||
label: '功能名称',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '如:病历' },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'description',
|
||||
label: '说明',
|
||||
componentProps: { type: 'textarea', rows: 2 },
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: { min: 0 },
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: { options: STATUS_OPTIONS },
|
||||
defaultValue: 1,
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
};
|
||||
169
apps/web-antd/src/views/system/vip/feature/index.vue
Normal file
169
apps/web-antd/src/views/system/vip/feature/index.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* VIP 功能字典列表:供等级 permissions 勾选的功能码维护
|
||||
*/
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Empty,
|
||||
Input,
|
||||
Pagination,
|
||||
Select,
|
||||
Tag,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import { deleteVipFeature, getVipFeatureList } from './api';
|
||||
import FormModal from './components/modal.vue';
|
||||
import { STATUS_OPTIONS } from './config/constants';
|
||||
|
||||
defineOptions({ name: 'VipFeature' });
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<any[]>([]);
|
||||
const total = ref(0);
|
||||
const page = reactive({ current: 1, pageSize: 12 });
|
||||
const filters = reactive({
|
||||
code: '',
|
||||
name: '',
|
||||
status: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const [FormModalComp, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModal,
|
||||
});
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getVipFeatureList({
|
||||
page: page.current,
|
||||
pageSize: page.pageSize,
|
||||
code: filters.code || undefined,
|
||||
name: filters.name || undefined,
|
||||
status: filters.status,
|
||||
});
|
||||
list.value = res?.items || [];
|
||||
total.value = Number(res?.total || 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
page.current = 1;
|
||||
loadList();
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
filters.code = '';
|
||||
filters.name = '';
|
||||
filters.status = undefined;
|
||||
onSearch();
|
||||
}
|
||||
|
||||
const showModal = (data: any = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
values: data,
|
||||
update: isUpdate,
|
||||
gridApi: { query: loadList, reload: loadList },
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
deleteVipFeature({ ids: [id] }).then(() => {
|
||||
message.success('删除成功');
|
||||
loadList();
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height title="VIP功能字典">
|
||||
<FormModalComp />
|
||||
<div class="p-4">
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<Input
|
||||
v-model:value="filters.code"
|
||||
allow-clear
|
||||
placeholder="功能编码"
|
||||
class="w-40"
|
||||
@press-enter="onSearch"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="filters.name"
|
||||
allow-clear
|
||||
placeholder="功能名称"
|
||||
class="w-40"
|
||||
@press-enter="onSearch"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="filters.status"
|
||||
allow-clear
|
||||
placeholder="状态"
|
||||
class="w-28"
|
||||
:options="STATUS_OPTIONS"
|
||||
/>
|
||||
<Button type="primary" @click="onSearch">查询</Button>
|
||||
<Button @click="onReset">重置</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增功能',
|
||||
type: 'primary',
|
||||
onClick: () => showModal({}, false),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="list.length" class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card v-for="item in list" :key="item.id" size="small" :loading="loading">
|
||||
<template #title>
|
||||
<span class="font-medium">{{ item.name }}</span>
|
||||
<Tag class="ml-2" :color="item.status === 1 ? 'success' : 'default'">
|
||||
{{ item.status_txt }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #extra>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{ label: '编辑', onClick: () => showModal(item, true) },
|
||||
{
|
||||
label: '删除',
|
||||
danger: true,
|
||||
popConfirm: {
|
||||
title: '确认删除该功能?',
|
||||
confirm: () => handleDelete(item.id),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<div class="text-sm text-gray-600">
|
||||
<div>编码:{{ item.code }}</div>
|
||||
<div class="mt-1">说明:{{ item.description || '-' }}</div>
|
||||
<div class="mt-1">排序:{{ item.sort }}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<Empty v-else description="暂无功能,请先新增或执行 SQL 种子" />
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Pagination
|
||||
v-model:current="page.current"
|
||||
v-model:page-size="page.pageSize"
|
||||
:total="total"
|
||||
show-size-changer
|
||||
@change="loadList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* VIP 等级新增/编辑弹窗
|
||||
* permissions 从 VIP 功能字典 option 多选
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
@@ -9,6 +10,7 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getVipFeatureOption } from '#/views/system/vip/feature/api';
|
||||
|
||||
import { createVipLevel, updateVipLevel } from '../api';
|
||||
import { modalFormProps } from '../config/form';
|
||||
@@ -37,12 +39,30 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (!isOpen) {
|
||||
formApi.resetFields();
|
||||
return;
|
||||
}
|
||||
// 打开时拉取功能字典,注入 permissions 下拉
|
||||
try {
|
||||
const opts = await getVipFeatureOption();
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'permissions',
|
||||
componentProps: {
|
||||
mode: 'multiple',
|
||||
allowClear: true,
|
||||
optionFilterProp: 'label',
|
||||
placeholder: '从功能字典多选',
|
||||
options: Array.isArray(opts) ? opts : [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error('加载 VIP 功能字典失败', e);
|
||||
}
|
||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||
isUpdate.value = !!update;
|
||||
if (values && update) {
|
||||
|
||||
@@ -72,11 +72,14 @@ export const modalFormProps: VbenFormProps = {
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'permissions',
|
||||
label: '权限码',
|
||||
label: 'VIP功能',
|
||||
help: '低等级勾选后,更高权重等级会自动补齐缺失功能',
|
||||
componentProps: {
|
||||
mode: 'tags',
|
||||
placeholder: '输入权限码后回车,预留业务权益',
|
||||
tokenSeparators: [','],
|
||||
mode: 'multiple',
|
||||
allowClear: true,
|
||||
optionFilterProp: 'label',
|
||||
placeholder: '从功能字典多选(需先在 VIP功能字典 维护)',
|
||||
options: [],
|
||||
},
|
||||
defaultValue: [],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user