Files
geo/apps/admin-web/src/components/kol/KolPackageFormModal.vue
T

238 lines
6.4 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { DeleteOutlined, PictureOutlined } from "@ant-design/icons-vue";
import { message } from "ant-design-vue";
import { computed, reactive, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import type { CreateKolPackageRequest } from "@geo/shared-types";
import CoverPickerModal from "@/components/CoverPickerModal.vue";
import { imagesApi, resolveApiURL } from "@/lib/api";
import { deriveCoverFileName } from "@/lib/cover-requirements";
const props = defineProps<{
open: boolean;
initialValue?: Partial<CreateKolPackageRequest>;
}>();
const emit = defineEmits<{
(e: "update:open", value: boolean): void;
(e: "submit", value: CreateKolPackageRequest): void;
}>();
const { t } = useI18n();
const coverPickerOpen = ref(false);
const formState = reactive<CreateKolPackageRequest>({
name: "",
description: "",
industry: "",
tags: [],
price_note: "",
cover_url: "",
});
watch(
() => props.open,
(isOpen) => {
if (isOpen) {
Object.assign(formState, {
name: props.initialValue?.name || "",
description: props.initialValue?.description || "",
industry: props.initialValue?.industry || "",
tags: [...(props.initialValue?.tags || [])],
price_note: props.initialValue?.price_note || "",
cover_url: props.initialValue?.cover_url || "",
});
return;
}
coverPickerOpen.value = false;
},
);
const coverPreviewUrl = computed(() => resolveApiURL(formState.cover_url));
const coverFileName = computed(() => deriveCoverFileName(formState.cover_url));
function handleOk() {
if (!formState.name.trim()) {
message.warning(t("kol.manage.packageForm.nameRequired"));
return;
}
emit("submit", { ...formState, tags: [...formState.tags] });
emit("update:open", false);
}
function handleCancel() {
emit("update:open", false);
}
function handleRemoveCover() {
formState.cover_url = "";
}
function handleCoverConfirmed(payload: { url: string }) {
formState.cover_url = resolveApiURL(payload.url);
}
async function uploadCover(file: File) {
const uploaded = await imagesApi.upload(file);
return {
url: uploaded.url,
fileName: uploaded.name,
assetId: uploaded.id,
};
}
</script>
<template>
<a-modal
:open="open"
:title="initialValue ? t('kol.manage.editPackage') : t('kol.manage.createPackage')"
:ok-text="t('common.confirm')"
:cancel-text="t('common.cancel')"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form layout="vertical">
<a-form-item :label="t('common.title')" required>
<a-input v-model:value="formState.name" :placeholder="t('common.inputPlease')" />
</a-form-item>
<a-form-item :label="t('common.description')">
<a-textarea v-model:value="formState.description" :rows="3" />
</a-form-item>
<a-form-item :label="t('kol.marketplace.filter.industry')">
<a-input v-model:value="formState.industry" />
</a-form-item>
<a-form-item :label="t('kol.manage.packageForm.tags')">
<a-select
v-model:value="formState.tags"
mode="tags"
style="width: 100%"
:placeholder="t('kol.manage.packageForm.tagsPlaceholder')"
/>
</a-form-item>
<a-form-item :label="t('kol.manage.packageForm.priceNote')">
<a-input
v-model:value="formState.price_note"
:placeholder="t('kol.manage.packageForm.priceNotePlaceholder')"
/>
</a-form-item>
<a-form-item :label="t('kol.manage.packageForm.cover')">
<div class="package-cover-field">
<button
type="button"
class="package-cover-picker"
@click="coverPickerOpen = true"
>
<template v-if="coverPreviewUrl">
<img :src="coverPreviewUrl" :alt="t('kol.manage.packageForm.cover')" class="package-cover-picker__image" />
</template>
<template v-else>
<div class="package-cover-picker__empty">
<PictureOutlined class="package-cover-picker__icon" />
<strong>{{ t("kol.manage.packageForm.selectCover") }}</strong>
<span>{{ t("kol.manage.packageForm.coverHint") }}</span>
</div>
</template>
</button>
<div class="package-cover-actions">
<a-button @click="coverPickerOpen = true">
{{ coverPreviewUrl ? t("kol.manage.packageForm.changeCover") : t("kol.manage.packageForm.selectCover") }}
</a-button>
<a-button v-if="coverPreviewUrl" danger @click="handleRemoveCover">
<template #icon><DeleteOutlined /></template>
{{ t("kol.manage.packageForm.removeCover") }}
</a-button>
</div>
</div>
</a-form-item>
</a-form>
</a-modal>
<CoverPickerModal
v-model:open="coverPickerOpen"
:article-id="null"
:platform-ids="[]"
:current-url="coverPreviewUrl"
:current-file-name="coverFileName"
:title="t('kol.manage.packageForm.coverPicker.title')"
:local-tab-label="t('kol.manage.packageForm.coverPicker.localTab')"
:empty-upload-title="t('kol.manage.packageForm.coverPicker.emptyTitle')"
:empty-upload-hint="t('kol.manage.packageForm.coverPicker.emptyHint')"
:upload-handler="uploadCover"
@confirmed="handleCoverConfirmed"
/>
</template>
<style scoped>
.package-cover-field {
display: flex;
flex-direction: column;
gap: 12px;
}
.package-cover-picker {
width: 100%;
min-height: 180px;
border: 1px dashed #d0d5dd;
border-radius: 12px;
padding: 0;
overflow: hidden;
background: #f8fafc;
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.package-cover-picker:hover {
border-color: #1677ff;
box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.08);
}
.package-cover-picker__image {
display: block;
width: 100%;
height: 180px;
object-fit: cover;
}
.package-cover-picker__empty {
display: flex;
min-height: 180px;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px;
color: #475467;
text-align: center;
}
.package-cover-picker__empty strong {
color: #111827;
font-size: 15px;
font-weight: 700;
}
.package-cover-picker__empty span {
max-width: 320px;
font-size: 12px;
line-height: 1.6;
}
.package-cover-picker__icon {
font-size: 28px;
color: #1677ff;
}
.package-cover-actions {
display: flex;
gap: 8px;
}
</style>