hl-core 0.0.10-beta.7 → 0.0.10-beta.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +0 -2
  2. package/api/base.api.ts +425 -134
  3. package/api/interceptors.ts +162 -62
  4. package/components/Dialog/Dialog.vue +5 -1
  5. package/components/Dialog/DigitalDocumentsDialog.vue +129 -0
  6. package/components/Dialog/FamilyDialog.vue +15 -4
  7. package/components/Form/DigitalDocument.vue +52 -0
  8. package/components/Form/FormSource.vue +30 -0
  9. package/components/Form/ManagerAttachment.vue +85 -11
  10. package/components/Form/ProductConditionsBlock.vue +12 -6
  11. package/components/Input/Datepicker.vue +5 -0
  12. package/components/Input/FileInput.vue +1 -1
  13. package/components/Input/FormInput.vue +7 -0
  14. package/components/Input/OtpInput.vue +25 -0
  15. package/components/Input/RoundedInput.vue +2 -0
  16. package/components/Input/RoundedSelect.vue +2 -0
  17. package/components/Input/TextAreaField.vue +71 -0
  18. package/components/Input/TextHint.vue +13 -0
  19. package/components/Layout/SettingsPanel.vue +2 -1
  20. package/components/Menu/MenuNav.vue +2 -1
  21. package/components/Pages/Anketa.vue +207 -176
  22. package/components/Pages/Auth.vue +10 -3
  23. package/components/Pages/ContragentForm.vue +24 -18
  24. package/components/Pages/Documents.vue +488 -66
  25. package/components/Pages/MemberForm.vue +1009 -268
  26. package/components/Pages/ProductConditions.vue +1424 -273
  27. package/components/Panel/PanelHandler.vue +329 -126
  28. package/components/Utilities/Chip.vue +1 -1
  29. package/components/Utilities/JsonViewer.vue +1 -2
  30. package/composables/classes.ts +136 -20
  31. package/composables/constants.ts +168 -1
  32. package/composables/index.ts +467 -9
  33. package/composables/styles.ts +8 -24
  34. package/configs/i18n.ts +2 -0
  35. package/configs/pwa.ts +1 -7
  36. package/layouts/clear.vue +1 -1
  37. package/layouts/default.vue +2 -2
  38. package/layouts/full.vue +1 -1
  39. package/locales/kz.json +1239 -0
  40. package/locales/ru.json +133 -21
  41. package/nuxt.config.ts +8 -6
  42. package/package.json +14 -13
  43. package/plugins/head.ts +7 -1
  44. package/plugins/helperFunctionsPlugins.ts +1 -0
  45. package/store/data.store.ts +1080 -552
  46. package/store/member.store.ts +19 -8
  47. package/store/rules.ts +75 -8
  48. package/types/enum.ts +52 -2
  49. package/types/index.ts +143 -6
@@ -1,46 +1,106 @@
1
- import { AxiosError, type AxiosInstance } from 'axios';
1
+ import { AxiosError, type AxiosInstance, type InternalAxiosRequestConfig, isAxiosError } from 'axios';
2
2
 
