ReportSchemeEdit.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <script setup lang="ts">
  2. import RemoteSelect from '@/libs/v-select-page/RemoteSelect.vue';
  3. import type { ReportSchemeItemModel } from '@/model';
  4. import {
  5. acupointsMethod,
  6. herbalMedicineUnitMethod,
  7. medicinesMethod,
  8. schemeCategoryOptionsMethod,
  9. } from '@/request/api/dictionary.api';
  10. import { editSchemeMethod, searchSchemeMethod } from '@/request/api/report.api';
  11. import { withResolvers } from '@/tools/promise';
  12. import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
  13. import { useRequest, useWatcher } from 'alova/client';
  14. import { Form, message as Message, notification as Notification } from 'ant-design-vue';
  15. import type { Rule } from 'ant-design-vue/es/form';
  16. import { h } from 'vue';
  17. const getModel = (item: Partial<ReportSchemeItemModel> = {}): ReportSchemeItemModel => {
  18. model.id = item.id;
  19. model.name = item.name;
  20. model.category = item.category;
  21. model.type = item.type;
  22. model.content = item.content?.map(_ => Object.assign({}, _)) ?? [];
  23. model.descriptions = item.descriptions?.map(_ => Object.assign({}, _)) ?? [];
  24. };
  25. const dataValidValidator = (_rule, value: number, callback): Rule['validator'] => {
  26. return model.content?.filter(t => t.name)?.length || model.descriptions?.filter(t => t?.name)?.length
  27. ? Promise.resolve()
  28. : Promise.reject();
  29. };
  30. const groupValidatorDescription = (_rule, value: Record<string, string>, callback): Rule['validator'] => {
  31. if ( value ) return Promise.resolve();
  32. const index = _rule.field.split('.')[ 1 ];
  33. const values = [ model.descriptions[ index ]?.name, model.descriptions[ index ]?.description ];
  34. return values.some(v => !!v) ? Promise.reject() : Promise.resolve();
  35. };
  36. const groupValidatorContent = (_rule, value: Record<string, string>, callback): Rule['validator'] => {
  37. if ( value ) return Promise.resolve();
  38. const index = _rule.field.split('.')[ 1 ];
  39. const values = [ model.content[ index ]?.name, model.content[ index ]?.doase ];
  40. return values.some(v => !!v) ? Promise.reject() : Promise.resolve();
  41. };
  42. const props = defineProps<{
  43. value?: ReportSchemeItemModel,
  44. reportId?: string;
  45. }>();
  46. const emits = defineEmits<{
  47. 'update:value': [ model: ReportSchemeItemModel ],
  48. 'destroy': [],
  49. }>();
  50. const model = reactive<ReportSchemeItemModel>({
  51. count: 0,
  52. });
  53. const rules = reactive<Record<string, Rule[]>>({
  54. category: [ { required: true, message: '请选择方案类型' } ],
  55. count: [],
  56. });
  57. const nextDestroy = ref(false);
  58. const { validateInfos } = Form.useForm(model, rules);
  59. const { data: medicineUnitOptions, loading: medicineUnitLoading } = useRequest(
  60. herbalMedicineUnitMethod,
  61. { initialData: [] },
  62. );
  63. const { data: categoryOptions, loading: categoryLoading } = useRequest(
  64. schemeCategoryOptionsMethod,
  65. { initialData: [] },
  66. ).onSuccess(({ data }) => {
  67. if ( !model.category ) model.category = data[ 0 ]?.value;
  68. });
  69. const { data: scheme, loading: schemeLoading, send: search } = useWatcher(
  70. (keyword) => searchSchemeMethod(keyword, model),
  71. [ () => model.category ],
  72. {
  73. middleware: (_, next) => { if ( model.category ) next(); },
  74. },
  75. );
  76. const { send: submit } = useRequest(
  77. () => editSchemeMethod(props.reportId, model),
  78. { immediate: false },
  79. ).onSuccess(({ data }) => {
  80. Notification.success({
  81. message: '操作成功',
  82. });
  83. emits('update:value', data);
  84. if ( nextDestroy.value ) emits('destroy');
  85. });
  86. const editableContent = computed(() => [ 'acupoint', 'medicine' ].includes(model.type));
  87. const appendContent = (data?: Record<string, any>) => {
  88. if ( !editableContent.value ) return;
  89. model.content?.push({ id: `custom-${ Date.now() }`, ...data });
  90. };
  91. const removeContent = (data: Record<string, any>, index?: number) => {
  92. if ( !editableContent.value ) return;
  93. index ??= model.content?.findIndex(t => t.id === data.id);
  94. if ( index != null && index > -1 ) model.content?.splice(index, 1);
  95. if ( model.content?.length === 0 ) appendContent();
  96. };
  97. const updateContent = (data: Record<string, any> | null, index: number) => {
  98. const old = model.content[ index ];
  99. const name = data?.name;
  100. const doase = data?.doase ?? old?.doase;
  101. const imgUrl = data?.photo;
  102. let unit = data?.unit ?? old?.unit;
  103. if ( model.type === 'medicine' ) unit ??= medicineUnitOptions.value[ 0 ]?.value;
  104. model.content[ index ] = { ...model.content[ index ], name, imgUrl, doase, unit, type: model.type };
  105. };
  106. const appendDescription = (data?: Record<string, any>) => {
  107. model.descriptions?.push({ id: `custom-${ Date.now() }`, name: '', description: '', ...data });
  108. };
  109. const removeDescription = (data: Record<string, any>, index?: number) => {
  110. index ??= model.descriptions?.findIndex(t => t.id === data.id);
  111. if ( index != null && index > -1 ) model.descriptions?.splice(index, 1);
  112. if ( model.descriptions?.length === 0 ) appendDescription();
  113. };
  114. const toggleTypeConfirmProps = shallowReactive({
  115. show: false,
  116. title: '确定切换?',
  117. resolve: () => void 0,
  118. });
  119. const toggleTypeConfirm = (value: string | null) => {
  120. const { promise, resolve } = withResolvers<boolean>();
  121. setTimeout(() => document.documentElement.addEventListener('click', () => resolve(false), { once: true }), 20);
  122. toggleTypeConfirmProps.resolve = resolve;
  123. toggleTypeConfirmProps.title = value ? `确定切换数据` : `确定取消数据`;
  124. toggleTypeConfirmProps.show = true;
  125. promise.then(() => toggleTypeConfirmProps.show = false);
  126. return promise;
  127. };
  128. const toggleType = async (value) => {
  129. let toggle = !model.content?.some(t => t.name) || await toggleTypeConfirm(value);
  130. if ( toggle ) {
  131. model.type = value;
  132. model.content = [];
  133. appendContent();
  134. }
  135. };
  136. onBeforeMount(() => {
  137. getModel(props.value);
  138. if ( !model.content?.length ) appendContent();
  139. if ( !model.descriptions?.length ) appendDescription();
  140. });
  141. function onFinishFailed(e) {
  142. Message.warning(`请补充完整!`);
  143. }
  144. function selectSchemeHandle(name: string) {
  145. const data = scheme.value.find((s) => s.label === name);
  146. Object.keys(data ?? {}).forEach((key: string) => {
  147. const value = data[ key ];
  148. if ( Array.isArray(value) ) {
  149. model[ key ] = [ ...value ];
  150. } else if ( value && typeof value === 'object' ) {
  151. model[ key ] = { ...value };
  152. } else {
  153. model[ key ] = value;
  154. }
  155. });
  156. }
  157. </script>
  158. <template>
  159. <a-form
  160. class="form-wrapper" :model="model" scroll-to-first-error
  161. @finish="submit()" @finishFailed="onFinishFailed"
  162. >
  163. <a-form-item
  164. label="方案类型" name="category"
  165. :rules="{ required: true, message: '请选择方案类型' }"
  166. >
  167. <a-select v-model:value="model.category" placeholder="方案类型" showSearch
  168. :options="categoryOptions" :loading="categoryLoading"
  169. />
  170. </a-form-item>
  171. <a-form-item label="方案名称" v-bind="validateInfos.name">
  172. <a-select
  173. show-search placeholder="方案名称"
  174. :options="scheme" :loading="schemeLoading"
  175. :field-names="{label: 'label', value: 'label'}"
  176. v-model:value="model.name" @update:value="selectSchemeHandle"
  177. allow-clear @deselect="model.name = void 0;"
  178. @search="search" @dropdownVisibleChange="$event && search('');"
  179. not-found-content="请输入方案名称"
  180. ></a-select>
  181. </a-form-item>
  182. <a-form-item name="name">
  183. <template #label>
  184. <div>数据类型</div>
  185. <a-popconfirm :title="toggleTypeConfirmProps.title" description="确定后将重置【数据】"
  186. placement="topLeft" :open="toggleTypeConfirmProps.show"
  187. okText="确定" @confirm="()=>toggleTypeConfirmProps.resolve(true)"
  188. cancelText="取消" @cancel="()=>toggleTypeConfirmProps.resolve(false)"
  189. />
  190. </template>
  191. <vxe-radio-group :modelValue="model.type" :strict="false" @update:modelValue="toggleType">
  192. <vxe-radio label="medicine" content="中药"></vxe-radio>
  193. <vxe-radio label="acupoint" content="穴位"></vxe-radio>
  194. </vxe-radio-group>
  195. </a-form-item>
  196. <!-- 可编辑数据 -->
  197. <template v-if="editableContent">
  198. <a-space-compact v-for="(row, index) in model.content" :key="row.id" block>
  199. <!-- 穴位 -->
  200. <template v-if="model.type === 'acupoint'">
  201. <a-form-item
  202. :label="index ? ' ' : '穴位数据'" :colon="!index"
  203. :name="['descriptions', index, 'name']" class="w-full"
  204. >
  205. <RemoteSelect
  206. :load="acupointsMethod" key-prop="name" v-model:value="row.name"
  207. @update="updateContent($event, index)"
  208. />
  209. </a-form-item>
  210. </template>
  211. <template v-else-if="model.type === 'medicine'">
  212. <a-form-item
  213. class="w-80%" :label="index ? ' ' : '中药数据'" :colon="!index"
  214. :name="['content', index, 'name']"
  215. :rules="{ validator: groupValidatorContent, message: '请选择中药' }"
  216. >
  217. <RemoteSelect
  218. :load="medicinesMethod" key-prop="name" v-model:value="row.name"
  219. @update="updateContent($event, index)"
  220. />
  221. </a-form-item>
  222. <a-form-item
  223. :name="['content', index, 'doase']"
  224. :rules="{ validator: groupValidatorContent, message: '请输入剂量' }"
  225. >
  226. <a-input-number v-model:value="row.doase" :min="0" :max="100" :precision="2" placeholder="剂量" />
  227. </a-form-item>
  228. <a-form-item
  229. class="w-120px"
  230. :name="['content', index, 'unit']"
  231. :rules="{ validator: groupValidatorContent, message: '请选择单位' }"
  232. >
  233. <a-select v-model:value="row.unit" placeholder="单位" :options="medicineUnitOptions"
  234. :loading="medicineUnitLoading" allow-clear
  235. ></a-select>
  236. </a-form-item>
  237. </template>
  238. <span class="m-l-8px">
  239. <span class="p-b-24px flex items-center h-full">
  240. <MinusCircleOutlined @click="removeContent(row, index)" />
  241. </span>
  242. </span>
  243. </a-space-compact>
  244. <a-form-item label=" " :colon="false" class="p-r-22px">
  245. <a-button type="dashed" block :icon="h(PlusOutlined)" @click="appendContent">
  246. 添加一条数据
  247. </a-button>
  248. </a-form-item>
  249. </template>
  250. <!-- 说明 -->
  251. <a-space-compact v-for="(row, index) in model.descriptions" :key="row.id" block>
  252. <a-form-item
  253. :label="index ? ' ' : '说明'" :colon="!index"
  254. :name="['descriptions', index, 'name']"
  255. :rules="{ validator: groupValidatorDescription, message: '请输入标题' }"
  256. >
  257. <a-input v-model:value.trim="model.descriptions[index].name" placeholder="标题" />
  258. </a-form-item>
  259. <a-form-item
  260. class="w-80%"
  261. :name="['descriptions', index, 'description']"
  262. :rules="{ validator: groupValidatorDescription, message: '请输入内容' }"
  263. >
  264. <a-textarea v-model:value="row.description" placeholder="内容" :auto-size="{ minRows: 1, maxRows: 10 }" />
  265. </a-form-item>
  266. <span class="m-l-8px">
  267. <span class="p-b-24px flex items-center h-full">
  268. <MinusCircleOutlined @click="removeDescription(row, index)" />
  269. </span>
  270. </span>
  271. </a-space-compact>
  272. <a-form-item label=" " :colon="false" class="p-r-22px">
  273. <a-button type="dashed" block :icon="h(PlusOutlined)" @click="appendDescription">
  274. 添加一条说明
  275. </a-button>
  276. <div></div>
  277. </a-form-item>
  278. <a-form-item
  279. name="count"
  280. :rules="{ validator: dataValidValidator, message: '数据或者说明至少需要添加一项' }"
  281. >
  282. <a-space>
  283. <a-button type="primary" html-type="submit" @click="nextDestroy=false">保存</a-button>
  284. <a-button type="default" html-type="submit" @click="nextDestroy=true">保存并关闭</a-button>
  285. <a-button type="dashed" @click="emits('destroy')">关闭</a-button>
  286. </a-space>
  287. </a-form-item>
  288. </a-form>
  289. </template>
  290. <style scoped lang="scss">
  291. .form-wrapper {
  292. padding: 12px 24px;
  293. :deep(.ant-form-item) {
  294. &.max {
  295. flex: 1 1 80%;
  296. }
  297. .ant-form-item-label {
  298. width: 80px;
  299. }
  300. }
  301. }
  302. </style>