mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-06 17:09:08 +08:00
feat(goods): 完善商品定时上下架功能
- 新增定时下架支持,商品列表同时展示上架和下架计划 - 添加定时计划的编辑和删除操作入口 - 新增取消定时上下架API接口 - 商品选择对话框支持商品模式,适配SPU维度选择 - 商品选择对话框增加高级筛选折叠和品牌联动逻辑 - 优化商品列表筛选布局为弹性自适应宽度 - 管理端定时计划页面增加定时下架标签页 - 商家端定时计划页面重构为完整CRUD抽屉交互 - 修改楼层列表新建逻辑改为路由跳转装修页
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user