feat: 新增全局布局样式并提升组件响应式表现

- 在 global-layout.scss 中引入全局布局样式,统一页面宽度与对齐方式
- 更新多个组件与页面以优化响应式,包括宽度、间距及 flex 布局等调整
- 在 API 请求中增加 loading 状态管理,改善用户体验
- 优化领券中心与商品详情页,提升功能与 UI 一致性
This commit is contained in:
田香琪
2026-06-23 10:17:35 +08:00
parent c1447b4376
commit 8a60e23214
129 changed files with 5236 additions and 2232 deletions

View File

@@ -25,58 +25,155 @@
</el-table-column>
</el-table>
<el-table
ref="table"
v-loading="loading"
border
class="operation"
:data="goodsList"
style="width: 100%"
>
<el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip />
<el-table-column label="商品价格" width="110">
<template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }">
{{ $filters.unitPrice(row.originalPrice, "") }}</span>
</template>
</el-table-column>
<el-table-column label="库存" width="90">
<template #default="{ row }">
<span v-if="row">{{ row.quantity }}</span>
</template>
</el-table-column>
<el-table-column label="活动价格" width="100">
<template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }">
{{ $filters.unitPrice(row.price, "") }}</span>
</template>
</el-table-column>
<el-table-column prop="storeName" label="商家名称" min-width="100" show-overflow-tooltip />
<el-table-column label="活动场次" width="100">
<template #default="{ row }">
<el-tag v-if="row">{{ row.timeLine + ":00" }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="{ row, $index }">
<a v-if="row" class="link-text" @click="delGoods($index, row)">删除</a>
</template>
</el-table-column>
</el-table>
<template v-if="!readonly">
<div class="operation">
<el-button type="primary" @click="openSkuList">选择商品</el-button>
</div>
<el-tabs v-model="tabCurrent" type="card" class="operation">
<el-tab-pane
v-for="(tab, tabIndex) in goodsList"
:key="tabIndex"
:label="tab.hour"
:name="String(tabIndex)"
>
<el-table
v-loading="loading"
border
:data="tab.list"
style="width: 100%"
>
<el-table-column prop="goodsName" label="商品名称" min-width="140" show-overflow-tooltip />
<el-table-column label="商品价格" width="110">
<template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }">
{{ $filters.unitPrice(row.originalPrice, "") }}
</span>
</template>
</el-table-column>
<el-table-column label="库存" width="130">
<template #default="{ row, $index }">
<el-input-number
v-if="row"
v-model="row.quantity"
:min="1"
:disabled="row.promotionApplyStatus === 'PASS'"
controls-position="right"
size="small"
@change="(val) => updateGoodsField(tabIndex, $index, 'quantity', val)"
/>
</template>
</el-table-column>
<el-table-column label="活动价格" width="140">
<template #default="{ row, $index }">
<el-input-number
v-if="row"
v-model="row.price"
:min="0.01"
:precision="2"
:disabled="row.promotionApplyStatus === 'PASS'"
controls-position="right"
size="small"
@change="(val) => updateGoodsField(tabIndex, $index, 'price', val)"
/>
</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }">
<template v-if="row">
<el-tag :type="applyStatusTagType(row.promotionApplyStatus)" size="small">
{{ applyStatusText(row.promotionApplyStatus) }}
</el-tag>
<a
v-if="row.promotionApplyStatus === 'REFUSE' && row.failReason"
class="reason link-text"
@click="showReason(row.failReason)"
>
拒绝原因
</a>
</template>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default="{ row, $index }">
<a v-if="row" class="link-text" @click="delGoods(tabIndex, $index, row)">删除</a>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</template>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="changePage"
@size-change="changePageSize"
/>
<template v-else>
<el-table
ref="table"
v-loading="loading"
border
class="operation"
:data="readonlyGoodsList"
style="width: 100%"
>
<el-table-column prop="goodsName" label="商品名称" min-width="140" show-overflow-tooltip />
<el-table-column label="商品价格" width="110">
<template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }">
{{ $filters.unitPrice(row.originalPrice, "") }}
</span>
</template>
</el-table-column>
<el-table-column label="库存" width="90">
<template #default="{ row }">
<span v-if="row">{{ row.quantity }}</span>
</template>
</el-table-column>
<el-table-column label="活动价格" width="100">
<template #default="{ row }">
<span v-if="row" :style="{ color: $mainColor }">
{{ $filters.unitPrice(row.price, "") }}
</span>
</template>
</el-table-column>
<el-table-column label="活动场次" width="100">
<template #default="{ row }">
<el-tag v-if="row">{{ row.timeLine }}:00</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag v-if="row" :type="applyStatusTagType(row.promotionApplyStatus)" size="small">
{{ applyStatusText(row.promotionApplyStatus) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="changePage"
@size-change="changePageSize"
/>
</div>
</template>
<div class="foot-btn">
<el-button @click="closeCurrentPage">返回</el-button>
<el-button
v-if="!readonly"
type="primary"
:loading="submitLoading"
@click="save"
>
提交
</el-button>
</div>
</el-card>
<liliDialog ref="liliDialog" @selectedGoodsData="selectedGoodsData" />
</div>
</template>
@@ -84,14 +181,21 @@
import {
seckillGoodsList,
seckillDetail,
setSeckillGoods,
delSeckillGoods,
} from "@/api/promotion.js";
import liliDialog from "@/views/lili-dialog";
export default {
components: {
liliDialog,
},
data() {
return {
tabCurrent: "0",
promotionStatus: "",
loading: false,
submitLoading: false,
searchForm: {
pageNumber: 1,
pageSize: 20,
@@ -99,85 +203,220 @@ export default {
total: 0,
data: [],
goodsList: [],
readonlyGoodsList: [],
};
},
computed: {
readonly() {
return this.$route.query.mode === "view" || this.promotionStatus !== "NEW";
},
tabIndex() {
return Number(this.tabCurrent) || 0;
},
},
methods: {
seckillStatusText(status) {
const map = {
NEW: "新建",
START: "开始",
END: "结束",
CLOSE: "废弃",
NEW: "未开始",
START: "开始",
END: "结束",
CLOSE: "已关闭",
};
return map[status] || status || "-";
},
seckillStatusTagType(status) {
const map = {
NEW: "danger",
NEW: "info",
START: "success",
END: "danger",
CLOSE: "danger",
};
return map[status] || "danger";
},
applyStatusText(status) {
const map = {
APPLY: "申请中",
PASS: "已通过",
REFUSE: "已拒绝",
};
return map[status] || "未申请";
},
applyStatusTagType(status) {
const map = {
APPLY: "warning",
PASS: "success",
REFUSE: "danger",
};
return map[status] || "info";
},
closeCurrentPage() {
this.$router.back();
},
init() {
this.getSeckillMsg();
},
changePage() {
this.getDataList();
this.getReadonlyGoodsList();
},
changePageSize() {
this.searchForm.pageNumber = 1;
this.getDataList();
this.getReadonlyGoodsList();
},
getDataList() {
getSeckillMsg() {
seckillDetail(this.$route.query.id).then((res) => {
if (res.success && res.result) {
this.data = [res.result];
this.promotionStatus = res.result.promotionStatus;
if (this.readonly) {
this.getReadonlyGoodsList();
} else {
this.getManageGoodsList();
}
}
});
},
getManageGoodsList() {
if (!this.data[0]?.hours) {
return;
}
this.loading = true;
const hourValues = this.data[0].hours.split(",");
const hourLabels = this.unixHours(this.data[0].hours);
this.goodsList = hourLabels.map((hour, index) => ({
hour,
timeLine: hourValues[index],
list: [],
}));
seckillGoodsList({
seckillId: this.$route.query.id,
pageNumber: 1,
pageSize: 1000,
}).then((res) => {
this.loading = false;
if (res.success && res.result?.records?.length) {
res.result.records.forEach((item) => {
const slot = this.goodsList.find(
(g) => String(g.timeLine) === String(item.timeLine)
);
if (slot) {
slot.list.push(item);
}
});
}
});
},
getReadonlyGoodsList() {
this.loading = true;
this.searchForm.seckillId = this.$route.query.id;
seckillGoodsList(this.searchForm).then((res) => {
this.loading = false;
if (res.success && res.result) {
this.goodsList = res.result.records;
this.total = res.result.total;
this.readonlyGoodsList = res.result.records || [];
this.total = res.result.total || 0;
}
});
},
getSeckillMsg() {
seckillDetail(this.$route.query.id).then((res) => {
if (res.success && res.result) {
this.data = [];
this.data.push(res.result);
this.promotionStatus = res.result.promotionStatus;
this.getDataList();
openSkuList() {
const existing = this.goodsList[this.tabIndex]?.list || [];
this.$refs.liliDialog.goodsData = existing.map((item) => ({
...item,
id: item.skuId || item.id,
}));
this.$refs.liliDialog.open("goods");
},
selectedGoodsData(selected) {
const timeLine = this.goodsList[this.tabIndex]?.timeLine;
if (timeLine === undefined) {
return;
}
const existingMap = new Map(
(this.goodsList[this.tabIndex].list || []).map((item) => [item.skuId, item])
);
const list = selected.map((item) => {
const existed = existingMap.get(item.id);
if (existed) {
return existed;
}
return {
goodsName: item.goodsName,
price: item.price,
originalPrice: item.price,
promotionApplyStatus: item.promotionApplyStatus || "",
quantity: item.quantity,
seckillId: this.$route.query.id,
storeId: item.storeId,
storeName: item.storeName,
skuId: item.id,
timeLine,
};
});
this.goodsList[this.tabIndex].list = list;
},
updateGoodsField(tabIndex, index, field, value) {
if (this.goodsList[tabIndex]?.list?.[index]) {
this.goodsList[tabIndex].list[index][field] = value;
}
},
delGoods(tabIndex, index, row) {
if (row.promotionApplyStatus === "PASS") {
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除该商品吗?删除后不可恢复",
onOk: () => {
delSeckillGoods({
seckillId: row.seckillId,
id: row.id,
}).then((res) => {
if (res.success) {
this.goodsList[tabIndex].list.splice(index, 1);
this.$Message.success("删除成功");
}
});
},
});
return;
}
this.goodsList[tabIndex].list.splice(index, 1);
this.$Message.success("删除成功");
},
save() {
const applyVos = [];
this.goodsList.forEach((slot) => {
slot.list.forEach((item) => {
applyVos.push(item);
});
});
if (!applyVos.length) {
this.$Message.warning("请先选择活动商品");
return;
}
this.submitLoading = true;
setSeckillGoods({
seckillId: this.$route.query.id,
applyVos,
}).then((res) => {
this.submitLoading = false;
if (res?.success) {
this.$Message.success("提交活动商品成功");
this.closeCurrentPage();
}
});
},
delGoods(index, row) {
this.$Modal.confirm({
title: "确认删除",
content: "您确认要删除该商品吗?删除后不可恢复",
onOk: () => {
const params = {
seckillId: row.seckillId,
id: row.id,
};
delSeckillGoods(params).then((res) => {
if (res.success) {
this.goodsList.splice(index, 1);
this.$Message.success("删除成功!");
}
});
},
showReason(reason) {
this.$Modal.info({
title: "拒绝原因",
content: reason,
});
},
unixDate(time) {
return this.$filters.unixToDate(new Date(time) / 1000);
},
unixHours(item) {
const hourArr = item.split(",");
for (let i = 0; i < hourArr.length; i++) {
hourArr[i] += ":00";
if (!item) {
return [];
}
return hourArr;
return item.split(",").map((hour) => `${hour}:00`);
},
},
mounted() {
@@ -188,7 +427,7 @@ export default {
<style lang="scss" scoped>
.operation {
margin: 10px 0;
margin: 16px 0 10px;
}
.hour-tag {
@@ -202,6 +441,18 @@ export default {
text-decoration: none;
}
.reason {
display: block;
margin-top: 4px;
font-size: 12px;
}
.foot-btn {
margin-top: 16px;
display: flex;
gap: 10px;
}
.mt_10 {
margin-top: 10px;
}

View File

@@ -47,102 +47,74 @@
</el-card>
<el-card>
<el-tabs v-model="activeTab" class="mt_10">
<el-tab-pane label="秒杀活动列表" name="list">
<el-table
ref="table"
v-loading="loading"
border
:data="data"
class="mt_10"
style="width: 100%"
>
<el-table-column
prop="promotionName"
label="活动名称"
min-width="140"
show-overflow-tooltip
/>
<el-table-column prop="startTime" label="开始时间" width="180" />
<el-table-column prop="applyEndTime" label="申请截止时间" width="180" />
<el-table-column label="活动状态" width="110">
<template #default="{ row }">
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
{{ promotionStatusText(row.promotionStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="seckillRule"
label="申请规则"
min-width="120"
show-overflow-tooltip
/>
<el-table-column label="操作" width="250" align="center" fixed="right">
<template #default="{ row }">
<template v-if="row">
<a
v-if="row.promotionStatus === 'CLOSE' || row.promotionStatus === 'NEW'"
class="link-text"
@click="edit(row)"
>
编辑
</a>
<a v-else class="link-text" @click="manage(row)">查看</a>
<span v-if="row.promotionStatus" class="op-split">|</span>
<a
v-if="row.promotionStatus == 'NEW'"
class="link-text"
@click="manage(row)"
>
管理
</a>
<span v-if="row.promotionStatus == 'NEW'" class="op-split">|</span>
<a
v-if="row.promotionStatus == 'START' || row.promotionStatus == 'NEW'"
class="link-text"
@click="off(row)"
>
关闭
</a>
</template>
</template>
</el-table-column>
</el-table>
<el-table
ref="table"
v-loading="loading"
border
:data="data"
class="mt_10"
style="width: 100%"
>
<el-table-column
prop="promotionName"
label="活动名称"
min-width="140"
show-overflow-tooltip
/>
<el-table-column prop="startTime" label="开始时间" width="180" />
<el-table-column prop="applyEndTime" label="申请截止时间" width="180" />
<el-table-column label="活动状态" width="110">
<template #default="{ row }">
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
{{ promotionStatusText(row.promotionStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="seckillRule"
label="申请规则"
min-width="120"
show-overflow-tooltip
/>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<template v-if="row">
<a
v-if="row.promotionStatus === 'NEW'"
class="link-text"
@click="manage(row)"
>
管理
</a>
<a v-else class="link-text" @click="view(row)">查看</a>
</template>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="changePage"
@size-change="changePageSize"
/>
</div>
</el-tab-pane>
<el-tab-pane label="秒杀活动设置" name="setup" lazy>
<setupSeckill />
</el-tab-pane>
</el-tabs>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="changePage"
@size-change="changePageSize"
/>
</div>
</el-card>
</div>
</template>
<script>
import { seckillList } from "@/api/promotion";
import setupSeckill from "@/views/promotion/seckill/seckill-setup";
export default {
name: "seckill",
components: {
setupSeckill,
},
data() {
return {
activeTab: "list",
selectDate: [],
loading: true,
searchForm: {
@@ -191,40 +163,12 @@ export default {
this.searchForm.pageSize = 20;
this.getDataList();
},
edit(v) {
this.$router.push({ name: "manager-seckill-add", query: { id: v.id } });
view(v) {
this.$router.push({ name: "seckill-goods", query: { id: v.id, mode: "view" } });
},
manage(v) {
this.$router.push({ name: "seckill-goods", query: { id: v.id } });
},
off(v) {
this.$Modal.confirm({
title: "提示",
content: "您确定要下架该活动吗?",
onOk: () => {
updateSeckillStatus(v.id).then((res) => {
if (res.success) {
this.$Message.success("下架成功");
this.getDataList();
}
});
},
});
},
expire(v) {
this.$Modal.confirm({
title: "提示",
content: "您确定要作废该活动吗?",
onOk: () => {
delSeckill(v.id).then((res) => {
if (res.success) {
this.$Message.success("作废成功");
this.getDataList();
}
});
},
});
},
getDataList() {
this.loading = true;
if (this.selectDate && this.selectDate[0] && this.selectDate[1]) {
@@ -234,7 +178,7 @@ export default {
this.searchForm.startTime = null;
this.searchForm.endTime = null;
}
getSeckillList(this.searchForm)
seckillList(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records;