feat: 优化商品列表编辑交互与图片展示
Some checks failed
Build and Deploy TenantUI / build-and-deploy (push) Has been cancelled

This commit is contained in:
2026-02-26 10:50:54 +08:00
parent a678ca632a
commit 3575141a97
5 changed files with 52 additions and 62 deletions

View File

@@ -25,7 +25,6 @@ interface StatusOption {
} }
interface Props { interface Props {
isLoading: boolean;
isStoreLoading: boolean; isStoreLoading: boolean;
kind: '' | ProductKind; kind: '' | ProductKind;
kindOptions: KindOption[]; kindOptions: KindOption[];
@@ -40,7 +39,6 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'reset'): void;
(event: 'search'): void; (event: 'search'): void;
(event: 'update:kind', value: '' | ProductKind): void; (event: 'update:kind', value: '' | ProductKind): void;
(event: 'update:keyword', value: string): void; (event: 'update:keyword', value: string): void;

View File

@@ -51,6 +51,7 @@ const emit = defineEmits<{
(event: 'delete', item: ProductListItemDto): void; (event: 'delete', item: ProductListItemDto): void;
(event: 'detail', item: ProductListItemDto): void; (event: 'detail', item: ProductListItemDto): void;
(event: 'edit', item: ProductListItemDto): void; (event: 'edit', item: ProductListItemDto): void;
(event: 'quickEdit', item: ProductListItemDto): void;
(event: 'pageChange', payload: ProductPaginationChangePayload): void; (event: 'pageChange', payload: ProductPaginationChangePayload): void;
(event: 'soldout', item: ProductListItemDto): void; (event: 'soldout', item: ProductListItemDto): void;
( (
@@ -67,7 +68,9 @@ function isChecked(productId: string) {
/** 解析“更多”动作菜单。 */ /** 解析“更多”动作菜单。 */
function getMoreActionOptions(item: ProductListItemDto) { 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') { if (item.status === 'on_sale') {
actions.push({ key: 'off', label: '下架' }); actions.push({ key: 'off', label: '下架' });
} else { } else {
@@ -83,6 +86,10 @@ function handleMoreAction(
payload: { key: number | string }, payload: { key: number | string },
) { ) {
const key = String(payload.key); const key = String(payload.key);
if (key === 'quick_edit') {
emit('quickEdit', item);
return;
}
if (key === 'on') { if (key === 'on') {
emit('changeStatus', { item, status: 'on_sale' }); emit('changeStatus', { item, status: 'on_sale' });
return; 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) { function handleSingleCheck(productId: string, event: unknown) {
const checked = Boolean( const checked = Boolean(
@@ -152,7 +170,13 @@ function handlePageSizeChange(current: number, size: number) {
/> />
<div class="product-card-cover"> <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>
<div class="product-card-body"> <div class="product-card-body">
@@ -268,7 +292,15 @@ function handlePageSizeChange(current: number, size: number) {
</span> </span>
<span class="cell image-cell"> <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>
<span class="cell info-cell"> <span class="cell info-cell">

View File

@@ -21,7 +21,6 @@ import { useRouter } from 'vue-router';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { uploadTenantFileApi } from '#/api/files';
import { searchProductPickerApi } from '#/api/product'; import { searchProductPickerApi } from '#/api/product';
import { createBatchActions } from './product-list-page/batch-actions'; import { createBatchActions } from './product-list-page/batch-actions';
@@ -55,7 +54,6 @@ export function useProductListPage() {
const isCategoryLoading = ref(false); const isCategoryLoading = ref(false);
const isListLoading = ref(false); const isListLoading = ref(false);
const isEditorSubmitting = ref(false); const isEditorSubmitting = ref(false);
const isEditorImageUploading = ref(false);
const isQuickEditSubmitting = ref(false); const isQuickEditSubmitting = ref(false);
const isSoldoutSubmitting = ref(false); const isSoldoutSubmitting = ref(false);
@@ -257,48 +255,6 @@ export function useProductListPage() {
editorForm.description = value; 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() { function normalizeComboGroups() {
editorForm.comboGroups = editorForm.comboGroups.map( editorForm.comboGroups = editorForm.comboGroups.map(
(group, groupIndex) => ({ (group, groupIndex) => ({
@@ -511,10 +467,6 @@ export function useProductListPage() {
editorForm.stock = Math.max(0, Math.floor(Number(value || 0))); editorForm.stock = Math.max(0, Math.floor(Number(value || 0)));
} }
function setEditorTagsText(value: string) {
editorForm.tagsText = value;
}
function setEditorShelfMode(value: 'draft' | 'now' | 'scheduled') { function setEditorShelfMode(value: 'draft' | 'now' | 'scheduled') {
editorForm.shelfMode = value; editorForm.shelfMode = value;
} }
@@ -679,7 +631,6 @@ export function useProductListPage() {
isComboPickerLoading, isComboPickerLoading,
isComboPickerOpen, isComboPickerOpen,
isEditorDrawerOpen, isEditorDrawerOpen,
isEditorImageUploading,
isEditorSubmitting, isEditorSubmitting,
isListLoading, isListLoading,
isQuickEditDrawerOpen, isQuickEditDrawerOpen,
@@ -724,11 +675,7 @@ export function useProductListPage() {
setEditorShelfMode, setEditorShelfMode,
setEditorStock, setEditorStock,
setEditorSubtitle, setEditorSubtitle,
setEditorTagsText,
setEditorTimedOnShelfAt, setEditorTimedOnShelfAt,
removeEditorImage,
setEditorPrimaryImage,
uploadEditorImage,
setFilterKind, setFilterKind,
setFilterKeyword, setFilterKeyword,
setFilterStatus, setFilterStatus,

View File

@@ -56,6 +56,7 @@ const {
isSoldoutSubmitting, isSoldoutSubmitting,
isStoreLoading, isStoreLoading,
openCreateProductDrawer, openCreateProductDrawer,
openEditDrawer,
openProductDetail, openProductDetail,
openProductDetailById, openProductDetailById,
openQuickEditDrawer, openQuickEditDrawer,
@@ -63,7 +64,6 @@ const {
products, products,
quickEditForm, quickEditForm,
quickEditSummary, quickEditSummary,
resetSearchFilters,
selectedCount, selectedCount,
selectedProductIds, selectedProductIds,
selectedStoreId, selectedStoreId,
@@ -165,14 +165,12 @@ function onBatchAction(action: ProductBatchAction) {
:status-options="PRODUCT_STATUS_OPTIONS" :status-options="PRODUCT_STATUS_OPTIONS"
:kind-options="PRODUCT_KIND_OPTIONS" :kind-options="PRODUCT_KIND_OPTIONS"
:view-mode="viewMode" :view-mode="viewMode"
:is-loading="isListLoading"
@update:selected-store-id="setSelectedStoreId" @update:selected-store-id="setSelectedStoreId"
@update:keyword="setFilterKeyword" @update:keyword="setFilterKeyword"
@update:status="setFilterStatus" @update:status="setFilterStatus"
@update:kind="setFilterKind" @update:kind="setFilterKind"
@update:view-mode="setViewMode" @update:view-mode="setViewMode"
@search="applySearchFilters" @search="applySearchFilters"
@reset="resetSearchFilters"
/> />
<ProductActionBar <ProductActionBar
@@ -198,7 +196,8 @@ function onBatchAction(action: ProductBatchAction) {
@toggle-select="handleToggleSelect" @toggle-select="handleToggleSelect"
@toggle-select-all="handleToggleSelectAll" @toggle-select-all="handleToggleSelectAll"
@page-change="handlePageChange" @page-change="handlePageChange"
@edit="openQuickEditDrawer" @edit="openEditDrawer"
@quick-edit="openQuickEditDrawer"
@detail="openProductDetail" @detail="openProductDetail"
@delete="deleteProduct" @delete="deleteProduct"
@change-status="handleSingleStatusChange" @change-status="handleSingleStatusChange"

View File

@@ -56,6 +56,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden;
height: 156px; height: 156px;
font-size: 36px; font-size: 36px;
font-weight: 700; font-weight: 700;
@@ -63,6 +64,12 @@
background: linear-gradient(135deg, #f8fafc 0%, #eef2f7 100%); background: linear-gradient(135deg, #f8fafc 0%, #eef2f7 100%);
} }
.product-cover-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-card-body { .product-card-body {
padding: 12px 14px 10px; padding: 12px 14px 10px;
} }
@@ -215,6 +222,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden;
width: 46px; width: 46px;
height: 46px; height: 46px;
font-size: 18px; font-size: 18px;
@@ -224,6 +232,12 @@
border-radius: 8px; border-radius: 8px;
} }
.list-cover-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.info-cell { .info-cell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;