Initial commit: Hedena Gold monorepo.
First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
<!--
|
||||
One document slot for "تکمیل پروفایل".
|
||||
|
||||
Ports henza_pwa's SingleImageUploader (/@/components/SingleImageUploader),
|
||||
which the reference used three times on completeProfile.vue:45-56. Its
|
||||
contract is the reference's: pick one file, upload it immediately, and emit
|
||||
the stored id through `done` — the form only holds ids, never the files.
|
||||
|
||||
This is the user-app styling. The SCRIPT is
|
||||
identical in both.
|
||||
-->
|
||||
<template>
|
||||
<div class="siu" :class="{ 'is-done': Boolean(fileId), 'is-error': Boolean(error) }">
|
||||
<input
|
||||
ref="input"
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp,application/pdf"
|
||||
class="siu__input"
|
||||
@change="onPick"
|
||||
/>
|
||||
|
||||
<button type="button" class="siu__box" :disabled="busy" @click="input?.click()">
|
||||
<img v-if="preview" :src="preview" class="siu__preview" alt="" />
|
||||
|
||||
<span v-else class="siu__icon">
|
||||
<Icon :icon="busy ? 'vuesax-linear:refresh' : 'vuesax-linear:gallery-add'" size="26" />
|
||||
</span>
|
||||
|
||||
<span class="siu__text">
|
||||
{{ busy ? 'در حال بارگذاری…' : fileId ? 'تغییر تصویر' : uploadText }}
|
||||
</span>
|
||||
|
||||
<span v-if="fileId && !busy" class="siu__badge">
|
||||
<Icon icon="vuesax-linear:tick-circle" size="16" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<p v-if="error" class="siu__error">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import Icon from '../icon/Icon.vue'
|
||||
import { api } from '../../api/client'
|
||||
|
||||
const props = defineProps<{
|
||||
uploadText: string
|
||||
/** Sent through as the stored file's `purpose`, so documents stay labelled. */
|
||||
purpose: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ (e: 'done', id: string): void }>()
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
const busy = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const fileId = ref<string | null>(null)
|
||||
const preview = ref<string | null>(null)
|
||||
|
||||
/** 8 MB — the same ceiling the API enforces, checked here for a fast message. */
|
||||
const MAX_BYTES = 8 * 1024 * 1024
|
||||
|
||||
async function onPick(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
// Reset immediately so re-picking the same file fires `change` again.
|
||||
target.value = ''
|
||||
if (!file) return
|
||||
|
||||
error.value = null
|
||||
|
||||
if (file.size > MAX_BYTES) {
|
||||
error.value = 'حجم فایل بیش از حد مجاز است (۸ مگابایت)'
|
||||
return
|
||||
}
|
||||
|
||||
busy.value = true
|
||||
try {
|
||||
const stored = await api.uploadDocument(file, props.purpose)
|
||||
fileId.value = stored.id
|
||||
preview.value = file.type.startsWith('image/') ? URL.createObjectURL(file) : null
|
||||
emit('done', stored.id)
|
||||
} catch {
|
||||
error.value = 'بارگذاری انجام نشد. دوباره تلاش کنید.'
|
||||
fileId.value = null
|
||||
preview.value = null
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.siu__input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.siu__box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 132px;
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border: 1px dashed @hairline;
|
||||
border-radius: 18px;
|
||||
background: @canvasSoft;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.siu__box:disabled {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.is-done .siu__box {
|
||||
border-style: solid;
|
||||
border-color: @green;
|
||||
}
|
||||
|
||||
.is-error .siu__box {
|
||||
border-color: @red;
|
||||
}
|
||||
|
||||
.siu__preview {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.32;
|
||||
}
|
||||
|
||||
.siu__icon,
|
||||
.siu__text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.siu__text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.siu__badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
color: @green;
|
||||
}
|
||||
|
||||
.siu__error {
|
||||
margin: 7px 2px 0;
|
||||
font-size: 12px;
|
||||
color: @red;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user