support solid cover product
This commit is contained in:
@@ -52,7 +52,7 @@
|
|||||||
<photo-scroller
|
<photo-scroller
|
||||||
v-model="attachmentUrlList"
|
v-model="attachmentUrlList"
|
||||||
v-model:photo-items="attachmentItemList"
|
v-model:photo-items="attachmentItemList"
|
||||||
title="附件列表"
|
title="附件列表(取第一张图片作为封面)"
|
||||||
:edit-mode="true"
|
:edit-mode="true"
|
||||||
:loading="isAttachmentUploading"
|
:loading="isAttachmentUploading"
|
||||||
:show-viewer-actions="false"
|
:show-viewer-actions="false"
|
||||||
@@ -61,6 +61,24 @@
|
|||||||
@add-other="openAttachmentPicker('OTHER')"
|
@add-other="openAttachmentPicker('OTHER')"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
|
<t-form-item label="使用纯色作为封面" label-width="8rem" content-align="right">
|
||||||
|
<t-button
|
||||||
|
size="extra-small"
|
||||||
|
variant="outline"
|
||||||
|
class="color-trigger"
|
||||||
|
:disabled="isAttachmentUploading"
|
||||||
|
@click="openCoverColorPicker"
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<span
|
||||||
|
class="swatch"
|
||||||
|
:class="{ empty: !solidColorCoverColor }"
|
||||||
|
:style="{ background: solidColorCoverColor || undefined }"
|
||||||
|
/>
|
||||||
|
<span v-text="solidColorCoverColor || '选择颜色'" />
|
||||||
|
</div>
|
||||||
|
</t-button>
|
||||||
|
</t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
<card-container
|
<card-container
|
||||||
title="产品单位"
|
title="产品单位"
|
||||||
@@ -75,8 +93,8 @@
|
|||||||
>
|
>
|
||||||
<t-form-item label="单位名称" required-mark>
|
<t-form-item label="单位名称" required-mark>
|
||||||
<t-input v-model.trim="unit.name" borderless maxlength="32" placeholder="如 瓶、次" />
|
<t-input v-model.trim="unit.name" borderless maxlength="32" placeholder="如 瓶、次" />
|
||||||
<!-- <t-button v-if="1 < form.unitList.length" theme="danger" variant="text" size="small" @click="removeUnit(index)">删除</t-button>-->
|
|
||||||
<t-button
|
<t-button
|
||||||
|
v-if="1 < form.unitList.length"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
variant="text"
|
variant="text"
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
@@ -139,11 +157,29 @@
|
|||||||
<t-popup v-model="statusPickerVisible" placement="bottom">
|
<t-popup v-model="statusPickerVisible" placement="bottom">
|
||||||
<t-picker v-model="statusDraft" :columns="statusOptions" title="产品状态" @cancel="statusPickerVisible = false" @confirm="confirmStatus" />
|
<t-picker v-model="statusDraft" :columns="statusOptions" title="产品状态" @cancel="statusPickerVisible = false" @confirm="confirmStatus" />
|
||||||
</t-popup>
|
</t-popup>
|
||||||
|
<bottom-popup
|
||||||
|
v-model="coverColorPickerVisible"
|
||||||
|
title="颜色选择"
|
||||||
|
:right-button-text="isAttachmentUploading ? '上传中' : '确定'"
|
||||||
|
@left-button-click="cancelCoverColorPicker"
|
||||||
|
@right-button-click="confirmCoverColorPicker"
|
||||||
|
>
|
||||||
|
<t-color-picker
|
||||||
|
v-if="coverColorPickerVisible"
|
||||||
|
v-model="coverColorDraft"
|
||||||
|
format="HEX"
|
||||||
|
type="multiple"
|
||||||
|
:swatch-colors="colorSwatchList"
|
||||||
|
@palette-bar-change="updateCoverColorDraftFromPalette"
|
||||||
|
/>
|
||||||
|
</bottom-popup>
|
||||||
<input ref="attachmentInputRef" class="file-input" type="file" accept="image/*" @change="handleAttachmentSelected" />
|
<input ref="attachmentInputRef" class="file-input" type="file" accept="image/*" @change="handleAttachmentSelected" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
|
import type { ColorObject } from "tdesign-mobile-vue";
|
||||||
import { type Attachment, AttachmentAPI, useUserStore } from "timi-web-api";
|
import { type Attachment, AttachmentAPI, useUserStore } from "timi-web-api";
|
||||||
import ProductAPI from "@/api/ProductAPI";
|
import ProductAPI from "@/api/ProductAPI";
|
||||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||||
@@ -162,8 +198,13 @@ type ProductUnitForm = ProductUnit & { clientKey: string; salePriceText: string
|
|||||||
type ProductAttachmentItem = PhotoItem & {
|
type ProductAttachmentItem = PhotoItem & {
|
||||||
attachment?: Attachment;
|
attachment?: Attachment;
|
||||||
attachType: ProductAttachmentType;
|
attachType: ProductAttachmentType;
|
||||||
|
isSolidColorCover?: boolean;
|
||||||
|
solidColor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SOLID_COLOR_COVER_PURPOSE = "PRODUCT_SOLID_COLOR_COVER";
|
||||||
|
const DEFAULT_COVER_COLOR = "#2F88FF";
|
||||||
|
|
||||||
defineOptions({ name: "ProductEdit" });
|
defineOptions({ name: "ProductEdit" });
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -192,6 +233,8 @@ const statusPickerVisible = ref(false);
|
|||||||
const attachmentInputRef = ref<HTMLInputElement>();
|
const attachmentInputRef = ref<HTMLInputElement>();
|
||||||
const attachmentPickerTarget = ref<ProductAttachmentType>("PHOTO");
|
const attachmentPickerTarget = ref<ProductAttachmentType>("PHOTO");
|
||||||
const isAttachmentUploading = ref(false);
|
const isAttachmentUploading = ref(false);
|
||||||
|
const coverColorPickerVisible = ref(false);
|
||||||
|
const coverColorDraft = ref(DEFAULT_COVER_COLOR);
|
||||||
const typeDraft = ref<PickerValue[]>([]);
|
const typeDraft = ref<PickerValue[]>([]);
|
||||||
const categoryDraft = ref<PickerValue[]>([]);
|
const categoryDraft = ref<PickerValue[]>([]);
|
||||||
const statusDraft = ref<PickerValue[]>([]);
|
const statusDraft = ref<PickerValue[]>([]);
|
||||||
@@ -205,6 +248,18 @@ const statusOptions = [
|
|||||||
{ label: "启用", value: "ACTIVE" },
|
{ label: "启用", value: "ACTIVE" },
|
||||||
{ label: "停用", value: "INACTIVE" }
|
{ label: "停用", value: "INACTIVE" }
|
||||||
];
|
];
|
||||||
|
const colorSwatchList = [
|
||||||
|
"#2F88FF",
|
||||||
|
"#0EA5E9",
|
||||||
|
"#10B981",
|
||||||
|
"#22C55E",
|
||||||
|
"#F59E0B",
|
||||||
|
"#F97316",
|
||||||
|
"#EF4444",
|
||||||
|
"#EC4899",
|
||||||
|
"#8B5CF6",
|
||||||
|
"#64748B"
|
||||||
|
];
|
||||||
const selectedCategoryName = computed(() => categoryList.value.find(item => item.id === form.categoryId)?.name || "不设置分类");
|
const selectedCategoryName = computed(() => categoryList.value.find(item => item.id === form.categoryId)?.name || "不设置分类");
|
||||||
const attachmentAddActions = [
|
const attachmentAddActions = [
|
||||||
{
|
{
|
||||||
@@ -230,6 +285,7 @@ const attachmentUrlList = computed<string[]>({
|
|||||||
attachmentItemList.value = attachmentItemList.value.filter(item => urlSet.has(item.url));
|
attachmentItemList.value = attachmentItemList.value.filter(item => urlSet.has(item.url));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const solidColorCoverColor = computed(() => attachmentItemList.value.find(item => item.isSolidColorCover)?.solidColor || "");
|
||||||
|
|
||||||
function createUnit(unit?: Partial<ProductUnit>): ProductUnitForm {
|
function createUnit(unit?: Partial<ProductUnit>): ProductUnitForm {
|
||||||
return {
|
return {
|
||||||
@@ -326,6 +382,77 @@ function openAttachmentPicker(target: ProductAttachmentType): void {
|
|||||||
attachmentInputRef.value?.click();
|
attachmentInputRef.value?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCoverColorPicker(): void {
|
||||||
|
coverColorDraft.value = solidColorCoverColor.value || DEFAULT_COVER_COLOR;
|
||||||
|
coverColorPickerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelCoverColorPicker(): void {
|
||||||
|
coverColorPickerVisible.value = false;
|
||||||
|
coverColorDraft.value = solidColorCoverColor.value || DEFAULT_COVER_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCoverColorDraftFromPalette(context: { color: ColorObject }): void {
|
||||||
|
coverColorDraft.value = context.color.hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSolidColorPNG(color: string): Promise<File> {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = 1;
|
||||||
|
canvas.height = 1;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) {
|
||||||
|
return Promise.reject(new Error("纯色封面生成失败"));
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.fillRect(0, 0, 1, 1);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
if (!blob) {
|
||||||
|
reject(new Error("纯色封面生成失败"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(new File([blob], `product-cover-${Date.now()}.png`, { type: "image/png" }));
|
||||||
|
}, "image/png");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmCoverColorPicker(): Promise<void> {
|
||||||
|
if (isAttachmentUploading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const color = coverColorDraft.value;
|
||||||
|
if (color === solidColorCoverColor.value) {
|
||||||
|
coverColorPickerVisible.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
coverColorPickerVisible.value = false;
|
||||||
|
isAttachmentUploading.value = true;
|
||||||
|
try {
|
||||||
|
const [tempFile] = await AttachmentAPI.upload(await createSolidColorPNG(color));
|
||||||
|
if (!tempFile?.id) {
|
||||||
|
throw new Error("纯色封面上传失败");
|
||||||
|
}
|
||||||
|
attachmentItemList.value = [
|
||||||
|
{
|
||||||
|
attachType: "PHOTO",
|
||||||
|
isSolidColorCover: true,
|
||||||
|
solidColor: color,
|
||||||
|
tempFileId: tempFile.id,
|
||||||
|
url: AttachmentAPI.getTempReadURL(tempFile.id)
|
||||||
|
},
|
||||||
|
...attachmentItemList.value.filter(item => !item.isSolidColorCover)
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "纯色封面上传失败"));
|
||||||
|
} finally {
|
||||||
|
isAttachmentUploading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleAttachmentSelected(event: Event): Promise<void> {
|
async function handleAttachmentSelected(event: Event): Promise<void> {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
const file = input.files?.[0];
|
const file = input.files?.[0];
|
||||||
@@ -362,7 +489,11 @@ function buildAttachmentList(): Attachment[] {
|
|||||||
if (item.tempFileId) {
|
if (item.tempFileId) {
|
||||||
return [{
|
return [{
|
||||||
attachType: item.attachType,
|
attachType: item.attachType,
|
||||||
tempFileId: item.tempFileId
|
tempFileId: item.tempFileId,
|
||||||
|
metadata: item.isSolidColorCover && item.solidColor ? {
|
||||||
|
purpose: SOLID_COLOR_COVER_PURPOSE,
|
||||||
|
color: item.solidColor
|
||||||
|
} : undefined
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -428,7 +559,10 @@ function validate(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save(): Promise<void> {
|
async function save(): Promise<void> {
|
||||||
if (isSaving.value || !userStore.hasPermissions(savePermissionCode.value) || !validate()) {
|
if (isSaving.value || isAttachmentUploading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!userStore.hasPermissions(savePermissionCode.value) || !validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
@@ -499,12 +633,18 @@ async function load(): Promise<void> {
|
|||||||
});
|
});
|
||||||
attachmentItemList.value = (result.attachmentList || [])
|
attachmentItemList.value = (result.attachmentList || [])
|
||||||
.filter(item => !!item.id)
|
.filter(item => !!item.id)
|
||||||
.map(item => ({
|
.map(item => {
|
||||||
attachment: item,
|
const metadata = item.metadata;
|
||||||
attachType: item.attachType === "OTHER" ? "OTHER" : "PHOTO",
|
const solidColor = metadata?.purpose === SOLID_COLOR_COVER_PURPOSE && typeof metadata.color === "string" ? metadata.color : undefined;
|
||||||
previewUrl: getAttachmentReadURL(item.id || ""),
|
return {
|
||||||
url: getAttachmentReadURL(item.id || "", 90)
|
attachment: item,
|
||||||
}));
|
attachType: item.attachType === "OTHER" ? "OTHER" : "PHOTO",
|
||||||
|
isSolidColorCover: !!solidColor,
|
||||||
|
solidColor,
|
||||||
|
previewUrl: getAttachmentReadURL(item.id || ""),
|
||||||
|
url: getAttachmentReadURL(item.id || "", 90)
|
||||||
|
};
|
||||||
|
});
|
||||||
minStockQuantityText.value = result.minStockQuantity === null || result.minStockQuantity === undefined ? "" : String(result.minStockQuantity);
|
minStockQuantityText.value = result.minStockQuantity === null || result.minStockQuantity === undefined ? "" : String(result.minStockQuantity);
|
||||||
if (!form.unitList.length) {
|
if (!form.unitList.length) {
|
||||||
addUnit();
|
addUnit();
|
||||||
@@ -541,6 +681,38 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-trigger {
|
||||||
|
--td-button-extra-small-padding-horizontal: 2px;
|
||||||
|
|
||||||
|
gap: .75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: .875rem;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
border: 1px solid var(--td-component-stroke);
|
||||||
|
display: block;
|
||||||
|
border-radius: .35rem;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
background: var(--td-bg-color-secondarycontainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.units {
|
.units {
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
|
|
||||||
@@ -587,4 +759,5 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user