@rynt/sdk 0.9.53
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 +122 -0
- package/REGISTRIES.md +189 -0
- package/env.d.ts +11 -0
- package/host-shims.d.ts +30 -0
- package/package.json +88 -0
- package/src/extension-marketplace/api-types.ts +141 -0
- package/src/extension-marketplace/client.ts +296 -0
- package/src/extension-marketplace/index.ts +22 -0
- package/src/extension-marketplace/schemas.ts +178 -0
- package/src/extensions/ExtensionRoutePage.vue +17 -0
- package/src/extensions/context.ts +37 -0
- package/src/extensions/disabled-folder.ts +21 -0
- package/src/extensions/extension-expose-map.ts +5 -0
- package/src/extensions/extension-expose.ts +48 -0
- package/src/extensions/graph.ts +67 -0
- package/src/extensions/index.ts +251 -0
- package/src/extensions/invite-handler/types.ts +20 -0
- package/src/extensions/launcher-entities/create-launcher-entity.ts +25 -0
- package/src/extensions/launcher-entities/keys.ts +46 -0
- package/src/extensions/launcher-entities/launcher-entity-components.ts +177 -0
- package/src/extensions/launcher-entities/props-map.ts +69 -0
- package/src/extensions/launcher-entities/registry.ts +32 -0
- package/src/extensions/launcher-models/apis/accounts-contracts.ts +102 -0
- package/src/extensions/launcher-models/apis/launcher-model-apis.ts +553 -0
- package/src/extensions/launcher-models/keys.ts +23 -0
- package/src/extensions/launcher-models/public.ts +9 -0
- package/src/extensions/launcher-models/registry-core.ts +34 -0
- package/src/extensions/manifest-types.ts +22 -0
- package/src/extensions/manifest.ts +46 -0
- package/src/extensions/marketplace-open-key.ts +26 -0
- package/src/extensions/plugin-types.ts +44 -0
- package/src/extensions/plugin.ts +62 -0
- package/src/extensions/registries/bootstrap.ts +11 -0
- package/src/extensions/registries/builtins/account-provider.ts +6 -0
- package/src/extensions/registries/builtins/app-topbar-left-widgets.ts +6 -0
- package/src/extensions/registries/builtins/app-topbar-right-widgets.ts +6 -0
- package/src/extensions/registries/builtins/app-topbar-status-widgets.ts +6 -0
- package/src/extensions/registries/builtins/build-card-actions.ts +6 -0
- package/src/extensions/registries/builtins/build-card-after-meta.ts +6 -0
- package/src/extensions/registries/builtins/build-card-before-media.ts +6 -0
- package/src/extensions/registries/builtins/build-card-before-meta.ts +6 -0
- package/src/extensions/registries/builtins/build-card-footer-actions.ts +6 -0
- package/src/extensions/registries/builtins/build-detail-after-content.ts +6 -0
- package/src/extensions/registries/builtins/build-detail-before-content.ts +6 -0
- package/src/extensions/registries/builtins/build-detail-before-hero.ts +6 -0
- package/src/extensions/registries/builtins/build-detail-header-actions.ts +6 -0
- package/src/extensions/registries/builtins/build-detail-mod-row-actions.ts +6 -0
- package/src/extensions/registries/builtins/build-detail-resourcepack-row-actions.ts +6 -0
- package/src/extensions/registries/builtins/build-detail-right-column-bottom.ts +6 -0
- package/src/extensions/registries/builtins/build-detail-right-column-top.ts +6 -0
- package/src/extensions/registries/builtins/dialog-footer-actions.ts +6 -0
- package/src/extensions/registries/builtins/feed-after-content.ts +6 -0
- package/src/extensions/registries/builtins/feed-before-content.ts +6 -0
- package/src/extensions/registries/builtins/file-editor.ts +19 -0
- package/src/extensions/registries/builtins/friends-after-list.ts +6 -0
- package/src/extensions/registries/builtins/friends-before-list.ts +6 -0
- package/src/extensions/registries/builtins/index.ts +141 -0
- package/src/extensions/registries/builtins/invite-handler.ts +7 -0
- package/src/extensions/registries/builtins/library-after-content.ts +6 -0
- package/src/extensions/registries/builtins/library-before-content.ts +6 -0
- package/src/extensions/registries/builtins/loader.ts +8 -0
- package/src/extensions/registries/builtins/map-card-actions.ts +6 -0
- package/src/extensions/registries/builtins/map-card-after-meta.ts +6 -0
- package/src/extensions/registries/builtins/map-card-before-meta.ts +6 -0
- package/src/extensions/registries/builtins/map-card-footer-actions.ts +6 -0
- package/src/extensions/registries/builtins/map-detail-after-content.ts +6 -0
- package/src/extensions/registries/builtins/map-detail-before-content.ts +6 -0
- package/src/extensions/registries/builtins/map-detail-header-actions.ts +6 -0
- package/src/extensions/registries/builtins/markdown-editor-tiptap-extensions.ts +7 -0
- package/src/extensions/registries/builtins/markdown-editor-toolbar-actions.ts +6 -0
- package/src/extensions/registries/builtins/markdown-renderer-after-content.ts +6 -0
- package/src/extensions/registries/builtins/markdown-renderer-before-content.ts +6 -0
- package/src/extensions/registries/builtins/mod-details-footer-actions.ts +6 -0
- package/src/extensions/registries/builtins/mod-manage-actions.ts +6 -0
- package/src/extensions/registries/builtins/mod-provider.ts +5 -0
- package/src/extensions/registries/builtins/nav.ts +7 -0
- package/src/extensions/registries/builtins/page.ts +13 -0
- package/src/extensions/registries/builtins/projects-after-content.ts +6 -0
- package/src/extensions/registries/builtins/projects-before-content.ts +6 -0
- package/src/extensions/registries/builtins/resourcepack-manage-actions.ts +7 -0
- package/src/extensions/registries/builtins/server-card-actions.ts +6 -0
- package/src/extensions/registries/builtins/server-card-after-meta.ts +6 -0
- package/src/extensions/registries/builtins/server-card-before-meta.ts +6 -0
- package/src/extensions/registries/builtins/server-card-footer-actions.ts +6 -0
- package/src/extensions/registries/builtins/server-detail-after-content.ts +6 -0
- package/src/extensions/registries/builtins/server-detail-before-content.ts +6 -0
- package/src/extensions/registries/builtins/server-detail-header-actions.ts +6 -0
- package/src/extensions/registries/builtins/settings-after-sections.ts +6 -0
- package/src/extensions/registries/builtins/settings-before-sections.ts +6 -0
- package/src/extensions/registries/builtins/settings-section-widgets.ts +6 -0
- package/src/extensions/registries/builtins/shaderpack-manage-actions.ts +7 -0
- package/src/extensions/registries/builtins/shell.ts +5 -0
- package/src/extensions/registries/builtins/sidebar-after-content.ts +6 -0
- package/src/extensions/registries/builtins/sidebar-before-content.ts +6 -0
- package/src/extensions/registries/builtins/sidebar-footer-widgets.ts +6 -0
- package/src/extensions/registries/builtins/sidebar-header-widgets.ts +6 -0
- package/src/extensions/registries/builtins/sidebar.ts +11 -0
- package/src/extensions/registries/builtins/theme.ts +5 -0
- package/src/extensions/registries/builtins/user-card-after-meta.ts +6 -0
- package/src/extensions/registries/builtins/user-card-before-meta.ts +6 -0
- package/src/extensions/registries/builtins/user-menu-actions.ts +6 -0
- package/src/extensions/registries/builtins/user-menu-after-actions.ts +6 -0
- package/src/extensions/registries/builtins/user-menu-before-actions.ts +6 -0
- package/src/extensions/registries/builtins/user-strip.ts +5 -0
- package/src/extensions/registries/clear-extension-ui-registries.ts +15 -0
- package/src/extensions/registries/define-extension-registry.ts +58 -0
- package/src/extensions/registries/extension-host-api.ts +41 -0
- package/src/extensions/registries/extension-registry-api.ts +103 -0
- package/src/extensions/registries/extension-registry-payload-map.ts +9 -0
- package/src/extensions/registries/extension-scope.ts +41 -0
- package/src/extensions/registries/get-registry.ts +23 -0
- package/src/extensions/registries/index.ts +58 -0
- package/src/extensions/registries/manifest-rynt.ts +193 -0
- package/src/extensions/registries/registry-slot.ts +40 -0
- package/src/extensions/registries/registry-value-map.ts +89 -0
- package/src/extensions/registries/store.ts +206 -0
- package/src/extensions/resolve-extensions.ts +245 -0
- package/src/extensions/router-bridge.ts +103 -0
- package/src/extensions/session.ts +6 -0
- package/src/extensions/slug.ts +23 -0
- package/src/extensions/version.ts +147 -0
- package/src/host/extensions-composables.ts +33 -0
- package/src/host/extensions-init.ts +194 -0
- package/src/host/index.ts +11 -0
- package/src/host/launcher-models/index.ts +4 -0
- package/src/index.ts +229 -0
- package/src/minecraft-loader/base-loader.ts +102 -0
- package/src/minecraft-loader/index.ts +11 -0
- package/src/minecraft-loader/loader-registry.ts +72 -0
- package/src/shared/api/assets.ts +112 -0
- package/src/shared/api/auth.ts +283 -0
- package/src/shared/api/builds.ts +647 -0
- package/src/shared/api/config.ts +19 -0
- package/src/shared/api/download-stats.ts +103 -0
- package/src/shared/api/downloads.ts +36 -0
- package/src/shared/api/entity-authorship.ts +60 -0
- package/src/shared/api/events.ts +393 -0
- package/src/shared/api/friends.ts +140 -0
- package/src/shared/api/graphql.ts +87 -0
- package/src/shared/api/index.ts +23 -0
- package/src/shared/api/invites.ts +262 -0
- package/src/shared/api/library.ts +44 -0
- package/src/shared/api/maps.ts +385 -0
- package/src/shared/api/notify-websocket.ts +140 -0
- package/src/shared/api/posts.ts +357 -0
- package/src/shared/api/projectServers.ts +379 -0
- package/src/shared/api/serverMembers.ts +173 -0
- package/src/shared/api/users.ts +294 -0
- package/src/shared/composables/buildEditor/useBuildEditor.ts +66 -0
- package/src/shared/composables/buildManifest/buildManifest.ts +447 -0
- package/src/shared/composables/filesEditor/filesEditor.ts +346 -0
- package/src/shared/composables/index.ts +10 -0
- package/src/shared/composables/modsEditor/modsEditor.ts +1678 -0
- package/src/shared/composables/registrySlot/registry-slot-utils.ts +25 -0
- package/src/shared/composables/registrySlot/useRegistrySlotMissing.ts +35 -0
- package/src/shared/composables/resourcePacksEditor/resourcePacksEditor.ts +448 -0
- package/src/shared/composables/shaderPacksEditor/shaderPacksEditor.ts +395 -0
- package/src/shared/composables/useSkinRender.ts +70 -0
- package/src/shared/composables/useZlDeepLink.ts +178 -0
- package/src/shared/definitions/defineGraphCache.ts +216 -0
- package/src/shared/definitions/defineStore.ts +32 -0
- package/src/shared/definitions/index.ts +2 -0
- package/src/shared/minecraft-types/build-manifest.ts +611 -0
- package/src/shared/minecraft-types/index.ts +3 -0
- package/src/shared/minecraft-types/launcher-versions.ts +32 -0
- package/src/shared/minecraft-types/minecraft-launcher-types.ts +276 -0
- package/src/shared/mocks/index.ts +1 -0
- package/src/shared/mocks/navigation.ts +17 -0
- package/src/shared/mods/http.ts +45 -0
- package/src/shared/mods/index.ts +5 -0
- package/src/shared/mods/marketplace-editor-search.ts +266 -0
- package/src/shared/mods/marketplace-search-utils.ts +42 -0
- package/src/shared/mods/mod-marketplace-registry.ts +66 -0
- package/src/shared/mods/mod-marketplace-types.ts +28 -0
- package/src/shared/mods/providers/curseforge.ts +464 -0
- package/src/shared/mods/providers/index.ts +8 -0
- package/src/shared/mods/providers/modrinth.ts +402 -0
- package/src/shared/mods/resolve-mods-provider-loader-ids.ts +77 -0
- package/src/shared/mods/types.ts +76 -0
- package/src/shared/styles/index.css +713 -0
- package/src/shared/themes/index.ts +23 -0
- package/src/shared/themes/theme-tokens-black.json +126 -0
- package/src/shared/themes/theme-tokens-classic.json +126 -0
- package/src/shared/themes/theme-tokens-pink.json +126 -0
- package/src/shared/themes/theme-tokens.json +126 -0
- package/src/shared/themes/types.ts +85 -0
- package/src/shared/types/API_DOCUMENTATION.md +422 -0
- package/src/shared/types/account.ts +40 -0
- package/src/shared/types/build.ts +8 -0
- package/src/shared/types/entities.ts +181 -0
- package/src/shared/types/index.ts +6 -0
- package/src/shared/types/invite-payloads.ts +60 -0
- package/src/shared/types/navigation.ts +16 -0
- package/src/shared/types/running-build.ts +51 -0
- package/src/shared/types/serverMember.ts +17 -0
- package/src/shared/types/user.ts +55 -0
- package/src/shared/ui/base/Avatar.vue +262 -0
- package/src/shared/ui/base/Badge.vue +47 -0
- package/src/shared/ui/base/Button.vue +78 -0
- package/src/shared/ui/base/Divider.vue +42 -0
- package/src/shared/ui/base/Icon.vue +597 -0
- package/src/shared/ui/base/StatusIndicator.vue +44 -0
- package/src/shared/ui/base/index.ts +7 -0
- package/src/shared/ui/cards/InviteCard.vue +47 -0
- package/src/shared/ui/cards/index.ts +2 -0
- package/src/shared/ui/dialog/Dialog.vue +71 -0
- package/src/shared/ui/dialog/DialogContent.vue +31 -0
- package/src/shared/ui/dialog/DialogFooter.vue +14 -0
- package/src/shared/ui/dialog/DialogHeader.vue +41 -0
- package/src/shared/ui/dialog/index.ts +5 -0
- package/src/shared/ui/editors/AttachmentImagesEditor.vue +133 -0
- package/src/shared/ui/editors/ContentAttachmentsDisplay.vue +76 -0
- package/src/shared/ui/editors/MarkdownEditor.vue +956 -0
- package/src/shared/ui/editors/MarkdownRenderer.vue +299 -0
- package/src/shared/ui/editors/RichContentImageViewer.vue +85 -0
- package/src/shared/ui/editors/SocialPostMediaZone.vue +320 -0
- package/src/shared/ui/editors/index.ts +6 -0
- package/src/shared/ui/editors/markdown-editor-gallery.ts +234 -0
- package/src/shared/ui/editors/markdown-editor-image.ts +178 -0
- package/src/shared/ui/form/Checkbox.vue +38 -0
- package/src/shared/ui/form/FormField.vue +30 -0
- package/src/shared/ui/form/FormGrid.vue +38 -0
- package/src/shared/ui/form/ImageEditor.vue +598 -0
- package/src/shared/ui/form/Input.vue +72 -0
- package/src/shared/ui/form/Range.vue +65 -0
- package/src/shared/ui/form/Select.vue +76 -0
- package/src/shared/ui/form/Switch.vue +38 -0
- package/src/shared/ui/form/Textarea.vue +144 -0
- package/src/shared/ui/form/index.ts +9 -0
- package/src/shared/ui/index.ts +9 -0
- package/src/shared/ui/layout/BusyOverlay.vue +31 -0
- package/src/shared/ui/layout/Callout.vue +44 -0
- package/src/shared/ui/layout/Card.vue +38 -0
- package/src/shared/ui/layout/Container.vue +36 -0
- package/src/shared/ui/layout/EmptyState.vue +99 -0
- package/src/shared/ui/layout/EntityMediaRow.vue +54 -0
- package/src/shared/ui/layout/FilterResultsLayout.vue +22 -0
- package/src/shared/ui/layout/FloatingPanel.vue +37 -0
- package/src/shared/ui/layout/FullscreenDimmer.vue +11 -0
- package/src/shared/ui/layout/Grid.vue +40 -0
- package/src/shared/ui/layout/Inline.vue +59 -0
- package/src/shared/ui/layout/LoadingState.vue +39 -0
- package/src/shared/ui/layout/MediaBox.vue +47 -0
- package/src/shared/ui/layout/OverlayPanel.vue +28 -0
- package/src/shared/ui/layout/OverlayWaitPanel.vue +22 -0
- package/src/shared/ui/layout/PageSection.vue +43 -0
- package/src/shared/ui/layout/PageToolbar.vue +29 -0
- package/src/shared/ui/layout/Panel.vue +39 -0
- package/src/shared/ui/layout/ProgressBar.vue +49 -0
- package/src/shared/ui/layout/Section.vue +30 -0
- package/src/shared/ui/layout/SegmentedControl.vue +43 -0
- package/src/shared/ui/layout/SelectableCard.vue +46 -0
- package/src/shared/ui/layout/SelectableRow.vue +41 -0
- package/src/shared/ui/layout/Skeleton.vue +25 -0
- package/src/shared/ui/layout/SkeletonAvatar.vue +30 -0
- package/src/shared/ui/layout/SkeletonEntityCard.vue +20 -0
- package/src/shared/ui/layout/SkeletonFeedPost.vue +22 -0
- package/src/shared/ui/layout/SkeletonGrid.vue +18 -0
- package/src/shared/ui/layout/SkeletonListRow.vue +31 -0
- package/src/shared/ui/layout/SkeletonText.vue +25 -0
- package/src/shared/ui/layout/Stack.vue +42 -0
- package/src/shared/ui/layout/StateBlock.vue +44 -0
- package/src/shared/ui/layout/TwoPaneLayout.vue +35 -0
- package/src/shared/ui/layout/VirtualList.vue +160 -0
- package/src/shared/ui/layout/index.ts +35 -0
- package/src/shared/ui/layout/skeletonSurfaceStyles.ts +24 -0
- package/src/shared/ui/navigation/NavItem.vue +139 -0
- package/src/shared/ui/navigation/Tab.vue +61 -0
- package/src/shared/ui/navigation/Tabs.vue +37 -0
- package/src/shared/ui/navigation/index.ts +4 -0
- package/src/shared/ui/primitives/Action.vue +19 -0
- package/src/shared/ui/primitives/Block.vue +28 -0
- package/src/shared/ui/primitives/CanvasView.vue +19 -0
- package/src/shared/ui/primitives/Control.vue +24 -0
- package/src/shared/ui/primitives/ControlSelect.vue +19 -0
- package/src/shared/ui/primitives/ControlTextarea.vue +17 -0
- package/src/shared/ui/primitives/FieldLabel.vue +19 -0
- package/src/shared/ui/primitives/Form.vue +19 -0
- package/src/shared/ui/primitives/Heading.vue +29 -0
- package/src/shared/ui/primitives/Image.vue +17 -0
- package/src/shared/ui/primitives/LineBreak.vue +3 -0
- package/src/shared/ui/primitives/Link.vue +19 -0
- package/src/shared/ui/primitives/List.vue +28 -0
- package/src/shared/ui/primitives/ListItem.vue +19 -0
- package/src/shared/ui/primitives/OptionItem.vue +19 -0
- package/src/shared/ui/primitives/Text.vue +28 -0
- package/src/shared/ui/primitives/VideoView.vue +19 -0
- package/src/shared/ui/primitives/index.ts +19 -0
- package/src/shared/ui/primitives/resolveElement.ts +25 -0
- package/src/shared/ui/special/AngularAccent.vue +106 -0
- package/src/shared/ui/special/ExtensionRegistrySlotButton.vue +143 -0
- package/src/shared/ui/special/InfoRow.vue +39 -0
- package/src/shared/ui/special/LogViewer.vue +53 -0
- package/src/shared/ui/special/PageHeader.vue +23 -0
- package/src/shared/ui/special/RegistrySlotMissingCallout.vue +48 -0
- package/src/shared/ui/special/WelcomeCard.vue +32 -0
- package/src/shared/ui/special/index.ts +9 -0
- package/src/shared/utils/app-paths.ts +50 -0
- package/src/shared/utils/attachments.ts +16 -0
- package/src/shared/utils/autostart.ts +213 -0
- package/src/shared/utils/build-files.ts +439 -0
- package/src/shared/utils/build-manifest-init.ts +176 -0
- package/src/shared/utils/cloudinary.ts +67 -0
- package/src/shared/utils/cn.ts +7 -0
- package/src/shared/utils/download-stats-week.ts +165 -0
- package/src/shared/utils/entity-api-to-cache.ts +84 -0
- package/src/shared/utils/entity-build-from-api.ts +1 -0
- package/src/shared/utils/entity-display.ts +27 -0
- package/src/shared/utils/entity-map-from-api.ts +1 -0
- package/src/shared/utils/file-hash.ts +65 -0
- package/src/shared/utils/formatSize.ts +5 -0
- package/src/shared/utils/formatTime.ts +157 -0
- package/src/shared/utils/getAccountSkinRender.ts +32 -0
- package/src/shared/utils/index.ts +34 -0
- package/src/shared/utils/local-mods.ts +678 -0
- package/src/shared/utils/local-settings.ts +217 -0
- package/src/shared/utils/member-join-stats.ts +35 -0
- package/src/shared/utils/platform.ts +86 -0
- package/src/shared/utils/play-host-slug.ts +92 -0
- package/src/shared/utils/rich-content.ts +294 -0
- package/src/shared/utils/safeRequest.ts +23 -0
- package/src/shared/utils/semver.ts +81 -0
- package/src/shared/utils/serverPermissions.ts +155 -0
- package/src/shared/utils/skin-render-cache.ts +372 -0
- package/src/shared/utils/stripMarkdown.ts +45 -0
- package/src/shared/utils/transliterate.ts +74 -0
- package/src/shared/utils/updateAccountSkinRender.ts +64 -0
- package/src/shared/utils/updater.ts +218 -0
- package/src/shared/utils/uploadImage.ts +195 -0
- package/src/shared/utils/user-status.ts +9 -0
- package/src/tiptap/index.ts +7 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<label
|
|
4
|
+
v-if="label"
|
|
5
|
+
class="block text-muted-foreground mb-1.5 text-sm font-medium"
|
|
6
|
+
>{{ label }}</label
|
|
7
|
+
>
|
|
8
|
+
|
|
9
|
+
<!-- Превью изображения -->
|
|
10
|
+
<div
|
|
11
|
+
class="relative bg-surface-overlay border border-border overflow-hidden transition-colors"
|
|
12
|
+
:class="[
|
|
13
|
+
previewClasses,
|
|
14
|
+
isDragging && 'border-primary',
|
|
15
|
+
(isUploading || disabled) && 'opacity-50',
|
|
16
|
+
]"
|
|
17
|
+
@click="handleClick"
|
|
18
|
+
@dragover.prevent="isDragging = true"
|
|
19
|
+
@dragleave.prevent="isDragging = false"
|
|
20
|
+
@drop.prevent="handleDrop"
|
|
21
|
+
>
|
|
22
|
+
<input
|
|
23
|
+
ref="fileInput"
|
|
24
|
+
type="file"
|
|
25
|
+
accept="image/png,image/jpeg,image/jpg,image/gif,image/webp"
|
|
26
|
+
class="hidden"
|
|
27
|
+
@change="handleFileSelect"
|
|
28
|
+
:disabled="isUploading || disabled"
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
<!-- Изображение или пустое состояние -->
|
|
32
|
+
<div v-if="modelValue && !isUploading" class="relative w-full h-full">
|
|
33
|
+
<img
|
|
34
|
+
:src="getPreviewUrl(modelValue)"
|
|
35
|
+
alt="Preview"
|
|
36
|
+
class="w-full h-full object-cover"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<!-- Overlay с кнопками (в square — только иконки) -->
|
|
40
|
+
<div
|
|
41
|
+
class="absolute inset-0 bg-black/45 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center gap-2"
|
|
42
|
+
>
|
|
43
|
+
<button
|
|
44
|
+
@click.stop="handleClick"
|
|
45
|
+
:class="[
|
|
46
|
+
'bg-primary hover:bg-[var(--primary-hover)] text-primary-foreground transition-colors flex items-center justify-center',
|
|
47
|
+
mode === 'square' ? 'p-2' : 'px-4 py-2 gap-2',
|
|
48
|
+
]"
|
|
49
|
+
:title="mode === 'square' ? 'Изменить' : undefined"
|
|
50
|
+
:disabled="disabled"
|
|
51
|
+
>
|
|
52
|
+
<Icon name="edit" size="sm" />
|
|
53
|
+
<span v-if="mode !== 'square'">Изменить</span>
|
|
54
|
+
</button>
|
|
55
|
+
<button
|
|
56
|
+
@click.stop="handleRemove"
|
|
57
|
+
:class="[
|
|
58
|
+
'bg-destructive hover:bg-destructive/90 text-destructive-foreground transition-colors flex items-center justify-center',
|
|
59
|
+
mode === 'square' ? 'p-2' : 'px-4 py-2 gap-2',
|
|
60
|
+
]"
|
|
61
|
+
:title="mode === 'square' ? 'Удалить' : undefined"
|
|
62
|
+
:disabled="disabled"
|
|
63
|
+
>
|
|
64
|
+
<Icon name="trash" size="sm" />
|
|
65
|
+
<span v-if="mode !== 'square'">Удалить</span>
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Состояние загрузки -->
|
|
71
|
+
<div
|
|
72
|
+
v-else-if="isUploading"
|
|
73
|
+
class="w-full h-full flex flex-col items-center justify-center py-8"
|
|
74
|
+
>
|
|
75
|
+
<div class="animate-spin mb-2">
|
|
76
|
+
<Icon name="refresh-cw" size="lg" class="text-accent-secondary" />
|
|
77
|
+
</div>
|
|
78
|
+
<p class="text-muted-foreground text-sm">Загрузка изображения...</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Пустое состояние -->
|
|
82
|
+
<div
|
|
83
|
+
v-else
|
|
84
|
+
class="w-full h-full flex flex-col items-center justify-center py-8 cursor-pointer"
|
|
85
|
+
>
|
|
86
|
+
<Icon name="upload" size="xl" class="text-subtle-fg mb-2" />
|
|
87
|
+
<p class="text-muted-foreground text-sm mb-1 text-center">
|
|
88
|
+
Нажмите или перетащите изображение
|
|
89
|
+
</p>
|
|
90
|
+
<p class="text-subtle-fg text-xs">PNG, JPG, GIF, WEBP до 10MB</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<p v-if="error" class="text-red-400 text-xs mt-1">{{ error }}</p>
|
|
95
|
+
|
|
96
|
+
<!-- Диалог обрезки -->
|
|
97
|
+
<Dialog
|
|
98
|
+
:open="showCropDialog"
|
|
99
|
+
size="xl"
|
|
100
|
+
@close="showCropDialog = false"
|
|
101
|
+
:closeDisabled="true"
|
|
102
|
+
>
|
|
103
|
+
<DialogHeader
|
|
104
|
+
title="Обрезка изображения"
|
|
105
|
+
description="Выберите область для обрезки"
|
|
106
|
+
@close="showCropDialog = false"
|
|
107
|
+
/>
|
|
108
|
+
<DialogContent class="p-4">
|
|
109
|
+
<div class="flex gap-4">
|
|
110
|
+
<!-- Cropper -->
|
|
111
|
+
<div
|
|
112
|
+
class="flex-1 relative overflow-hidden"
|
|
113
|
+
:style="{ height: '70vh', maxWidth: '100%' }"
|
|
114
|
+
>
|
|
115
|
+
<Cropper
|
|
116
|
+
ref="cropper"
|
|
117
|
+
:src="cropImageSrc"
|
|
118
|
+
:stencil-props="stencilProps"
|
|
119
|
+
class="cropper"
|
|
120
|
+
@change="updatePreviews"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Превью в двух размерах для banner режима -->
|
|
125
|
+
<div v-if="mode === 'banner'" class="w-80 space-y-4">
|
|
126
|
+
<div>
|
|
127
|
+
<p class="text-muted-foreground text-sm mb-2">
|
|
128
|
+
Превью баннера (1152x344)
|
|
129
|
+
</p>
|
|
130
|
+
<div
|
|
131
|
+
class="bg-surface-overlay border border-border aspect-[1152/344] overflow-hidden"
|
|
132
|
+
>
|
|
133
|
+
<img
|
|
134
|
+
v-if="cropPreviewBanner"
|
|
135
|
+
:src="cropPreviewBanner"
|
|
136
|
+
alt="Banner preview"
|
|
137
|
+
class="w-full h-full object-cover"
|
|
138
|
+
/>
|
|
139
|
+
<div
|
|
140
|
+
v-else
|
|
141
|
+
class="w-full h-full flex items-center justify-center text-subtle-fg text-sm"
|
|
142
|
+
>
|
|
143
|
+
Превью появится после выбора области
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div>
|
|
148
|
+
<p class="text-muted-foreground text-sm mb-2">
|
|
149
|
+
Превью 16:9 (центр)
|
|
150
|
+
</p>
|
|
151
|
+
<div
|
|
152
|
+
class="bg-surface-overlay border border-border aspect-video overflow-hidden"
|
|
153
|
+
>
|
|
154
|
+
<img
|
|
155
|
+
v-if="cropPreview16x9"
|
|
156
|
+
:src="cropPreview16x9"
|
|
157
|
+
alt="16:9 preview"
|
|
158
|
+
class="w-full h-full object-cover"
|
|
159
|
+
/>
|
|
160
|
+
<div
|
|
161
|
+
v-else
|
|
162
|
+
class="w-full h-full flex items-center justify-center text-subtle-fg text-sm"
|
|
163
|
+
>
|
|
164
|
+
Превью появится после выбора области
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</DialogContent>
|
|
171
|
+
<DialogFooter>
|
|
172
|
+
<Button
|
|
173
|
+
variant="secondary"
|
|
174
|
+
size="md"
|
|
175
|
+
@click="showCropDialog = false"
|
|
176
|
+
:disabled="isUploading"
|
|
177
|
+
>
|
|
178
|
+
Отмена
|
|
179
|
+
</Button>
|
|
180
|
+
<Button
|
|
181
|
+
variant="primary"
|
|
182
|
+
size="md"
|
|
183
|
+
@click="handleCrop"
|
|
184
|
+
:disabled="isUploading"
|
|
185
|
+
>
|
|
186
|
+
<Icon name="check" size="sm" />
|
|
187
|
+
Применить
|
|
188
|
+
</Button>
|
|
189
|
+
</DialogFooter>
|
|
190
|
+
</Dialog>
|
|
191
|
+
</div>
|
|
192
|
+
</template>
|
|
193
|
+
|
|
194
|
+
<script setup lang="ts">
|
|
195
|
+
import { ref, computed, watch } from 'vue';
|
|
196
|
+
import { Cropper } from 'vue-advanced-cropper';
|
|
197
|
+
import 'vue-advanced-cropper/dist/style.css';
|
|
198
|
+
import {
|
|
199
|
+
Icon,
|
|
200
|
+
Dialog,
|
|
201
|
+
DialogHeader,
|
|
202
|
+
DialogContent,
|
|
203
|
+
DialogFooter,
|
|
204
|
+
Button,
|
|
205
|
+
} from '..';
|
|
206
|
+
import { transformCloudinaryUrl } from '../../utils/cloudinary';
|
|
207
|
+
|
|
208
|
+
type ImageMode = 'square' | 'banner' | 'base';
|
|
209
|
+
|
|
210
|
+
const props = withDefaults(
|
|
211
|
+
defineProps<{
|
|
212
|
+
label?: string;
|
|
213
|
+
disabled?: boolean;
|
|
214
|
+
folder?: string;
|
|
215
|
+
mode?: ImageMode;
|
|
216
|
+
}>(),
|
|
217
|
+
{
|
|
218
|
+
label: '',
|
|
219
|
+
disabled: false,
|
|
220
|
+
folder: undefined,
|
|
221
|
+
mode: 'banner',
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const modelValue = defineModel<string>('modelValue', { default: '' });
|
|
226
|
+
|
|
227
|
+
const fileInput = ref<HTMLInputElement | null>(null);
|
|
228
|
+
const cropper = ref<InstanceType<typeof Cropper> | null>(null);
|
|
229
|
+
const isDragging = ref(false);
|
|
230
|
+
const isUploading = ref(false);
|
|
231
|
+
const error = ref<string>('');
|
|
232
|
+
const showCropDialog = ref(false);
|
|
233
|
+
const cropImageSrc = ref<string>('');
|
|
234
|
+
const cropPreviewBanner = ref<string>('');
|
|
235
|
+
const cropPreview16x9 = ref<string>('');
|
|
236
|
+
|
|
237
|
+
// Поддержка двух вариантов конфигурации:
|
|
238
|
+
// 1. Отдельные переменные: VITE_CLOUDINARY_CLOUD_NAME и VITE_CLOUDINARY_UPLOAD_PRESET
|
|
239
|
+
// 2. CLOUDINARY_URL: cloudinary://api_key:api_secret@cloud_name (парсим для cloud_name)
|
|
240
|
+
const CLOUDINARY_URL = (import.meta as any).env?.VITE_CLOUDINARY_URL as
|
|
241
|
+
| string
|
|
242
|
+
| undefined;
|
|
243
|
+
const CLOUDINARY_CLOUD_NAME = (import.meta as any).env
|
|
244
|
+
?.VITE_CLOUDINARY_CLOUD_NAME as string | undefined;
|
|
245
|
+
const CLOUDINARY_UPLOAD_PRESET = (import.meta as any).env
|
|
246
|
+
?.VITE_CLOUDINARY_UPLOAD_PRESET as string | undefined;
|
|
247
|
+
|
|
248
|
+
// Парсим CLOUDINARY_URL если он указан: cloudinary://api_key:api_secret@cloud_name
|
|
249
|
+
const parseCloudinaryUrl = (url: string): string | null => {
|
|
250
|
+
try {
|
|
251
|
+
// Формат: cloudinary://api_key:api_secret@cloud_name
|
|
252
|
+
const match = url.match(/cloudinary:\/\/[^:]+:[^@]+@(.+)/);
|
|
253
|
+
return match ? match[1] : null;
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Получаем cloud_name из переменных окружения или из CLOUDINARY_URL
|
|
260
|
+
const cloudName =
|
|
261
|
+
CLOUDINARY_CLOUD_NAME ||
|
|
262
|
+
(CLOUDINARY_URL ? parseCloudinaryUrl(CLOUDINARY_URL) : null);
|
|
263
|
+
|
|
264
|
+
// Вычисляем aspect ratio и размеры для превью
|
|
265
|
+
const previewClasses = computed(() => {
|
|
266
|
+
if (props.mode === 'square') {
|
|
267
|
+
return 'aspect-square';
|
|
268
|
+
} else if (props.mode === 'banner') {
|
|
269
|
+
return 'aspect-[1152/344]';
|
|
270
|
+
} else {
|
|
271
|
+
// base режим - 16:9
|
|
272
|
+
return 'aspect-video';
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Настройки cropper
|
|
277
|
+
const stencilProps = computed(() => {
|
|
278
|
+
if (props.mode === 'square') {
|
|
279
|
+
return {
|
|
280
|
+
aspectRatio: 1,
|
|
281
|
+
};
|
|
282
|
+
} else if (props.mode === 'banner') {
|
|
283
|
+
// Для режима banner используем aspect ratio 1152/344
|
|
284
|
+
return {
|
|
285
|
+
aspectRatio: 1152 / 344,
|
|
286
|
+
};
|
|
287
|
+
} else {
|
|
288
|
+
// base режим - 16:9
|
|
289
|
+
return {
|
|
290
|
+
aspectRatio: 16 / 9,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Обновляем превью при изменении области обрезки
|
|
296
|
+
const updatePreviews = async () => {
|
|
297
|
+
if (!cropper.value || props.mode !== 'banner' || !cropImageSrc.value)
|
|
298
|
+
return;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const result = cropper.value.getResult();
|
|
302
|
+
if (!result.canvas || !result.coordinates) return;
|
|
303
|
+
|
|
304
|
+
// Загружаем исходное изображение
|
|
305
|
+
const img = new Image();
|
|
306
|
+
img.crossOrigin = 'anonymous';
|
|
307
|
+
|
|
308
|
+
await new Promise((resolve, reject) => {
|
|
309
|
+
img.onload = resolve;
|
|
310
|
+
img.onerror = reject;
|
|
311
|
+
img.src = cropImageSrc.value;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const { coordinates } = result;
|
|
315
|
+
const sourceWidth = coordinates.width;
|
|
316
|
+
const sourceHeight = coordinates.height;
|
|
317
|
+
const sourceLeft = coordinates.left;
|
|
318
|
+
const sourceTop = coordinates.top;
|
|
319
|
+
|
|
320
|
+
// Превью баннера (1152x344)
|
|
321
|
+
const bannerCanvas = document.createElement('canvas');
|
|
322
|
+
bannerCanvas.width = 1152;
|
|
323
|
+
bannerCanvas.height = 344;
|
|
324
|
+
const bannerCtx = bannerCanvas.getContext('2d');
|
|
325
|
+
if (bannerCtx) {
|
|
326
|
+
// Рисуем из исходного изображения с учетом координат
|
|
327
|
+
bannerCtx.drawImage(
|
|
328
|
+
img,
|
|
329
|
+
sourceLeft,
|
|
330
|
+
sourceTop,
|
|
331
|
+
sourceWidth,
|
|
332
|
+
sourceHeight,
|
|
333
|
+
0,
|
|
334
|
+
0,
|
|
335
|
+
1152,
|
|
336
|
+
344,
|
|
337
|
+
);
|
|
338
|
+
cropPreviewBanner.value = bannerCanvas.toDataURL('image/png', 0.9);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Превью 16:9 (центр изображения)
|
|
342
|
+
// Вычисляем размеры для 16:9 из области 1152x344
|
|
343
|
+
// Высота 344, для 16:9 ширина = 344 * 16 / 9 ≈ 611.56
|
|
344
|
+
const width16x9 = Math.round((344 * 16) / 9);
|
|
345
|
+
const left16x9 = Math.round((1152 - width16x9) / 2); // Центрируем
|
|
346
|
+
|
|
347
|
+
// Вычисляем координаты для 16:9 в исходном изображении
|
|
348
|
+
const sourceWidth16x9 = (width16x9 / 1152) * sourceWidth;
|
|
349
|
+
const sourceLeft16x9 = sourceLeft + (left16x9 / 1152) * sourceWidth;
|
|
350
|
+
|
|
351
|
+
const canvas16x9 = document.createElement('canvas');
|
|
352
|
+
canvas16x9.width = width16x9;
|
|
353
|
+
canvas16x9.height = 344;
|
|
354
|
+
const ctx16x9 = canvas16x9.getContext('2d');
|
|
355
|
+
if (ctx16x9) {
|
|
356
|
+
// Рисуем центральную часть из исходного изображения
|
|
357
|
+
ctx16x9.drawImage(
|
|
358
|
+
img,
|
|
359
|
+
sourceLeft16x9,
|
|
360
|
+
sourceTop,
|
|
361
|
+
sourceWidth16x9,
|
|
362
|
+
sourceHeight,
|
|
363
|
+
0,
|
|
364
|
+
0,
|
|
365
|
+
width16x9,
|
|
366
|
+
344,
|
|
367
|
+
);
|
|
368
|
+
cropPreview16x9.value = canvas16x9.toDataURL('image/png', 0.9);
|
|
369
|
+
}
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error('Ошибка при обновлении превью:', error);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const handleClick = () => {
|
|
376
|
+
if (props.disabled || isUploading.value) return;
|
|
377
|
+
fileInput.value?.click();
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const handleFileSelect = async (event: Event) => {
|
|
381
|
+
const target = event.target as HTMLInputElement;
|
|
382
|
+
const file = target.files?.[0];
|
|
383
|
+
if (file) {
|
|
384
|
+
await processFile(file);
|
|
385
|
+
}
|
|
386
|
+
target.value = '';
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const handleDrop = async (event: DragEvent) => {
|
|
390
|
+
isDragging.value = false;
|
|
391
|
+
if (props.disabled || isUploading.value) return;
|
|
392
|
+
|
|
393
|
+
const file = event.dataTransfer?.files[0];
|
|
394
|
+
if (file && file.type.startsWith('image/')) {
|
|
395
|
+
await processFile(file);
|
|
396
|
+
} else {
|
|
397
|
+
error.value = 'Пожалуйста, выберите изображение';
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const processFile = async (file: File) => {
|
|
402
|
+
// Проверка размера файла (10MB)
|
|
403
|
+
if (file.size > 10 * 1024 * 1024) {
|
|
404
|
+
error.value = 'Размер файла не должен превышать 10MB';
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
error.value = '';
|
|
409
|
+
|
|
410
|
+
// Создаем URL для превью
|
|
411
|
+
const reader = new FileReader();
|
|
412
|
+
reader.onload = (e) => {
|
|
413
|
+
cropImageSrc.value = e.target?.result as string;
|
|
414
|
+
cropPreviewBanner.value = '';
|
|
415
|
+
cropPreview16x9.value = '';
|
|
416
|
+
showCropDialog.value = true;
|
|
417
|
+
// Обновляем превью после открытия диалога
|
|
418
|
+
setTimeout(() => {
|
|
419
|
+
updatePreviews();
|
|
420
|
+
}, 100);
|
|
421
|
+
};
|
|
422
|
+
reader.readAsDataURL(file);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const handleCrop = async () => {
|
|
426
|
+
if (!cropper.value) return;
|
|
427
|
+
|
|
428
|
+
const { canvas } = cropper.value.getResult();
|
|
429
|
+
if (!canvas) {
|
|
430
|
+
error.value = 'Не удалось обрезать изображение';
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Конвертируем canvas в blob
|
|
435
|
+
canvas.toBlob(
|
|
436
|
+
async (blob: Blob | null) => {
|
|
437
|
+
if (!blob) {
|
|
438
|
+
error.value = 'Ошибка при обработке изображения';
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Создаем File из blob
|
|
443
|
+
const file = new File([blob], 'cropped-image.png', {
|
|
444
|
+
type: 'image/png',
|
|
445
|
+
});
|
|
446
|
+
await uploadImage(file);
|
|
447
|
+
|
|
448
|
+
showCropDialog.value = false;
|
|
449
|
+
cropImageSrc.value = '';
|
|
450
|
+
cropPreviewBanner.value = '';
|
|
451
|
+
cropPreview16x9.value = '';
|
|
452
|
+
},
|
|
453
|
+
'image/png',
|
|
454
|
+
0.95,
|
|
455
|
+
);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const uploadImage = async (file: File) => {
|
|
459
|
+
if (!cloudName) {
|
|
460
|
+
error.value = 'Cloudinary Cloud Name не настроен';
|
|
461
|
+
console.error(
|
|
462
|
+
'Установите VITE_CLOUDINARY_CLOUD_NAME или VITE_CLOUDINARY_URL в переменных окружения',
|
|
463
|
+
);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!CLOUDINARY_UPLOAD_PRESET) {
|
|
468
|
+
error.value = 'Cloudinary Upload Preset не настроен';
|
|
469
|
+
console.error(
|
|
470
|
+
'VITE_CLOUDINARY_UPLOAD_PRESET не установлен в переменных окружения',
|
|
471
|
+
);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
isUploading.value = true;
|
|
476
|
+
error.value = '';
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
const formData = new FormData();
|
|
480
|
+
formData.append('file', file);
|
|
481
|
+
formData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET);
|
|
482
|
+
// Добавляем folder если указан
|
|
483
|
+
if (props.folder) {
|
|
484
|
+
formData.append('folder', props.folder);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const response = await fetch(
|
|
488
|
+
`https://api.cloudinary.com/v1_1/${cloudName}/image/upload`,
|
|
489
|
+
{
|
|
490
|
+
method: 'POST',
|
|
491
|
+
body: formData,
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
if (!response.ok) {
|
|
496
|
+
const errorData = await response.json().catch(() => ({}));
|
|
497
|
+
throw new Error(
|
|
498
|
+
errorData?.error?.message ||
|
|
499
|
+
`Ошибка загрузки: ${response.statusText}`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const data = await response.json();
|
|
504
|
+
|
|
505
|
+
if (data.secure_url) {
|
|
506
|
+
// Cloudinary возвращает secure_url (HTTPS) для загруженного изображения
|
|
507
|
+
modelValue.value = data.secure_url;
|
|
508
|
+
} else if (data.url) {
|
|
509
|
+
// Fallback на обычный url, если secure_url недоступен
|
|
510
|
+
modelValue.value = data.url;
|
|
511
|
+
} else {
|
|
512
|
+
throw new Error('Не удалось получить ссылку на изображение');
|
|
513
|
+
}
|
|
514
|
+
} catch (err: any) {
|
|
515
|
+
error.value = err.message || 'Ошибка при загрузке изображения';
|
|
516
|
+
console.error('Ошибка загрузки изображения на Cloudinary:', err);
|
|
517
|
+
} finally {
|
|
518
|
+
isUploading.value = false;
|
|
519
|
+
// Сброс input для возможности повторной загрузки того же файла
|
|
520
|
+
if (fileInput.value) {
|
|
521
|
+
fileInput.value.value = '';
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const handleRemove = () => {
|
|
527
|
+
if (props.disabled || isUploading.value) return;
|
|
528
|
+
modelValue.value = '';
|
|
529
|
+
error.value = '';
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// Получаем URL изображения с трансформациями для превью
|
|
533
|
+
const getPreviewUrl = (url: string | undefined) => {
|
|
534
|
+
if (!url) return '';
|
|
535
|
+
|
|
536
|
+
if (props.mode === 'square') {
|
|
537
|
+
return transformCloudinaryUrl(url, {
|
|
538
|
+
width: 400,
|
|
539
|
+
height: 400,
|
|
540
|
+
crop: 'fill',
|
|
541
|
+
quality: 'auto',
|
|
542
|
+
format: 'auto',
|
|
543
|
+
});
|
|
544
|
+
} else if (props.mode === 'banner') {
|
|
545
|
+
// Для banner режима используем размеры 1152x344
|
|
546
|
+
return transformCloudinaryUrl(url, {
|
|
547
|
+
width: 1152,
|
|
548
|
+
height: 344,
|
|
549
|
+
crop: 'fill',
|
|
550
|
+
quality: 'auto',
|
|
551
|
+
format: 'auto',
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
// base режим - 16:9
|
|
555
|
+
return transformCloudinaryUrl(url, {
|
|
556
|
+
width: 1920,
|
|
557
|
+
height: 1080,
|
|
558
|
+
crop: 'fill',
|
|
559
|
+
quality: 'auto',
|
|
560
|
+
format: 'auto',
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// Закрываем диалог при изменении modelValue извне
|
|
566
|
+
watch(
|
|
567
|
+
() => modelValue.value,
|
|
568
|
+
(newValue) => {
|
|
569
|
+
if (!newValue && showCropDialog.value) {
|
|
570
|
+
showCropDialog.value = false;
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
);
|
|
574
|
+
</script>
|
|
575
|
+
|
|
576
|
+
<style scoped>
|
|
577
|
+
.cropper {
|
|
578
|
+
height: 100%;
|
|
579
|
+
width: 100%;
|
|
580
|
+
max-width: 100%;
|
|
581
|
+
background: var(--surface-overlay);
|
|
582
|
+
overflow: hidden;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.cropper :deep(.vue-advanced-cropper) {
|
|
586
|
+
max-width: 100%;
|
|
587
|
+
overflow: hidden;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.cropper :deep(.vue-advanced-cropper__area) {
|
|
591
|
+
max-width: 100%;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.cropper :deep(.vue-advanced-cropper__background) {
|
|
595
|
+
max-width: 100%;
|
|
596
|
+
overflow: hidden;
|
|
597
|
+
}
|
|
598
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<input
|
|
3
|
+
:type="type"
|
|
4
|
+
:value="displayValue"
|
|
5
|
+
:placeholder="placeholder"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
:class="inputClasses"
|
|
8
|
+
@input="
|
|
9
|
+
$emit('update:modelValue', ($event.target as HTMLInputElement).value)
|
|
10
|
+
"
|
|
11
|
+
@blur="$emit('blur', $event)"
|
|
12
|
+
@focus="$emit('focus', $event)"
|
|
13
|
+
/>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed } from 'vue';
|
|
18
|
+
import { cn } from '../../utils/cn';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
modelValue?: string | number;
|
|
22
|
+
type?:
|
|
23
|
+
| 'text'
|
|
24
|
+
| 'email'
|
|
25
|
+
| 'password'
|
|
26
|
+
| 'number'
|
|
27
|
+
| 'search'
|
|
28
|
+
| 'datetime-local'
|
|
29
|
+
| 'date'
|
|
30
|
+
| 'time';
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
size?: 'sm' | 'md' | 'lg';
|
|
34
|
+
error?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
38
|
+
type: 'text',
|
|
39
|
+
size: 'md',
|
|
40
|
+
disabled: false,
|
|
41
|
+
error: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const emit = defineEmits<{
|
|
45
|
+
'update:modelValue': [value: string];
|
|
46
|
+
blur: [event: FocusEvent];
|
|
47
|
+
focus: [event: FocusEvent];
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
// Преобразуем значение в строку для отображения
|
|
51
|
+
const displayValue = computed(() => {
|
|
52
|
+
if (props.modelValue === null || props.modelValue === undefined) return '';
|
|
53
|
+
return String(props.modelValue);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const sizeClasses = {
|
|
57
|
+
sm: 'px-2 py-1 text-xs',
|
|
58
|
+
md: 'px-3 py-2 text-sm',
|
|
59
|
+
lg: 'px-4 py-2.5 text-base',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const inputClasses = computed(() => {
|
|
63
|
+
return cn(
|
|
64
|
+
'w-full bg-surface-overlay text-foreground border border-border focus:outline-none transition-colors rounded',
|
|
65
|
+
sizeClasses[props.size],
|
|
66
|
+
props.error
|
|
67
|
+
? 'border-destructive focus:ring-2 focus:ring-destructive'
|
|
68
|
+
: 'focus:border-accent-secondary focus:ring-2 focus:ring-ring/60',
|
|
69
|
+
props.disabled && 'opacity-50 cursor-not-allowed',
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
</script>
|