3
- export default function (axios: AxiosInstance) {
3
+ /**
4
+ * Обновляет baseURL в зависимости от текущего хоста
5
+ */
6
+ function updateBaseUrlForEnvironment(request: InternalAxiosRequestConfig): void {
7
+ if (!request.url || !request.baseURL) return;
8
+
9
+ const host = window.location.hostname;
10
+
11
+ if (host.startsWith('bpmsrv02') || host.startsWith('vega')) {
12
+ if (request.baseURL === getStrValuePerEnv('baseApi')) {
13
+ request.baseURL = getStrValuePerEnv('baseApiLocal');
14
+ }
15
+ if (request.baseURL === getStrValuePerEnv('efoBaseApi')) {
16
+ request.baseURL = getStrValuePerEnv('efoBaseApiLocal');
17
+ }
18
+ if (request.baseURL === getStrValuePerEnv('amlBaseApi')) {
19
+ request.baseURL = getStrValuePerEnv('amlBaseApiLocal');
20
+ }
21
+ if (request.baseURL === getStrValuePerEnv('gatewayApiUrl')) {
22
+ request.baseURL = getStrValuePerEnv('gatewayApiUrlLocal');
23
+ }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Обрабатывает специальную логику для нового API
29
+ */
30
+ function handleNewApiLogic(request: InternalAxiosRequestConfig): void {
31
+ if (!request.url || !request.baseURL) return;
32
+
33
+ if (import.meta.env.VITE_ON_NEW_API === 'true' && request.url.includes('api/Application')) {
34
+ if (request.baseURL === 'http://vega:84') {
35
+ request.baseURL = 'http://efo-dev.halyklife.nb/api';
36
+ }
37
+ if (request.baseURL === 'http://bpmsrv02.halyklife.nb') {
38
+ request.baseURL = 'http://efo-prod.halyklife.nb/api';
39
+ }
40
+
41
+ request.url = request.url.replace('/api/Application', '');
42
+
43
+ if (request.baseURL.includes('api/v1/insis')) {
44
+ request.baseURL = request.baseURL.replace('api/v1/insis', 'efo/api');
45
+ }
46
+ if (request.baseURL.includes('api/v1/test/insis')) {
47
+ request.baseURL = request.baseURL.replace('api/v1/test/insis', 'test/efo/api');
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Извлекает полезное сообщение об ошибке из ответа
54
+ */
55
+ function extractErrorMessage(data: any, status: number, dataStore: any): string {
56
+ // .NET ProblemDetails
57
+ if (data && typeof data === 'object') {
58
+ if (data.title || data.detail) {
59
+ return data.title || data.detail;
60
+ }
61
+ // Распространённые поля
62
+ if (data.message || data.status || data.statusName) {
63
+ return data.message || data.status || data.statusName;
64
+ }
65
+ }
66
+
67
+ // Если строка/HTML
68
+ if (typeof data === 'string') {
69
+ return data.slice(0, 200);
70
+ }
71
+
72
+ // Фолбэк
73
+ return status === 500 ? dataStore.t('toaster.serverError') : `HTTP ${status}`;
74
+ }
75
+
76
+ /**
77
+ * Настраивает interceptors для axios instance
78
+ *
79
+ * @param axios - Экземпляр axios для настройки
80
+ *
81
+ * Включает:
82
+ * - Автоматическое добавление токена авторизации
83
+ * - Обработку различных окружений (dev/prod)
84
+ * - Комплексную обработку ошибок с информативными сообщениями
85
+ * - Защиту от дублирования 401 ошибок
86
+ */
87
+ export default function setupInterceptors(axios: AxiosInstance): void {
4
88
  axios.interceptors.request.use(
5
- request => {
89
+ (request: InternalAxiosRequestConfig) => {
6
90
  const dataStore = useDataStore();
91
+
92
+ // Добавляем токен авторизации
7
93
  if (dataStore.accessToken) {
94
+ request.headers = request.headers || {};
8
95
  request.headers.Authorization = `Bearer ${dataStore.accessToken}`;
9
96
  }
10
- if (request.url && request.baseURL) {
11
- const host = window.location.hostname;
12
- if (host.startsWith('bpmsrv02') || host.startsWith('vega')) {
13
- if (request.baseURL === getStrValuePerEnv('baseApi')) {
14
- request.baseURL = getStrValuePerEnv('baseApiLocal');
15
- }
16
- if (request.baseURL === getStrValuePerEnv('efoBaseApi')) {
17
- request.baseURL = getStrValuePerEnv('efoBaseApiLocal');
18
- }
19
- if (request.baseURL === getStrValuePerEnv('amlBaseApi')) {
20
- request.baseURL = getStrValuePerEnv('amlBaseApiLocal');
21
- }
22
- if (request.baseURL === getStrValuePerEnv('gatewayApiUrl')) {
23
- request.baseURL = getStrValuePerEnv('gatewayApiUrlLocal');
24
- }
25
- }
26
- if (import.meta.env.VITE_ON_NEW_API === 'true') {
27
- if (request.url.includes('api/Application')) {
28
- if (request.baseURL === 'http://vega:84') {
29
- request.baseURL = 'http://efo-dev.halyklife.nb/api';
30
- }
31
- if (request.baseURL === 'http://bpmsrv02.halyklife.nb') {
32
- request.baseURL = 'http://efo-prod.halyklife.nb/api';
33
- }
34
- request.url = request.url.replace('/api/Application', '');
35
- if (request.baseURL.includes('api/v1/insis')) {
36
- request.baseURL = request.baseURL.replace('api/v1/insis', 'efo/api');
37
- }
38
- if (request.baseURL.includes('api/v1/test/insis')) {
39
- request.baseURL = request.baseURL.replace('api/v1/test/insis', 'test/efo/api');
40
- }
41
- }
42
- }
43
- }
97
+
98
+ // Обновляем baseURL для разных окружений
99
+ updateBaseUrlForEnvironment(request);
100
+
101
+ // Обрабатываем логику нового API
102
+ handleNewApiLogic(request);
103
+
44
104
  return request;
45
105
  },
46
106
  (error: AxiosError) => {
@@ -48,34 +108,74 @@ export default function (axios: AxiosInstance) {
48
108
  },
49
109
  );
50
110
  axios.interceptors.response.use(
51
- response => {
52
- return response;
53
- },
54
- (error: AxiosError) => {
111
+ response => response,
112
+ (err: unknown) => {
55
113
  const dataStore = useDataStore();
56
- if (!dataStore.isCalculator) {
57
- const router = useRouter();
58
- if (error && error.response && error.response.status) {
59
- if (error.response.status === 401) {
60
- dataStore.$reset();
61
- localStorage.clear();
62
- if (dataStore.isBridge) {
63
- router.push({ name: 'Auth', query: { error: 401 } });
64
- } else {
65
- dataStore.sendToParent(constants.postActions.Error401, 401);
66
- }
67
- }
68
- if (error.response.status >= 500) {
69
- if (router.currentRoute.value.name !== 'Auth') {
70
- dataStore.showToaster('error', error.stack ?? dataStore.t('toaster.error'), 5000);
71
- }
72
- }
73
- if (error.response.status === 403 && error.response.config.url) {
74
- dataStore.showToaster('error', `Нет доступа на запрос: ${error.response.config.url.substring(error.response.config.url.lastIndexOf('/') + 1)}`, 5000);
75
- }
114
+
115
+ // если калькулятор, выходим (твоя логика)
116
+ if (dataStore.isCalculator) {
117
+ return Promise.reject(err);
118
+ }
119
+
120
+ // 1) Не-Axios ошибка (например, thrown Error)
121
+ if (!isAxiosError(err)) {
122
+ dataStore.showToaster('error', dataStore.t('toaster.unknownError'), 5000);
123
+ return Promise.reject(err);
124
+ }
125
+
126
+ const { response, request, code, message, config } = err;
127
+
128
+ // 2) Нет ответа вообще (timeout, сеть, CORS)
129
+ if (!response) {
130
+ const isTimeout = code === 'ECONNABORTED' || /timeout/i.test(String(message));
131
+ dataStore.showToaster('error', isTimeout ? dataStore.t('toaster.timeout') : dataStore.t('toaster.networkError'), 5000);
132
+ return Promise.reject(err);
133
+ }
134
+
135
+ const status = response.status;
136
+ const data = response.data;
137
+ const isSilent = !!response.config.params?.silent;
138
+
139
+ // 3) 401 — простая обработка, показываем ошибку
140
+ if (status === 401) {
141
+ dataStore.showToaster('error', dataStore.t('error.401'), 5000);
142
+ return Promise.reject(err);
143
+ }
144
+
145
+ // 4) 403 — вытаскиваем только path без query/hash
146
+ if (status === 403) {
147
+ try {
148
+ const rawUrl = response.config?.url ?? '';
149
+ const urlObj = new URL(rawUrl, window.location.origin);
150
+ const pathSegment = urlObj.pathname.split('/').filter(Boolean).pop() ?? 'resource';
151
+ dataStore.showToaster('error', dataStore.t('error.403', { text: pathSegment }), 5000);
152
+ } catch (urlError) {
153
+ console.warn('Error parsing URL for 403 response:', urlError);
154
+ dataStore.showToaster('error', dataStore.t('error.403'), 5000);
76
155
  }
156
+ return Promise.reject(err);
77
157
  }
78
- return Promise.reject(error);
158
+
159
+ // 5) 404
160
+ if (status === 404 && !isSilent) {
161
+ dataStore.showToaster('error', dataStore.t('error.404'), 5000);
162
+ return Promise.reject(err);
163
+ }
164
+
165
+ // 6) 413
166
+ if (status === 413) {
167
+ dataStore.showToaster('error', dataStore.t('error.exceedUploadLimitFile'), 5000);
168
+ return Promise.reject(err);
169
+ }
170
+
171
+ // 7) 500 — явная обработка
172
+ if (status === 500) {
173
+ const errorMessage = extractErrorMessage(data, status, dataStore);
174
+ dataStore.showToaster('error', String(errorMessage), 5000);
175
+ return Promise.reject(err);
176
+ }
177
+
178
+ return Promise.reject(err);
79
179
  },
80
180
  );
81
181
  }
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <v-dialog class="base-dialog" :model-value="Boolean(modelValue)" @update:modelValue="$emit('update:modelValue', $event)" :persistent="true">
2
+ <v-dialog class="base-dialog" :model-value="Boolean(modelValue)" @update:modelValue="$emit('update:modelValue', $event)" :persistent="persistent">
3
3
  <v-card class="self-center w-full sm:w-4/4 md:w-2/3 lg:w-[35%] xl:w-[500px] rounded-lg !p-[36px]">
4
4
  <div class="flex sm:flex-row flex-col place-items-center sm:place-items-start">
5
5
  <div class="h-20 w-20 place-items-start pt-1">
@@ -44,6 +44,10 @@ export default defineComponent({
44
44
  type: Boolean as PropType<boolean | null>,
45
45
  default: false,
46
46
  },
47
+ persistent: {
48
+ type: Boolean as PropType<boolean>,
49
+ default: true,
50
+ },
47
51
  title: {
48
52
  type: String,
49
53
  default() {
@@ -0,0 +1,129 @@
1
+ <template>
2
+ <div class="flex flex-col gap-[10px] w-full align-center">
3
+ <v-expansion-panels :flat="true">
4
+ <v-expansion-panel class="!rounded-[8px]">
5
+ <v-expansion-panel-title class="!text-[12px] border border-[#00000014]">
6
+ Как получить цифровой документ
7
+ </v-expansion-panel-title>
8
+ <v-expansion-panel-text class="text-[12px] text-[#464f60]">
9
+ 1. Выберите тип документа.<br /><br />
10
+ 2. Через приложение eGov mobile и другие приложения: <br />
11
+ • Откройте раздел "Цифровые документы". <br />
12
+ • Выберите нужный документ и откройте доступ. <br />
13
+ • Введите 6-значный код в поле «Код подтверждения». <br />
14
+ • Нажмите "Получить документ".<br /><br />
15
+ 3. Через SMS: <br />
16
+ • Нажмите "Отправить код". <br />
17
+ • Введите полученный SMS-код. <br />
18
+ • Нажмите "Получить документ".<br /><br />
19
+ 4. При ошибке нажмите <a href="javascript:void(0);" class="text-blue-600" @click.prevent="emit('updateDigitalDocuments')">обновить профиль</a><br />
20
+ </v-expansion-panel-text>
21
+ </v-expansion-panel>
22
+ </v-expansion-panels>
23
+ <div class="d-flex flex-col gap-0.5 w-full">
24
+ <base-rounded-select
25
+ v-model="documentType"
26
+ class="document-type-select"
27
+ :items="documentItems"
28
+ :label="$dataStore.t('form.documentType')"
29
+ hide-details
30
+ />
31
+ <div class="digital-document-otp flex flex-col">
32
+ <base-otp-input
33
+ v-model="otpCode"
34
+ @keyup.enter.prevent="otpCode.length === otpLength && emitGetCode()"
35
+ />
36
+ <span
37
+ v-if="!loading && otpSendDisabled"
38
+ class="text-center"
39
+ :class="[$styles.mutedText]"
40
+ >
41
+ Введите код цифрового документа из <span class="underline underline-offset-2">eGov Mobile</span> или <span class="underline underline-offset-2">банковского приложения</span>.
42
+ </span>
43
+ </div>
44
+ </div>
45
+ <div class="w-full d-flex gap-4">
46
+ <base-btn
47
+ v-if="!otpSendDisabled"
48
+ :disabled="loading"
49
+ :loading="loading"
50
+ :btn="$styles.whiteBorderBtn"
51
+ text="Отправить SMS-код"
52
+ @click="emitGetCode"
53
+ />
54
+ <base-btn
55
+ :disabled="loading"
56
+ :loading="loading"
57
+ text="Получить документ"
58
+ @click="emitGetDocument"
59
+ />
60
+ </div>
61
+ </div>
62
+ </template>
63
+
64
+ <script setup lang="ts">
65
+ import type { DigitalDocTypes } from '../../types';
66
+
67
+ const props = defineProps({
68
+ documentItems: {
69
+ type: Array,
70
+ required: true,
71
+ },
72
+ loading: {
73
+ type: Boolean,
74
+ default: false,
75
+ },
76
+ otpLength: {
77
+ type: Number,
78
+ default: 6,
79
+ },
80
+ otpSendDisabled: {
81
+ type: Boolean,
82
+ default: false,
83
+ }
84
+ });
85
+ const emit = defineEmits(['getCode', 'getDigitalDocument', 'updateDigitalDocuments']);
86
+
87
+ const dataStore = useDataStore();
88
+ const documentType = ref<DigitalDocTypes | null>(null);
89
+ const otpCode = ref<string>('');
90
+
91
+ const emitGetCode = () => {
92
+ if (!documentType.value) {
93
+ dataStore.showToaster('error', 'Выберите тип документа', 3000);
94
+ return;
95
+ }
96
+
97
+ emit('getCode', documentType.value);
98
+ }
99
+
100
+ const emitGetDocument = () => {
101
+ if (!otpCode.value) {
102
+ dataStore.showToaster('error', 'Введите код подтверждения', 3000);
103
+ return;
104
+ }
105
+
106
+ emit('getDigitalDocument', otpCode.value);
107
+ }
108
+ </script>
109
+
110
+ <style scoped>
111
+ :deep(.v-otp-input__content) {
112
+ max-width: 360px;
113
+ gap: 12px!important;
114
+ }
115
+ .v-expansion-panel-title {
116
+ height: 60px!important;
117
+ }
118
+ .v-expansion-panel--active > .v-expansion-panel-title {
119
+ border-bottom-left-radius: inherit;
120
+ border-bottom-right-radius: inherit;
121
+ }
122
+ .document-type-select:deep(.v-field) {
123
+ height: 60px;
124
+ border: 1px solid #dadada!important;
125
+ }
126
+ .document-type-select:deep(.v-label.v-field-label--floating) {
127
+ top: 0;
128
+ }
129
+ </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <v-list lines="two" v-if="formStore.birthInfos && formStore.birthInfos.length" class="w-full !py-0">
2
+ <v-list lines="two" v-if="(formStore.birthInfos && formStore.birthInfos.length) || $dataStore.isGons" class="w-full !py-0">
3
3
  <v-list-item
4
4
  @click="$emit('reset')"
5
5
  :append-icon="selected && Object.keys(selected).length === 0 ? `mdi-radiobox-marked ${$styles.greenText}` : 'mdi-radiobox-blank text-[#636363]'"
@@ -10,7 +10,11 @@
10
10
  v-for="familyMember of formStore.birthInfos"
11
11
  :key="familyMember.childIIN"
12
12
  @click="$emit('selectFamilyMember', familyMember)"
13
- :append-icon="familyMember && selected && selected.childIIN === familyMember.childIIN ? `mdi-radiobox-marked ${$styles.greenText}` : 'mdi-radiobox-blank text-[#636363]'"
13
+ :append-icon="
14
+ familyMember && selected && typeof selected === 'object' && selected.childIIN === familyMember.childIIN
15
+ ? `mdi-radiobox-marked ${$styles.greenText}`
16
+ : 'mdi-radiobox-blank text-[#636363]'
17
+ "
14
18
  >
15
19
  <v-list-item-title :class="[$styles.greenText, $styles.textTitle]">{{
16
20
  `${familyMember.childSurName} ${familyMember.childName} ${familyMember.childPatronymic ? familyMember.childPatronymic : ''}`
@@ -20,6 +24,13 @@
20
24
  >{{ ` ${$reformatIin(familyMember.childIIN!)}` }}</v-list-item-subtitle
21
25
  >
22
26
  </v-list-item>
27
+ <v-list-item
28
+ v-if="$dataStore.isGons"
29
+ @click="$emit('addChild')"
30
+ :append-icon="selected && selected === $dataStore.t('form.addBeneficiary') ? `mdi-radiobox-marked ${$styles.greenText}` : 'mdi-radiobox-blank text-[#636363]'"
31
+ >
32
+ <v-list-item-title :class="[$styles.greenText, $styles.textTitle]">{{ $dataStore.t('form.addBeneficiary') }}</v-list-item-title>
33
+ </v-list-item>
23
34
  </v-list>
24
35
  <base-list-empty class="w-full" v-else />
25
36
  </template>
@@ -30,10 +41,10 @@ import type { Api } from '../../types';
30
41
  export default defineComponent({
31
42
  props: {
32
43
  selected: {
33
- type: Object as PropType<Api.GKB.BirthInfo>,
44
+ type: [Object, String] as PropType<Api.GKB.BirthInfo | string>,
34
45
  },
35
46
  },
36
- emits: ['selectFamilyMember', 'reset'],
47
+ emits: ['selectFamilyMember', 'reset', 'addChild'],
37
48
  setup() {
38
49
  const formStore = useFormStore();
39
50
  return {
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <section v-if="member && member.iin" class="mb-2">
3
+ <base-form-section :title="`${title} ${number === 0 ? '' : number}`" class="mx-[10px] mt-[14px] d-flex">
4
+ <base-form-input v-model="member.iin" :label="$dataStore.t('form.iin')" :readonly="true" />
5
+ <base-form-input v-model.trim="member.longName" :label="$dataStore.t('labels.userFullName')" :readonly="true" />
6
+ <base-panel-input
7
+ v-if="!!member.digitalDocument"
8
+ v-model="member.digitalDocument.fileName"
9
+ label="Цифровой документ"
10
+ :readonly="disabled"
11
+ :clearable="!disabled"
12
+ append-inner-icon="mdi mdi-chevron-right"
13
+ @click="$emit('openPanel', member.digitalDocument)"
14
+ />
15
+ <base-content-block
16
+ v-if="!disabled && !member.digitalDocument"
17
+ class="d-flex align-center justify-between !py-3.5 !pr-5"
18
+ :class="[$styles.whiteBg]"
19
+ @click="$emit('openDigitalDocPanel', member.iin)"
20
+ >
21
+ <p :class="[$styles.greyText]">Получить цифровой документ</p>
22
+ <div class="cursor-pointer">
23
+ <i class="mdi mdi-file-document text-xl" :class="[$styles.blueText]"></i>
24
+ </div>
25
+ </base-content-block>
26
+ </base-form-section>
27
+ </section>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import type { Base } from '../../types';
32
+
33
+ defineEmits(['openDigitalDocPanel', 'openPanel']);
34
+ defineProps({
35
+ title: {
36
+ type: String,
37
+ default: '',
38
+ },
39
+ member: {
40
+ type: Object as PropType<Base.Document.Digital>,
41
+ default: null,
42
+ },
43
+ number: {
44
+ type: Number,
45
+ default: 0,
46
+ },
47
+ disabled: {
48
+ type: Boolean,
49
+ default: false,
50
+ },
51
+ });
52
+ </script>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <base-panel-input
3
+ class="source-form-input"
4
+ v-model="formStore.Source"
5
+ :value="formStore.Source?.nameRu"
6
+ :readonly="true"
7
+ :clearable="false"
8
+ :label="$dataStore.t('form.source')"
9
+ />
10
+ </template>
11
+
12
+ <script lang="ts">
13
+ export default defineComponent({
14
+ setup(props) {
15
+ const formStore = useFormStore();
16
+
17
+ return {
18
+ // State
19
+ formStore,
20
+ };
21
+ },
22
+ });
23
+ </script>
24
+
25
+ <style scoped>
26
+ .source-form-input {
27
+ border-radius: 4px;
28
+ border: 1px solid #e5e7eb;
29
+ }
30
+ </style>