@amirjalili1374/ui-kit 1.2.0
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.
- package/README.md +275 -0
- package/dist/_redirects +1 -0
- package/dist/components/Loading.vue.d.ts +3 -0
- package/dist/components/Loading.vue.d.ts.map +1 -0
- package/dist/components/common/AppStepper.vue.d.ts +75 -0
- package/dist/components/common/AppStepper.vue.d.ts.map +1 -0
- package/dist/components/shared/BaseBreadcrumb.vue.d.ts +18 -0
- package/dist/components/shared/BaseBreadcrumb.vue.d.ts.map +1 -0
- package/dist/components/shared/BaseIcon.vue.d.ts +33 -0
- package/dist/components/shared/BaseIcon.vue.d.ts.map +1 -0
- package/dist/components/shared/ConfirmDialog.vue.d.ts +38 -0
- package/dist/components/shared/ConfirmDialog.vue.d.ts.map +1 -0
- package/dist/components/shared/CustomAutocomplete.vue.d.ts +81 -0
- package/dist/components/shared/CustomAutocomplete.vue.d.ts.map +1 -0
- package/dist/components/shared/CustomDataTable.vue.d.ts +59 -0
- package/dist/components/shared/CustomDataTable.vue.d.ts.map +1 -0
- package/dist/components/shared/DescriptionInput.vue.d.ts +34 -0
- package/dist/components/shared/DescriptionInput.vue.d.ts.map +1 -0
- package/dist/components/shared/DownloadButton.vue.d.ts +25 -0
- package/dist/components/shared/DownloadButton.vue.d.ts.map +1 -0
- package/dist/components/shared/MoneyInput.vue.d.ts +127 -0
- package/dist/components/shared/MoneyInput.vue.d.ts.map +1 -0
- package/dist/components/shared/PdfViewer.vue.d.ts +67 -0
- package/dist/components/shared/PdfViewer.vue.d.ts.map +1 -0
- package/dist/components/shared/ShamsiDatePicker.vue.d.ts +48 -0
- package/dist/components/shared/ShamsiDatePicker.vue.d.ts.map +1 -0
- package/dist/components/shared/UiChildCard.vue.d.ts +14 -0
- package/dist/components/shared/UiChildCard.vue.d.ts.map +1 -0
- package/dist/components/shared/UiParentCard.vue.d.ts +18 -0
- package/dist/components/shared/UiParentCard.vue.d.ts.map +1 -0
- package/dist/components/shared/VPriceTextField.vue.d.ts +30 -0
- package/dist/components/shared/VPriceTextField.vue.d.ts.map +1 -0
- package/dist/composables/useDataTable.d.ts +36 -0
- package/dist/composables/useDataTable.d.ts.map +1 -0
- package/dist/composables/useTableActions.d.ts +294 -0
- package/dist/composables/useTableActions.d.ts.map +1 -0
- package/dist/composables/useTableHeaders.d.ts +80 -0
- package/dist/composables/useTableHeaders.d.ts.map +1 -0
- package/dist/composables/useTableSelection.d.ts +32 -0
- package/dist/composables/useTableSelection.d.ts.map +1 -0
- package/dist/constants/enums/booleanEnum.d.ts +13 -0
- package/dist/constants/enums/booleanEnum.d.ts.map +1 -0
- package/dist/directives/v-digit-limit.d.ts +6 -0
- package/dist/directives/v-digit-limit.d.ts.map +1 -0
- package/dist/directives/v-permission.d.ts +3 -0
- package/dist/directives/v-permission.d.ts.map +1 -0
- package/dist/favicon.svg +13 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/persian.json +1 -0
- package/dist/plugins/mdi-icon.d.ts +16 -0
- package/dist/plugins/mdi-icon.d.ts.map +1 -0
- package/dist/services/apiService.d.ts +21 -0
- package/dist/services/apiService.d.ts.map +1 -0
- package/dist/services/axiosInstance.d.ts +11 -0
- package/dist/services/axiosInstance.d.ts.map +1 -0
- package/dist/stores/customizer.d.ts +47 -0
- package/dist/stores/customizer.d.ts.map +1 -0
- package/dist/stores/permissions.d.ts +47 -0
- package/dist/stores/permissions.d.ts.map +1 -0
- package/dist/style.css +5 -0
- package/dist/types/componentTypes/DataTableType.d.ts +14 -0
- package/dist/types/componentTypes/DataTableType.d.ts.map +1 -0
- package/dist/types/componentTypes/DataTableTypes.d.ts +120 -0
- package/dist/types/componentTypes/DataTableTypes.d.ts.map +1 -0
- package/dist/ui-kit.cjs.js +2 -0
- package/dist/ui-kit.cjs.js.map +1 -0
- package/dist/ui-kit.es.js +36840 -0
- package/dist/ui-kit.es.js.map +1 -0
- package/dist/utils/NationalCodeValidator.d.ts +4 -0
- package/dist/utils/NationalCodeValidator.d.ts.map +1 -0
- package/dist/utils/date-convertor.d.ts +5 -0
- package/dist/utils/date-convertor.d.ts.map +1 -0
- package/dist/utils/greetingUtils.d.ts +35 -0
- package/dist/utils/greetingUtils.d.ts.map +1 -0
- package/dist/utils/helpers/fetch-wrapper.d.ts +23 -0
- package/dist/utils/helpers/fetch-wrapper.d.ts.map +1 -0
- package/dist/utils/number-formatter.d.ts +16 -0
- package/dist/utils/number-formatter.d.ts.map +1 -0
- package/dist/validators/nationalCodeRule.d.ts +2 -0
- package/dist/validators/nationalCodeRule.d.ts.map +1 -0
- package/package.json +134 -0
- package/src/assets/fonts/A Massir Spray.ttf +0 -0
- package/src/assets/fonts/BYekan.ttf +0 -0
- package/src/assets/fonts/BYekan.woff +0 -0
- package/src/assets/fonts/BYekan.woff2 +0 -0
- package/src/assets/fonts/Dima Shekasteh 2 Free.ttf +0 -0
- package/src/assets/fonts/Dima Shekasteh Free Regular.ttf +0 -0
- package/src/assets/fonts/IRANSansWeb.ts +1 -0
- package/src/assets/fonts/IRANSansWeb.ttf +0 -0
- package/src/assets/fonts/IRANSansXBlack.ttf +0 -0
- package/src/assets/fonts/IRANSansXBold.ttf +0 -0
- package/src/assets/fonts/IRANSansXDemiBold.ttf +0 -0
- package/src/assets/fonts/IRANSansXExtraBold.ttf +0 -0
- package/src/assets/fonts/IRANSansXLight.ttf +0 -0
- package/src/assets/fonts/IRANSansXMedium.ttf +0 -0
- package/src/assets/fonts/IRANSansXRegular.ttf +0 -0
- package/src/assets/fonts/IRANSansXThin.ttf +0 -0
- package/src/assets/fonts/IRANSansXUltraLight.ttf +0 -0
- package/src/assets/fonts/IranNastaliq.ttf +0 -0
- package/src/assets/fonts/Vazir-Medium-FD.ttf +0 -0
- package/src/assets/fonts/Vazir-Medium-FD.woff +0 -0
- package/src/assets/fonts/Vazir-Medium-FD.woff2 +0 -0
- package/src/assets/fonts/Vazir-Regular-FD.eot +0 -0
- package/src/assets/fonts/kalamehBold.woff +0 -0
- package/src/assets/fonts/kalamehBold.woff2 +0 -0
- package/src/assets/fonts/kalamehHeavy.woff +0 -0
- package/src/assets/fonts/kalamehHeavy.woff2 +0 -0
- package/src/assets/fonts/kalamehLight.woff +0 -0
- package/src/assets/fonts/kalamehLight.woff2 +0 -0
- package/src/assets/fonts/kalamehRegular.woff +0 -0
- package/src/assets/fonts/kalamehRegular.woff2 +0 -0
- package/src/assets/images/auth/social-google.svg +6 -0
- package/src/assets/images/favicon.svg +18 -0
- package/src/assets/images/icons/icon-card.svg +5 -0
- package/src/assets/images/logos/logo.svg +12 -0
- package/src/assets/images/logos/logolight.svg +12 -0
- package/src/assets/images/maintenance/img-error-bg.svg +34 -0
- package/src/assets/images/maintenance/img-error-blue.svg +43 -0
- package/src/assets/images/maintenance/img-error-purple.svg +42 -0
- package/src/assets/images/maintenance/img-error-text.svg +27 -0
- package/src/assets/images/profile/profile-user-account-svgrepo-com.svg +12 -0
- package/src/assets/images/profile/user-round.svg +15 -0
- package/src/assets/images/template/template-01.ts +1 -0
- package/src/assets/images/vectors/colorized-bg.svg +40 -0
- package/src/assets/images/vectors/logo_stroke_1px.svg +26 -0
- package/src/assets/images/vectors/logo_stroke_2px.svg +26 -0
- package/src/assets/scss/components/_approval-sections.scss +75 -0
- package/src/assets/styles/fonts.scss +77 -0
- package/src/components/Loading.vue +88 -0
- package/src/components/common/AppStepper.vue +139 -0
- package/src/components/shared/BaseBreadcrumb.vue +55 -0
- package/src/components/shared/BaseIcon.vue +27 -0
- package/src/components/shared/ConfirmDialog.vue +72 -0
- package/src/components/shared/CustomAutocomplete.vue +306 -0
- package/src/components/shared/CustomDataTable.vue +1859 -0
- package/src/components/shared/DescriptionInput.vue +204 -0
- package/src/components/shared/DownloadButton.vue +169 -0
- package/src/components/shared/MoneyInput.vue +105 -0
- package/src/components/shared/PdfViewer.vue +645 -0
- package/src/components/shared/ShamsiDatePicker.vue +444 -0
- package/src/components/shared/UiChildCard.vue +17 -0
- package/src/components/shared/UiParentCard.vue +21 -0
- package/src/components/shared/VPriceTextField.vue +136 -0
- package/src/composables/useDataTable.ts +152 -0
- package/src/composables/usePermissions.ts +90 -0
- package/src/composables/useRouteGuard.ts +36 -0
- package/src/composables/useTableActions.ts +207 -0
- package/src/composables/useTableHeaders.ts +172 -0
- package/src/composables/useTableSelection.ts +201 -0
- package/src/constants/enums/approval.ts +13 -0
- package/src/constants/enums/booleanEnum.ts +11 -0
- package/src/constants/enums/contractType.ts +11 -0
- package/src/constants/enums/lcProductType.ts +21 -0
- package/src/constants/enums/repaymentType.ts +11 -0
- package/src/directives/v-digit-limit.ts +15 -0
- package/src/directives/v-permission.ts +31 -0
- package/src/features/index.ts +48 -0
- package/src/index.ts +119 -0
- package/src/plugins/key-clock.ts +39 -0
- package/src/plugins/mdi-icon.ts +31 -0
- package/src/plugins/vuetify.ts +74 -0
- package/src/scss/_override.scss +72 -0
- package/src/scss/_variables.scss +124 -0
- package/src/scss/components/_VButtons.scss +23 -0
- package/src/scss/components/_VCard.scss +20 -0
- package/src/scss/components/_VCustomDataTable.scss +282 -0
- package/src/scss/components/_VField.scss +9 -0
- package/src/scss/components/_VInput.scss +17 -0
- package/src/scss/components/_VNavigationDrawer.scss +3 -0
- package/src/scss/components/_VShadow.scss +3 -0
- package/src/scss/components/_VStepper.scss +235 -0
- package/src/scss/components/_VTabs.scss +11 -0
- package/src/scss/components/_VTextField.scss +40 -0
- package/src/scss/components/_approval.scss +128 -0
- package/src/scss/layout/_container.scss +147 -0
- package/src/scss/layout/_sidebar.scss +138 -0
- package/src/scss/layout/_topbar.scss +39 -0
- package/src/scss/pages/_dashboards.scss +97 -0
- package/src/scss/style.scss +21 -0
- package/src/services/apiService.ts +59 -0
- package/src/services/axiosInstance.ts +14 -0
- package/src/stores/customizer.ts +55 -0
- package/src/stores/permissions.ts +237 -0
- package/src/theme/darkThemes/DarkModernTheme.ts +54 -0
- package/src/theme/darkThemes/DarkOrangeTheme.ts +53 -0
- package/src/theme/darkThemes/DarkPurpleTheme.ts +54 -0
- package/src/theme/darkThemes/DarkRedTheme.ts +54 -0
- package/src/theme/darkThemes/DarkSilverTheme.ts +53 -0
- package/src/theme/darkThemes/DarkSteelTealGreen.ts +53 -0
- package/src/theme/darkThemes/DarkTealTheme.ts +52 -0
- package/src/theme/lightThemes/ModernTheme.ts +55 -0
- package/src/theme/lightThemes/OrangeTheme.ts +54 -0
- package/src/theme/lightThemes/PurpleTheme.ts +54 -0
- package/src/theme/lightThemes/RedTheme.ts +55 -0
- package/src/theme/lightThemes/SilverTheme.ts +55 -0
- package/src/theme/lightThemes/SteelTealGreen.ts +54 -0
- package/src/theme/lightThemes/TealTheme.ts +54 -0
- package/src/types/approval/approvalType.ts +473 -0
- package/src/types/cartable/cartableTypes.ts +169 -0
- package/src/types/componentTypes/DataTableType.ts +14 -0
- package/src/types/componentTypes/DataTableTypes.ts +130 -0
- package/src/types/enums/global.ts +267 -0
- package/src/types/jalaali-js.d.ts +6 -0
- package/src/types/models/Base.ts +4 -0
- package/src/types/models/env.d.ts +10 -0
- package/src/types/models/person.ts +13 -0
- package/src/types/models/userInfo.ts +29 -0
- package/src/types/preApproval/preApprovalTypes.ts +67 -0
- package/src/types/shims-tabler-icons.d.ts +58 -0
- package/src/types/themeTypes/ThemeType.ts +47 -0
- package/src/types/vue-apexcharts.d.ts +1 -0
- package/src/types/vue3-print-nb.d.ts +1 -0
- package/src/types/vue_tabler_icon.d.ts +10 -0
- package/src/utils/NationalCodeValidator.ts +33 -0
- package/src/utils/date-convertor.ts +40 -0
- package/src/utils/greetingUtils.ts +97 -0
- package/src/utils/helpers/fake-backend.ts +68 -0
- package/src/utils/helpers/fetch-wrapper.ts +86 -0
- package/src/utils/number-formatter.ts +33 -0
- package/src/validators/nationalCodeRule.ts +6 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="description-input">
|
|
3
|
+
<v-textarea
|
|
4
|
+
v-model="internalValue"
|
|
5
|
+
:label="label"
|
|
6
|
+
:placeholder="placeholder"
|
|
7
|
+
:rows="rows"
|
|
8
|
+
:maxlength="maxlength"
|
|
9
|
+
:disabled="disabled"
|
|
10
|
+
:readonly="readonly"
|
|
11
|
+
:clearable="clearable"
|
|
12
|
+
:counter="showCounter || undefined"
|
|
13
|
+
variant="outlined"
|
|
14
|
+
density="comfortable"
|
|
15
|
+
auto-grow
|
|
16
|
+
@keydown="handleKeydown"
|
|
17
|
+
@input="handleInput"
|
|
18
|
+
@blur="handleBlur"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { ref, watch } from 'vue';
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
modelValue?: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
rows?: number;
|
|
31
|
+
maxlength?: number;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
readonly?: boolean;
|
|
34
|
+
clearable?: boolean;
|
|
35
|
+
showCounter?: boolean;
|
|
36
|
+
autoNumbering?: boolean; // Enable/disable auto numbering
|
|
37
|
+
startNumber?: number; // Starting number for auto numbering
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
41
|
+
modelValue: '',
|
|
42
|
+
label: 'توضیحات',
|
|
43
|
+
placeholder: 'توضیحات خود را وارد کنید...',
|
|
44
|
+
rows: 4,
|
|
45
|
+
maxlength: 1000,
|
|
46
|
+
disabled: false,
|
|
47
|
+
readonly: false,
|
|
48
|
+
clearable: true,
|
|
49
|
+
showCounter: true,
|
|
50
|
+
autoNumbering: true,
|
|
51
|
+
startNumber: 1
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const emit = defineEmits<{
|
|
55
|
+
(e: 'update:modelValue', value: string): void;
|
|
56
|
+
(e: 'change', value: string): void;
|
|
57
|
+
}>();
|
|
58
|
+
|
|
59
|
+
const internalValue = ref(props.modelValue);
|
|
60
|
+
|
|
61
|
+
// Watch for external modelValue changes
|
|
62
|
+
watch(() => props.modelValue, (newValue) => {
|
|
63
|
+
internalValue.value = newValue;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Watch for internal value changes and emit
|
|
67
|
+
watch(internalValue, (newValue) => {
|
|
68
|
+
emit('update:modelValue', newValue);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Handle keydown events for auto numbering
|
|
72
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
73
|
+
if (!props.autoNumbering || props.disabled || props.readonly) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const target = event.target as HTMLTextAreaElement;
|
|
78
|
+
const { key, ctrlKey, shiftKey, altKey } = event;
|
|
79
|
+
|
|
80
|
+
// Handle Enter key (new line with incrementing number)
|
|
81
|
+
if (key === 'Enter' && !ctrlKey && !shiftKey && !altKey) {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
insertNumberedLine(target, true);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle Inter key (new line with decrementing number)
|
|
87
|
+
if (key === 'Inter' || (key === 'Enter' && shiftKey)) {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
insertNumberedLine(target, false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Insert a numbered line
|
|
94
|
+
const insertNumberedLine = (textarea: HTMLTextAreaElement, increment: boolean) => {
|
|
95
|
+
const { selectionStart, selectionEnd, value } = textarea;
|
|
96
|
+
|
|
97
|
+
// Get current line number
|
|
98
|
+
const currentLineNumber = getCurrentLineNumber(value, selectionStart);
|
|
99
|
+
|
|
100
|
+
// Calculate new line number
|
|
101
|
+
const newLineNumber = increment ? currentLineNumber + 1 : Math.max(1, currentLineNumber - 1);
|
|
102
|
+
|
|
103
|
+
// Create the new line text
|
|
104
|
+
const newLineText = `\n${newLineNumber} - `;
|
|
105
|
+
|
|
106
|
+
// Insert the new line
|
|
107
|
+
const newValue = value.slice(0, selectionStart) + newLineText + value.slice(selectionEnd);
|
|
108
|
+
internalValue.value = newValue;
|
|
109
|
+
|
|
110
|
+
// Set cursor position after the new line
|
|
111
|
+
const newCursorPosition = selectionStart + newLineText.length;
|
|
112
|
+
|
|
113
|
+
// Use nextTick to ensure DOM is updated
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
textarea.setSelectionRange(newCursorPosition, newCursorPosition);
|
|
116
|
+
textarea.focus();
|
|
117
|
+
}, 0);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Get the current line number from the text
|
|
121
|
+
const getCurrentLineNumber = (text: string, cursorPosition: number): number => {
|
|
122
|
+
// Find the current line
|
|
123
|
+
const lines = text.split('\n');
|
|
124
|
+
let currentPos = 0;
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
127
|
+
const lineLength = lines[i].length + 1; // +1 for newline
|
|
128
|
+
|
|
129
|
+
if (currentPos + lineLength > cursorPosition) {
|
|
130
|
+
// We found the current line
|
|
131
|
+
const line = lines[i];
|
|
132
|
+
|
|
133
|
+
// Check if line starts with a number pattern (e.g., "1 - ", "2 - ", etc.)
|
|
134
|
+
const numberMatch = line.match(/^(\d+)\s*-\s*/);
|
|
135
|
+
if (numberMatch) {
|
|
136
|
+
return parseInt(numberMatch[1]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// If no number found, return the line index + startNumber
|
|
140
|
+
return props.startNumber + i;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
currentPos += lineLength;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If we're at the end, return the next number
|
|
147
|
+
return props.startNumber + lines.length;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Handle input changes
|
|
151
|
+
const handleInput = (event: Event) => {
|
|
152
|
+
const target = event.target as HTMLTextAreaElement;
|
|
153
|
+
const value = target.value;
|
|
154
|
+
|
|
155
|
+
// Auto-format existing text to add numbering if needed
|
|
156
|
+
if (props.autoNumbering && value && !value.match(/^\d+\s*-\s/)) {
|
|
157
|
+
const formattedValue = formatTextWithNumbering(value);
|
|
158
|
+
if (formattedValue !== value) {
|
|
159
|
+
internalValue.value = formattedValue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Format text to add numbering
|
|
165
|
+
const formatTextWithNumbering = (text: string): string => {
|
|
166
|
+
const lines = text.split('\n');
|
|
167
|
+
const formattedLines = lines.map((line, index) => {
|
|
168
|
+
// Skip empty lines
|
|
169
|
+
if (!line.trim()) {
|
|
170
|
+
return line;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// If line already has numbering, keep it
|
|
174
|
+
if (line.match(/^\d+\s*-\s/)) {
|
|
175
|
+
return line;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Add numbering
|
|
179
|
+
return `${props.startNumber + index} - ${line}`;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return formattedLines.join('\n');
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Handle blur event
|
|
186
|
+
const handleBlur = () => {
|
|
187
|
+
emit('change', internalValue.value);
|
|
188
|
+
};
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<style scoped>
|
|
192
|
+
.description-input {
|
|
193
|
+
width: 100%;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.description-input :deep(.v-textarea textarea) {
|
|
197
|
+
font-family: 'Vazir', 'Tahoma', sans-serif;
|
|
198
|
+
line-height: 1.6;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.description-input :deep(.v-textarea textarea:focus) {
|
|
202
|
+
outline: none;
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-btn
|
|
3
|
+
v-bind="$attrs"
|
|
4
|
+
:loading="loading"
|
|
5
|
+
@click="handleDownload"
|
|
6
|
+
class="download-button"
|
|
7
|
+
>
|
|
8
|
+
<v-icon v-if="icon" :start="iconStart" :end="iconEnd" :class="iconClass">{{ icon }}</v-icon>
|
|
9
|
+
<span class="download-title">{{ title }}</span>
|
|
10
|
+
<v-icon v-if="!icon" start :icon="icons.download" :class="iconClass"></v-icon>
|
|
11
|
+
</v-btn>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import { ref, computed } from 'vue'
|
|
16
|
+
import { icons } from '@/plugins/mdi-icon'
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
url: string
|
|
20
|
+
title?: string
|
|
21
|
+
icon?: string
|
|
22
|
+
iconStart?: boolean
|
|
23
|
+
iconEnd?: boolean
|
|
24
|
+
filename?: string
|
|
25
|
+
method?: 'anchor' | 'fetch'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
+
title: 'دانلود',
|
|
30
|
+
iconStart: true,
|
|
31
|
+
iconEnd: false,
|
|
32
|
+
method: 'anchor'
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
download: [url: string]
|
|
37
|
+
error: [error: Error]
|
|
38
|
+
success: [filename: string]
|
|
39
|
+
}>()
|
|
40
|
+
|
|
41
|
+
const loading = ref(false)
|
|
42
|
+
|
|
43
|
+
// Computed class for icon positioning
|
|
44
|
+
const iconClass = computed(() => {
|
|
45
|
+
if (props.iconStart) return 'icon-start'
|
|
46
|
+
if (props.iconEnd) return 'icon-end'
|
|
47
|
+
return ''
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const handleDownload = async () => {
|
|
51
|
+
if (!props.url) {
|
|
52
|
+
const error = new Error('URL is required for download')
|
|
53
|
+
emit('error', error)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
loading.value = true
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
if (props.method === 'fetch') {
|
|
61
|
+
await downloadWithFetch()
|
|
62
|
+
} else {
|
|
63
|
+
await downloadWithAnchor()
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Download failed:', error)
|
|
67
|
+
emit('error', error as Error)
|
|
68
|
+
} finally {
|
|
69
|
+
loading.value = false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const downloadWithAnchor = async () => {
|
|
74
|
+
// Create a temporary anchor element to trigger download
|
|
75
|
+
const link = document.createElement('a')
|
|
76
|
+
link.href = props.url
|
|
77
|
+
link.download = props.filename || ''
|
|
78
|
+
link.target = '_blank'
|
|
79
|
+
link.rel = 'noopener noreferrer'
|
|
80
|
+
|
|
81
|
+
// Append to body, click, and remove
|
|
82
|
+
document.body.appendChild(link)
|
|
83
|
+
link.click()
|
|
84
|
+
document.body.removeChild(link)
|
|
85
|
+
|
|
86
|
+
emit('download', props.url)
|
|
87
|
+
emit('success', props.filename || 'downloaded-file')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const downloadWithFetch = async () => {
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(props.url)
|
|
93
|
+
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const blob = await response.blob()
|
|
99
|
+
const url = window.URL.createObjectURL(blob)
|
|
100
|
+
|
|
101
|
+
const link = document.createElement('a')
|
|
102
|
+
link.href = url
|
|
103
|
+
link.download = props.filename || getFilenameFromUrl(props.url) || 'download'
|
|
104
|
+
|
|
105
|
+
document.body.appendChild(link)
|
|
106
|
+
link.click()
|
|
107
|
+
document.body.removeChild(link)
|
|
108
|
+
|
|
109
|
+
// Clean up the object URL
|
|
110
|
+
window.URL.revokeObjectURL(url)
|
|
111
|
+
|
|
112
|
+
emit('download', props.url)
|
|
113
|
+
emit('success', props.filename || 'downloaded-file')
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new Error(`Fetch download failed: ${error}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const getFilenameFromUrl = (url: string): string => {
|
|
120
|
+
try {
|
|
121
|
+
const urlObj = new URL(url)
|
|
122
|
+
const pathname = urlObj.pathname
|
|
123
|
+
const filename = pathname.split('/').pop()
|
|
124
|
+
return filename || 'download'
|
|
125
|
+
} catch {
|
|
126
|
+
return 'download'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Inherit all Vuetify button attributes except the ones we override
|
|
131
|
+
defineOptions({
|
|
132
|
+
inheritAttrs: false
|
|
133
|
+
})
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<style scoped>
|
|
137
|
+
.download-button {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: 8px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.download-title {
|
|
144
|
+
margin: 0 4px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.icon-start {
|
|
148
|
+
margin-right: 4px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.icon-end {
|
|
152
|
+
margin-left: 4px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* RTL Support */
|
|
156
|
+
[dir="rtl"] .download-button {
|
|
157
|
+
gap: 8px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
[dir="rtl"] .icon-start {
|
|
161
|
+
margin-right: 0;
|
|
162
|
+
margin-left: 4px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
[dir="rtl"] .icon-end {
|
|
166
|
+
margin-left: 0;
|
|
167
|
+
margin-right: 4px;
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
modelValue: {
|
|
6
|
+
type: Number,
|
|
7
|
+
required: false,
|
|
8
|
+
default: 0,
|
|
9
|
+
validator: (value) => value === null || value === undefined || typeof value === 'string' || typeof value === 'number'
|
|
10
|
+
},
|
|
11
|
+
label: {
|
|
12
|
+
type: String,
|
|
13
|
+
default: ''
|
|
14
|
+
},
|
|
15
|
+
placeholder: {
|
|
16
|
+
type: String,
|
|
17
|
+
default: ''
|
|
18
|
+
},
|
|
19
|
+
disabled: {
|
|
20
|
+
type: Boolean,
|
|
21
|
+
default: false
|
|
22
|
+
},
|
|
23
|
+
readonly: {
|
|
24
|
+
type: Boolean,
|
|
25
|
+
default: false
|
|
26
|
+
},
|
|
27
|
+
clearable: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
default: false
|
|
30
|
+
},
|
|
31
|
+
variant: {
|
|
32
|
+
type: String,
|
|
33
|
+
default: 'outlined'
|
|
34
|
+
},
|
|
35
|
+
errorMessages: {
|
|
36
|
+
type: [String, Array],
|
|
37
|
+
default: () => []
|
|
38
|
+
},
|
|
39
|
+
min: {
|
|
40
|
+
type: Number,
|
|
41
|
+
default: undefined
|
|
42
|
+
},
|
|
43
|
+
max: {
|
|
44
|
+
type: Number,
|
|
45
|
+
default: undefined
|
|
46
|
+
},
|
|
47
|
+
suffix: {
|
|
48
|
+
type: String,
|
|
49
|
+
default: ''
|
|
50
|
+
},
|
|
51
|
+
prefix: {
|
|
52
|
+
type: String,
|
|
53
|
+
default: ''
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const emit = defineEmits(['update:modelValue']);
|
|
58
|
+
|
|
59
|
+
// Memoization for better performance
|
|
60
|
+
const lastProcessedValue = ref<string | number | null | undefined>(null);
|
|
61
|
+
const lastFormattedValue = ref<string>('');
|
|
62
|
+
|
|
63
|
+
const displayValue = computed({
|
|
64
|
+
get() {
|
|
65
|
+
// Format with commas for display
|
|
66
|
+
const val = props.modelValue;
|
|
67
|
+
if (val === undefined || val === null || val === 0) return '';
|
|
68
|
+
return val.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
69
|
+
},
|
|
70
|
+
set(val: string | null | undefined) {
|
|
71
|
+
// Remove all non-digit characters and emit as number
|
|
72
|
+
const safeVal = (val ?? '').replace(/[^\d]/g, '');
|
|
73
|
+
emit('update:modelValue', safeVal === '' ? 0 : Number(safeVal));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function onKeyPress(event: KeyboardEvent) {
|
|
78
|
+
if (!/[0-9]/.test(event.key)) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<v-text-field
|
|
86
|
+
v-model="displayValue"
|
|
87
|
+
:label="props.label"
|
|
88
|
+
:placeholder="props.placeholder"
|
|
89
|
+
:disabled="props.disabled"
|
|
90
|
+
:readonly="props.readonly"
|
|
91
|
+
:clearable="props.clearable"
|
|
92
|
+
:variant="props.variant as any"
|
|
93
|
+
:error-messages="props.errorMessages as string[]"
|
|
94
|
+
:suffix="props.suffix"
|
|
95
|
+
:prefix="props.prefix"
|
|
96
|
+
inputmode="numeric"
|
|
97
|
+
pattern="[0-9]*"
|
|
98
|
+
type="text"
|
|
99
|
+
@keypress="onKeyPress"
|
|
100
|
+
>
|
|
101
|
+
<template v-for="(slot) in $slots" #[slot]="scope">
|
|
102
|
+
<slot :name="slot" v-bind="scope || {}" />
|
|
103
|
+
</template>
|
|
104
|
+
</v-text-field>
|
|
105
|
+
</template>
|