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 {
|
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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user