feat: 优化商品列表编辑交互与图片展示
Some checks failed
Build and Deploy TenantUI / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy TenantUI / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -25,7 +25,6 @@ interface StatusOption {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
isStoreLoading: boolean;
|
||||
kind: '' | ProductKind;
|
||||
kindOptions: KindOption[];
|
||||
@@ -40,7 +39,6 @@ interface Props {
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'reset'): void;
|
||||
(event: 'search'): void;
|
||||
(event: 'update:kind', value: '' | ProductKind): void;
|
||||
(event: 'update:keyword', value: string): void;
|
||||
|
||||
@@ -51,6 +51,7 @@ const emit = defineEmits<{
|
||||
(event: 'delete', item: ProductListItemDto): void;
|
||||
(event: 'detail', item: ProductListItemDto): void;
|
||||
(event: 'edit', item: ProductListItemDto): void;
|
||||
(event: 'quickEdit', item: ProductListItemDto): void;
|
||||
(event: 'pageChange', payload: ProductPaginationChangePayload): void;
|
||||
(event: 'soldout', item: ProductListItemDto): void;
|
||||
(
|
||||
@@ -67,7 +68,9 @@ function isChecked(productId: string) {
|
||||
|
||||
/** 解析“更多”动作菜单。 */
|
||||
function getMoreActionOptions(item: ProductListItemDto) {
|
||||
const actions: Array<{ key: string; label: string }> = [];
|
||||
const actions: Array<{ key: string; label: string }> = [
|
||||
{ key: 'quick_edit', label: '快速编辑' },
|
||||
];
|
||||
if (item.status === 'on_sale') {
|
||||
actions.push({ key: 'off', label: '下架' });
|
||||
} else {
|
||||
@@ -83,6 +86,10 @@ function handleMoreAction(
|
||||
payload: { key: number | string },
|
||||
) {
|
||||
const key = String(payload.key);
|
||||
if (key === 'quick_edit') {
|
||||
emit('quickEdit', item);
|
||||
return;
|
||||
}
|
||||
if (key === 'on') {
|
||||
emit('changeStatus', { item, status: 'on_sale' });
|
||||
return;
|
||||
@@ -96,6 +103,17 @@ function handleMoreAction(
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析商品展示图。 */
|
||||
function resolveImageUrl(item: ProductListItemDto) {
|
||||
return String(item.imageUrl || '').trim();
|
||||
}
|
||||
|
||||
/** 解析图片占位首字。 */
|
||||
function resolveImageFallbackText(item: ProductListItemDto) {
|
||||
const name = String(item.name || '').trim();
|
||||
return name ? name.slice(0, 1) : '?';
|
||||
}
|
||||
|
||||
/** 处理单项勾选变化。 */
|
||||
function handleSingleCheck(productId: string, event: unknown) {
|
||||
const checked = Boolean(
|
||||
@@ -152,7 +170,13 @@ function handlePageSizeChange(current: number, size: number) {
|
||||
/>
|
||||
|
||||
<div class="product-card-cover">
|
||||
<span>{{ item.name.slice(0, 1) }}</span>
|
||||
<img
|
||||
v-if="resolveImageUrl(item)"
|
||||
:src="resolveImageUrl(item)"
|
||||
:alt="item.name"
|
||||
class="product-cover-image"
|
||||
/>
|
||||
<span v-else>{{ resolveImageFallbackText(item) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="product-card-body">
|
||||
@@ -268,7 +292,15 @@ function handlePageSizeChange(current: number, size: number) {
|
||||
</span>
|
||||
|
||||
<span class="cell image-cell">
|
||||
<span class="list-cover">{{ item.name.slice(0, 1) }}</span>
|
||||
<span class="list-cover">
|
||||
<img
|
||||
v-if="resolveImageUrl(item)"
|
||||
:src="resolveImageUrl(item)"
|
||||
:alt="item.name"
|
||||
class="list-cover-image"
|
||||
/>
|
||||
<span v-else>{{ resolveImageFallbackText(item) }}</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="cell info-cell">
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useRouter } from 'vue-router';
|
||||
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
import { uploadTenantFileApi } from '#/api/files';
|
||||
import { searchProductPickerApi } from '#/api/product';
|
||||
|
||||
import { createBatchActions } from './product-list-page/batch-actions';
|
||||
@@ -55,7 +54,6 @@ export function useProductListPage() {
|
||||
const isCategoryLoading = ref(false);
|
||||
const isListLoading = ref(false);
|
||||
const isEditorSubmitting = ref(false);
|
||||
const isEditorImageUploading = ref(false);
|
||||
const isQuickEditSubmitting = ref(false);
|
||||
const isSoldoutSubmitting = ref(false);
|
||||
|
||||
@@ -257,48 +255,6 @@ export function useProductListPage() {
|
||||
editorForm.description = value;
|
||||
}
|
||||
|
||||
function removeEditorImage(index: number) {
|
||||
editorForm.imageUrls = editorForm.imageUrls.filter(
|
||||
(_, idx) => idx !== index,
|
||||
);
|
||||
}
|
||||
|
||||
function setEditorPrimaryImage(index: number) {
|
||||
if (index <= 0 || index >= editorForm.imageUrls.length) return;
|
||||
const next = [...editorForm.imageUrls];
|
||||
const [selected] = next.splice(index, 1);
|
||||
if (!selected) return;
|
||||
editorForm.imageUrls = [selected, ...next];
|
||||
}
|
||||
|
||||
async function uploadEditorImage(file: File) {
|
||||
if (editorForm.imageUrls.length >= 5) {
|
||||
message.warning('最多上传 5 张图片');
|
||||
return;
|
||||
}
|
||||
|
||||
isEditorImageUploading.value = true;
|
||||
try {
|
||||
const uploaded = await uploadTenantFileApi(file, 'dish_image');
|
||||
const url = String(uploaded.url || '').trim();
|
||||
if (!url) {
|
||||
message.error('图片上传失败');
|
||||
return;
|
||||
}
|
||||
|
||||
editorForm.imageUrls = [...editorForm.imageUrls, url]
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
.filter((item, index, source) => source.indexOf(item) === index)
|
||||
.slice(0, 5);
|
||||
message.success('图片上传成功');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
isEditorImageUploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeComboGroups() {
|
||||
editorForm.comboGroups = editorForm.comboGroups.map(
|
||||
(group, groupIndex) => ({
|
||||
@@ -511,10 +467,6 @@ export function useProductListPage() {
|
||||
editorForm.stock = Math.max(0, Math.floor(Number(value || 0)));
|
||||
}
|
||||
|
||||
function setEditorTagsText(value: string) {
|
||||
editorForm.tagsText = value;
|
||||
}
|
||||
|
||||
function setEditorShelfMode(value: 'draft' | 'now' | 'scheduled') {
|
||||
editorForm.shelfMode = value;
|
||||
}
|
||||
@@ -679,7 +631,6 @@ export function useProductListPage() {
|
||||
isComboPickerLoading,
|
||||
isComboPickerOpen,
|
||||
isEditorDrawerOpen,
|
||||
isEditorImageUploading,
|
||||
isEditorSubmitting,
|
||||
isListLoading,
|
||||
isQuickEditDrawerOpen,
|
||||
@@ -724,11 +675,7 @@ export function useProductListPage() {
|
||||
setEditorShelfMode,
|
||||
setEditorStock,
|
||||
setEditorSubtitle,
|
||||
setEditorTagsText,
|
||||
setEditorTimedOnShelfAt,
|
||||
removeEditorImage,
|
||||
setEditorPrimaryImage,
|
||||
uploadEditorImage,
|
||||
setFilterKind,
|
||||
setFilterKeyword,
|
||||
setFilterStatus,
|
||||
|
||||
@@ -56,6 +56,7 @@ const {
|
||||
isSoldoutSubmitting,
|
||||
isStoreLoading,
|
||||
openCreateProductDrawer,
|
||||
openEditDrawer,
|
||||
openProductDetail,
|
||||
openProductDetailById,
|
||||
openQuickEditDrawer,
|
||||
@@ -63,7 +64,6 @@ const {
|
||||
products,
|
||||
quickEditForm,
|
||||
quickEditSummary,
|
||||
resetSearchFilters,
|
||||
selectedCount,
|
||||
selectedProductIds,
|
||||
selectedStoreId,
|
||||
@@ -165,14 +165,12 @@ function onBatchAction(action: ProductBatchAction) {
|
||||
:status-options="PRODUCT_STATUS_OPTIONS"
|
||||
:kind-options="PRODUCT_KIND_OPTIONS"
|
||||
:view-mode="viewMode"
|
||||
:is-loading="isListLoading"
|
||||
@update:selected-store-id="setSelectedStoreId"
|
||||
@update:keyword="setFilterKeyword"
|
||||
@update:status="setFilterStatus"
|
||||
@update:kind="setFilterKind"
|
||||
@update:view-mode="setViewMode"
|
||||
@search="applySearchFilters"
|
||||
@reset="resetSearchFilters"
|
||||
/>
|
||||
|
||||
<ProductActionBar
|
||||
@@ -198,7 +196,8 @@ function onBatchAction(action: ProductBatchAction) {
|
||||
@toggle-select="handleToggleSelect"
|
||||
@toggle-select-all="handleToggleSelectAll"
|
||||
@page-change="handlePageChange"
|
||||
@edit="openQuickEditDrawer"
|
||||
@edit="openEditDrawer"
|
||||
@quick-edit="openQuickEditDrawer"
|
||||
@detail="openProductDetail"
|
||||
@delete="deleteProduct"
|
||||
@change-status="handleSingleStatusChange"
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
height: 156px;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
@@ -63,6 +64,12 @@
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #eef2f7 100%);
|
||||
}
|
||||
|
||||
.product-cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.product-card-body {
|
||||
padding: 12px 14px 10px;
|
||||
}
|
||||
@@ -215,6 +222,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
font-size: 18px;
|
||||
@@ -224,6 +232,12 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.list-cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.info-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user