feat(goods): 完善商品定时上下架功能

- 新增定时下架支持,商品列表同时展示上架和下架计划
- 添加定时计划的编辑和删除操作入口
- 新增取消定时上下架API接口
- 商品选择对话框支持商品模式,适配SPU维度选择
- 商品选择对话框增加高级筛选折叠和品牌联动逻辑
- 优化商品列表筛选布局为弹性自适应宽度
- 管理端定时计划页面增加定时下架标签页
- 商家端定时计划页面重构为完整CRUD抽屉交互
- 修改楼层列表新建逻辑改为路由跳转装修页
This commit is contained in:
pikachu1995@126.com
2026-11-04 11:07:37 +08:00
parent f6f106e12b
commit 2e0db35441
11 changed files with 366 additions and 90 deletions

View File

@@ -1,6 +1,10 @@
// 统一请求路径前缀在libs/axios.js中修改
import { getRequest, postRequest, putRequest, deleteRequest} from '@/libs/axios';
export const cancelGoodsMarketSchedule = (goodsId, status) => {
return deleteRequest(`/goods/goods/schedule/market/${goodsId}`, { status });
};
// 获取商品品牌分页列表
export const getManagerBrandPage = (params) => {
return getRequest('/goods/brand/getByPage', params)

View File

@@ -70,6 +70,7 @@
</el-card>
<el-card>
<slot v-if="scheduledOnly" name="scheduled-controls" />
<div v-if="!scheduledOnly" class="goods-tab">
<el-tabs v-model="currentStatus" @tab-click="onStatusTabClick">
<el-tab-pane
@@ -142,6 +143,7 @@
</template>
</el-table-column>
<el-table-column v-if="currentStatus === 'SCHEDULED_UPPER'" prop="scheduledUpperTime" label="计划上架时间" width="190" />
<el-table-column v-if="currentStatus === 'SCHEDULED_DOWN'" prop="scheduledDownTime" label="计划下架时间" width="190" />
<el-table-column label="价格" width="200">
<template #default="{ row }">
<span :style="{ color: $mainColor }">{{ unitPrice(row.price, '¥') }}</span>
@@ -154,7 +156,7 @@
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="150">
<el-table-column v-if="!scheduledOnly" label="状态" width="150">
<template #default="{ row }">
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
</template>
@@ -165,7 +167,12 @@
</template>
</el-table-column>
<el-table-column prop="storeName" label="店铺名称" width="200" show-overflow-tooltip />
<el-table-column v-if="!readOnly" label="操作" width="200" align="center" fixed="right">
<el-table-column v-if="scheduledOnly" label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="emit('delete-schedule', row)">删除</a>
</template>
</el-table-column>
<el-table-column v-else-if="!readOnly" label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<template v-if="row.authFlag === 'TOBEAUDITED'">
<a class="link-text" @click="openAuditModal(row)">审核</a>
@@ -299,7 +306,9 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { unitPrice, customRouterPush } from "@/utils/filters";
defineOptions({ name: "goods" });
const emit = defineEmits(["delete-schedule"]);
const props = defineProps({
scheduledStatus: { type: String, default: "SCHEDULED_UPPER" },
scheduledOnly: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
});
@@ -320,7 +329,7 @@ const auditModalVisible = ref(false);
const auditSubmitLoading = ref(false);
const currentAuditGoods = ref(null);
const submitLoading = ref(false);
const currentStatus = ref(props.scheduledOnly ? "SCHEDULED_UPPER" : "ALL");
const currentStatus = ref(props.scheduledOnly ? props.scheduledStatus : "ALL");
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
getGoodsListData,
@@ -338,11 +347,11 @@ const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch
},
beforeRequest: (p) => {
const params = { ...p };
const status = props.scheduledOnly ? "SCHEDULED_UPPER" : currentStatus.value;
const status = props.scheduledOnly ? props.scheduledStatus : currentStatus.value;
if (status && status !== "ALL") {
params.goodsStatus = status;
if (status === "SCHEDULED_UPPER") {
params.sort = "scheduled_upper_time";
if (["SCHEDULED_UPPER", "SCHEDULED_DOWN"].includes(status)) {
params.sort = status === "SCHEDULED_UPPER" ? "scheduled_upper_time" : "scheduled_down_time";
params.order = "asc";
}
} else {
@@ -357,7 +366,7 @@ const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch
);
function handleReset() {
currentStatus.value = props.scheduledOnly ? "SCHEDULED_UPPER" : "ALL";
currentStatus.value = props.scheduledOnly ? props.scheduledStatus : "ALL";
resetSearch();
}
const goodsNumerData = ref({});

View File

@@ -1,5 +1,12 @@
<template>
<goods-list scheduled-only read-only />
<goods-list :key="`${activeStatus}-${refreshKey}`" scheduled-only read-only :scheduled-status="activeStatus" @delete-schedule="deleteSchedule">
<template #scheduled-controls>
<el-tabs v-model="activeStatus">
<el-tab-pane label="定时上架" name="SCHEDULED_UPPER" />
<el-tab-pane label="定时下架" name="SCHEDULED_DOWN" />
</el-tabs>
</template>
</goods-list>
</template>
<script setup>
@@ -9,6 +16,30 @@
* @since 2026-11-03
*/
import GoodsList from "@/views/goods/goods-info/goods.vue";
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { cancelGoodsMarketSchedule } from "@/api/goods";
defineOptions({ name: "goodsScheduledUpper" });
const activeStatus = ref("SCHEDULED_UPPER");
const refreshKey = ref(0);
async function deleteSchedule(row) {
const status = activeStatus.value === "SCHEDULED_UPPER" ? "UPPER" : "DOWN";
try {
await ElMessageBox.confirm(`确认删除“${row.goodsName}”的定时${status === 'UPPER' ? '上架' : '下架'}计划?`, "删除定时计划", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" });
} catch {
return;
}
try {
const res = await cancelGoodsMarketSchedule(row.id, status);
if (res.success) {
ElMessage.success("定时计划已删除");
refreshKey.value++;
}
} catch {
// 接口错误由统一拦截器提示。
}
}
</script>

View File

@@ -266,6 +266,10 @@ export const lowGoods = params => {
};
// 定时上下架商品
export const cancelGoodsMarketSchedule = (goodsId, status) => {
return deleteRequest(`/goods/goods/schedule/market/${goodsId}`, { status });
};
export const scheduleGoodsMarket = params => {
return postRequest(`/goods/goods/schedule/market`, params, {
"Content-Type": "application/json"

View File

@@ -111,6 +111,7 @@
</el-card>
<el-card>
<slot v-if="scheduledOnly" name="scheduled-controls" />
<div v-if="!scheduledOnly" class="goods-tab">
<el-tabs v-model="currentStatus" @tab-click="onStatusTabClick">
<el-tab-pane
@@ -122,7 +123,7 @@
</el-tabs>
</div>
<div class="batch-operations" style="margin: 10px 0">
<div v-if="!scheduledOnly" class="batch-operations" style="margin: 10px 0">
<el-button
type="success"
:disabled="selectedRows.length === 0"
@@ -149,7 +150,7 @@
row-key="id"
@selection-change="onSelectionChange"
>
<el-table-column type="selection" width="100" align="center" />
<el-table-column v-if="!scheduledOnly" type="selection" width="100" align="center" />
<el-table-column prop="id" label="SPU编码" width="200" show-overflow-tooltip />
<el-table-column label="商品图片" width="100" align="center">
<template #default="{ row }">
@@ -170,6 +171,7 @@
</template>
</el-table-column>
<el-table-column v-if="currentStatus === 'SCHEDULED_UPPER'" prop="scheduledUpperTime" label="计划上架时间" width="190" />
<el-table-column v-if="currentStatus === 'SCHEDULED_DOWN'" prop="scheduledDownTime" label="计划下架时间" width="190" />
<el-table-column label="价格" width="120">
<template #default="{ row }">
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, '¥') }}</span>
@@ -187,7 +189,7 @@
{{ platformCategoryText(row?.categoryPath) }}
</template>
</el-table-column>
<el-table-column label="状态" width="150">
<el-table-column v-if="!scheduledOnly" label="状态" width="150">
<template #default="{ row }">
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
</template>
@@ -197,9 +199,15 @@
<span v-if="row">{{ authFlagText(row.authFlag) }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
<el-table-column label="操作" width="320" align="right" fixed="right">
<el-table-column v-if="!scheduledOnly" prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
<el-table-column label="操作" :width="scheduledOnly ? 150 : 320" align="right" fixed="right">
<template #default="{ row }">
<template v-if="scheduledOnly">
<a class="link-text" @click="emit('edit-schedule', row)">编辑</a>
<span class="op-split">|</span>
<a class="link-text" @click="emit('delete-schedule', row)">删除</a>
</template>
<template v-else>
<template v-if="row.marketEnable === 'DOWN'">
<a class="link-text" @click="upper(row)">上架</a>
<span class="op-split">|</span>
@@ -217,6 +225,7 @@
<span class="op-split">|</span>
<a class="link-text" @click="goCardKeyPool(row)">卡池管理</a>
</template>
</template>
</template>
</el-table-column>
</el-table>
@@ -265,8 +274,9 @@ import { Message, Modal } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
defineOptions({ name: "goods" });
const emit = defineEmits(["edit-schedule", "delete-schedule"]);
const props = defineProps({ scheduledOnly: { type: Boolean, default: false } });
const props = defineProps({ scheduledOnly: { type: Boolean, default: false }, scheduledStatus: { type: String, default: "SCHEDULED_UPPER" } });
const route = useRoute();
const router = useRouter();
@@ -318,7 +328,7 @@ const underForm = ref({
const submitLoading = ref(false); // 添加或编辑提交状态
const data = ref([]); // 表单数据
const total = ref(0); // 表单数据总数
const currentStatus = ref(props.scheduledOnly ? "SCHEDULED_UPPER" : "ALL");
const currentStatus = ref(props.scheduledOnly ? props.scheduledStatus : "ALL");
const goodsNumerData = ref({});
const selectedRows = ref([]); // 选中的行数据
const selectAll = ref(false); // 全选状态
@@ -437,10 +447,10 @@ function buildSearchParams(includeGoodsStatus = true) {
if (!includeGoodsStatus) {
delete params.goodsStatus;
} else if (props.scheduledOnly) {
params.goodsStatus = "SCHEDULED_UPPER";
params.goodsStatus = props.scheduledStatus;
}
if (includeGoodsStatus && currentStatus.value === "SCHEDULED_UPPER") {
params.sort = "scheduled_upper_time";
if (includeGoodsStatus && ["SCHEDULED_UPPER", "SCHEDULED_DOWN"].includes(currentStatus.value)) {
params.sort = currentStatus.value === "SCHEDULED_UPPER" ? "scheduled_upper_time" : "scheduled_down_time";
params.order = "asc";
}

View File

@@ -534,8 +534,8 @@
<div class="form-item-view">
<el-form-item class="form-item-view-el" label="上下架" prop="release">
<el-radio-group v-model="baseInfoForm.release">
<el-radio-button :value="1">立即上架</el-radio-button>
<el-radio-button :value="0">立即下架</el-radio-button>
<el-radio-button :value="1">上架</el-radio-button>
<el-radio-button :value="0">下架</el-radio-button>
<el-radio-button :value="2">定时上架</el-radio-button>
</el-radio-group>
</el-form-item>
@@ -1437,7 +1437,7 @@ async function GET_GoodData(id, draftId) {
invalidGoodsGalleryFiles.value = [];
baseInfoForm.value = { ...baseInfoForm.value, ...response.result };
baseInfoForm.value.salesModel = "RETAIL";
baseInfoForm.value.release = 1; //即使是被放入仓库,修改的时候也会显示会立即发布
baseInfoForm.value.release = !draftId && !route.query.copyId && response.result.marketEnable === "DOWN" ? 0 : 1;
// 已设置定时上架的商品,编辑时回显为定时上架
if (!draftId && !route.query.copyId && response.result.scheduledUpperTime) {
baseInfoForm.value.release = 2;

View File

@@ -1,5 +1,49 @@
<template>
<goods-list scheduled-only />
<div>
<goods-list :key="`${activeStatus}-${refreshKey}`" scheduled-only :scheduled-status="activeStatus" @edit-schedule="editSchedule" @delete-schedule="deleteSchedule">
<template #scheduled-controls>
<el-tabs v-model="activeStatus">
<el-tab-pane label="定时上架" name="SCHEDULED_UPPER" />
<el-tab-pane label="定时下架" name="SCHEDULED_DOWN" />
</el-tabs>
<div class="schedule-actions">
<el-button type="primary" @click="openSchedule('UPPER')">添加定时上架</el-button>
<el-button type="primary" plain @click="openSchedule('DOWN')">添加定时下架</el-button>
</div>
</template>
</goods-list>
<el-drawer v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="min(560px, 100vw)" :close-on-click-modal="false" :close-on-press-escape="!saving" :show-close="!saving" append-to-body>
<el-form label-width="90px" @submit.prevent>
<el-form-item v-if="!editing" label="选择商品" required>
<div class="selected-goods-field">
<el-button v-if="!editing" :disabled="saving" @click="openGoodsPicker">选择商品</el-button>
<span class="selected-count">已选 {{ form.goodsIds.length }} 件商品</span>
<div v-if="selectedProducts.length" class="selected-products" aria-label="已选商品列表">
<div v-for="goods in selectedProducts" :key="goods.goodsId" class="selected-product">
<el-image :src="goods.thumbnail || goods.original" fit="contain" class="selected-product-image">
<template #error><span class="selected-product-placeholder">暂无图片</span></template>
</el-image>
<div class="selected-product-info">
<div class="selected-product-name" :title="goods.goodsName">{{ goods.goodsName }}</div>
<div class="selected-product-code">SPU:{{ goods.goodsId }}</div>
</div>
<el-button link type="primary" :disabled="saving" :aria-label="`移除${goods.goodsName}`" @click="removeGoods(goods.goodsId)">移除</el-button>
</div>
</div>
</div>
</el-form-item>
<el-form-item :label="form.status === 'UPPER' ? '上架时间' : '下架时间'" required>
<el-date-picker v-model="form.triggerTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择执行时间" :disabled-date="disabledPastDate" style="width: 100%" />
</el-form-item>
<el-form-item label="原因"><el-input v-model="form.reason" maxlength="200" placeholder="选填" /></el-form-item>
</el-form>
<template #footer>
<el-button :disabled="saving" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="saveSchedule">确定</el-button>
</template>
</el-drawer>
<lili-dialog ref="goodsPicker" product-mode :market-enable="form.status === 'UPPER' ? 'DOWN' : 'UPPER'" auth-flag="" @selected-goods-data="selectGoods" />
</div>
</template>
<script setup>
@@ -9,6 +53,119 @@
* @since 2026-11-03
*/
import GoodsList from "@/views/goods/goods-seller/goods.vue";
import { computed, ref } from "vue";
import { scheduleGoodsMarket, cancelGoodsMarketSchedule } from "@/api/goods";
import LiliDialog from "@/views/lili-dialog/index.vue";
import { Message, Modal } from "@/utils/message";
defineOptions({ name: "goodsScheduledUpper" });
const activeStatus = ref("SCHEDULED_UPPER");
const refreshKey = ref(0);
const dialogVisible = ref(false);
const saving = ref(false);
const editing = ref(false);
const goodsPicker = ref(null);
const selectedGoods = ref([]);
const selectedProducts = computed(() => [...new Map(selectedGoods.value.map(goods => [goods.goodsId, goods])).values()]);
const form = ref({ status: "UPPER", goodsIds: [], triggerTime: "", reason: "" });
const dialogTitle = computed(() => `${editing.value ? '编辑' : '添加'}定时${form.value.status === 'UPPER' ? '上架' : '下架'}`);
function openSchedule(status) {
editing.value = false;
form.value = { status, goodsIds: [], triggerTime: "", reason: "" };
selectedGoods.value = [];
dialogVisible.value = true;
}
function editSchedule(row) {
const upper = activeStatus.value === "SCHEDULED_UPPER";
editing.value = true;
selectedGoods.value = [{ goodsId: row.id, goodsName: row.goodsName }];
form.value = {
status: upper ? "UPPER" : "DOWN",
goodsIds: [row.id],
triggerTime: (upper ? row.scheduledUpperTime : row.scheduledDownTime) || "",
reason: (upper ? row.scheduledUpperReason : row.scheduledDownReason) || "",
};
dialogVisible.value = true;
}
function deleteSchedule(row) {
const status = activeStatus.value === "SCHEDULED_UPPER" ? "UPPER" : "DOWN";
Modal.confirm({
title: "删除定时计划",
content: `确认删除“${row.goodsName}”的定时${status === 'UPPER' ? '上架' : '下架'}计划?`,
onOk: async () => {
const res = await cancelGoodsMarketSchedule(row.id, status);
if (res.success) {
Message.success("定时计划已删除");
refreshKey.value++;
}
},
});
}
function openGoodsPicker() {
goodsPicker.value.goodsData = selectedGoods.value.slice();
goodsPicker.value.open("goods");
}
function selectGoods(goods) {
selectedGoods.value = (goods || []).filter(item => item.goodsId);
// 通用选择器返回SKU;上下架按商品操作,同一商品的多个SKU只提交一次。
form.value.goodsIds = [...new Set(selectedGoods.value.map(item => item.goodsId))];
}
function removeGoods(goodsId) {
selectGoods(selectedGoods.value.filter(item => item.goodsId !== goodsId));
}
function disabledPastDate(date) {
const today = new Date();
today.setHours(0, 0, 0, 0);
return date.getTime() < today.getTime();
}
async function saveSchedule() {
if (saving.value) return;
if (!form.value.goodsIds.length) return Message.warning("请选择商品");
const time = new Date((form.value.triggerTime || "").replace(/-/g, "/")).getTime();
if (!Number.isFinite(time) || time <= Date.now()) return Message.warning("请选择晚于当前时间的执行时间");
saving.value = true;
try {
const res = await scheduleGoodsMarket({ ...form.value });
if (res.success) {
Message.success(editing.value ? "定时计划修改成功" : "定时任务添加成功");
activeStatus.value = form.value.status === "UPPER" ? "SCHEDULED_UPPER" : "SCHEDULED_DOWN";
refreshKey.value++;
dialogVisible.value = false;
}
} catch {
// 保留表单以便重试,请求错误由全局拦截器提示。
} finally {
saving.value = false;
}
}
</script>
<style scoped>
.schedule-actions { margin: 10px 0; }
.selected-goods-field { width: 100%; min-width: 0; }
.selected-count { margin-left: 12px; color: #909399; }
.selected-products {
width: 100%;
max-height: 320px;
overflow-y: auto;
margin-top: 12px;
border: 1px solid #ebeef5;
border-radius: 6px;
}
.selected-product { display: flex; align-items: center; gap: 12px; padding: 12px; }
.selected-product + .selected-product { border-top: 1px solid #ebeef5; }
.selected-product-image { width: 48px; height: 48px; flex-shrink: 0; border-radius: 4px; background: #f5f7fa; }
.selected-product-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 11px; color: #909399; }
.selected-product-info { flex: 1; min-width: 0; }
.selected-product-name { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; color: #303133; line-height: 20px; }
.selected-product-code { margin-top: 4px; color: #909399; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
</style>

View File

@@ -35,17 +35,19 @@
<el-option label="实物商品" value="PHYSICAL_GOODS" />
<el-option label="虚拟商品" value="VIRTUAL_GOODS" />
<el-option label="电子卡券" value="E_COUPON" />
<el-option label="预约商品" value="APPOINTMENT_GOODS" />
</el-select>
</div>
</div>
</el-form-item>
<el-form-item label-width="0" class="filter-row-item">
<el-form-item v-if="showAdvanced" label-width="0" class="filter-row-item">
<div class="filter-row">
<div class="filter-row-field">
<span class="filter-row-label">品牌</span>
<el-select
v-model="filterForm.brandId"
placeholder="请选择品牌"
:placeholder="filterForm.categoryPath ? '请选择品牌' : '请先选择平台类目'"
:disabled="!filterForm.categoryPath"
clearable
filterable
class="filter-control"
@@ -82,7 +84,7 @@
</div>
</div>
</el-form-item>
<el-form-item label-width="0" class="range-form-item range-filter-row-item">
<el-form-item v-if="showAdvanced" label-width="0" class="range-form-item range-filter-row-item">
<div class="range-filter-row">
<div class="range-group">
<span class="filter-row-label">价格</span>
@@ -113,6 +115,7 @@
<el-form-item>
<el-button type="primary" @click="onSearchGoods">搜索</el-button>
<el-button @click="resetFilter">重置</el-button>
<el-button link type="primary" @click="showAdvanced = !showAdvanced">{{ showAdvanced ? '收起筛选' : '更多筛选' }}</el-button>
</el-form-item>
</el-form>
</div>
@@ -122,7 +125,6 @@
ref="goodsTableRef"
v-loading="loading"
:data="goodsData"
border
:height="tableHeight"
:highlight-current-row="type !== 'multiple'"
class="goods-sku-table"
@@ -137,38 +139,30 @@
align="center"
:reserve-selection="true"
/>
<el-table-column prop="sn" label="SKU编码" min-width="150" show-overflow-tooltip />
<el-table-column label="商品图片" width="90" align="center">
<el-table-column label="商品信息" min-width="320" class-name="goods-name-column">
<template #default="{ row }">
<img v-if="row" :src="row.thumbnail" class="goods-thumb" alt="" />
<div v-if="row" class="goods-info">
<el-image :src="row.thumbnail || row.original" fit="contain" class="goods-thumb">
<template #error><span class="no-image">暂无图片</span></template>
</el-image>
<div class="goods-info-copy">
<div class="goods-name-text">{{ row.goodsName }}</div>
<div class="goods-code">{{ productMode ? 'SPU' : 'SKU' }}:{{ productMode ? row.id : (row.sn || row.id) }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="商品名称" min-width="160" class-name="goods-name-column">
<template #default="{ row }">
<span v-if="row" class="goods-name-text">{{ row.goodsName }}</span>
</template>
</el-table-column>
<el-table-column label="规格" min-width="120" show-overflow-tooltip>
<el-table-column v-if="!productMode" label="规格" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row?.simpleSpecs || "-" }}
</template>
</el-table-column>
<el-table-column label="商品类型" width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ goodsTypeText(row?.goodsType) }}
</template>
</el-table-column>
<el-table-column label="店铺分类" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ storeCategoryText(row?.storeCategoryPath) }}
</template>
</el-table-column>
<el-table-column label="平台类目" min-width="120" show-overflow-tooltip>
<el-table-column v-if="showAdvanced" label="平台类目" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ platformCategoryText(row?.categoryPath) }}
</template>
</el-table-column>
<el-table-column label="品牌" min-width="100" show-overflow-tooltip>
<el-table-column v-if="showAdvanced" label="品牌" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ brandText(row?.brandId) }}
</template>
@@ -184,8 +178,13 @@
</template>
</el-table-column>
<el-table-column prop="quantity" label="库存" width="80" align="center" />
<el-table-column v-if="productMode" label="审核状态" width="100">
<template #default="{ row }">
<span>{{ row.authFlag === 'PASS' ? '审核通过' : row.authFlag === 'REFUSE' ? '审核拒绝' : '待审核' }}</span>
</template>
</el-table-column>
<template #empty>
<div class="empty">{{ loading ? "" : "暂无商品信息" }}</div>
<el-empty v-if="!loading" description="没有符合条件的商品,请调整筛选条件" :image-size="72" />
</template>
</el-table>
</div>
@@ -222,6 +221,9 @@ const defaultFilterForm = () => ({
});
const props = defineProps({
productMode: { type: Boolean, default: false },
marketEnable: { type: String, default: "UPPER" },
authFlag: { type: String, default: "PASS" },
selectedWay: {
type: Array,
default: () => [],
@@ -230,6 +232,7 @@ const props = defineProps({
const emit = defineEmits(["selected"]);
const type = ref("multiple");
const showAdvanced = ref(false);
const selectedList = ref([]);
const platformCategoryList = ref([]);
const shopCategoryList = ref([]);
@@ -250,13 +253,26 @@ const shopCategoryProps = {
emitPath: false,
};
const filterForm = ref(defaultFilterForm());
let brandRequest = 0;
watch(() => filterForm.value.categoryPath, async (categoryId) => {
const request = ++brandRequest;
filterForm.value.brandId = "";
brandList.value = [];
if (!categoryId) return;
try {
const res = await API_Goods.getCategoryBrandListDataSeller(categoryId);
if (request === brandRequest) brandList.value = Array.isArray(res) ? res : res?.result || [];
} catch {
// 请求失败由统一拦截器提示。
}
});
const total = ref(0);
const goodsParams = reactive({
pageNumber: 1,
pageSize: 20,
order: "desc",
marketEnable: "UPPER",
authFlag: "PASS",
marketEnable: props.marketEnable,
authFlag: props.authFlag,
sort: "createTime",
});
const goodsData = ref([]);
@@ -272,6 +288,7 @@ watch(
() => props.selectedWay,
(val) => {
selectedList.value = Array.isArray(val) ? val.slice() : [];
syncTableSelectionFromSelected();
},
{ deep: true, immediate: true }
);
@@ -292,6 +309,13 @@ function resetFilter() {
filterForm.value = defaultFilterForm();
onSearchGoods();
}
function syncTableSelectionFromSelected() {
nextTick(() => {
const ids = new Set(selectedList.value.map(item => String(item.id || item.skuId)));
goodsData.value.forEach(item => { item.selected = ids.has(String(item.id)); });
syncTableSelection();
});
}
function changePageSize(v) {
goodsParams.pageNumber = v;
getQueryGoodsList();
@@ -330,7 +354,13 @@ function buildQueryParams() {
}
function getQueryGoodsList() {
loading.value = true;
API_Goods.getGoodsSkuData(buildQueryParams())
const query = props.productMode ? API_Goods.getGoodsListData : API_Goods.getGoodsSkuData;
const params = buildQueryParams();
if (props.productMode) {
params.id = params.goodsId;
delete params.goodsId;
}
query(params)
.then((res) => {
initGoods(res);
})
@@ -349,6 +379,7 @@ function initGoods(res) {
.map(String)
);
records.forEach((item) => {
if (props.productMode) item.goodsId = item.id;
item.___type = "goods";
item.selected = selectedIds.has(String(item.id));
});
@@ -412,12 +443,6 @@ function formatShopCategory(list) {
item.children && item.children.length ? formatShopCategory(item.children) : undefined,
}));
}
function goodsTypeText(value) {
if (value === "PHYSICAL_GOODS") return "实物商品";
if (value === "VIRTUAL_GOODS") return "虚拟商品";
if (value === "E_COUPON") return "电子卡券";
return "-";
}
function brandText(brandId) {
if (!brandId) return "-";
const brand = brandList.value.find((item) => String(item.id) === String(brandId));
@@ -433,18 +458,6 @@ function findTreeNodeLabel(tree, targetId, valueKey, labelKey) {
}
return "";
}
function categoryPathText(path, tree, valueKey, labelKey) {
if (!path) return "-";
const ids = String(path).split(",").filter(Boolean);
if (!ids.length) return "-";
const labels = ids
.map((id) => findTreeNodeLabel(tree, id, valueKey, labelKey))
.filter(Boolean);
return labels.length ? labels.join(" / ") : "-";
}
function storeCategoryText(path) {
return categoryPathText(path, shopCategoryList.value, "value", "label");
}
function platformCategoryText(path) {
if (!path) return "-";
const ids = String(path).split(",").filter(Boolean);
@@ -469,13 +482,10 @@ function init() {
}
})
.catch(() => {});
API_Goods.getBrandListData()
.then((res) => {
brandList.value = Array.isArray(res) ? res : res?.result || [];
})
.catch(() => {});
}
watch(showAdvanced, calcTableHeight);
onMounted(() => {
init();
nextTick(calcTableHeight);
@@ -555,6 +565,7 @@ defineExpose({ type });
.filter-row {
display: flex;
width: 100%;
flex-wrap: nowrap;
align-items: center;
gap: 16px;
@@ -563,12 +574,14 @@ defineExpose({ type });
.filter-row-field {
display: inline-flex;
align-items: center;
flex: 0 0 244px;
min-width: 244px;
flex: 1 1 0;
min-width: 0;
}
.filter-control {
width: 160px;
flex: 1;
width: 100%;
min-width: 0;
}
.filter-row-label {
@@ -585,6 +598,7 @@ defineExpose({ type });
.range-filter-row {
display: flex;
width: 100%;
flex-wrap: nowrap;
align-items: center;
gap: 16px;
@@ -593,16 +607,20 @@ defineExpose({ type });
.range-group {
display: inline-flex;
align-items: center;
flex: 0 0 244px;
min-width: 244px;
flex: 1 1 0;
min-width: 0;
}
.range-group-inputs {
display: inline-flex;
align-items: center;
flex: 1;
min-width: 0;
:deep(.el-input) {
width: 72px;
flex: 1;
width: 100%;
min-width: 0;
}
}
@@ -620,7 +638,15 @@ defineExpose({ type });
white-space: normal;
word-break: break-word;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.goods-info { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
.goods-info-copy { min-width: 0; }
.goods-code { margin-top: 6px; font-size: 12px; color: #909399; overflow-wrap: anywhere; }
.no-image { display: flex; height: 100%; align-items: center; justify-content: center; font-size: 11px; color: #a8abb2; background: #f5f7fa; }
:deep(.goods-name-column .cell) {
white-space: normal;
@@ -633,6 +659,9 @@ defineExpose({ type });
height: 48px;
object-fit: contain;
vertical-align: middle;
flex-shrink: 0;
border-radius: 6px;
border: 1px solid #ebeef5;
}
.goods-price {

View File

@@ -1,19 +1,27 @@
<template>
<el-drawer
v-model="goodsVisible"
title="选择商品"
direction="rtl"
size="85%"
size="min(1180px, 94vw)"
:z-index="10000"
:close-on-click-modal="false"
append-to-body
destroy-on-close
class="lili-goods-drawer"
>
<template #header>
<div class="picker-heading">
<h3>选择商品</h3>
<p v-if="!productMode">选择需要的商品规格,支持跨页多选</p>
</div>
</template>
<goodsDialog
v-if="goodsVisible"
ref="goodsDialogRef"
:selectedWay="goodsData"
:market-enable="marketEnable"
:auth-flag="authFlag"
:product-mode="productMode"
@selected="
(val) => {
goodsData = val;
@@ -21,8 +29,15 @@
"
/>
<template #footer>
<el-button @click="clickClose">取消</el-button>
<el-button type="primary" @click="clickOK">确定</el-button>
<div class="picker-footer">
<div class="picker-selection">已选 <strong>{{ goodsData.length }}</strong> {{ productMode ? '件商品' : '个规格' }}
<el-button v-if="goodsData.length" link type="primary" @click="clearGoodsSelected">清空</el-button>
</div>
<div>
<el-button @click="clickClose">取消</el-button>
<el-button type="primary" @click="clickOK">确认选择{{ goodsData.length ? `(${goodsData.length})` : '' }}</el-button>
</div>
</div>
</template>
</el-drawer>
@@ -57,6 +72,11 @@ import goodsDialog from "./goods-dialog";
import linkDialog from "./link-dialog";
const emit = defineEmits(["closeFlag", "selectedGoodsData", "selectedLink"]);
defineProps({
productMode: { type: Boolean, default: false },
marketEnable: { type: String, default: "UPPER" },
authFlag: { type: String, default: "PASS" },
});
const goodsVisible = ref(false);
const linkVisible = ref(false);
@@ -138,11 +158,22 @@ defineExpose({
});
</script>
<style scoped lang="scss">
.picker-heading h3 { margin: 0 0 6px; color: #303133; font-size: 18px; }
.picker-heading p { margin: 0; color: #909399; font-size: 13px; }
.picker-footer { display: flex; justify-content: space-between; align-items: center; }
.picker-selection { color: #606266; font-size: 14px; }
.picker-selection strong { color: #409eff; margin: 0 4px; }
.picker-selection .el-button { margin-left: 16px; }
.lili-goods-drawer {
:deep(.el-drawer__header) {
margin-bottom: 0;
padding: 20px 20px 12px;
}
:deep(.el-drawer__body) {
display: flex;
flex-direction: column;
padding: 12px 20px;
padding: 0 20px 12px;
overflow: hidden;
}

View File

@@ -156,9 +156,10 @@ function handleTabChange(type) {
}
function createTemp() {
formRef.value?.resetFields();
formData.value = { status: false, name: "" };
showModal.value = true;
router.push({
name: "renovation",
query: { pageType: searchForm.pageType },
});
}
function Template(item) {

View File

@@ -1692,9 +1692,9 @@ onMounted(() => {
pageInfo.id = '';
pageInfo.name = route.query.name || '平台首页装修';
pageInfo.name = route.query.name || (pageType === 'SPECIAL' ? '新建PC专题页' : '新建PC首页');
pageInfo.pageShow = true;
pageInfo.pageShow = false;
handleAddNewRow();