Compare commits
5 Commits
37b3ef693d
...
371a56c9b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 371a56c9b1 | |||
| 5e8b4c9e5a | |||
| fb6b9945c8 | |||
| 4c7b6e98da | |||
| 9e6d599c6b |
57
.gitea/workflows/deploy.yml
Normal file
57
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Build and Deploy TenantUI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: host
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cd /opt/deploy/tenantui || mkdir -p /opt/deploy/tenantui
|
||||||
|
cd /opt/deploy/tenantui
|
||||||
|
|
||||||
|
# 如果已有仓库就 pull,否则 clone
|
||||||
|
if [ -d ".git" ]; then
|
||||||
|
git fetch origin main
|
||||||
|
git reset --hard origin/main
|
||||||
|
git clean -fd
|
||||||
|
else
|
||||||
|
git clone --branch main ssh://git@git.laosankeji.com:2222/msumshk/TakeoutSaaS.TenantUI.git .
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build on host
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cd /opt/deploy/tenantui
|
||||||
|
corepack enable
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
VITE_GLOB_API_URL=https://api-tenant-dev.laosankeji.com/api/tenant/v1 pnpm run build:antd
|
||||||
|
|
||||||
|
rm -rf publish/web-antd-dist
|
||||||
|
mkdir -p publish
|
||||||
|
cp -r apps/web-antd/dist publish/web-antd-dist
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cd /opt/deploy/tenantui
|
||||||
|
docker build -t takeoutsaas-tenantui:latest -f scripts/deploy/Dockerfile.runtime .
|
||||||
|
|
||||||
|
- name: Deploy container
|
||||||
|
run: |
|
||||||
|
docker stop tenantui || true
|
||||||
|
docker rm tenantui || true
|
||||||
|
docker run -d \
|
||||||
|
--name tenantui \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 8010:8080 \
|
||||||
|
takeoutsaas-tenantui:latest
|
||||||
|
|
||||||
|
- name: Clean up old images
|
||||||
|
run: |
|
||||||
|
docker image prune -f
|
||||||
@@ -35,6 +35,14 @@ import {
|
|||||||
normalizeSkuRows,
|
normalizeSkuRows,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
|
||||||
|
const MAX_PRODUCT_IMAGE_COUNT = 5;
|
||||||
|
const ALLOWED_PRODUCT_IMAGE_MIME_SET = new Set([
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/webp',
|
||||||
|
]);
|
||||||
|
const ALLOWED_PRODUCT_IMAGE_EXT = ['.jpg', '.jpeg', '.png', '.webp'];
|
||||||
|
|
||||||
interface CreateProductDetailDataActionsOptions {
|
interface CreateProductDetailDataActionsOptions {
|
||||||
addonGroupOptions: Ref<ProductAddonGroupDto[]>;
|
addonGroupOptions: Ref<ProductAddonGroupDto[]>;
|
||||||
buildSkuRows: () => void;
|
buildSkuRows: () => void;
|
||||||
@@ -94,7 +102,7 @@ export function createProductDetailDataActions(
|
|||||||
form.imageUrls = dedupeTextList([
|
form.imageUrls = dedupeTextList([
|
||||||
...(data.imageUrls || []),
|
...(data.imageUrls || []),
|
||||||
data.imageUrl,
|
data.imageUrl,
|
||||||
]).slice(0, 5);
|
]).slice(0, MAX_PRODUCT_IMAGE_COUNT);
|
||||||
|
|
||||||
form.price = Number(data.price || 0);
|
form.price = Number(data.price || 0);
|
||||||
form.originalPrice =
|
form.originalPrice =
|
||||||
@@ -158,22 +166,90 @@ export function createProductDetailDataActions(
|
|||||||
form.labelIds = [...selected];
|
form.labelIds = [...selected];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadImage(file: File) {
|
function isAllowedProductImageFile(file: File) {
|
||||||
|
const mimeType = String(file.type || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (mimeType && ALLOWED_PRODUCT_IMAGE_MIME_SET.has(mimeType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerName = String(file.name || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
return ALLOWED_PRODUCT_IMAGE_EXT.some((ext) => lowerName.endsWith(ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImages(files: File[]) {
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
if (form.imageUrls.length >= MAX_PRODUCT_IMAGE_COUNT) {
|
||||||
|
message.warning(`最多上传 ${MAX_PRODUCT_IMAGE_COUNT} 张图片`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidFiles = files.filter(
|
||||||
|
(file) => !isAllowedProductImageFile(file),
|
||||||
|
);
|
||||||
|
if (invalidFiles.length > 0) {
|
||||||
|
message.warning('仅支持 JPG/PNG/WEBP 格式');
|
||||||
|
}
|
||||||
|
|
||||||
|
const validFiles = files.filter((file) => isAllowedProductImageFile(file));
|
||||||
|
if (validFiles.length === 0) return;
|
||||||
|
|
||||||
|
const remainCount = MAX_PRODUCT_IMAGE_COUNT - form.imageUrls.length;
|
||||||
|
let uploadQueue = validFiles;
|
||||||
|
if (validFiles.length > remainCount) {
|
||||||
|
uploadQueue = validFiles.slice(0, remainCount);
|
||||||
|
message.warning(
|
||||||
|
`最多上传 ${MAX_PRODUCT_IMAGE_COUNT} 张图片,本次仅上传前 ${remainCount} 张`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failedCount = 0;
|
||||||
|
|
||||||
isUploadingImage.value = true;
|
isUploadingImage.value = true;
|
||||||
|
try {
|
||||||
|
for (const file of uploadQueue) {
|
||||||
try {
|
try {
|
||||||
const uploaded = await uploadTenantFileApi(file, 'dish_image');
|
const uploaded = await uploadTenantFileApi(file, 'dish_image');
|
||||||
const url = String(uploaded.url || '').trim();
|
const url = String(uploaded.url || '').trim();
|
||||||
if (!url) {
|
if (!url) {
|
||||||
message.error('图片上传失败');
|
failedCount += 1;
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
form.imageUrls = dedupeTextList([...form.imageUrls, url]).slice(0, 5);
|
form.imageUrls = dedupeTextList([...form.imageUrls, url]).slice(
|
||||||
message.success('图片上传成功');
|
0,
|
||||||
|
MAX_PRODUCT_IMAGE_COUNT,
|
||||||
|
);
|
||||||
|
successCount += 1;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
failedCount += 1;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isUploadingImage.value = false;
|
isUploadingImage.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
message.success(
|
||||||
|
successCount === 1 ? '图片上传成功' : `已上传 ${successCount} 张图片`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (failedCount > 0) {
|
||||||
|
message.error(
|
||||||
|
failedCount === 1
|
||||||
|
? '1 张图片上传失败'
|
||||||
|
: `${failedCount} 张图片上传失败`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(file: File) {
|
||||||
|
await uploadImages([file]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeImage(index: number) {
|
function removeImage(index: number) {
|
||||||
@@ -405,5 +481,6 @@ export function createProductDetailDataActions(
|
|||||||
toggleLabel,
|
toggleLabel,
|
||||||
toggleSaleStatus,
|
toggleSaleStatus,
|
||||||
uploadImage,
|
uploadImage,
|
||||||
|
uploadImages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,31 @@ export function createProductDetailSkuActions(
|
|||||||
) {
|
) {
|
||||||
const { form, skuBatch, specTemplateOptions } = options;
|
const { form, skuBatch, specTemplateOptions } = options;
|
||||||
|
|
||||||
|
function normalizeMoney(value: number) {
|
||||||
|
return Number(Number(value).toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSpecOptionExtraPrice(templateId: string, optionId: string) {
|
||||||
|
const template = specTemplateOptions.value.find(
|
||||||
|
(item) => item.id === templateId,
|
||||||
|
);
|
||||||
|
if (!template) return 0;
|
||||||
|
const option = template.values.find((item) => item.id === optionId);
|
||||||
|
if (!option) return 0;
|
||||||
|
const extraPrice = Number(option.extraPrice || 0);
|
||||||
|
return Number.isFinite(extraPrice) ? extraPrice : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSkuAttrsExtraPrice(
|
||||||
|
attrs: ProductDetailSkuRowState['attributes'],
|
||||||
|
) {
|
||||||
|
return attrs.reduce(
|
||||||
|
(sum, item) =>
|
||||||
|
sum + resolveSpecOptionExtraPrice(item.templateId, item.optionId),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getTemplateName(templateId: string) {
|
function getTemplateName(templateId: string) {
|
||||||
return (
|
return (
|
||||||
specTemplateOptions.value.find((item) => item.id === templateId)?.name ||
|
specTemplateOptions.value.find((item) => item.id === templateId)?.name ||
|
||||||
@@ -42,6 +67,19 @@ export function createProductDetailSkuActions(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatOptionExtraPrice(extraPrice: number) {
|
||||||
|
const normalized = normalizeMoney(extraPrice);
|
||||||
|
const prefix = normalized >= 0 ? '+' : '-';
|
||||||
|
return `${prefix}¥${Math.abs(normalized).toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptionDisplayText(templateId: string, optionId: string) {
|
||||||
|
if (!optionId) return '-';
|
||||||
|
const name = getOptionName(templateId, optionId);
|
||||||
|
const extraPrice = resolveSpecOptionExtraPrice(templateId, optionId);
|
||||||
|
return `${name} (${formatOptionExtraPrice(extraPrice)})`;
|
||||||
|
}
|
||||||
|
|
||||||
function getSkuAttrOptionId(
|
function getSkuAttrOptionId(
|
||||||
row: ProductDetailSkuRowState,
|
row: ProductDetailSkuRowState,
|
||||||
templateId: string,
|
templateId: string,
|
||||||
@@ -97,15 +135,23 @@ export function createProductDetailSkuActions(
|
|||||||
const key = buildSkuKey(attrs);
|
const key = buildSkuKey(attrs);
|
||||||
const cached = previousMap.get(key);
|
const cached = previousMap.get(key);
|
||||||
const skuIndex = index + 1;
|
const skuIndex = index + 1;
|
||||||
|
const totalExtraPrice = resolveSkuAttrsExtraPrice(attrs);
|
||||||
|
const defaultPrice = normalizeMoney(
|
||||||
|
Math.max(0, Number(form.price || 0) + totalExtraPrice),
|
||||||
|
);
|
||||||
|
const defaultOriginalPrice =
|
||||||
|
form.originalPrice !== null &&
|
||||||
|
form.originalPrice !== undefined &&
|
||||||
|
form.originalPrice > 0
|
||||||
|
? normalizeMoney(
|
||||||
|
Math.max(0, Number(form.originalPrice) + totalExtraPrice),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
id: cached?.id || '',
|
id: cached?.id || '',
|
||||||
skuCode: cached?.skuCode || buildLocalSkuCode(skuIndex),
|
skuCode: cached?.skuCode || buildLocalSkuCode(skuIndex),
|
||||||
price: cached?.price ?? Number(form.price || 0),
|
price: cached?.price ?? defaultPrice,
|
||||||
originalPrice:
|
originalPrice: cached?.originalPrice ?? defaultOriginalPrice,
|
||||||
cached?.originalPrice ??
|
|
||||||
(form.originalPrice && form.originalPrice > 0
|
|
||||||
? form.originalPrice
|
|
||||||
: null),
|
|
||||||
stock:
|
stock:
|
||||||
cached?.stock ?? Math.max(0, Math.floor(Number(form.stock || 0))),
|
cached?.stock ?? Math.max(0, Math.floor(Number(form.stock || 0))),
|
||||||
isEnabled: cached?.isEnabled ?? true,
|
isEnabled: cached?.isEnabled ?? true,
|
||||||
@@ -185,6 +231,7 @@ export function createProductDetailSkuActions(
|
|||||||
applySkuBatchPrice,
|
applySkuBatchPrice,
|
||||||
applySkuBatchStock,
|
applySkuBatchStock,
|
||||||
buildSkuRows,
|
buildSkuRows,
|
||||||
|
getOptionDisplayText,
|
||||||
getOptionName,
|
getOptionName,
|
||||||
getSkuAttrOptionId,
|
getSkuAttrOptionId,
|
||||||
getTemplateName,
|
getTemplateName,
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export function useProductDetailPage() {
|
|||||||
deleteCurrentProduct: dataActions.deleteCurrentProduct,
|
deleteCurrentProduct: dataActions.deleteCurrentProduct,
|
||||||
detail,
|
detail,
|
||||||
form,
|
form,
|
||||||
|
getOptionDisplayText: skuActions.getOptionDisplayText,
|
||||||
getOptionName: skuActions.getOptionName,
|
getOptionName: skuActions.getOptionName,
|
||||||
getSkuAttrOptionId: skuActions.getSkuAttrOptionId,
|
getSkuAttrOptionId: skuActions.getSkuAttrOptionId,
|
||||||
getTemplateName: skuActions.getTemplateName,
|
getTemplateName: skuActions.getTemplateName,
|
||||||
@@ -177,5 +178,6 @@ export function useProductDetailPage() {
|
|||||||
toggleSaleStatus: dataActions.toggleSaleStatus,
|
toggleSaleStatus: dataActions.toggleSaleStatus,
|
||||||
toggleSpecTemplate: skuActions.toggleSpecTemplate,
|
toggleSpecTemplate: skuActions.toggleSpecTemplate,
|
||||||
uploadImage: dataActions.uploadImage,
|
uploadImage: dataActions.uploadImage,
|
||||||
|
uploadImages: dataActions.uploadImages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Modal,
|
Modal,
|
||||||
Radio,
|
|
||||||
Select,
|
Select,
|
||||||
Spin,
|
Spin,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -48,7 +47,7 @@ const {
|
|||||||
deleteCurrentProduct,
|
deleteCurrentProduct,
|
||||||
detail,
|
detail,
|
||||||
form,
|
form,
|
||||||
getOptionName,
|
getOptionDisplayText,
|
||||||
getSkuAttrOptionId,
|
getSkuAttrOptionId,
|
||||||
goBack,
|
goBack,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -83,7 +82,7 @@ const {
|
|||||||
toggleLabel,
|
toggleLabel,
|
||||||
toggleSaleStatus,
|
toggleSaleStatus,
|
||||||
toggleSpecTemplate,
|
toggleSpecTemplate,
|
||||||
uploadImage,
|
uploadImages,
|
||||||
} = useProductDetailPage();
|
} = useProductDetailPage();
|
||||||
|
|
||||||
const activeSectionId = ref('detail-basic');
|
const activeSectionId = ref('detail-basic');
|
||||||
@@ -119,8 +118,40 @@ const timedOnShelfValue = computed<Dayjs | undefined>({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
|
let pendingUploadBatchSignature = '';
|
||||||
await uploadImage(file as File);
|
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = async (file, fileList) => {
|
||||||
|
const selectedFiles = (fileList?.length ? fileList : [file])
|
||||||
|
.map((item) => {
|
||||||
|
const uploadFile = item as { originFileObj?: File };
|
||||||
|
return uploadFile.originFileObj ?? (item as File);
|
||||||
|
})
|
||||||
|
.filter((item): item is File => item instanceof File);
|
||||||
|
if (selectedFiles.length === 0) return false;
|
||||||
|
|
||||||
|
const batchSignature = selectedFiles
|
||||||
|
.map((item) => `${item.name}:${item.size}:${item.lastModified}`)
|
||||||
|
.join('|');
|
||||||
|
if (batchSignature && batchSignature === pendingUploadBatchSignature) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUid = String((file as { uid?: string }).uid || '');
|
||||||
|
const firstUid = String(
|
||||||
|
(fileList?.[0] as undefined | { uid?: string })?.uid || '',
|
||||||
|
);
|
||||||
|
if (currentUid && firstUid && currentUid !== firstUid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingUploadBatchSignature = batchSignature;
|
||||||
|
try {
|
||||||
|
await uploadImages(selectedFiles);
|
||||||
|
} finally {
|
||||||
|
if (pendingUploadBatchSignature === batchSignature) {
|
||||||
|
pendingUploadBatchSignature = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,53 +320,56 @@ watch(
|
|||||||
|
|
||||||
<Card id="detail-images" :bordered="false" class="pd-section-card">
|
<Card id="detail-images" :bordered="false" class="pd-section-card">
|
||||||
<template #title>商品图片</template>
|
<template #title>商品图片</template>
|
||||||
<div class="pd-upload-row">
|
<Upload.Dragger
|
||||||
<Upload
|
class="pd-upload-dragger"
|
||||||
|
:class="{ 'is-disabled': maxUploadReached }"
|
||||||
|
multiple
|
||||||
:show-upload-list="false"
|
:show-upload-list="false"
|
||||||
|
accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
:disabled="isUploadingImage || maxUploadReached"
|
:disabled="isUploadingImage || maxUploadReached"
|
||||||
>
|
>
|
||||||
<div
|
<div class="pd-upload-dragger-inner">
|
||||||
class="pd-upload-trigger"
|
<span class="pd-upload-dragger-plus">+</span>
|
||||||
:class="{ disabled: maxUploadReached }"
|
<p class="pd-upload-dragger-title">
|
||||||
>
|
{{
|
||||||
<span>+</span>
|
maxUploadReached
|
||||||
</div>
|
? '已达到最多 5 张'
|
||||||
</Upload>
|
: '点击或拖拽上传图片'
|
||||||
<div class="pd-upload-hint">
|
}}
|
||||||
|
</p>
|
||||||
|
<p class="pd-upload-dragger-hint">
|
||||||
建议尺寸 750×750,最多 5 张,首张为主图
|
建议尺寸 750×750,最多 5 张,首张为主图
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Upload.Dragger>
|
||||||
|
|
||||||
<div class="pd-thumbs">
|
<div v-if="form.imageUrls.length > 0" class="pd-thumbs">
|
||||||
<div
|
<div
|
||||||
v-for="(url, index) in form.imageUrls"
|
v-for="(url, index) in form.imageUrls"
|
||||||
:key="url"
|
:key="url"
|
||||||
class="pd-thumb"
|
class="pd-thumb"
|
||||||
:class="{ 'pd-thumb-main': index === 0 }"
|
:class="{ 'pd-thumb-main': index === 0 }"
|
||||||
|
@click="index > 0 && setPrimaryImage(index)"
|
||||||
>
|
>
|
||||||
<img :src="url" alt="product-image" />
|
<img :src="url" alt="product-image" />
|
||||||
<button
|
<button
|
||||||
class="pd-thumb-del"
|
class="pd-thumb-del"
|
||||||
type="button"
|
type="button"
|
||||||
@click="removeImage(index)"
|
@click.stop="removeImage(index)"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
<span v-if="index === 0" class="pd-thumb-badge">主图</span>
|
<span v-if="index === 0" class="pd-thumb-badge">主图</span>
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
type="button"
|
|
||||||
class="pd-thumb-set-main"
|
|
||||||
@click="setPrimaryImage(index)"
|
|
||||||
>
|
|
||||||
设主图
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card id="detail-pricing" :bordered="false" class="pd-section-card">
|
<Card
|
||||||
|
id="detail-pricing"
|
||||||
|
:bordered="false"
|
||||||
|
class="pd-section-card pd-pricing-card"
|
||||||
|
>
|
||||||
<template #title>价格库存</template>
|
<template #title>价格库存</template>
|
||||||
<div v-if="form.skus.length > 1" class="pd-price-alert">
|
<div v-if="form.skus.length > 1" class="pd-price-alert">
|
||||||
已启用多规格,售价和库存以下方 SKU 管理中的组合为准
|
已启用多规格,售价和库存以下方 SKU 管理中的组合为准
|
||||||
@@ -343,8 +377,8 @@ watch(
|
|||||||
<div class="pd-row">
|
<div class="pd-row">
|
||||||
<label class="pd-label required">售价</label>
|
<label class="pd-label required">售价</label>
|
||||||
<div class="pd-ctrl">
|
<div class="pd-ctrl">
|
||||||
<div class="pd-inline">
|
<div class="pd-pricing-inline">
|
||||||
<span class="pd-unit">¥</span>
|
<span class="pd-pricing-unit">¥</span>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model:value="form.price"
|
v-model:value="form.price"
|
||||||
:min="0"
|
:min="0"
|
||||||
@@ -352,14 +386,15 @@ watch(
|
|||||||
:step="0.1"
|
:step="0.1"
|
||||||
class="pd-input-fluid"
|
class="pd-input-fluid"
|
||||||
/>
|
/>
|
||||||
|
<span class="pd-pricing-unit is-empty"> 份 </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pd-row">
|
<div class="pd-row">
|
||||||
<label class="pd-label">划线价</label>
|
<label class="pd-label">划线价</label>
|
||||||
<div class="pd-ctrl">
|
<div class="pd-ctrl">
|
||||||
<div class="pd-inline">
|
<div class="pd-pricing-inline">
|
||||||
<span class="pd-unit">¥</span>
|
<span class="pd-pricing-unit">¥</span>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model:value="form.originalPrice"
|
v-model:value="form.originalPrice"
|
||||||
:min="0"
|
:min="0"
|
||||||
@@ -367,13 +402,15 @@ watch(
|
|||||||
:step="0.1"
|
:step="0.1"
|
||||||
class="pd-input-fluid"
|
class="pd-input-fluid"
|
||||||
/>
|
/>
|
||||||
|
<span class="pd-pricing-unit is-empty"> 份 </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pd-row">
|
<div class="pd-row">
|
||||||
<label class="pd-label">库存数量</label>
|
<label class="pd-label">库存数量</label>
|
||||||
<div class="pd-ctrl">
|
<div class="pd-ctrl">
|
||||||
<div class="pd-inline">
|
<div class="pd-pricing-inline">
|
||||||
|
<span class="pd-pricing-unit is-empty"> ¥ </span>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model:value="form.stock"
|
v-model:value="form.stock"
|
||||||
:min="0"
|
:min="0"
|
||||||
@@ -381,14 +418,15 @@ watch(
|
|||||||
:step="1"
|
:step="1"
|
||||||
class="pd-input-fluid"
|
class="pd-input-fluid"
|
||||||
/>
|
/>
|
||||||
<span class="pd-unit">份</span>
|
<span class="pd-pricing-unit">份</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pd-row">
|
<div class="pd-row">
|
||||||
<label class="pd-label">库存预警</label>
|
<label class="pd-label">库存预警</label>
|
||||||
<div class="pd-ctrl">
|
<div class="pd-ctrl">
|
||||||
<div class="pd-inline">
|
<div class="pd-pricing-inline">
|
||||||
|
<span class="pd-pricing-unit is-empty"> ¥ </span>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model:value="form.warningStock"
|
v-model:value="form.warningStock"
|
||||||
:min="0"
|
:min="0"
|
||||||
@@ -396,15 +434,15 @@ watch(
|
|||||||
:step="1"
|
:step="1"
|
||||||
class="pd-input-fluid"
|
class="pd-input-fluid"
|
||||||
/>
|
/>
|
||||||
<span class="pd-unit">份</span>
|
<span class="pd-pricing-unit">份</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pd-row">
|
<div class="pd-row">
|
||||||
<label class="pd-label">打包费</label>
|
<label class="pd-label">打包费</label>
|
||||||
<div class="pd-ctrl">
|
<div class="pd-ctrl">
|
||||||
<div class="pd-inline">
|
<div class="pd-pricing-inline">
|
||||||
<span class="pd-unit">¥</span>
|
<span class="pd-pricing-unit">¥</span>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model:value="form.packingFee"
|
v-model:value="form.packingFee"
|
||||||
:min="0"
|
:min="0"
|
||||||
@@ -412,7 +450,7 @@ watch(
|
|||||||
:step="0.1"
|
:step="0.1"
|
||||||
class="pd-input-fluid"
|
class="pd-input-fluid"
|
||||||
/>
|
/>
|
||||||
<span class="pd-unit">/份</span>
|
<span class="pd-pricing-unit"> /份 </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -496,7 +534,7 @@ watch(
|
|||||||
>
|
>
|
||||||
<span class="pd-sku-spec">
|
<span class="pd-sku-spec">
|
||||||
{{
|
{{
|
||||||
getOptionName(
|
getOptionDisplayText(
|
||||||
column.id,
|
column.id,
|
||||||
getSkuAttrOptionId(row, column.id) || '',
|
getSkuAttrOptionId(row, column.id) || '',
|
||||||
)
|
)
|
||||||
@@ -746,10 +784,24 @@ watch(
|
|||||||
<div class="pd-row">
|
<div class="pd-row">
|
||||||
<label class="pd-label">商品类型</label>
|
<label class="pd-label">商品类型</label>
|
||||||
<div class="pd-ctrl">
|
<div class="pd-ctrl">
|
||||||
<Radio.Group :value="form.kind" @update:value="onKindChange">
|
<div class="pd-kind-pills">
|
||||||
<Radio.Button value="single">单品</Radio.Button>
|
<button
|
||||||
<Radio.Button value="combo">套餐</Radio.Button>
|
type="button"
|
||||||
</Radio.Group>
|
class="pd-kind-pill"
|
||||||
|
:class="{ active: form.kind === 'single' }"
|
||||||
|
@click="onKindChange('single')"
|
||||||
|
>
|
||||||
|
单品
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pd-kind-pill"
|
||||||
|
:class="{ active: form.kind === 'combo' }"
|
||||||
|
@click="onKindChange('combo')"
|
||||||
|
>
|
||||||
|
套餐
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -790,9 +842,12 @@ watch(
|
|||||||
<div class="pd-shelf-opt-body">
|
<div class="pd-shelf-opt-body">
|
||||||
<div class="pd-shelf-opt-title">定时上架</div>
|
<div class="pd-shelf-opt-title">定时上架</div>
|
||||||
<div class="pd-shelf-opt-desc">到达指定时间后自动上架</div>
|
<div class="pd-shelf-opt-desc">到达指定时间后自动上架</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="form.shelfMode === 'scheduled'"
|
v-if="form.shelfMode === 'scheduled'"
|
||||||
class="pd-shelf-time"
|
class="pd-shelf-time-row"
|
||||||
>
|
>
|
||||||
<span class="pd-unit">上架时间</span>
|
<span class="pd-unit">上架时间</span>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
@@ -802,9 +857,6 @@ watch(
|
|||||||
class="pd-input-fluid"
|
class="pd-input-fluid"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card :bordered="false" class="pd-save-bar">
|
<Card :bordered="false" class="pd-save-bar">
|
||||||
|
|||||||
@@ -161,60 +161,77 @@
|
|||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-upload-row {
|
.pd-upload-dragger {
|
||||||
display: flex;
|
.ant-upload-drag {
|
||||||
gap: 12px;
|
min-height: 160px;
|
||||||
align-items: center;
|
padding: 24px 16px;
|
||||||
}
|
background: #fafafa;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
.pd-upload-trigger {
|
border-radius: 14px;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 88px;
|
|
||||||
height: 88px;
|
|
||||||
font-size: 28px;
|
|
||||||
color: #9ca3af;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #f8f9fb;
|
|
||||||
border: 1px dashed #d1d5db;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-upload-trigger:hover {
|
.ant-upload-btn {
|
||||||
color: #1677ff;
|
padding: 0;
|
||||||
background: #f3f8ff;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-upload-dragger:not(.is-disabled):hover .ant-upload-drag {
|
||||||
|
background: #f6f9ff;
|
||||||
border-color: #1677ff;
|
border-color: #1677ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-upload-trigger.disabled {
|
.pd-upload-dragger.is-disabled .ant-upload-drag {
|
||||||
color: #d1d5db;
|
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
background: #f3f4f6;
|
background: #f7f7f7;
|
||||||
border-color: #e5e7eb;
|
border-color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-upload-hint {
|
.pd-upload-dragger-inner {
|
||||||
font-size: 12px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 110px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-upload-dragger-plus {
|
||||||
|
font-size: 48px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-upload-dragger-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-upload-dragger-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-thumbs {
|
.pd-thumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 14px;
|
||||||
margin-top: 12px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-thumb {
|
.pd-thumb {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 88px;
|
width: 120px;
|
||||||
height: 88px;
|
height: 120px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
background: #f8f9fb;
|
background: #f8f9fb;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-thumb img {
|
.pd-thumb img {
|
||||||
@@ -226,7 +243,7 @@
|
|||||||
|
|
||||||
.pd-thumb-main {
|
.pd-thumb-main {
|
||||||
border-color: #1677ff;
|
border-color: #1677ff;
|
||||||
box-shadow: 0 0 0 1px #1677ff;
|
box-shadow: 0 0 0 2px rgb(22 119 255 / 25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-thumb-badge {
|
.pd-thumb-badge {
|
||||||
@@ -234,9 +251,10 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 18px;
|
height: 26px;
|
||||||
font-size: 11px;
|
font-size: 13px;
|
||||||
line-height: 18px;
|
font-weight: 600;
|
||||||
|
line-height: 26px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: #1677ff;
|
background: #1677ff;
|
||||||
@@ -244,11 +262,11 @@
|
|||||||
|
|
||||||
.pd-thumb-del {
|
.pd-thumb-del {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -6px;
|
top: -10px;
|
||||||
right: -6px;
|
right: -10px;
|
||||||
width: 18px;
|
width: 28px;
|
||||||
height: 18px;
|
height: 28px;
|
||||||
font-size: 12px;
|
font-size: 18px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -257,19 +275,6 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-thumb-set-main {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 20px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
background: rgb(0 0 0 / 45%);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pd-price-alert {
|
.pd-price-alert {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
@@ -280,6 +285,31 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pd-pricing-card .pd-ctrl {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 460px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-pricing-inline {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 18px minmax(0, 1fr) 34px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-pricing-unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-pricing-unit.is-empty {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.pd-pills {
|
.pd-pills {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -367,7 +397,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pd-table-wrap {
|
.pd-table-wrap {
|
||||||
overflow-x: auto;
|
max-height: 520px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-sku-table {
|
.pd-sku-table {
|
||||||
@@ -377,6 +409,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pd-sku-table th {
|
.pd-sku-table th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
padding: 9px 12px;
|
padding: 9px 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -538,15 +573,50 @@
|
|||||||
|
|
||||||
.pd-shelf-group {
|
.pd-shelf-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 8px;
|
||||||
gap: 10px;
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-kind-pills {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-kind-pill {
|
||||||
|
min-width: 64px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-kind-pill:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
background: #f8fbff;
|
||||||
|
border-color: #8ec5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-kind-pill.active {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1677ff;
|
||||||
|
background: rgb(22 119 255 / 8%);
|
||||||
|
border-color: #1677ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-shelf-opt {
|
.pd-shelf-opt {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
flex: 1;
|
||||||
|
gap: 6px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 12px;
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 9px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -563,31 +633,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pd-shelf-opt input[type='radio'] {
|
.pd-shelf-opt input[type='radio'] {
|
||||||
margin-top: 3px;
|
margin-top: 1px;
|
||||||
accent-color: #1677ff;
|
accent-color: #1677ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-shelf-opt-body {
|
.pd-shelf-opt-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-shelf-opt-title {
|
.pd-shelf-opt-title {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
color: #1a1a2e;
|
color: #1a1a2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-shelf-opt-desc {
|
.pd-shelf-opt-desc {
|
||||||
margin-top: 2px;
|
margin-top: 0;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
line-height: 1.3;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-shelf-time {
|
.pd-shelf-time-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 8px;
|
max-width: 420px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pd-save-bar {
|
.pd-save-bar {
|
||||||
@@ -680,5 +754,43 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pd-pricing-card .pd-ctrl {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-table-wrap {
|
||||||
|
max-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-kind-pills {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-kind-pill {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-shelf-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-upload-dragger .ant-upload-drag {
|
||||||
|
min-height: 140px;
|
||||||
|
padding: 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-thumb {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pd-thumb-badge {
|
||||||
|
height: 22px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
scripts/deploy/Dockerfile.runtime
Normal file
16
scripts/deploy/Dockerfile.runtime
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM nginx:stable-alpine AS runtime
|
||||||
|
|
||||||
|
# 配置 nginx
|
||||||
|
RUN echo "types { application/javascript js mjs; }" > /etc/nginx/conf.d/mjs.conf \
|
||||||
|
&& rm -rf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 复制宿主机构建产物
|
||||||
|
COPY publish/web-antd-dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制 nginx 配置
|
||||||
|
COPY scripts/deploy/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# 启动 nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
Reference in New Issue
Block a user