feat(project): align category manager with prototype and clean drawer implementation
This commit is contained in:
@@ -6,6 +6,10 @@ import type { ProductPreviewItem } from '../types';
|
||||
*/
|
||||
import type { ProductCategoryManageDto } from '#/api/product';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
CATEGORY_CHANNEL_META,
|
||||
CATEGORY_CHANNEL_ORDER,
|
||||
@@ -29,6 +33,17 @@ interface Emits {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
function isImageUrl(value: string) {
|
||||
const source = value.trim();
|
||||
if (!source) return false;
|
||||
return (
|
||||
source.startsWith('data:image/') ||
|
||||
source.startsWith('http://') ||
|
||||
source.startsWith('https://') ||
|
||||
source.startsWith('/')
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -43,16 +58,22 @@ const emit = defineEmits<Emits>();
|
||||
</div>
|
||||
</div>
|
||||
<div class="pcat-info-actions">
|
||||
<button type="button" class="pcat-btn" @click="emit('edit')">
|
||||
<Button class="pcat-action-btn" @click="emit('edit')">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:pencil" />
|
||||
</template>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pcat-btn pcat-btn-danger"
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
class="pcat-action-btn pcat-action-btn-danger"
|
||||
@click="emit('remove')"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:trash-2" />
|
||||
</template>
|
||||
删除
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +112,13 @@ const emit = defineEmits<Emits>();
|
||||
<div class="pcat-attr-item">
|
||||
<span class="pcat-attr-label">分类图标</span>
|
||||
<span class="pcat-attr-value icon-value">
|
||||
{{ props.selectedCategory.icon || '未设置' }}
|
||||
<img
|
||||
v-if="isImageUrl(props.selectedCategory.icon)"
|
||||
:src="props.selectedCategory.icon"
|
||||
class="pcat-category-icon-preview"
|
||||
alt="分类图标"
|
||||
/>
|
||||
<span v-else class="pcat-category-icon-empty">未设置</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,7 +136,10 @@ const emit = defineEmits<Emits>();
|
||||
class="pcat-ch-icon"
|
||||
:class="CATEGORY_CHANNEL_META[channel].cardClass"
|
||||
>
|
||||
{{ CATEGORY_CHANNEL_META[channel].shortLabel }}
|
||||
<IconifyIcon
|
||||
:icon="CATEGORY_CHANNEL_META[channel].icon"
|
||||
class="pcat-ch-icon-svg"
|
||||
/>
|
||||
</div>
|
||||
<div class="pcat-ch-info">
|
||||
<div class="pcat-ch-name">
|
||||
@@ -142,13 +172,12 @@ const emit = defineEmits<Emits>();
|
||||
分类商品
|
||||
<span>{{ props.categoryProducts.length }} 个商品</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="pcat-btn pcat-btn-sm"
|
||||
@click="emit('openPicker')"
|
||||
>
|
||||
<Button class="pcat-add-product-btn" @click="emit('openPicker')">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:plus" />
|
||||
</template>
|
||||
添加商品到此分类
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<table class="pcat-prod-table">
|
||||
<thead>
|
||||
@@ -170,7 +199,19 @@ const emit = defineEmits<Emits>();
|
||||
<tr v-for="item in props.categoryProducts" v-else :key="item.id">
|
||||
<td>
|
||||
<div class="pcat-prod-name">
|
||||
<div class="pcat-prod-thumb">图</div>
|
||||
<div class="pcat-prod-thumb">
|
||||
<img
|
||||
v-if="item.imageUrl"
|
||||
:src="item.imageUrl"
|
||||
:alt="item.name"
|
||||
class="pcat-prod-thumb-img"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-else
|
||||
icon="lucide:image"
|
||||
class="pcat-prod-thumb-icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="pcat-prod-info">
|
||||
<div class="name">{{ item.name }}</div>
|
||||
<div class="spu">{{ item.spuCode }}</div>
|
||||
|
||||
@@ -11,7 +11,18 @@ import { computed, onBeforeUnmount, ref } from 'vue';
|
||||
import { VCropper } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { message, Modal, Upload } from 'ant-design-vue';
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Switch,
|
||||
Upload,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { uploadTenantFileApi } from '#/api/files';
|
||||
|
||||
@@ -38,6 +49,7 @@ interface Emits {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const cropOpen = ref(false);
|
||||
const cropConfirming = ref(false);
|
||||
const cropImageUrl = ref('');
|
||||
@@ -56,26 +68,22 @@ const canPreviewIcon = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
function setName(event: Event) {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
emit('setName', target?.value ?? '');
|
||||
function setName(value: string | undefined) {
|
||||
emit('setName', value ?? '');
|
||||
}
|
||||
|
||||
function setDescription(event: Event) {
|
||||
const target = event.target as HTMLTextAreaElement | null;
|
||||
emit('setDescription', target?.value ?? '');
|
||||
function setDescription(value: string | undefined) {
|
||||
emit('setDescription', value ?? '');
|
||||
}
|
||||
|
||||
function setSort(event: Event) {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
const value = Number(target?.value ?? '0');
|
||||
emit('setSort', value);
|
||||
}
|
||||
|
||||
function getChannelIcon(channel: ProductCategoryChannel) {
|
||||
if (channel === 'wm') return 'lucide:bike';
|
||||
if (channel === 'pickup') return 'lucide:shopping-bag';
|
||||
return 'lucide:utensils';
|
||||
function setSort(value: null | number | string) {
|
||||
let nextValue = 0;
|
||||
if (typeof value === 'number') {
|
||||
nextValue = value;
|
||||
} else if (typeof value === 'string') {
|
||||
nextValue = Number(value);
|
||||
}
|
||||
emit('setSort', nextValue);
|
||||
}
|
||||
|
||||
function isAllowedIconFile(file: File) {
|
||||
@@ -124,6 +132,7 @@ async function confirmCrop() {
|
||||
120,
|
||||
120,
|
||||
);
|
||||
|
||||
if (
|
||||
typeof cropResult !== 'string' ||
|
||||
!cropResult.startsWith('data:image/')
|
||||
@@ -173,6 +182,50 @@ function beforeUpload(file: File) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleDrawerOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
function isChannelChecked(channel: ProductCategoryChannel) {
|
||||
return props.form.channels.includes(channel);
|
||||
}
|
||||
|
||||
function getChannelButtonStyle(channel: ProductCategoryChannel) {
|
||||
if (isChannelChecked(channel)) {
|
||||
return {
|
||||
background: 'rgb(22 119 255 / 6%)',
|
||||
borderColor: '#1677ff',
|
||||
color: '#1677ff',
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: '#fff',
|
||||
borderColor: '#e5e7eb',
|
||||
color: '#4b5563',
|
||||
};
|
||||
}
|
||||
|
||||
function getChannelCheckStyle(channel: ProductCategoryChannel) {
|
||||
if (isChannelChecked(channel)) {
|
||||
return {
|
||||
background: '#1677ff',
|
||||
borderColor: '#1677ff',
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: '#fff',
|
||||
borderColor: '#d1d5db',
|
||||
};
|
||||
}
|
||||
|
||||
function getChannelCheckIconStyle(channel: ProductCategoryChannel) {
|
||||
return {
|
||||
color: isChannelChecked(channel) ? '#fff' : 'transparent',
|
||||
};
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (cropObjectUrl) {
|
||||
URL.revokeObjectURL(cropObjectUrl);
|
||||
@@ -182,67 +235,65 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="pcat-drawer-mask"
|
||||
:class="{ open: props.open }"
|
||||
@click="emit('close')"
|
||||
></div>
|
||||
<div class="pcat-drawer" :class="{ open: props.open }">
|
||||
<div class="pcat-drawer-hd">
|
||||
<span class="pcat-drawer-title">{{ props.title }}</span>
|
||||
<Drawer
|
||||
class="pcat-editor-drawer-wrap"
|
||||
:open="props.open"
|
||||
:title="props.title"
|
||||
:width="480"
|
||||
:closable="false"
|
||||
destroy-on-close
|
||||
@close="emit('close')"
|
||||
@update:open="handleDrawerOpenChange"
|
||||
>
|
||||
<template #extra>
|
||||
<button type="button" class="pcat-drawer-close" @click="emit('close')">
|
||||
×
|
||||
<IconifyIcon icon="lucide:x" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="pcat-drawer-bd">
|
||||
<div class="pcat-form-group">
|
||||
<label class="pcat-form-label required">分类名称</label>
|
||||
<input
|
||||
</template>
|
||||
|
||||
<Form layout="vertical" class="pcat-drawer-form">
|
||||
<FormItem label="分类名称" required>
|
||||
<Input
|
||||
:value="props.form.name"
|
||||
type="text"
|
||||
class="pcat-input"
|
||||
maxlength="20"
|
||||
:maxlength="20"
|
||||
placeholder="请输入分类名称,如:热销推荐"
|
||||
@input="setName"
|
||||
@update:value="setName"
|
||||
/>
|
||||
</div>
|
||||
<div class="pcat-form-group">
|
||||
<label class="pcat-form-label">分类描述</label>
|
||||
<textarea
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="分类描述">
|
||||
<Input.TextArea
|
||||
:value="props.form.description"
|
||||
class="pcat-textarea"
|
||||
rows="2"
|
||||
maxlength="100"
|
||||
:rows="2"
|
||||
:maxlength="100"
|
||||
placeholder="请输入分类描述,帮助顾客理解分类内容"
|
||||
@input="setDescription"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="pcat-form-group">
|
||||
<label class="pcat-form-label">分类图标</label>
|
||||
<div class="pcat-icon-upload-wrap">
|
||||
<Upload
|
||||
:show-upload-list="false"
|
||||
accept=".png,.svg,image/png,image/svg+xml"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<div class="pcat-icon-upload">
|
||||
<img
|
||||
v-if="canPreviewIcon"
|
||||
:src="props.form.icon"
|
||||
class="pcat-icon-preview"
|
||||
alt="分类图标"
|
||||
/>
|
||||
<template v-else>
|
||||
<span class="pcat-icon-upload-sign">+</span>
|
||||
<span class="pcat-icon-upload-text">上传图标</span>
|
||||
</template>
|
||||
</div>
|
||||
</Upload>
|
||||
</div>
|
||||
@update:value="setDescription"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="分类图标">
|
||||
<Upload
|
||||
:show-upload-list="false"
|
||||
accept=".png,.svg,image/png,image/svg+xml"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<div class="pcat-icon-upload">
|
||||
<img
|
||||
v-if="canPreviewIcon"
|
||||
:src="props.form.icon"
|
||||
class="pcat-icon-preview"
|
||||
alt="分类图标"
|
||||
/>
|
||||
<template v-else>
|
||||
<span class="pcat-icon-plus">+</span>
|
||||
<span class="pcat-icon-upload-text">上传图标</span>
|
||||
</template>
|
||||
</div>
|
||||
</Upload>
|
||||
<div class="pcat-hint">建议尺寸 120x120px,支持 PNG/SVG</div>
|
||||
</div>
|
||||
<div class="pcat-form-group">
|
||||
<label class="pcat-form-label required">销售渠道</label>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="销售渠道" required>
|
||||
<div class="pcat-hint mb-8">选择该分类在哪些渠道展示,至少选择一个</div>
|
||||
<div class="pcat-drawer-channels">
|
||||
<button
|
||||
@@ -250,65 +301,69 @@ onBeforeUnmount(() => {
|
||||
:key="`drawer-channel-${channel}`"
|
||||
type="button"
|
||||
class="pcat-drawer-ch"
|
||||
:class="{ checked: props.form.channels.includes(channel) }"
|
||||
:style="getChannelButtonStyle(channel)"
|
||||
@click="emit('toggleChannel', channel)"
|
||||
>
|
||||
<span class="pcat-ch-check">✓</span>
|
||||
<span class="pcat-ch-check" :style="getChannelCheckStyle(channel)">
|
||||
<IconifyIcon
|
||||
icon="lucide:check"
|
||||
class="pcat-ch-check-icon"
|
||||
:style="getChannelCheckIconStyle(channel)"
|
||||
/>
|
||||
</span>
|
||||
<IconifyIcon
|
||||
class="pcat-drawer-ch-icon"
|
||||
:icon="getChannelIcon(channel)"
|
||||
:icon="CATEGORY_CHANNEL_META[channel].icon"
|
||||
/>
|
||||
{{ CATEGORY_CHANNEL_META[channel].label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pcat-form-group">
|
||||
<label class="pcat-form-label">排序</label>
|
||||
<input
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="排序">
|
||||
<InputNumber
|
||||
:value="props.form.sort"
|
||||
type="number"
|
||||
class="pcat-input pcat-input-small"
|
||||
min="1"
|
||||
class="pcat-sort-input"
|
||||
:min="1"
|
||||
:controls="false"
|
||||
placeholder="数字越小越靠前,如:1"
|
||||
@input="setSort"
|
||||
@update:value="setSort"
|
||||
/>
|
||||
</div>
|
||||
<div class="pcat-form-group">
|
||||
<label class="pcat-form-label">启用状态</label>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="启用状态">
|
||||
<div class="pcat-toggle-row">
|
||||
<button
|
||||
type="button"
|
||||
class="pcat-toggle"
|
||||
:class="{ on: props.form.status === 'enabled' }"
|
||||
@click="emit('toggleStatus')"
|
||||
></button>
|
||||
<Switch
|
||||
:checked="props.form.status === 'enabled'"
|
||||
@change="emit('toggleStatus')"
|
||||
/>
|
||||
<span class="pcat-toggle-label">
|
||||
{{ props.form.status === 'enabled' ? '启用' : '停用' }}
|
||||
</span>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<template #footer>
|
||||
<div class="pcat-drawer-ft">
|
||||
<Button @click="emit('close')">取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="props.submitting"
|
||||
@click="emit('submit')"
|
||||
>
|
||||
{{ props.submitting ? '保存中...' : props.submitText }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pcat-drawer-ft">
|
||||
<button type="button" class="pcat-btn" @click="emit('close')">
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pcat-btn pcat-btn-primary"
|
||||
:disabled="props.submitting"
|
||||
@click="emit('submit')"
|
||||
>
|
||||
{{ props.submitting ? '保存中...' : props.submitText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
:open="cropOpen"
|
||||
title="裁剪分类图标"
|
||||
:width="560"
|
||||
:mask-closable="false"
|
||||
:keyboard="false"
|
||||
:get-container="false"
|
||||
:confirm-loading="cropConfirming"
|
||||
destroy-on-close
|
||||
ok-text="确认"
|
||||
|
||||
@@ -6,6 +6,10 @@ import type { ChannelFilter } from '../types';
|
||||
*/
|
||||
import type { ProductCategoryManageDto } from '#/api/product';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Input } from 'ant-design-vue';
|
||||
|
||||
import { CATEGORY_CHANNEL_META, CATEGORY_CHANNEL_ORDER } from '../types';
|
||||
|
||||
interface Props {
|
||||
@@ -14,6 +18,7 @@ interface Props {
|
||||
keyword: string;
|
||||
loading: boolean;
|
||||
selectedCategoryId: string;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -25,9 +30,8 @@ interface Emits {
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
function setKeyword(event: Event) {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
emit('update:keyword', target?.value ?? '');
|
||||
function setKeyword(value: string | undefined) {
|
||||
emit('update:keyword', value ?? '');
|
||||
}
|
||||
|
||||
function getDisplayChannels(channels: ProductCategoryManageDto['channels']) {
|
||||
@@ -40,7 +44,7 @@ function getDisplayChannels(channels: ProductCategoryManageDto['channels']) {
|
||||
<div class="pcat-left-hd">
|
||||
<div class="pcat-left-title">
|
||||
全部分类
|
||||
<span>共 {{ props.categories.length }} 个</span>
|
||||
<span>共 {{ props.totalCount }} 个</span>
|
||||
</div>
|
||||
<div class="pcat-ch-filter">
|
||||
<span
|
||||
@@ -73,12 +77,16 @@ function getDisplayChannels(channels: ProductCategoryManageDto['channels']) {
|
||||
</span>
|
||||
</div>
|
||||
<div class="pcat-search">
|
||||
<input
|
||||
<Input
|
||||
:value="props.keyword"
|
||||
type="text"
|
||||
allow-clear
|
||||
placeholder="搜索分类名称…"
|
||||
@input="setKeyword"
|
||||
/>
|
||||
@update:value="setKeyword"
|
||||
>
|
||||
<template #prefix>
|
||||
<IconifyIcon icon="lucide:search" class="pcat-search-icon" />
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,31 +95,34 @@ function getDisplayChannels(channels: ProductCategoryManageDto['channels']) {
|
||||
<div v-else-if="props.categories.length === 0" class="pcat-left-empty">
|
||||
暂无分类
|
||||
</div>
|
||||
<div
|
||||
v-for="item in props.categories"
|
||||
v-else
|
||||
:key="item.id"
|
||||
class="pcat-cat-item"
|
||||
:class="{
|
||||
active: item.id === props.selectedCategoryId,
|
||||
disabled: item.status === 'disabled',
|
||||
}"
|
||||
@click="emit('select', item.id)"
|
||||
>
|
||||
<span class="pcat-drag-handle">⋮⋮</span>
|
||||
<span class="pcat-cat-name">{{ item.name }}</span>
|
||||
<span class="pcat-cat-tags">
|
||||
<span
|
||||
v-for="channel in getDisplayChannels(item.channels)"
|
||||
:key="`${item.id}-${channel}`"
|
||||
class="pcat-ch-mini"
|
||||
:class="CATEGORY_CHANNEL_META[channel].filterClass"
|
||||
>
|
||||
{{ CATEGORY_CHANNEL_META[channel].shortLabel }}
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="item in props.categories"
|
||||
:key="item.id"
|
||||
class="pcat-cat-item"
|
||||
:class="{
|
||||
active: item.id === props.selectedCategoryId,
|
||||
disabled: item.status === 'disabled',
|
||||
}"
|
||||
@click="emit('select', item.id)"
|
||||
>
|
||||
<span class="pcat-drag-handle">
|
||||
<IconifyIcon icon="lucide:grip-vertical" />
|
||||
</span>
|
||||
</span>
|
||||
<span class="pcat-cat-badge">{{ item.productCount }}</span>
|
||||
</div>
|
||||
<span class="pcat-cat-name">{{ item.name }}</span>
|
||||
<span class="pcat-cat-tags">
|
||||
<span
|
||||
v-for="channel in getDisplayChannels(item.channels)"
|
||||
:key="`${item.id}-${channel}`"
|
||||
class="pcat-ch-mini"
|
||||
:class="CATEGORY_CHANNEL_META[channel].filterClass"
|
||||
>
|
||||
{{ CATEGORY_CHANNEL_META[channel].shortLabel }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="pcat-cat-badge">{{ item.productCount }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
* 2. 统一封装分类 CRUD 与商品绑定/解绑流程。
|
||||
*/
|
||||
import type {
|
||||
ProductCategoryChannel,
|
||||
ProductCategoryManageDto,
|
||||
ProductPickerItemDto,
|
||||
} from '#/api/product';
|
||||
@@ -34,6 +35,31 @@ import { getStoreListApi } from '#/api/store';
|
||||
|
||||
import { CATEGORY_CHANNEL_ORDER, deriveMonthlySales } from '../types';
|
||||
|
||||
function normalizeCategoryChannels(
|
||||
channels: ProductCategoryManageDto['channels'],
|
||||
) {
|
||||
const source = Array.isArray(channels)
|
||||
? (channels as Array<'ts' | 'zt' | ProductCategoryChannel>)
|
||||
: [];
|
||||
|
||||
const normalized = source
|
||||
.map((channel) => {
|
||||
if (channel === 'zt') return 'pickup';
|
||||
if (channel === 'ts') return 'dine_in';
|
||||
return channel;
|
||||
})
|
||||
.filter((channel): channel is ProductCategoryChannel =>
|
||||
CATEGORY_CHANNEL_ORDER.includes(channel as ProductCategoryChannel),
|
||||
);
|
||||
|
||||
const unique = [...new Set(normalized)];
|
||||
if (unique.length > 0) {
|
||||
return unique;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/** 分类管理页面组合式状态。 */
|
||||
export function useProductCategoryPage() {
|
||||
const stores = ref<StoreListItemDto[]>([]);
|
||||
@@ -199,7 +225,12 @@ export function useProductCategoryPage() {
|
||||
const result = await getProductCategoryManageListApi({
|
||||
storeId: selectedStoreId.value,
|
||||
});
|
||||
categories.value = [...result].toSorted((a, b) => a.sort - b.sort);
|
||||
categories.value = [...result]
|
||||
.map((item) => ({
|
||||
...item,
|
||||
channels: normalizeCategoryChannels(item.channels),
|
||||
}))
|
||||
.toSorted((a, b) => a.sort - b.sort);
|
||||
|
||||
const nextCategoryId =
|
||||
(preferredCategoryId &&
|
||||
@@ -360,7 +391,7 @@ export function useProductCategoryPage() {
|
||||
form.name = current.name;
|
||||
form.description = current.description;
|
||||
form.icon = current.icon;
|
||||
form.channels = [...current.channels];
|
||||
form.channels = normalizeCategoryChannels(current.channels);
|
||||
form.sort = current.sort;
|
||||
form.status = current.status;
|
||||
isDrawerOpen.value = true;
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* 2. 复用门店维度工具栏与复制弹窗组件。
|
||||
*/
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Card, Empty } from 'ant-design-vue';
|
||||
import { Button, Card, Empty } from 'ant-design-vue';
|
||||
|
||||
import CopyToStoresModal from '../../shared/components/CopyToStoresModal.vue';
|
||||
import StoreScopeToolbar from '../../shared/components/StoreScopeToolbar.vue';
|
||||
@@ -92,13 +93,16 @@ const {
|
||||
@copy="openCopyModal"
|
||||
>
|
||||
<template #actions>
|
||||
<button
|
||||
type="button"
|
||||
class="pcat-btn pcat-btn-primary pcat-btn-lg"
|
||||
<Button
|
||||
type="primary"
|
||||
class="pcat-add-category-btn"
|
||||
@click="openCreateDrawer"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:plus" />
|
||||
</template>
|
||||
添加分类
|
||||
</button>
|
||||
</Button>
|
||||
</template>
|
||||
</StoreScopeToolbar>
|
||||
|
||||
@@ -116,6 +120,7 @@ const {
|
||||
:selected-category-id="selectedCategoryId"
|
||||
:keyword="categoryKeyword"
|
||||
:channel-filter="channelFilter"
|
||||
:total-count="stats.totalCategories"
|
||||
@select="selectCategory"
|
||||
@update:keyword="setCategoryKeyword"
|
||||
@update:channel-filter="setChannelFilter"
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.pcat-toolbar-card .ant-card-body {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.pcat-btn {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
@@ -40,36 +44,16 @@
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.pcat-btn-primary {
|
||||
color: #fff;
|
||||
background: var(--pcat-primary);
|
||||
border-color: var(--pcat-primary);
|
||||
}
|
||||
|
||||
.pcat-btn-primary:hover {
|
||||
color: #fff;
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
|
||||
.pcat-btn-danger {
|
||||
color: #ff4d4f;
|
||||
background: #fff2f0;
|
||||
border-color: #ffccc7;
|
||||
}
|
||||
|
||||
.pcat-btn-danger:hover {
|
||||
color: #ff4d4f;
|
||||
background: #fff1f0;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.pcat-btn-sm {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pcat-btn-lg {
|
||||
.pcat-add-category-btn.ant-btn {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 18px;
|
||||
font-size: 15px;
|
||||
@@ -77,6 +61,11 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pcat-add-category-btn.ant-btn .iconify {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.pcat-main {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
|
||||
@@ -44,6 +44,28 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pcat-action-btn.ant-btn {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pcat-action-btn.ant-btn .anticon,
|
||||
.pcat-action-btn.ant-btn .iconify {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.pcat-action-btn.ant-btn-dangerous {
|
||||
color: #ff4d4f;
|
||||
background: #fff2f0;
|
||||
border-color: #ffccc7;
|
||||
}
|
||||
|
||||
.pcat-attr-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
@@ -74,11 +96,26 @@
|
||||
}
|
||||
|
||||
.pcat-attr-value.icon-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.pcat-category-icon-preview {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #f3f4f6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pcat-category-icon-empty {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.pcat-status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -143,11 +180,14 @@
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pcat-ch-icon-svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.pcat-ch-icon.wm {
|
||||
color: #1890ff;
|
||||
background: rgb(24 144 255 / 12%);
|
||||
@@ -272,12 +312,23 @@
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
color: #bbb;
|
||||
background: #f3f4f6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pcat-prod-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pcat-prod-thumb-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.pcat-prod-info .name {
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -305,6 +356,17 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pcat-add-product-btn.ant-btn {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pcat-table-empty {
|
||||
padding: 30px 18px !important;
|
||||
color: #9ca3af !important;
|
||||
|
||||
@@ -1,291 +1,238 @@
|
||||
.page-product-category {
|
||||
.pcat-drawer-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
background: rgb(15 23 42 / 28%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.pcat-drawer-mask.open {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pcat-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(480px, 100vw);
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
box-shadow: -8px 0 24px rgb(15 23 42 / 16%);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.24s ease;
|
||||
}
|
||||
|
||||
.pcat-drawer.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.pcat-drawer-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
.pcat-editor-drawer-wrap {
|
||||
.ant-drawer-header {
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.pcat-drawer-title {
|
||||
.ant-drawer-header-title {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.ant-drawer-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.pcat-drawer-close {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pcat-drawer-close:hover {
|
||||
color: #4b5563;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.pcat-drawer-bd {
|
||||
flex: 1;
|
||||
.ant-drawer-body {
|
||||
padding: 16px 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pcat-drawer-ft {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
.ant-drawer-footer {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.pcat-form-group {
|
||||
.pcat-drawer-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pcat-drawer-close:hover {
|
||||
color: #4b5563;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.pcat-drawer-close .iconify {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.pcat-drawer-ft {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pcat-drawer-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.pcat-form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
.ant-form-item-label > label {
|
||||
height: auto;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.pcat-form-label.required::before {
|
||||
margin-right: 4px;
|
||||
color: #ff4d4f;
|
||||
content: '*';
|
||||
.ant-input,
|
||||
.ant-input-number-input,
|
||||
.ant-input-number,
|
||||
.ant-input-affix-wrapper,
|
||||
.ant-input-outlined {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pcat-input,
|
||||
.pcat-textarea {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
.ant-input,
|
||||
.ant-input-number-input {
|
||||
font-size: 13px;
|
||||
color: #1a1a2e;
|
||||
outline: none;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pcat-input:focus,
|
||||
.pcat-textarea:focus {
|
||||
border-color: var(--pcat-primary);
|
||||
box-shadow: 0 0 0 3px rgb(22 119 255 / 10%);
|
||||
}
|
||||
|
||||
.pcat-input-small {
|
||||
.ant-input-number {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.pcat-textarea {
|
||||
min-height: 72px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.pcat-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.pcat-hint.mb-8 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pcat-icon-upload-wrap {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pcat-icon-upload-wrap .ant-upload {
|
||||
display: block;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.pcat-icon-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #bbb;
|
||||
cursor: pointer;
|
||||
border: 1px dashed #e5e7eb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pcat-icon-upload:hover {
|
||||
color: var(--pcat-primary);
|
||||
border-color: var(--pcat-primary);
|
||||
}
|
||||
|
||||
.pcat-icon-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pcat-icon-upload-sign {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pcat-icon-upload-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pcat-drawer-channels {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pcat-drawer-ch {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: #fff;
|
||||
border: 1.5px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pcat-drawer-ch-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.pcat-drawer-ch:hover {
|
||||
border-color: var(--pcat-primary);
|
||||
}
|
||||
|
||||
.pcat-drawer-ch.checked {
|
||||
font-weight: 500;
|
||||
color: var(--pcat-primary);
|
||||
background: rgb(22 119 255 / 6%);
|
||||
border-color: var(--pcat-primary);
|
||||
}
|
||||
|
||||
.pcat-ch-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 10px;
|
||||
color: transparent;
|
||||
border: 1.5px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pcat-drawer-ch.checked .pcat-ch-check {
|
||||
color: #fff;
|
||||
background: var(--pcat-primary);
|
||||
border-color: var(--pcat-primary);
|
||||
}
|
||||
|
||||
.pcat-toggle-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pcat-toggle {
|
||||
position: relative;
|
||||
width: 42px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
background: #d9d9d9;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pcat-toggle::after {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
content: '';
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pcat-toggle.on {
|
||||
background: var(--pcat-primary);
|
||||
}
|
||||
|
||||
.pcat-toggle.on::after {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.pcat-toggle-label {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.pcat-cropper-wrap {
|
||||
height: 360px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pcat-crop-hint {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
.ant-input-number .ant-input-number-input {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.pcat-sort-input {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.pcat-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.pcat-hint.mb-8 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pcat-icon-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #bbb;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px dashed #e5e7eb;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pcat-icon-upload:hover {
|
||||
color: var(--pcat-primary);
|
||||
border-color: var(--pcat-primary);
|
||||
}
|
||||
|
||||
.pcat-icon-plus {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pcat-icon-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pcat-icon-upload-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pcat-editor-drawer-wrap .ant-upload-wrapper,
|
||||
.pcat-editor-drawer-wrap .ant-upload-select,
|
||||
.pcat-editor-drawer-wrap .ant-upload {
|
||||
display: block;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.pcat-drawer-channels {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pcat-drawer-ch {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: #fff;
|
||||
border: 1.5px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pcat-drawer-ch:hover {
|
||||
color: var(--pcat-primary);
|
||||
background: #fff;
|
||||
border-color: var(--pcat-primary);
|
||||
}
|
||||
|
||||
.pcat-drawer-ch:focus,
|
||||
.pcat-drawer-ch:active {
|
||||
color: var(--pcat-primary);
|
||||
background: #fff;
|
||||
border-color: var(--pcat-primary);
|
||||
box-shadow: 0 0 0 2px rgb(22 119 255 / 10%);
|
||||
}
|
||||
|
||||
.pcat-drawer-ch-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.pcat-ch-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border: 1.5px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pcat-ch-check-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.pcat-toggle-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pcat-toggle-label {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.pcat-cropper-wrap {
|
||||
height: 360px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pcat-crop-hint {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
@@ -64,26 +64,53 @@
|
||||
|
||||
.pcat-search input {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
color: #1a1a2e;
|
||||
outline: none;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pcat-search input:focus {
|
||||
.pcat-search .ant-input-affix-wrapper {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pcat-search .ant-input-affix-wrapper-focused,
|
||||
.pcat-search .ant-input-affix-wrapper:focus-within {
|
||||
border-color: var(--pcat-primary);
|
||||
box-shadow: 0 0 0 3px rgb(22 119 255 / 10%);
|
||||
}
|
||||
|
||||
.pcat-search .ant-input {
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.pcat-search .ant-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.pcat-search-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.pcat-left-list {
|
||||
flex: 1;
|
||||
padding: 6px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pcat-left-list::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.pcat-left-list::-webkit-scrollbar-thumb {
|
||||
background: #e5e7eb;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pcat-left-empty {
|
||||
padding: 30px 16px;
|
||||
font-size: 13px;
|
||||
@@ -118,9 +145,15 @@
|
||||
}
|
||||
|
||||
.pcat-drag-handle {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
color: #bbb;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.pcat-drag-handle svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.pcat-cat-name {
|
||||
|
||||
@@ -35,12 +35,14 @@ export interface CategoryStatsViewModel {
|
||||
export interface CategoryChannelMeta {
|
||||
cardClass: 'ts' | 'wm' | 'zt';
|
||||
filterClass: 'ts' | 'wm' | 'zt';
|
||||
icon: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
subText: string;
|
||||
}
|
||||
|
||||
export interface ProductPreviewItem extends ProductPickerItemDto {
|
||||
imageUrl?: string;
|
||||
monthlySales: number;
|
||||
}
|
||||
|
||||
@@ -60,6 +62,7 @@ export const CATEGORY_CHANNEL_META: Record<
|
||||
CategoryChannelMeta
|
||||
> = {
|
||||
wm: {
|
||||
icon: 'lucide:bike',
|
||||
label: '外卖',
|
||||
shortLabel: '外',
|
||||
subText: '外卖平台展示此分类',
|
||||
@@ -67,6 +70,7 @@ export const CATEGORY_CHANNEL_META: Record<
|
||||
cardClass: 'wm',
|
||||
},
|
||||
pickup: {
|
||||
icon: 'lucide:shopping-bag',
|
||||
label: '自提',
|
||||
shortLabel: '自',
|
||||
subText: '到店自提展示此分类',
|
||||
@@ -74,6 +78,7 @@ export const CATEGORY_CHANNEL_META: Record<
|
||||
cardClass: 'zt',
|
||||
},
|
||||
dine_in: {
|
||||
icon: 'lucide:utensils',
|
||||
label: '堂食',
|
||||
shortLabel: '堂',
|
||||
subText: '堂食扫码点餐展示此分类',
|
||||
|
||||
Reference in New Issue
Block a user