refactor: 拆分specs与addons页面结构对齐category
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import type { AddonItemForm } from '../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:加料管理常量与基础构造方法。
|
||||||
|
*/
|
||||||
|
export const DEFAULT_ITEM: AddonItemForm = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
price: 0,
|
||||||
|
stock: 999,
|
||||||
|
sort: 1,
|
||||||
|
status: 'enabled',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createDefaultAddonItem(sort = 1): AddonItemForm {
|
||||||
|
return {
|
||||||
|
...DEFAULT_ITEM,
|
||||||
|
sort,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { ProductAddonGroupDto } from '#/api/product';
|
||||||
|
import type { StoreListItemDto } from '#/api/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:加料管理数据动作。
|
||||||
|
* 1. 加载门店列表与加料组列表。
|
||||||
|
* 2. 维护门店切换时的数据一致性。
|
||||||
|
*/
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getProductAddonGroupListApi } from '#/api/product';
|
||||||
|
import { getStoreListApi } from '#/api/store';
|
||||||
|
|
||||||
|
interface CreateDataActionsOptions {
|
||||||
|
isLoading: Ref<boolean>;
|
||||||
|
isStoreLoading: Ref<boolean>;
|
||||||
|
rows: Ref<ProductAddonGroupDto[]>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
stores: Ref<StoreListItemDto[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDataActions(options: CreateDataActionsOptions) {
|
||||||
|
async function loadStores() {
|
||||||
|
options.isStoreLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await getStoreListApi({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
});
|
||||||
|
options.stores.value = result.items ?? [];
|
||||||
|
if (options.stores.value.length === 0) {
|
||||||
|
options.selectedStoreId.value = '';
|
||||||
|
options.rows.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSelected = options.stores.value.some(
|
||||||
|
(item) => item.id === options.selectedStoreId.value,
|
||||||
|
);
|
||||||
|
if (!hasSelected) {
|
||||||
|
options.selectedStoreId.value = options.stores.value[0]?.id ?? '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
message.error('加载门店失败');
|
||||||
|
} finally {
|
||||||
|
options.isStoreLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAddonGroups() {
|
||||||
|
if (!options.selectedStoreId.value) {
|
||||||
|
options.rows.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const list = await getProductAddonGroupListApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
});
|
||||||
|
options.rows.value = list;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
options.rows.value = [];
|
||||||
|
message.error('加载加料组失败');
|
||||||
|
} finally {
|
||||||
|
options.isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadAddonGroups,
|
||||||
|
loadStores,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ProductAddonGroupDto,
|
||||||
|
SaveProductAddonGroupDto,
|
||||||
|
} from '#/api/product';
|
||||||
|
import type {
|
||||||
|
AddonEditorForm,
|
||||||
|
AddonGroupCardViewModel,
|
||||||
|
} from '#/views/product/addons/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:加料编辑抽屉动作。
|
||||||
|
* 1. 管理加料组新增/编辑表单状态。
|
||||||
|
* 2. 处理表单校验与保存提交。
|
||||||
|
*/
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { saveProductAddonGroupApi } from '#/api/product';
|
||||||
|
|
||||||
|
import { createDefaultAddonItem } from './constants';
|
||||||
|
|
||||||
|
interface CreateDrawerActionsOptions {
|
||||||
|
drawerMode: Ref<'create' | 'edit'>;
|
||||||
|
editingGroupId: Ref<string>;
|
||||||
|
editingGroupName: Ref<string>;
|
||||||
|
editingProductIds: Ref<string[]>;
|
||||||
|
form: AddonEditorForm;
|
||||||
|
isDrawerOpen: Ref<boolean>;
|
||||||
|
isDrawerSubmitting: Ref<boolean>;
|
||||||
|
loadAddonGroups: () => Promise<void>;
|
||||||
|
rows: Ref<AddonGroupCardViewModel[]>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDrawerActions(options: CreateDrawerActionsOptions) {
|
||||||
|
function setDrawerOpen(value: boolean) {
|
||||||
|
options.isDrawerOpen.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormName(value: string) {
|
||||||
|
options.form.name = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormDescription(value: string) {
|
||||||
|
options.form.description = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormRequired(value: boolean) {
|
||||||
|
options.form.required = value;
|
||||||
|
if (value && options.form.minSelect < 1) {
|
||||||
|
options.form.minSelect = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormMinSelect(value: number) {
|
||||||
|
options.form.minSelect = Math.max(0, value);
|
||||||
|
if (options.form.required && options.form.minSelect < 1) {
|
||||||
|
options.form.minSelect = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormMaxSelect(value: number) {
|
||||||
|
options.form.maxSelect = Math.max(1, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItemName(index: number, value: string) {
|
||||||
|
const current = options.form.items[index];
|
||||||
|
if (!current) return;
|
||||||
|
current.name = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItemPrice(index: number, value: number) {
|
||||||
|
const current = options.form.items[index];
|
||||||
|
if (!current) return;
|
||||||
|
current.price = Number.isNaN(value) ? 0 : Math.max(0, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItemStock(index: number, value: number) {
|
||||||
|
const current = options.form.items[index];
|
||||||
|
if (!current) return;
|
||||||
|
current.stock = Number.isNaN(value) ? 0 : Math.max(0, Math.floor(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItem() {
|
||||||
|
options.form.items.push(
|
||||||
|
createDefaultAddonItem(options.form.items.length + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(index: number) {
|
||||||
|
if (options.form.items.length <= 1) {
|
||||||
|
message.warning('至少保留一个加料项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
options.form.items.splice(index, 1);
|
||||||
|
options.form.items.forEach((item, idx) => {
|
||||||
|
item.sort = idx + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
options.editingGroupId.value = '';
|
||||||
|
options.editingGroupName.value = '';
|
||||||
|
options.editingProductIds.value = [];
|
||||||
|
options.form.name = '';
|
||||||
|
options.form.description = '';
|
||||||
|
options.form.required = false;
|
||||||
|
options.form.minSelect = 0;
|
||||||
|
options.form.maxSelect = 1;
|
||||||
|
options.form.sort = options.rows.value.length + 1;
|
||||||
|
options.form.status = 'enabled';
|
||||||
|
options.form.items = [createDefaultAddonItem()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDrawer() {
|
||||||
|
options.drawerMode.value = 'create';
|
||||||
|
resetForm();
|
||||||
|
options.isDrawerOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDrawer(item: AddonGroupCardViewModel) {
|
||||||
|
options.drawerMode.value = 'edit';
|
||||||
|
options.editingGroupId.value = item.id;
|
||||||
|
options.editingGroupName.value = item.name;
|
||||||
|
options.editingProductIds.value = [...item.productIds];
|
||||||
|
options.form.name = item.name;
|
||||||
|
options.form.description = item.description;
|
||||||
|
options.form.required = item.required;
|
||||||
|
options.form.minSelect = item.minSelect;
|
||||||
|
options.form.maxSelect = item.maxSelect;
|
||||||
|
options.form.sort = item.sort;
|
||||||
|
options.form.status = item.status;
|
||||||
|
options.form.items = item.items.map((option, index) => ({
|
||||||
|
id: option.id,
|
||||||
|
name: option.name,
|
||||||
|
price: option.price,
|
||||||
|
stock: option.stock,
|
||||||
|
sort: option.sort || index + 1,
|
||||||
|
status: option.status,
|
||||||
|
}));
|
||||||
|
if (options.form.items.length === 0) {
|
||||||
|
options.form.items = [createDefaultAddonItem()];
|
||||||
|
}
|
||||||
|
options.isDrawerOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSubmitItems() {
|
||||||
|
return options.form.items
|
||||||
|
.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
name: item.name.trim(),
|
||||||
|
sort: index + 1,
|
||||||
|
price: Number(item.price.toFixed(2)),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSavePayload(
|
||||||
|
items: ProductAddonGroupDto['items'],
|
||||||
|
): SaveProductAddonGroupDto {
|
||||||
|
return {
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
id: options.editingGroupId.value || undefined,
|
||||||
|
name: options.form.name.trim(),
|
||||||
|
description: options.form.description.trim(),
|
||||||
|
required: options.form.required,
|
||||||
|
minSelect: options.form.minSelect,
|
||||||
|
maxSelect: options.form.maxSelect,
|
||||||
|
sort: options.form.sort,
|
||||||
|
status: options.form.status,
|
||||||
|
productIds: [...options.editingProductIds.value],
|
||||||
|
items: items.map((item) => ({
|
||||||
|
id: item.id || undefined,
|
||||||
|
name: item.name,
|
||||||
|
price: item.price,
|
||||||
|
stock: item.stock,
|
||||||
|
sort: item.sort,
|
||||||
|
status: item.status,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDrawer() {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
if (!options.form.name.trim()) {
|
||||||
|
message.warning('请输入加料组名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedItems = normalizeSubmitItems();
|
||||||
|
if (normalizedItems.length === 0) {
|
||||||
|
message.warning('至少保留一个加料项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueNames = new Set(
|
||||||
|
normalizedItems.map((item) => item.name.toLowerCase()),
|
||||||
|
);
|
||||||
|
if (uniqueNames.size !== normalizedItems.length) {
|
||||||
|
message.warning('加料项名称不能重复');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.form.maxSelect < options.form.minSelect) {
|
||||||
|
message.warning('最大可选数量不能小于最小可选数量');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.isDrawerSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
await saveProductAddonGroupApi(buildSavePayload(normalizedItems));
|
||||||
|
message.success(
|
||||||
|
options.drawerMode.value === 'create' ? '加料组已创建' : '加料组已更新',
|
||||||
|
);
|
||||||
|
options.isDrawerOpen.value = false;
|
||||||
|
await options.loadAddonGroups();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isDrawerSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addItem,
|
||||||
|
openCreateDrawer,
|
||||||
|
openEditDrawer,
|
||||||
|
removeItem,
|
||||||
|
setDrawerOpen,
|
||||||
|
setFormDescription,
|
||||||
|
setFormMaxSelect,
|
||||||
|
setFormMinSelect,
|
||||||
|
setFormName,
|
||||||
|
setFormRequired,
|
||||||
|
setItemName,
|
||||||
|
setItemPrice,
|
||||||
|
setItemStock,
|
||||||
|
submitDrawer,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ProductAddonGroupDto,
|
||||||
|
SaveProductAddonGroupDto,
|
||||||
|
} from '#/api/product';
|
||||||
|
import type { AddonGroupCardViewModel } from '#/views/product/addons/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:加料组卡片动作。
|
||||||
|
* 1. 处理卡片级启用/删除操作。
|
||||||
|
* 2. 处理加料项改名与移除。
|
||||||
|
*/
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { Input, message, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
changeProductAddonGroupStatusApi,
|
||||||
|
deleteProductAddonGroupApi,
|
||||||
|
saveProductAddonGroupApi,
|
||||||
|
} from '#/api/product';
|
||||||
|
|
||||||
|
interface CreateGroupActionsOptions {
|
||||||
|
loadAddonGroups: () => Promise<void>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGroupActions(options: CreateGroupActionsOptions) {
|
||||||
|
function removeGroup(item: AddonGroupCardViewModel) {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认删除加料组「${item.name}」吗?`,
|
||||||
|
okText: '确认删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
async onOk() {
|
||||||
|
await deleteProductAddonGroupApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
groupId: item.id,
|
||||||
|
});
|
||||||
|
message.success('加料组已删除');
|
||||||
|
await options.loadAddonGroups();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enableGroup(item: AddonGroupCardViewModel) {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
try {
|
||||||
|
await changeProductAddonGroupStatusApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
groupId: item.id,
|
||||||
|
status: 'enabled',
|
||||||
|
});
|
||||||
|
message.success('加料组已启用');
|
||||||
|
await options.loadAddonGroups();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveGroupInline(
|
||||||
|
group: AddonGroupCardViewModel,
|
||||||
|
items: ProductAddonGroupDto['items'],
|
||||||
|
) {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
|
||||||
|
const payload: SaveProductAddonGroupDto = {
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
description: group.description,
|
||||||
|
required: group.required,
|
||||||
|
minSelect: group.minSelect,
|
||||||
|
maxSelect: group.maxSelect,
|
||||||
|
sort: group.sort,
|
||||||
|
status: group.status,
|
||||||
|
productIds: [...group.productIds],
|
||||||
|
items: items.map((item, index) => ({
|
||||||
|
id: item.id || undefined,
|
||||||
|
name: item.name.trim(),
|
||||||
|
price: Number(item.price.toFixed(2)),
|
||||||
|
stock: item.stock,
|
||||||
|
sort: index + 1,
|
||||||
|
status: item.status,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveProductAddonGroupApi(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameItem(payload: {
|
||||||
|
group: AddonGroupCardViewModel;
|
||||||
|
itemId: string;
|
||||||
|
}) {
|
||||||
|
const currentItem = payload.group.items.find(
|
||||||
|
(item) => item.id === payload.itemId,
|
||||||
|
);
|
||||||
|
if (!currentItem) return;
|
||||||
|
|
||||||
|
let draftName = currentItem.name;
|
||||||
|
Modal.confirm({
|
||||||
|
title: `改名 - ${currentItem.name}`,
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
content: () =>
|
||||||
|
h(Input, {
|
||||||
|
value: draftName,
|
||||||
|
maxlength: 30,
|
||||||
|
placeholder: '请输入新名称',
|
||||||
|
'onUpdate:value': (value: string) => {
|
||||||
|
draftName = value;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async onOk() {
|
||||||
|
const nextName = draftName.trim();
|
||||||
|
if (!nextName) {
|
||||||
|
message.warning('加料项名称不能为空');
|
||||||
|
throw new Error('invalid-name');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextItems = payload.group.items.map((item) =>
|
||||||
|
item.id === payload.itemId ? { ...item, name: nextName } : item,
|
||||||
|
);
|
||||||
|
const uniqueNames = new Set(
|
||||||
|
nextItems.map((item) => item.name.toLowerCase()),
|
||||||
|
);
|
||||||
|
if (uniqueNames.size !== nextItems.length) {
|
||||||
|
message.warning('加料项名称不能重复');
|
||||||
|
throw new Error('duplicate-name');
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveGroupInline(payload.group, nextItems);
|
||||||
|
message.success('加料项名称已更新');
|
||||||
|
await options.loadAddonGroups();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCardItem(payload: {
|
||||||
|
group: AddonGroupCardViewModel;
|
||||||
|
itemId: string;
|
||||||
|
}) {
|
||||||
|
if (payload.group.items.length <= 1) {
|
||||||
|
message.warning('至少保留一个加料项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentItem = payload.group.items.find(
|
||||||
|
(item) => item.id === payload.itemId,
|
||||||
|
);
|
||||||
|
if (!currentItem) return;
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认移除选项「${currentItem.name}」吗?`,
|
||||||
|
okText: '确认移除',
|
||||||
|
cancelText: '取消',
|
||||||
|
async onOk() {
|
||||||
|
const nextItems = payload.group.items.filter(
|
||||||
|
(item) => item.id !== payload.itemId,
|
||||||
|
);
|
||||||
|
await saveGroupInline(payload.group, nextItems);
|
||||||
|
message.success('加料项已移除');
|
||||||
|
await options.loadAddonGroups();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enableGroup,
|
||||||
|
removeCardItem,
|
||||||
|
removeGroup,
|
||||||
|
renameItem,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { ProductPickerItemDto } from '#/api/product';
|
||||||
|
import type { AddonGroupCardViewModel } from '#/views/product/addons/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:加料关联商品动作。
|
||||||
|
* 1. 管理商品选择弹窗与检索状态。
|
||||||
|
* 2. 提交加料组关联商品绑定。
|
||||||
|
*/
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
bindProductAddonGroupProductsApi,
|
||||||
|
searchProductPickerApi,
|
||||||
|
} from '#/api/product';
|
||||||
|
|
||||||
|
interface CreatePickerActionsOptions {
|
||||||
|
bindingGroupId: Ref<string>;
|
||||||
|
isPickerLoading: Ref<boolean>;
|
||||||
|
isPickerOpen: Ref<boolean>;
|
||||||
|
isPickerSubmitting: Ref<boolean>;
|
||||||
|
loadAddonGroups: () => Promise<void>;
|
||||||
|
pickerKeyword: Ref<string>;
|
||||||
|
pickerProducts: Ref<ProductPickerItemDto[]>;
|
||||||
|
pickerSelectedIds: Ref<string[]>;
|
||||||
|
pickerTitle: Ref<string>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPickerActions(options: CreatePickerActionsOptions) {
|
||||||
|
async function loadPickerProducts() {
|
||||||
|
if (!options.selectedStoreId.value) {
|
||||||
|
options.pickerProducts.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
options.isPickerLoading.value = true;
|
||||||
|
try {
|
||||||
|
options.pickerProducts.value = await searchProductPickerApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
keyword: options.pickerKeyword.value.trim() || undefined,
|
||||||
|
limit: 500,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
options.pickerProducts.value = [];
|
||||||
|
message.error('加载商品失败');
|
||||||
|
} finally {
|
||||||
|
options.isPickerLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openBindProducts(item: AddonGroupCardViewModel) {
|
||||||
|
options.bindingGroupId.value = item.id;
|
||||||
|
options.pickerTitle.value = `关联商品 - ${item.name}`;
|
||||||
|
options.pickerKeyword.value = '';
|
||||||
|
options.pickerSelectedIds.value = [...item.productIds];
|
||||||
|
options.pickerProducts.value = [];
|
||||||
|
options.isPickerOpen.value = true;
|
||||||
|
await loadPickerProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPickerOpen(value: boolean) {
|
||||||
|
options.isPickerOpen.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPickerKeyword(value: string) {
|
||||||
|
options.pickerKeyword.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePickerProduct(id: string) {
|
||||||
|
if (options.pickerSelectedIds.value.includes(id)) {
|
||||||
|
options.pickerSelectedIds.value = options.pickerSelectedIds.value.filter(
|
||||||
|
(item) => item !== id,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
options.pickerSelectedIds.value = [...options.pickerSelectedIds.value, id];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitPicker() {
|
||||||
|
if (!options.selectedStoreId.value || !options.bindingGroupId.value) return;
|
||||||
|
|
||||||
|
options.isPickerSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
await bindProductAddonGroupProductsApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
groupId: options.bindingGroupId.value,
|
||||||
|
productIds: [...options.pickerSelectedIds.value],
|
||||||
|
});
|
||||||
|
message.success('关联商品已更新');
|
||||||
|
options.isPickerOpen.value = false;
|
||||||
|
await options.loadAddonGroups();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isPickerSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadPickerProducts,
|
||||||
|
openBindProducts,
|
||||||
|
setPickerKeyword,
|
||||||
|
setPickerOpen,
|
||||||
|
submitPicker,
|
||||||
|
togglePickerProduct,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import type {
|
import type { AddonEditorForm, AddonGroupCardViewModel } from '../types';
|
||||||
AddonEditorForm,
|
|
||||||
AddonGroupCardViewModel,
|
|
||||||
AddonItemForm,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
import type { ProductAddonGroupDto, ProductPickerItemDto } from '#/api/product';
|
import type { ProductPickerItemDto } from '#/api/product';
|
||||||
import type { StoreListItemDto } from '#/api/store';
|
import type { StoreListItemDto } from '#/api/store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,28 +8,14 @@ import type { StoreListItemDto } from '#/api/store';
|
|||||||
* 1. 管理门店、加料组卡片、统计与筛选状态。
|
* 1. 管理门店、加料组卡片、统计与筛选状态。
|
||||||
* 2. 封装加料组新增编辑、选项改名移除、关联商品流程。
|
* 2. 封装加料组新增编辑、选项改名移除、关联商品流程。
|
||||||
*/
|
*/
|
||||||
import { computed, h, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { Input, message, Modal } from 'ant-design-vue';
|
import { createDataActions } from './addons-page/data-actions';
|
||||||
|
import { createDrawerActions } from './addons-page/drawer-actions';
|
||||||
|
import { createGroupActions } from './addons-page/group-actions';
|
||||||
|
import { createPickerActions } from './addons-page/picker-actions';
|
||||||
|
|
||||||
import {
|
import { createDefaultAddonItem } from './addons-page/constants';
|
||||||
bindProductAddonGroupProductsApi,
|
|
||||||
changeProductAddonGroupStatusApi,
|
|
||||||
deleteProductAddonGroupApi,
|
|
||||||
getProductAddonGroupListApi,
|
|
||||||
saveProductAddonGroupApi,
|
|
||||||
searchProductPickerApi,
|
|
||||||
} from '#/api/product';
|
|
||||||
import { getStoreListApi } from '#/api/store';
|
|
||||||
|
|
||||||
const DEFAULT_ITEM: AddonItemForm = {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
price: 0,
|
|
||||||
stock: 999,
|
|
||||||
sort: 1,
|
|
||||||
status: 'enabled',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useProductAddonsPage() {
|
export function useProductAddonsPage() {
|
||||||
const stores = ref<StoreListItemDto[]>([]);
|
const stores = ref<StoreListItemDto[]>([]);
|
||||||
@@ -59,7 +41,7 @@ export function useProductAddonsPage() {
|
|||||||
maxSelect: 1,
|
maxSelect: 1,
|
||||||
sort: 1,
|
sort: 1,
|
||||||
status: 'enabled',
|
status: 'enabled',
|
||||||
items: [{ ...DEFAULT_ITEM }],
|
items: [createDefaultAddonItem()],
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPickerOpen = ref(false);
|
const isPickerOpen = ref(false);
|
||||||
@@ -106,54 +88,13 @@ export function useProductAddonsPage() {
|
|||||||
drawerMode.value === 'create' ? '确认保存' : '保存修改',
|
drawerMode.value === 'create' ? '确认保存' : '保存修改',
|
||||||
);
|
);
|
||||||
|
|
||||||
async function loadStores() {
|
const { loadAddonGroups, loadStores } = createDataActions({
|
||||||
isStoreLoading.value = true;
|
stores,
|
||||||
try {
|
selectedStoreId,
|
||||||
const result = await getStoreListApi({
|
isStoreLoading,
|
||||||
page: 1,
|
rows,
|
||||||
pageSize: 200,
|
isLoading,
|
||||||
});
|
});
|
||||||
stores.value = result.items ?? [];
|
|
||||||
if (stores.value.length === 0) {
|
|
||||||
selectedStoreId.value = '';
|
|
||||||
rows.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasSelected = stores.value.some(
|
|
||||||
(item) => item.id === selectedStoreId.value,
|
|
||||||
);
|
|
||||||
if (!hasSelected) {
|
|
||||||
selectedStoreId.value = stores.value[0]?.id ?? '';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
message.error('加载门店失败');
|
|
||||||
} finally {
|
|
||||||
isStoreLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAddonGroups() {
|
|
||||||
if (!selectedStoreId.value) {
|
|
||||||
rows.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
const list = await getProductAddonGroupListApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
});
|
|
||||||
rows.value = list;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
rows.value = [];
|
|
||||||
message.error('加载加料组失败');
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSelectedStoreId(value: string) {
|
function setSelectedStoreId(value: string) {
|
||||||
selectedStoreId.value = value;
|
selectedStoreId.value = value;
|
||||||
@@ -163,393 +104,59 @@ export function useProductAddonsPage() {
|
|||||||
keyword.value = value;
|
keyword.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDrawerOpen(value: boolean) {
|
const {
|
||||||
isDrawerOpen.value = value;
|
addItem,
|
||||||
}
|
openCreateDrawer,
|
||||||
|
openEditDrawer,
|
||||||
function setFormName(value: string) {
|
removeItem,
|
||||||
form.name = value;
|
setDrawerOpen,
|
||||||
}
|
setFormDescription,
|
||||||
|
setFormMaxSelect,
|
||||||
function setFormDescription(value: string) {
|
setFormMinSelect,
|
||||||
form.description = value;
|
setFormName,
|
||||||
}
|
setFormRequired,
|
||||||
|
setItemName,
|
||||||
function setFormRequired(value: boolean) {
|
setItemPrice,
|
||||||
form.required = value;
|
setItemStock,
|
||||||
if (value && form.minSelect < 1) {
|
submitDrawer,
|
||||||
form.minSelect = 1;
|
} = createDrawerActions({
|
||||||
}
|
drawerMode,
|
||||||
}
|
editingGroupId,
|
||||||
|
editingGroupName,
|
||||||
function setFormMinSelect(value: number) {
|
editingProductIds,
|
||||||
form.minSelect = Math.max(0, value);
|
form,
|
||||||
if (form.required && form.minSelect < 1) {
|
isDrawerOpen,
|
||||||
form.minSelect = 1;
|
isDrawerSubmitting,
|
||||||
}
|
loadAddonGroups,
|
||||||
}
|
rows,
|
||||||
|
selectedStoreId,
|
||||||
function setFormMaxSelect(value: number) {
|
|
||||||
form.maxSelect = Math.max(1, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setItemName(index: number, value: string) {
|
|
||||||
const current = form.items[index];
|
|
||||||
if (!current) return;
|
|
||||||
current.name = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setItemPrice(index: number, value: number) {
|
|
||||||
const current = form.items[index];
|
|
||||||
if (!current) return;
|
|
||||||
current.price = Number.isNaN(value) ? 0 : Math.max(0, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setItemStock(index: number, value: number) {
|
|
||||||
const current = form.items[index];
|
|
||||||
if (!current) return;
|
|
||||||
current.stock = Number.isNaN(value) ? 0 : Math.max(0, Math.floor(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function addItem() {
|
|
||||||
form.items.push({
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
price: 0,
|
|
||||||
stock: 999,
|
|
||||||
sort: form.items.length + 1,
|
|
||||||
status: 'enabled',
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function removeItem(index: number) {
|
const { enableGroup, removeCardItem, removeGroup, renameItem } =
|
||||||
if (form.items.length <= 1) {
|
createGroupActions({
|
||||||
message.warning('至少保留一个加料项');
|
selectedStoreId,
|
||||||
return;
|
loadAddonGroups,
|
||||||
}
|
|
||||||
form.items.splice(index, 1);
|
|
||||||
form.items.forEach((item, idx) => {
|
|
||||||
item.sort = idx + 1;
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
const {
|
||||||
editingGroupId.value = '';
|
loadPickerProducts,
|
||||||
editingGroupName.value = '';
|
openBindProducts,
|
||||||
editingProductIds.value = [];
|
setPickerKeyword,
|
||||||
form.name = '';
|
setPickerOpen,
|
||||||
form.description = '';
|
submitPicker,
|
||||||
form.required = false;
|
togglePickerProduct,
|
||||||
form.minSelect = 0;
|
} = createPickerActions({
|
||||||
form.maxSelect = 1;
|
selectedStoreId,
|
||||||
form.sort = rows.value.length + 1;
|
bindingGroupId,
|
||||||
form.status = 'enabled';
|
isPickerOpen,
|
||||||
form.items = [{ ...DEFAULT_ITEM }];
|
isPickerLoading,
|
||||||
}
|
isPickerSubmitting,
|
||||||
|
pickerKeyword,
|
||||||
function openCreateDrawer() {
|
pickerProducts,
|
||||||
drawerMode.value = 'create';
|
pickerSelectedIds,
|
||||||
resetForm();
|
pickerTitle,
|
||||||
isDrawerOpen.value = true;
|
loadAddonGroups,
|
||||||
}
|
|
||||||
|
|
||||||
function openEditDrawer(item: AddonGroupCardViewModel) {
|
|
||||||
drawerMode.value = 'edit';
|
|
||||||
editingGroupId.value = item.id;
|
|
||||||
editingGroupName.value = item.name;
|
|
||||||
editingProductIds.value = [...item.productIds];
|
|
||||||
form.name = item.name;
|
|
||||||
form.description = item.description;
|
|
||||||
form.required = item.required;
|
|
||||||
form.minSelect = item.minSelect;
|
|
||||||
form.maxSelect = item.maxSelect;
|
|
||||||
form.sort = item.sort;
|
|
||||||
form.status = item.status;
|
|
||||||
form.items = item.items.map((option, index) => ({
|
|
||||||
id: option.id,
|
|
||||||
name: option.name,
|
|
||||||
price: option.price,
|
|
||||||
stock: option.stock,
|
|
||||||
sort: option.sort || index + 1,
|
|
||||||
status: option.status,
|
|
||||||
}));
|
|
||||||
if (form.items.length === 0) {
|
|
||||||
form.items = [{ ...DEFAULT_ITEM }];
|
|
||||||
}
|
|
||||||
isDrawerOpen.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitDrawer() {
|
|
||||||
if (!selectedStoreId.value) return;
|
|
||||||
if (!form.name.trim()) {
|
|
||||||
message.warning('请输入加料组名称');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedItems = form.items
|
|
||||||
.map((item, index) => ({
|
|
||||||
...item,
|
|
||||||
name: item.name.trim(),
|
|
||||||
sort: index + 1,
|
|
||||||
price: Number(item.price.toFixed(2)),
|
|
||||||
}))
|
|
||||||
.filter((item) => item.name);
|
|
||||||
if (normalizedItems.length === 0) {
|
|
||||||
message.warning('至少保留一个加料项');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueNames = new Set(
|
|
||||||
normalizedItems.map((item) => item.name.toLowerCase()),
|
|
||||||
);
|
|
||||||
if (uniqueNames.size !== normalizedItems.length) {
|
|
||||||
message.warning('加料项名称不能重复');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (form.maxSelect < form.minSelect) {
|
|
||||||
message.warning('最大可选数量不能小于最小可选数量');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isDrawerSubmitting.value = true;
|
|
||||||
try {
|
|
||||||
await saveProductAddonGroupApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
id: editingGroupId.value || undefined,
|
|
||||||
name: form.name.trim(),
|
|
||||||
description: form.description.trim(),
|
|
||||||
required: form.required,
|
|
||||||
minSelect: form.minSelect,
|
|
||||||
maxSelect: form.maxSelect,
|
|
||||||
sort: form.sort,
|
|
||||||
status: form.status,
|
|
||||||
productIds: [...editingProductIds.value],
|
|
||||||
items: normalizedItems.map((item) => ({
|
|
||||||
id: item.id || undefined,
|
|
||||||
name: item.name,
|
|
||||||
price: item.price,
|
|
||||||
stock: item.stock,
|
|
||||||
sort: item.sort,
|
|
||||||
status: item.status,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
message.success(
|
|
||||||
drawerMode.value === 'create' ? '加料组已创建' : '加料组已更新',
|
|
||||||
);
|
|
||||||
isDrawerOpen.value = false;
|
|
||||||
await loadAddonGroups();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
isDrawerSubmitting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeGroup(item: AddonGroupCardViewModel) {
|
|
||||||
if (!selectedStoreId.value) return;
|
|
||||||
Modal.confirm({
|
|
||||||
title: `确认删除加料组「${item.name}」吗?`,
|
|
||||||
okText: '确认删除',
|
|
||||||
cancelText: '取消',
|
|
||||||
async onOk() {
|
|
||||||
await deleteProductAddonGroupApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
groupId: item.id,
|
|
||||||
});
|
|
||||||
message.success('加料组已删除');
|
|
||||||
await loadAddonGroups();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function enableGroup(item: AddonGroupCardViewModel) {
|
|
||||||
if (!selectedStoreId.value) return;
|
|
||||||
try {
|
|
||||||
await changeProductAddonGroupStatusApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
groupId: item.id,
|
|
||||||
status: 'enabled',
|
|
||||||
});
|
|
||||||
message.success('加料组已启用');
|
|
||||||
await loadAddonGroups();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveGroupInline(
|
|
||||||
group: AddonGroupCardViewModel,
|
|
||||||
items: ProductAddonGroupDto['items'],
|
|
||||||
) {
|
|
||||||
if (!selectedStoreId.value) return;
|
|
||||||
await saveProductAddonGroupApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
id: group.id,
|
|
||||||
name: group.name,
|
|
||||||
description: group.description,
|
|
||||||
required: group.required,
|
|
||||||
minSelect: group.minSelect,
|
|
||||||
maxSelect: group.maxSelect,
|
|
||||||
sort: group.sort,
|
|
||||||
status: group.status,
|
|
||||||
productIds: [...group.productIds],
|
|
||||||
items: items.map((item, index) => ({
|
|
||||||
id: item.id || undefined,
|
|
||||||
name: item.name.trim(),
|
|
||||||
price: Number(item.price.toFixed(2)),
|
|
||||||
stock: item.stock,
|
|
||||||
sort: index + 1,
|
|
||||||
status: item.status,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renameItem(payload: {
|
|
||||||
group: AddonGroupCardViewModel;
|
|
||||||
itemId: string;
|
|
||||||
}) {
|
|
||||||
const currentItem = payload.group.items.find(
|
|
||||||
(item) => item.id === payload.itemId,
|
|
||||||
);
|
|
||||||
if (!currentItem) return;
|
|
||||||
|
|
||||||
let draftName = currentItem.name;
|
|
||||||
Modal.confirm({
|
|
||||||
title: `改名 - ${currentItem.name}`,
|
|
||||||
okText: '确认',
|
|
||||||
cancelText: '取消',
|
|
||||||
content: () =>
|
|
||||||
h(Input, {
|
|
||||||
value: draftName,
|
|
||||||
maxlength: 30,
|
|
||||||
placeholder: '请输入新名称',
|
|
||||||
'onUpdate:value': (value: string) => {
|
|
||||||
draftName = value;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
async onOk() {
|
|
||||||
const nextName = draftName.trim();
|
|
||||||
if (!nextName) {
|
|
||||||
message.warning('加料项名称不能为空');
|
|
||||||
throw new Error('invalid-name');
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextItems = payload.group.items.map((item) =>
|
|
||||||
item.id === payload.itemId ? { ...item, name: nextName } : item,
|
|
||||||
);
|
|
||||||
const uniqueNames = new Set(
|
|
||||||
nextItems.map((item) => item.name.toLowerCase()),
|
|
||||||
);
|
|
||||||
if (uniqueNames.size !== nextItems.length) {
|
|
||||||
message.warning('加料项名称不能重复');
|
|
||||||
throw new Error('duplicate-name');
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveGroupInline(payload.group, nextItems);
|
|
||||||
message.success('加料项名称已更新');
|
|
||||||
await loadAddonGroups();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeCardItem(payload: {
|
|
||||||
group: AddonGroupCardViewModel;
|
|
||||||
itemId: string;
|
|
||||||
}) {
|
|
||||||
if (payload.group.items.length <= 1) {
|
|
||||||
message.warning('至少保留一个加料项');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentItem = payload.group.items.find(
|
|
||||||
(item) => item.id === payload.itemId,
|
|
||||||
);
|
|
||||||
if (!currentItem) return;
|
|
||||||
|
|
||||||
Modal.confirm({
|
|
||||||
title: `确认移除选项「${currentItem.name}」吗?`,
|
|
||||||
okText: '确认移除',
|
|
||||||
cancelText: '取消',
|
|
||||||
async onOk() {
|
|
||||||
const nextItems = payload.group.items.filter(
|
|
||||||
(item) => item.id !== payload.itemId,
|
|
||||||
);
|
|
||||||
await saveGroupInline(payload.group, nextItems);
|
|
||||||
message.success('加料项已移除');
|
|
||||||
await loadAddonGroups();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPickerProducts() {
|
|
||||||
if (!selectedStoreId.value) {
|
|
||||||
pickerProducts.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isPickerLoading.value = true;
|
|
||||||
try {
|
|
||||||
pickerProducts.value = await searchProductPickerApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
keyword: pickerKeyword.value.trim() || undefined,
|
|
||||||
limit: 500,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
pickerProducts.value = [];
|
|
||||||
message.error('加载商品失败');
|
|
||||||
} finally {
|
|
||||||
isPickerLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openBindProducts(item: AddonGroupCardViewModel) {
|
|
||||||
bindingGroupId.value = item.id;
|
|
||||||
pickerTitle.value = `关联商品 - ${item.name}`;
|
|
||||||
pickerKeyword.value = '';
|
|
||||||
pickerSelectedIds.value = [...item.productIds];
|
|
||||||
pickerProducts.value = [];
|
|
||||||
isPickerOpen.value = true;
|
|
||||||
await loadPickerProducts();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPickerOpen(value: boolean) {
|
|
||||||
isPickerOpen.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPickerKeyword(value: string) {
|
|
||||||
pickerKeyword.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglePickerProduct(id: string) {
|
|
||||||
if (pickerSelectedIds.value.includes(id)) {
|
|
||||||
pickerSelectedIds.value = pickerSelectedIds.value.filter(
|
|
||||||
(item) => item !== id,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pickerSelectedIds.value = [...pickerSelectedIds.value, id];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitPicker() {
|
|
||||||
if (!selectedStoreId.value || !bindingGroupId.value) return;
|
|
||||||
|
|
||||||
isPickerSubmitting.value = true;
|
|
||||||
try {
|
|
||||||
await bindProductAddonGroupProductsApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
groupId: bindingGroupId.value,
|
|
||||||
productIds: [...pickerSelectedIds.value],
|
|
||||||
});
|
|
||||||
message.success('关联商品已更新');
|
|
||||||
isPickerOpen.value = false;
|
|
||||||
await loadAddonGroups();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
isPickerSubmitting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(selectedStoreId, () => {
|
watch(selectedStoreId, () => {
|
||||||
keyword.value = '';
|
keyword.value = '';
|
||||||
|
|||||||
110
apps/web-antd/src/views/product/addons/styles/base.less
Normal file
110
apps/web-antd/src/views/product/addons/styles/base.less
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 文件职责:加料管理页面基础样式。
|
||||||
|
* 1. 定义全局变量与通用动作按钮。
|
||||||
|
* 2. 定义抽屉通用容器骨架样式。
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--g-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--g-shadow-sm: 0 1px 2px rgb(0 0 0 / 4%);
|
||||||
|
--g-shadow-md: 0 4px 12px rgb(0 0 0 / 7%), 0 1px 3px rgb(0 0 0 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-action {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1677ff;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-action + .g-action {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-action-danger {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
background: rgb(0 0 0 / 45%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-mask.open {
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: -6px 0 16px rgb(0 0 0 / 8%);
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-hd {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
height: 54px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-close {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-close:hover {
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-bd {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-ft {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
108
apps/web-antd/src/views/product/addons/styles/card.less
Normal file
108
apps/web-antd/src/views/product/addons/styles/card.less
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
.page-product-addons {
|
||||||
|
.pad-card {
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: var(--g-shadow-sm);
|
||||||
|
transition: var(--g-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-card:hover {
|
||||||
|
box-shadow: var(--g-shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-card.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-card-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-card-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-tag-required {
|
||||||
|
color: #ef4444;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-tag-optional {
|
||||||
|
color: #3b82f6;
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-tag-disabled {
|
||||||
|
color: #9ca3af;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-rule {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-table th {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: left;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-table td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-table tr:hover td {
|
||||||
|
background: rgb(22 119 255 / 3%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-stock-ok {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-stock-low {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-assoc {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-card-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
243
apps/web-antd/src/views/product/addons/styles/drawer.less
Normal file
243
apps/web-antd/src/views/product/addons/styles/drawer.less
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
.page-product-addons {
|
||||||
|
.pad-editor-drawer {
|
||||||
|
.g-form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-form-label {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-form-label.required::before {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: #ef4444;
|
||||||
|
content: '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-input,
|
||||||
|
.g-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: var(--g-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-input {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-textarea {
|
||||||
|
min-height: 66px;
|
||||||
|
padding-top: 8px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-input:focus,
|
||||||
|
.g-textarea:focus {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 3px rgb(22 119 255 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1f1f1f;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: var(--g-shadow-sm);
|
||||||
|
transition: all var(--g-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: var(--g-shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn-primary:hover {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-wrap {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input input {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-sl {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #d9d9d9;
|
||||||
|
border-radius: 11px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-sl::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
content: '';
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 1px 3px rgb(0 0 0 / 15%);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input input:checked + .g-toggle-sl {
|
||||||
|
background: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input input:checked + .g-toggle-sl::before {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-sel-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-sel-row span {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-input-num {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-list-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-list-header .h-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-list-header .h-price,
|
||||||
|
.pad-opt-list-header .h-stock {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-list-header .h-act {
|
||||||
|
width: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-price,
|
||||||
|
.pad-opt-stock {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-del {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-opt-del:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
background: #fef2f2;
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-btn-dashed {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #9ca3af;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-btn-dashed:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,620 +1,6 @@
|
|||||||
/**
|
@import './base.less';
|
||||||
* 文件职责:加料管理页面样式。
|
@import './layout.less';
|
||||||
* 1. 对齐原型的工具栏、统计条、卡片列表与抽屉视觉。
|
@import './card.less';
|
||||||
* 2. 提供加料项行编辑、库存状态和关联商品弹窗样式。
|
@import './drawer.less';
|
||||||
*/
|
@import './modal.less';
|
||||||
:root {
|
@import './responsive.less';
|
||||||
--g-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--g-shadow-sm: 0 1px 2px rgb(0 0 0 / 4%);
|
|
||||||
--g-shadow-md: 0 4px 12px rgb(0 0 0 / 7%), 0 1px 3px rgb(0 0 0 / 4%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-action {
|
|
||||||
padding: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1677ff;
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-action + .g-action {
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-action-danger {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-mask {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
pointer-events: none;
|
|
||||||
background: rgb(0 0 0 / 45%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-mask.open {
|
|
||||||
pointer-events: auto;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 1001;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: -6px 0 16px rgb(0 0 0 / 8%);
|
|
||||||
transform: translateX(100%);
|
|
||||||
transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer.open {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-hd {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
height: 54px;
|
|
||||||
padding: 0 20px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-title {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-close {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #999;
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-close:hover {
|
|
||||||
color: #333;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-bd {
|
|
||||||
flex: 1;
|
|
||||||
padding: 20px 24px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-ft {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-product-addons {
|
|
||||||
@media (width <= 1200px) {
|
|
||||||
.pad-toolbar {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-spacer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
max-width: 960px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: var(--g-shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-store-select {
|
|
||||||
width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-store-select .ant-select-selector {
|
|
||||||
height: 34px !important;
|
|
||||||
border-color: #e5e7eb !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-store-select .ant-select-selection-item {
|
|
||||||
line-height: 32px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-search {
|
|
||||||
width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-search .ant-input {
|
|
||||||
height: 34px;
|
|
||||||
padding-left: 32px;
|
|
||||||
font-size: 13px;
|
|
||||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")
|
|
||||||
10px center no-repeat;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #4b5563;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: var(--g-shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-stats span {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-stats strong {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-card {
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: var(--g-shadow-sm);
|
|
||||||
transition: var(--g-transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-card:hover {
|
|
||||||
box-shadow: var(--g-shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-card.disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-card-header {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-card-name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-tag-required {
|
|
||||||
color: #ef4444;
|
|
||||||
background: #fef2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-tag-optional {
|
|
||||||
color: #3b82f6;
|
|
||||||
background: #eff6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-tag-disabled {
|
|
||||||
color: #9ca3af;
|
|
||||||
background: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-rule {
|
|
||||||
margin-left: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-table {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-table th {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6b7280;
|
|
||||||
text-align: left;
|
|
||||||
background: #f8f9fb;
|
|
||||||
border-bottom: 1px solid #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-table td {
|
|
||||||
padding: 8px 12px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
border-bottom: 1px solid #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-table tr:hover td {
|
|
||||||
background: rgb(22 119 255 / 3%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-stock-ok {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #22c55e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-stock-low {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-assoc {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-card-footer {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-empty {
|
|
||||||
padding: 28px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #9ca3af;
|
|
||||||
text-align: center;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: var(--g-shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-editor-drawer {
|
|
||||||
.g-form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-form-label {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-form-label.required::before {
|
|
||||||
margin-right: 4px;
|
|
||||||
color: #ef4444;
|
|
||||||
content: '*';
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-hint {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-input,
|
|
||||||
.g-textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
outline: none;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: var(--g-transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-input {
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-textarea {
|
|
||||||
min-height: 66px;
|
|
||||||
padding-top: 8px;
|
|
||||||
line-height: 1.5;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-input:focus,
|
|
||||||
.g-textarea:focus {
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: 0 0 0 3px rgb(22 119 255 / 12%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1f1f1f;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: var(--g-shadow-sm);
|
|
||||||
transition: all var(--g-transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: var(--g-shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn-primary {
|
|
||||||
color: #fff;
|
|
||||||
background: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn-primary:hover {
|
|
||||||
color: #fff;
|
|
||||||
opacity: 0.88;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-wrap {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input {
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 40px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input input {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-sl {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #d9d9d9;
|
|
||||||
border-radius: 11px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-sl::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
content: '';
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 1px 3px rgb(0 0 0 / 15%);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input input:checked + .g-toggle-sl {
|
|
||||||
background: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input input:checked + .g-toggle-sl::before {
|
|
||||||
transform: translateX(18px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-sel-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-sel-row span {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-input-num {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-list-header {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-list-header .h-name {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-list-header .h-price,
|
|
||||||
.pad-opt-list-header .h-stock {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-list-header .h-act {
|
|
||||||
width: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-name {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-price,
|
|
||||||
.pad-opt-stock {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-del {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #9ca3af;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-opt-del:hover {
|
|
||||||
color: #ef4444;
|
|
||||||
background: #fef2f2;
|
|
||||||
border-color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-btn-dashed {
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 8px;
|
|
||||||
color: #9ca3af;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-btn-dashed:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-picker-search {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-btn {
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1f1f1f;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-btn-sm {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-picker-list {
|
|
||||||
max-height: 320px;
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-picker-item {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr 140px auto;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 1px solid #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-picker-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-picker-item:hover {
|
|
||||||
background: #fafcff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-picker-item .name {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-picker-item .spu {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-picker-item .price {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-picker-empty {
|
|
||||||
padding: 28px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #9ca3af;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
81
apps/web-antd/src/views/product/addons/styles/layout.less
Normal file
81
apps/web-antd/src/views/product/addons/styles/layout.less
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
.page-product-addons {
|
||||||
|
.pad-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: var(--g-shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-store-select {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-store-select .ant-select-selector {
|
||||||
|
height: 34px !important;
|
||||||
|
border-color: #e5e7eb !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-store-select .ant-select-selection-item {
|
||||||
|
line-height: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-search {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-search .ant-input {
|
||||||
|
height: 34px;
|
||||||
|
padding-left: 32px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")
|
||||||
|
10px center no-repeat;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: var(--g-shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-stats span {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-stats strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-empty {
|
||||||
|
padding: 28px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: var(--g-shadow-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
70
apps/web-antd/src/views/product/addons/styles/modal.less
Normal file
70
apps/web-antd/src/views/product/addons/styles/modal.less
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
.page-product-addons {
|
||||||
|
.pad-picker-search {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-btn {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1f1f1f;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-btn-sm {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-picker-list {
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-picker-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr 140px auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-picker-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-picker-item:hover {
|
||||||
|
background: #fafcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-picker-item .name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-picker-item .spu {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-picker-item .price {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-picker-empty {
|
||||||
|
padding: 28px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.page-product-addons {
|
||||||
|
@media (width <= 1200px) {
|
||||||
|
.pad-toolbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { SpecEditorValueForm } from '../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:规格做法页面常量与基础构造。
|
||||||
|
*/
|
||||||
|
export const DEFAULT_OPTION: SpecEditorValueForm = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
extraPrice: 0,
|
||||||
|
sort: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createDefaultOption(sort = 1): SpecEditorValueForm {
|
||||||
|
return {
|
||||||
|
...DEFAULT_OPTION,
|
||||||
|
sort,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { ProductSpecDto } from '#/api/product';
|
||||||
|
import type { StoreListItemDto } from '#/api/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:规格做法页面数据动作。
|
||||||
|
* 1. 加载门店列表与模板列表。
|
||||||
|
* 2. 维护门店切换时的数据一致性。
|
||||||
|
*/
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getProductSpecListApi } from '#/api/product';
|
||||||
|
import { getStoreListApi } from '#/api/store';
|
||||||
|
|
||||||
|
interface CreateDataActionsOptions {
|
||||||
|
isLoading: Ref<boolean>;
|
||||||
|
isStoreLoading: Ref<boolean>;
|
||||||
|
keyword: Ref<string>;
|
||||||
|
rows: Ref<ProductSpecDto[]>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
stores: Ref<StoreListItemDto[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDataActions(options: CreateDataActionsOptions) {
|
||||||
|
async function loadStores() {
|
||||||
|
options.isStoreLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await getStoreListApi({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
});
|
||||||
|
options.stores.value = result.items ?? [];
|
||||||
|
if (options.stores.value.length === 0) {
|
||||||
|
options.selectedStoreId.value = '';
|
||||||
|
options.rows.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSelected = options.stores.value.some(
|
||||||
|
(item) => item.id === options.selectedStoreId.value,
|
||||||
|
);
|
||||||
|
if (!hasSelected) {
|
||||||
|
options.selectedStoreId.value = options.stores.value[0]?.id ?? '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
message.error('加载门店失败');
|
||||||
|
} finally {
|
||||||
|
options.isStoreLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSpecs() {
|
||||||
|
if (!options.selectedStoreId.value) {
|
||||||
|
options.rows.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const list = await getProductSpecListApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
keyword: options.keyword.value.trim() || undefined,
|
||||||
|
});
|
||||||
|
options.rows.value = list;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
options.rows.value = [];
|
||||||
|
message.error('加载模板失败');
|
||||||
|
} finally {
|
||||||
|
options.isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadSpecs,
|
||||||
|
loadStores,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { ProductSpecDto, SaveProductSpecDto } from '#/api/product';
|
||||||
|
import type {
|
||||||
|
ProductSpecCardViewModel,
|
||||||
|
SpecEditorForm,
|
||||||
|
} from '#/views/product/specs/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:规格做法编辑抽屉动作。
|
||||||
|
* 1. 管理模板新增/编辑抽屉与表单状态。
|
||||||
|
* 2. 处理表单校验与保存提交。
|
||||||
|
*/
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { saveProductSpecApi } from '#/api/product';
|
||||||
|
|
||||||
|
import { createDefaultOption } from './constants';
|
||||||
|
|
||||||
|
interface CreateDrawerActionsOptions {
|
||||||
|
drawerMode: Ref<'create' | 'edit'>;
|
||||||
|
form: SpecEditorForm;
|
||||||
|
isDrawerOpen: Ref<boolean>;
|
||||||
|
isDrawerSubmitting: Ref<boolean>;
|
||||||
|
loadSpecs: () => Promise<void>;
|
||||||
|
rows: Ref<ProductSpecCardViewModel[]>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDrawerActions(options: CreateDrawerActionsOptions) {
|
||||||
|
function setDrawerOpen(value: boolean) {
|
||||||
|
options.isDrawerOpen.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormName(value: string) {
|
||||||
|
options.form.name = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormType(value: SpecEditorForm['type']) {
|
||||||
|
options.form.type = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormSelectionType(value: SpecEditorForm['selectionType']) {
|
||||||
|
options.form.selectionType = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormIsRequired(value: boolean) {
|
||||||
|
options.form.isRequired = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOptionName(index: number, value: string) {
|
||||||
|
const current = options.form.values[index];
|
||||||
|
if (!current) return;
|
||||||
|
current.name = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOptionExtraPrice(index: number, value: null | number) {
|
||||||
|
const current = options.form.values[index];
|
||||||
|
if (!current) return;
|
||||||
|
current.extraPrice = Number(value ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOption() {
|
||||||
|
options.form.values.push(
|
||||||
|
createDefaultOption(options.form.values.length + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeOption(index: number) {
|
||||||
|
if (options.form.values.length <= 1) {
|
||||||
|
message.warning('至少保留一个选项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
options.form.values.splice(index, 1);
|
||||||
|
options.form.values.forEach((item, idx) => {
|
||||||
|
item.sort = idx + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
options.form.id = '';
|
||||||
|
options.form.name = '';
|
||||||
|
options.form.type = 'spec';
|
||||||
|
options.form.selectionType = 'single';
|
||||||
|
options.form.isRequired = true;
|
||||||
|
options.form.sort = options.rows.value.length + 1;
|
||||||
|
options.form.status = 'enabled';
|
||||||
|
options.form.productIds = [];
|
||||||
|
options.form.values = [createDefaultOption()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDrawer() {
|
||||||
|
options.drawerMode.value = 'create';
|
||||||
|
resetForm();
|
||||||
|
options.isDrawerOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDrawer(item: ProductSpecDto) {
|
||||||
|
options.drawerMode.value = 'edit';
|
||||||
|
options.form.id = item.id;
|
||||||
|
options.form.name = item.name;
|
||||||
|
options.form.type = item.type;
|
||||||
|
options.form.selectionType = item.selectionType;
|
||||||
|
options.form.isRequired = item.isRequired;
|
||||||
|
options.form.sort = item.sort;
|
||||||
|
options.form.status = item.status;
|
||||||
|
options.form.productIds = [...item.productIds];
|
||||||
|
options.form.values = item.values.map((value, index) => ({
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
extraPrice: value.extraPrice,
|
||||||
|
sort: value.sort || index + 1,
|
||||||
|
}));
|
||||||
|
if (options.form.values.length === 0) {
|
||||||
|
options.form.values = [createDefaultOption()];
|
||||||
|
}
|
||||||
|
options.isDrawerOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSubmitValues() {
|
||||||
|
return options.form.values
|
||||||
|
.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
name: item.name.trim(),
|
||||||
|
sort: index + 1,
|
||||||
|
}))
|
||||||
|
.filter((item) => item.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSavePayload(
|
||||||
|
values: SpecEditorForm['values'],
|
||||||
|
): SaveProductSpecDto {
|
||||||
|
return {
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
id: options.form.id || undefined,
|
||||||
|
name: options.form.name.trim(),
|
||||||
|
type: options.form.type,
|
||||||
|
selectionType: options.form.selectionType,
|
||||||
|
isRequired: options.form.isRequired,
|
||||||
|
sort: options.form.sort,
|
||||||
|
status: options.form.status,
|
||||||
|
productIds: [...options.form.productIds],
|
||||||
|
values: values.map((item) => ({
|
||||||
|
id: item.id || undefined,
|
||||||
|
name: item.name,
|
||||||
|
extraPrice: item.extraPrice,
|
||||||
|
sort: item.sort,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDrawer() {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
if (!options.form.name.trim()) {
|
||||||
|
message.warning('请输入模板名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedValues = normalizeSubmitValues();
|
||||||
|
if (normalizedValues.length === 0) {
|
||||||
|
message.warning('请至少添加一个选项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueNames = new Set(
|
||||||
|
normalizedValues.map((item) => item.name.toLowerCase()),
|
||||||
|
);
|
||||||
|
if (uniqueNames.size !== normalizedValues.length) {
|
||||||
|
message.warning('选项名称不能重复');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.isDrawerSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
await saveProductSpecApi(buildSavePayload(normalizedValues));
|
||||||
|
message.success(
|
||||||
|
options.drawerMode.value === 'create' ? '模板已添加' : '模板已保存',
|
||||||
|
);
|
||||||
|
options.isDrawerOpen.value = false;
|
||||||
|
await options.loadSpecs();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isDrawerSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addOption,
|
||||||
|
openCreateDrawer,
|
||||||
|
openEditDrawer,
|
||||||
|
removeOption,
|
||||||
|
setDrawerOpen,
|
||||||
|
setFormIsRequired,
|
||||||
|
setFormName,
|
||||||
|
setFormSelectionType,
|
||||||
|
setFormType,
|
||||||
|
setOptionExtraPrice,
|
||||||
|
setOptionName,
|
||||||
|
submitDrawer,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { ProductSpecDto } from '#/api/product';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:规格做法模板卡片动作。
|
||||||
|
* 1. 处理模板删除。
|
||||||
|
* 2. 处理模板复制。
|
||||||
|
*/
|
||||||
|
import { message, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { copyProductSpecApi, deleteProductSpecApi } from '#/api/product';
|
||||||
|
|
||||||
|
interface CreateTemplateActionsOptions {
|
||||||
|
loadSpecs: () => Promise<void>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTemplateActions(options: CreateTemplateActionsOptions) {
|
||||||
|
function removeTemplate(item: ProductSpecDto) {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认删除模板「${item.name}」吗?`,
|
||||||
|
content: '删除后将移除该模板及其选项。',
|
||||||
|
okText: '确认删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
async onOk() {
|
||||||
|
await deleteProductSpecApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
specId: item.id,
|
||||||
|
});
|
||||||
|
message.success('模板已删除');
|
||||||
|
await options.loadSpecs();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyTemplate(item: ProductSpecDto) {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
try {
|
||||||
|
await copyProductSpecApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
specId: item.id,
|
||||||
|
});
|
||||||
|
message.success('模板复制成功');
|
||||||
|
await options.loadSpecs();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
copyTemplate,
|
||||||
|
removeTemplate,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
ProductSpecCardViewModel,
|
ProductSpecCardViewModel,
|
||||||
SpecEditorForm,
|
SpecEditorForm,
|
||||||
SpecEditorValueForm,
|
|
||||||
SpecsTypeFilter,
|
SpecsTypeFilter,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import type { ProductSpecDto } from '#/api/product';
|
|
||||||
import type { StoreListItemDto } from '#/api/store';
|
import type { StoreListItemDto } from '#/api/store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,22 +20,10 @@ import {
|
|||||||
watch,
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import { createDefaultOption } from './specs-page/constants';
|
||||||
|
import { createDataActions } from './specs-page/data-actions';
|
||||||
import {
|
import { createDrawerActions } from './specs-page/drawer-actions';
|
||||||
copyProductSpecApi,
|
import { createTemplateActions } from './specs-page/template-actions';
|
||||||
deleteProductSpecApi,
|
|
||||||
getProductSpecListApi,
|
|
||||||
saveProductSpecApi,
|
|
||||||
} from '#/api/product';
|
|
||||||
import { getStoreListApi } from '#/api/store';
|
|
||||||
|
|
||||||
const DEFAULT_OPTION: SpecEditorValueForm = {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
extraPrice: 0,
|
|
||||||
sort: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useProductSpecsPage() {
|
export function useProductSpecsPage() {
|
||||||
const stores = ref<StoreListItemDto[]>([]);
|
const stores = ref<StoreListItemDto[]>([]);
|
||||||
@@ -62,7 +48,7 @@ export function useProductSpecsPage() {
|
|||||||
sort: 1,
|
sort: 1,
|
||||||
status: 'enabled',
|
status: 'enabled',
|
||||||
productIds: [],
|
productIds: [],
|
||||||
values: [{ ...DEFAULT_OPTION }],
|
values: [createDefaultOption()],
|
||||||
});
|
});
|
||||||
|
|
||||||
const storeOptions = computed(() =>
|
const storeOptions = computed(() =>
|
||||||
@@ -99,55 +85,14 @@ export function useProductSpecsPage() {
|
|||||||
drawerMode.value === 'create' ? '确认添加' : '保存修改',
|
drawerMode.value === 'create' ? '确认添加' : '保存修改',
|
||||||
);
|
);
|
||||||
|
|
||||||
async function loadStores() {
|
const { loadSpecs, loadStores } = createDataActions({
|
||||||
isStoreLoading.value = true;
|
stores,
|
||||||
try {
|
selectedStoreId,
|
||||||
const result = await getStoreListApi({
|
isStoreLoading,
|
||||||
page: 1,
|
rows,
|
||||||
pageSize: 200,
|
isLoading,
|
||||||
|
keyword,
|
||||||
});
|
});
|
||||||
stores.value = result.items ?? [];
|
|
||||||
if (stores.value.length === 0) {
|
|
||||||
selectedStoreId.value = '';
|
|
||||||
rows.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasSelected = stores.value.some(
|
|
||||||
(item) => item.id === selectedStoreId.value,
|
|
||||||
);
|
|
||||||
if (!hasSelected) {
|
|
||||||
selectedStoreId.value = stores.value[0]?.id ?? '';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
message.error('加载门店失败');
|
|
||||||
} finally {
|
|
||||||
isStoreLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSpecs() {
|
|
||||||
if (!selectedStoreId.value) {
|
|
||||||
rows.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
const list = await getProductSpecListApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
keyword: keyword.value.trim() || undefined,
|
|
||||||
});
|
|
||||||
rows.value = list;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
rows.value = [];
|
|
||||||
message.error('加载模板失败');
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSelectedStoreId(value: string) {
|
function setSelectedStoreId(value: string) {
|
||||||
selectedStoreId.value = value;
|
selectedStoreId.value = value;
|
||||||
@@ -161,187 +106,33 @@ export function useProductSpecsPage() {
|
|||||||
typeFilter.value = value;
|
typeFilter.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDrawerOpen(value: boolean) {
|
const {
|
||||||
isDrawerOpen.value = value;
|
addOption,
|
||||||
}
|
openCreateDrawer,
|
||||||
|
openEditDrawer,
|
||||||
function setFormName(value: string) {
|
removeOption,
|
||||||
form.name = value;
|
setDrawerOpen,
|
||||||
}
|
setFormIsRequired,
|
||||||
|
setFormName,
|
||||||
function setFormType(value: SpecEditorForm['type']) {
|
setFormSelectionType,
|
||||||
form.type = value;
|
setFormType,
|
||||||
}
|
setOptionExtraPrice,
|
||||||
|
setOptionName,
|
||||||
function setFormSelectionType(value: SpecEditorForm['selectionType']) {
|
submitDrawer,
|
||||||
form.selectionType = value;
|
} = createDrawerActions({
|
||||||
}
|
drawerMode,
|
||||||
|
form,
|
||||||
function setFormIsRequired(value: boolean) {
|
isDrawerOpen,
|
||||||
form.isRequired = value;
|
isDrawerSubmitting,
|
||||||
}
|
loadSpecs,
|
||||||
|
rows,
|
||||||
function setOptionName(index: number, value: string) {
|
selectedStoreId,
|
||||||
const current = form.values[index];
|
|
||||||
if (!current) return;
|
|
||||||
current.name = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOptionExtraPrice(index: number, value: null | number) {
|
|
||||||
const current = form.values[index];
|
|
||||||
if (!current) return;
|
|
||||||
current.extraPrice = Number(value ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addOption() {
|
|
||||||
form.values.push({
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
extraPrice: 0,
|
|
||||||
sort: form.values.length + 1,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function removeOption(index: number) {
|
const { copyTemplate, removeTemplate } = createTemplateActions({
|
||||||
if (form.values.length <= 1) {
|
selectedStoreId,
|
||||||
message.warning('至少保留一个选项');
|
loadSpecs,
|
||||||
return;
|
|
||||||
}
|
|
||||||
form.values.splice(index, 1);
|
|
||||||
form.values.forEach((item, idx) => {
|
|
||||||
item.sort = idx + 1;
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
form.id = '';
|
|
||||||
form.name = '';
|
|
||||||
form.type = 'spec';
|
|
||||||
form.selectionType = 'single';
|
|
||||||
form.isRequired = true;
|
|
||||||
form.sort = rows.value.length + 1;
|
|
||||||
form.status = 'enabled';
|
|
||||||
form.productIds = [];
|
|
||||||
form.values = [{ ...DEFAULT_OPTION }];
|
|
||||||
}
|
|
||||||
|
|
||||||
function openCreateDrawer() {
|
|
||||||
drawerMode.value = 'create';
|
|
||||||
resetForm();
|
|
||||||
isDrawerOpen.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEditDrawer(item: ProductSpecDto) {
|
|
||||||
drawerMode.value = 'edit';
|
|
||||||
form.id = item.id;
|
|
||||||
form.name = item.name;
|
|
||||||
form.type = item.type;
|
|
||||||
form.selectionType = item.selectionType;
|
|
||||||
form.isRequired = item.isRequired;
|
|
||||||
form.sort = item.sort;
|
|
||||||
form.status = item.status;
|
|
||||||
form.productIds = [...item.productIds];
|
|
||||||
form.values = item.values.map((value, index) => ({
|
|
||||||
id: value.id,
|
|
||||||
name: value.name,
|
|
||||||
extraPrice: value.extraPrice,
|
|
||||||
sort: value.sort || index + 1,
|
|
||||||
}));
|
|
||||||
if (form.values.length === 0) {
|
|
||||||
form.values = [{ ...DEFAULT_OPTION }];
|
|
||||||
}
|
|
||||||
isDrawerOpen.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitDrawer() {
|
|
||||||
if (!selectedStoreId.value) return;
|
|
||||||
if (!form.name.trim()) {
|
|
||||||
message.warning('请输入模板名称');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedValues = form.values
|
|
||||||
.map((item, index) => ({
|
|
||||||
...item,
|
|
||||||
name: item.name.trim(),
|
|
||||||
sort: index + 1,
|
|
||||||
}))
|
|
||||||
.filter((item) => item.name);
|
|
||||||
if (normalizedValues.length === 0) {
|
|
||||||
message.warning('请至少添加一个选项');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueNames = new Set(
|
|
||||||
normalizedValues.map((item) => item.name.toLowerCase()),
|
|
||||||
);
|
|
||||||
if (uniqueNames.size !== normalizedValues.length) {
|
|
||||||
message.warning('选项名称不能重复');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isDrawerSubmitting.value = true;
|
|
||||||
try {
|
|
||||||
await saveProductSpecApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
id: form.id || undefined,
|
|
||||||
name: form.name.trim(),
|
|
||||||
type: form.type,
|
|
||||||
selectionType: form.selectionType,
|
|
||||||
isRequired: form.isRequired,
|
|
||||||
sort: form.sort,
|
|
||||||
status: form.status,
|
|
||||||
productIds: [...form.productIds],
|
|
||||||
values: normalizedValues.map((item) => ({
|
|
||||||
id: item.id || undefined,
|
|
||||||
name: item.name,
|
|
||||||
extraPrice: item.extraPrice,
|
|
||||||
sort: item.sort,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
message.success(
|
|
||||||
drawerMode.value === 'create' ? '模板已添加' : '模板已保存',
|
|
||||||
);
|
|
||||||
isDrawerOpen.value = false;
|
|
||||||
await loadSpecs();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
isDrawerSubmitting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTemplate(item: ProductSpecDto) {
|
|
||||||
if (!selectedStoreId.value) return;
|
|
||||||
Modal.confirm({
|
|
||||||
title: `确认删除模板「${item.name}」吗?`,
|
|
||||||
content: '删除后将移除该模板及其选项。',
|
|
||||||
okText: '确认删除',
|
|
||||||
cancelText: '取消',
|
|
||||||
async onOk() {
|
|
||||||
await deleteProductSpecApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
specId: item.id,
|
|
||||||
});
|
|
||||||
message.success('模板已删除');
|
|
||||||
await loadSpecs();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyTemplate(item: ProductSpecDto) {
|
|
||||||
if (!selectedStoreId.value) return;
|
|
||||||
try {
|
|
||||||
await copyProductSpecApi({
|
|
||||||
storeId: selectedStoreId.value,
|
|
||||||
specId: item.id,
|
|
||||||
});
|
|
||||||
message.success('模板复制成功');
|
|
||||||
await loadSpecs();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let keywordSearchTimer: null | ReturnType<typeof setTimeout> = null;
|
let keywordSearchTimer: null | ReturnType<typeof setTimeout> = null;
|
||||||
|
|
||||||
|
|||||||
93
apps/web-antd/src/views/product/specs/styles/base.less
Normal file
93
apps/web-antd/src/views/product/specs/styles/base.less
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* 文件职责:规格做法页面基础样式。
|
||||||
|
* 1. 定义通用变量。
|
||||||
|
* 2. 定义抽屉基础容器样式。
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--g-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--g-shadow-sm: 0 1px 2px rgb(0 0 0 / 4%);
|
||||||
|
--g-shadow-md: 0 4px 12px rgb(0 0 0 / 7%), 0 1px 3px rgb(0 0 0 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
background: rgb(0 0 0 / 45%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-mask.open {
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: -6px 0 16px rgb(0 0 0 / 8%);
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-hd {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
height: 54px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-close {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-close:hover {
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-bd {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-drawer-ft {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
365
apps/web-antd/src/views/product/specs/styles/card.less
Normal file
365
apps/web-antd/src/views/product/specs/styles/card.less
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
.page-product-specs {
|
||||||
|
.psp-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgb(15 23 42 / 6%);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-card:hover {
|
||||||
|
box-shadow: 0 8px 24px rgb(15 23 42 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-card.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-card-hd {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-card-hd .name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-card-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill .price {
|
||||||
|
margin-left: 2px;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-card-assoc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-card-ft {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-tag-blue {
|
||||||
|
color: #1677ff;
|
||||||
|
background: rgb(22 119 255 / 10%);
|
||||||
|
border-color: rgb(22 119 255 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-tag-orange {
|
||||||
|
color: #fa8c16;
|
||||||
|
background: rgb(250 140 22 / 10%);
|
||||||
|
border-color: rgb(250 140 22 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-tag-gray {
|
||||||
|
color: #6b7280;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-action {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-action:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-action-danger:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-form-group {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-form-label {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-form-label.required::before {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: #ef4444;
|
||||||
|
content: '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1f1f1f;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: var(--g-shadow-sm);
|
||||||
|
transition: all var(--g-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: var(--g-shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn-primary:hover {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-btn-dashed {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #9ca3af;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-btn-dashed:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill-btn {
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill-btn:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill-btn.active {
|
||||||
|
color: #fff;
|
||||||
|
background: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-wrap {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input input {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-sl {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #d9d9d9;
|
||||||
|
border-radius: 11px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-sl::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
content: '';
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 1px 3px rgb(0 0 0 / 15%);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input input:checked + .g-toggle-sl {
|
||||||
|
background: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input input:checked + .g-toggle-sl::before {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-row input[type='text'] {
|
||||||
|
flex: 1;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: var(--g-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-row input[type='text']:focus {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 3px rgb(22 119 255 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-price-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
width: 120px;
|
||||||
|
height: 34px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-price-wrap > span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #9ca3af;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-price-wrap input[type='number'] {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-del {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-del:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
background: #fef2f2;
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
258
apps/web-antd/src/views/product/specs/styles/drawer.less
Normal file
258
apps/web-antd/src/views/product/specs/styles/drawer.less
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
.psp-editor-drawer {
|
||||||
|
.g-form-group {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-form-label {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-form-label.required::before {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: #ef4444;
|
||||||
|
content: '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: var(--g-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-input:focus {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 3px rgb(22 119 255 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1f1f1f;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: var(--g-shadow-sm);
|
||||||
|
transition: all var(--g-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: var(--g-shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn-primary:hover {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill-btn {
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill-btn:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-pill-btn.active {
|
||||||
|
color: #fff;
|
||||||
|
background: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-wrap {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input input {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-sl {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #d9d9d9;
|
||||||
|
border-radius: 11px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-sl::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
content: '';
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 1px 3px rgb(0 0 0 / 15%);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input input:checked + .g-toggle-sl {
|
||||||
|
background: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-toggle-input input:checked + .g-toggle-sl::before {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-row input[type='text'] {
|
||||||
|
flex: 1;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: var(--g-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-row input[type='text']:focus {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 3px rgb(22 119 255 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-price-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
width: 120px;
|
||||||
|
height: 34px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-price-wrap > span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #9ca3af;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-price-wrap input[type='number'] {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-del {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-opt-del:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
background: #fef2f2;
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-btn-dashed {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #9ca3af;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-btn-dashed:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,845 +1,5 @@
|
|||||||
/**
|
@import './base.less';
|
||||||
* 文件职责:规格做法页面样式。
|
@import './layout.less';
|
||||||
* 1. 对齐原型的工具栏、统计条、卡片网格与抽屉视觉。
|
@import './card.less';
|
||||||
* 2. 统一模板标签、操作按钮与选项编辑样式。
|
@import './drawer.less';
|
||||||
*/
|
@import './responsive.less';
|
||||||
:root {
|
|
||||||
--g-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--g-shadow-sm: 0 1px 2px rgb(0 0 0 / 4%);
|
|
||||||
--g-shadow-md: 0 4px 12px rgb(0 0 0 / 7%), 0 1px 3px rgb(0 0 0 / 4%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-mask {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
pointer-events: none;
|
|
||||||
background: rgb(0 0 0 / 45%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-mask.open {
|
|
||||||
pointer-events: auto;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 1001;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: -6px 0 16px rgb(0 0 0 / 8%);
|
|
||||||
transform: translateX(100%);
|
|
||||||
transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer.open {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-hd {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
height: 54px;
|
|
||||||
padding: 0 20px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-title {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-close {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #999;
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-close:hover {
|
|
||||||
color: #333;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-bd {
|
|
||||||
flex: 1;
|
|
||||||
padding: 20px 24px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-drawer-ft {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-editor-drawer {
|
|
||||||
.g-form-group {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-form-label {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-form-label.required::before {
|
|
||||||
margin-right: 4px;
|
|
||||||
color: #ef4444;
|
|
||||||
content: '*';
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-input {
|
|
||||||
width: 100%;
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
outline: none;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: var(--g-transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-input:focus {
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: 0 0 0 3px rgb(22 119 255 / 12%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-hint {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1f1f1f;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: var(--g-shadow-sm);
|
|
||||||
transition: all var(--g-transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: var(--g-shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn-primary {
|
|
||||||
color: #fff;
|
|
||||||
background: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn-primary:hover {
|
|
||||||
color: #fff;
|
|
||||||
opacity: 0.88;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill-btn {
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill-btn:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill-btn.active {
|
|
||||||
color: #fff;
|
|
||||||
background: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-wrap {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input {
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 40px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input input {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-sl {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #d9d9d9;
|
|
||||||
border-radius: 11px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-sl::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
content: '';
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 1px 3px rgb(0 0 0 / 15%);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input input:checked + .g-toggle-sl {
|
|
||||||
background: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input input:checked + .g-toggle-sl::before {
|
|
||||||
transform: translateX(18px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-row input[type='text'] {
|
|
||||||
flex: 1;
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
outline: none;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: var(--g-transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-row input[type='text']:focus {
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: 0 0 0 3px rgb(22 119 255 / 12%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-price-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
width: 120px;
|
|
||||||
height: 34px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-price-wrap > span {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 8px;
|
|
||||||
color: #9ca3af;
|
|
||||||
background: #f8f9fb;
|
|
||||||
border-right: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-price-wrap input[type='number'] {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-del {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #9ca3af;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-del:hover {
|
|
||||||
color: #ef4444;
|
|
||||||
background: #fef2f2;
|
|
||||||
border-color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-btn-dashed {
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 8px;
|
|
||||||
color: #9ca3af;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-btn-dashed:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-product-specs {
|
|
||||||
@media (width <= 1200px) {
|
|
||||||
.psp-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-toolbar {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-spacer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 2px 10px rgb(15 23 42 / 6%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-store-select {
|
|
||||||
width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-store-select .ant-select-selector {
|
|
||||||
height: 34px !important;
|
|
||||||
font-size: 13px;
|
|
||||||
border-color: #e5e7eb !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-store-select .ant-select-selection-item {
|
|
||||||
line-height: 32px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-search {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-search .ant-input {
|
|
||||||
height: 34px;
|
|
||||||
padding-left: 32px;
|
|
||||||
font-size: 13px;
|
|
||||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")
|
|
||||||
10px center no-repeat;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-filter-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-filter-tab {
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #4b5563;
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-filter-tab:hover {
|
|
||||||
background: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-filter-tab.active {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1677ff;
|
|
||||||
background: rgb(22 119 255 / 10%);
|
|
||||||
border-color: rgb(22 119 255 / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-filter-tab .count {
|
|
||||||
margin-left: 3px;
|
|
||||||
font-size: 11px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-stat {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #4b5563;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgb(15 23 42 / 6%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-stat-num {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-stat-dot {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 2px 10px rgb(15 23 42 / 6%);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-card:hover {
|
|
||||||
box-shadow: 0 8px 24px rgb(15 23 42 / 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-card.disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-card-hd {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-card-hd .name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-card-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pills {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
background: #f8f9fb;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill .price {
|
|
||||||
margin-left: 2px;
|
|
||||||
color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-card-assoc {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-card-ft {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-top: 1px solid #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-tag {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 20px;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-tag-blue {
|
|
||||||
color: #1677ff;
|
|
||||||
background: rgb(22 119 255 / 10%);
|
|
||||||
border-color: rgb(22 119 255 / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-tag-orange {
|
|
||||||
color: #fa8c16;
|
|
||||||
background: rgb(250 140 22 / 10%);
|
|
||||||
border-color: rgb(250 140 22 / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-tag-gray {
|
|
||||||
color: #6b7280;
|
|
||||||
background: #f3f4f6;
|
|
||||||
border-color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-action {
|
|
||||||
padding: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #4b5563;
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-action:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-action-danger:hover {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-form-group {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-form-label {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-form-label.required::before {
|
|
||||||
margin-right: 4px;
|
|
||||||
color: #ef4444;
|
|
||||||
content: '*';
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-hint {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1f1f1f;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: var(--g-shadow-sm);
|
|
||||||
transition: all var(--g-transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: var(--g-shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn-primary {
|
|
||||||
color: #fff;
|
|
||||||
background: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn-primary:hover {
|
|
||||||
color: #fff;
|
|
||||||
opacity: 0.88;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-btn:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-btn-dashed {
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 8px;
|
|
||||||
color: #9ca3af;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-btn-dashed:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill-btn {
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill-btn:hover {
|
|
||||||
color: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-pill-btn.active {
|
|
||||||
color: #fff;
|
|
||||||
background: #1677ff;
|
|
||||||
border-color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-wrap {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input {
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 40px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input input {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-sl {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #d9d9d9;
|
|
||||||
border-radius: 11px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-sl::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
content: '';
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 1px 3px rgb(0 0 0 / 15%);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input input:checked + .g-toggle-sl {
|
|
||||||
background: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-input input:checked + .g-toggle-sl::before {
|
|
||||||
transform: translateX(18px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.g-toggle-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-row input[type='text'] {
|
|
||||||
flex: 1;
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
outline: none;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: var(--g-transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-row input[type='text']:focus {
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: 0 0 0 3px rgb(22 119 255 / 12%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-price-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
width: 120px;
|
|
||||||
height: 34px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-price-wrap > span {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 8px;
|
|
||||||
color: #9ca3af;
|
|
||||||
background: #f8f9fb;
|
|
||||||
border-right: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-price-wrap input[type='number'] {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-del {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #9ca3af;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.psp-opt-del:hover {
|
|
||||||
color: #ef4444;
|
|
||||||
background: #fef2f2;
|
|
||||||
border-color: #ef4444;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
114
apps/web-antd/src/views/product/specs/styles/layout.less
Normal file
114
apps/web-antd/src/views/product/specs/styles/layout.less
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
.page-product-specs {
|
||||||
|
.psp-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgb(15 23 42 / 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-store-select {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-store-select .ant-select-selector {
|
||||||
|
height: 34px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
border-color: #e5e7eb !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-store-select .ant-select-selection-item {
|
||||||
|
line-height: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-search {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-search .ant-input {
|
||||||
|
height: 34px;
|
||||||
|
padding-left: 32px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")
|
||||||
|
10px center no-repeat;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-filter-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-filter-tab {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #4b5563;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-filter-tab:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-filter-tab.active {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1677ff;
|
||||||
|
background: rgb(22 119 255 / 10%);
|
||||||
|
border-color: rgb(22 119 255 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-filter-tab .count {
|
||||||
|
margin-left: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-stat {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgb(15 23 42 / 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-stat-num {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-stat-dot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/web-antd/src/views/product/specs/styles/responsive.less
Normal file
15
apps/web-antd/src/views/product/specs/styles/responsive.less
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.page-product-specs {
|
||||||
|
@media (width <= 1200px) {
|
||||||
|
.psp-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-toolbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.psp-spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user