refactor(seller): 列表页抽离 useTable 重构与通用 UI 规范优化

- 抽离通用 useTable composable,并迁移 13 个典型列表页(采购/报损/订单/售后/统计/营销等)
- 增加表格与树组件全局空状态 (el-empty) 样式规范
- 补齐视图图片语义化 alt,并添加全局图片加载异常 SVG 占位兜底
- 替换遗留的 $Message 兼容语法为 ElMessage
- 将 WAP 店铺装修模板的 Options API 统一为 defineOptions
- 优化数据报表卡片视觉样式,并移除失效的品牌筛选选项
- 清理冗余依赖 vuex,将 sass-loader 调整至 devDependencies
This commit is contained in:
pikachu1995@126.com
2026-10-20 13:51:04 +08:00
parent 7d9bf776db
commit 3cb60139d5
49 changed files with 543 additions and 479 deletions

View File

@@ -26,7 +26,6 @@
"nprogress": "^0.2.0",
"pinia": "^4.0.3",
"sass": "^1.63.6",
"sass-loader": "^10.4.1",
"swiper": "^6.3.5",
"uuid": "^8.3.2",
"vue": "^3.5.35",
@@ -39,7 +38,6 @@
"@eslint/js": "^10.0.1",
"@vue/cli-plugin-babel": "^5.0.9",
"@vue/cli-plugin-router": "^5.0.9",
"@vue/cli-plugin-vuex": "^5.0.9",
"@vue/cli-service": "^5.0.9",
"@vue/compiler-sfc": "^3.5.35",
"compression-webpack-plugin": "^10.0.0",
@@ -49,6 +47,7 @@
"globals": "^17.13.0",
"less": "^4.2.0",
"less-loader": "^11.1.4",
"sass-loader": "^10.4.1",
"style-loader": "^3.3.4",
"style-resources-loader": "^1.5.0",
"unplugin-vue-components": "0.27.5",

View File

@@ -5,6 +5,7 @@ import App from "./App.vue";
import { router } from "./router/index";
import { setupElementPlus } from "@/plugins/element";
import { setupLegacyMessage } from "@/utils/message";
import { setupImgFallback } from "@/utils/imgFallback";
import liliDialog from "@/views/lili-dialog";
import PriceColorScheme from "@/components/price-color-scheme.vue";
import { install as installVueQr } from "vue-qr";
@@ -39,6 +40,7 @@ const app = createApp(App);
setupElementPlus(app);
setupLegacyMessage(app);
setupImgFallback();
installVueQr(app);
app.use(router);

View File

@@ -112,3 +112,30 @@
.el-overlay.is-message-box {
z-index: 4000 !important;
}
/* 统一空状态:表格 / 树 / 列表无数据时的展示(未自定义 #empty 插槽的页面均生效) */
.el-table__empty-block {
min-height: 160px;
}
.el-table__empty-text,
.el-tree__empty-text {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: 1.5;
color: #909399;
font-size: 14px;
&::before {
content: "";
display: block;
width: 64px;
height: 64px;
margin-bottom: 8px;
opacity: 0.7;
background: no-repeat center / contain
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64' fill='none' stroke='%23c0c4cc' stroke-width='2' stroke-linejoin='round'><path d='M8 36l8-20h32l8 20v12a4 4 0 0 1-4 4H12a4 4 0 0 1-4-4V36z'/><path d='M8 36h16a8 8 0 0 0 16 0h16'/></svg>");
}
}

View File

@@ -0,0 +1,31 @@
/**
* 全局图片加载失败占位:
* 在捕获阶段监听 <img> 的 error 事件(error 不冒泡),失败时替换为内联 SVG 占位图。
* - 每张图片只替换一次,占位图自身加载失败不会再次触发,避免死循环
* - 空 src 不处理
* - 需要自行处理 error 的图片可添加 data-no-fallback 属性跳过
*/
const PLACEHOLDER =
"data:image/svg+xml;utf8," +
encodeURIComponent(
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64' fill='none' stroke='#c0c4cc' stroke-width='2' stroke-linejoin='round'>" +
"<rect width='64' height='64' fill='#f5f7fa' stroke='none'/>" +
"<rect x='14' y='18' width='36' height='28' rx='3'/>" +
"<circle cx='25' cy='28' r='3'/>" +
"<path d='M14 42l10-9 8 7 6-5 12 9'/></svg>"
);
export function setupImgFallback() {
document.addEventListener(
"error",
(event) => {
const el = event.target;
if (!el || el.tagName !== "IMG") return;
if (el.dataset.noFallback !== undefined || el.dataset.fallbackApplied) return;
if (!el.getAttribute("src")) return;
el.dataset.fallbackApplied = "1";
el.src = PLACEHOLDER;
},
true
);
}

View File

@@ -0,0 +1,119 @@
import { onMounted, reactive, ref } from "vue";
/**
* 分页列表通用逻辑:loading / data / total / searchForm / 查询 / 重置 / 刷新。
*
* 用法:
* const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } =
* useTable(getXxxPage, { form: { name: "", status: "" } });
*
* <el-table v-loading="loading" :data="data" />
* <el-pagination
* v-model:current-page="searchForm.pageNumber"
* v-model:page-size="searchForm.pageSize"
* :total="total"
* @current-change="getDataList"
* @size-change="getDataList"
* />
*
* @param {(params: object) => Promise<any>} fetcher 请求函数,入参为 searchForm
* @param {object} [options]
* @param {object} [options.form] 查询条件默认值(会与分页/排序默认值合并)
* @param {number} [options.pageSize=10] 默认每页条数
* @param {string} [options.sort] 默认排序字段
* @param {string} [options.order] 默认排序方向
* @param {boolean} [options.immediate=true] 挂载后是否自动查询
* @param {(res: any) => ({records: any[], total: number}|null|undefined)} [options.resolve]
* 从响应中取出 { records, total };返回空表示请求失败(保持原数据)。
* 默认:res.success 为真时取 res.result.records / res.result.total
* @param {(params: object) => object} [options.beforeRequest] 请求前加工参数(返回新对象,不影响 searchForm)
* @param {(res: any, parsed: {records: any[], total: number}) => void} [options.onSuccess]
* 查询成功且数据已更新后回调(仅最后一次请求触发),可用于刷新状态计数等
*/
export function useTable(fetcher, options = {}) {
const {
form = {},
pageSize = 10,
sort,
order,
immediate = true,
resolve = defaultResolve,
beforeRequest,
onSuccess,
} = options;
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const initialForm = { ...form };
const searchForm = reactive({
pageNumber: 1,
pageSize,
...(sort ? { sort } : {}),
...(order ? { order } : {}),
...form,
});
// 只采纳最后一次请求的结果,避免快速切换页码/条件时旧响应覆盖新数据
let requestId = 0;
function getDataList() {
const current = ++requestId;
loading.value = true;
const params = beforeRequest ? beforeRequest({ ...searchForm }) : searchForm;
return Promise.resolve(fetcher(params))
.then((res) => {
if (current !== requestId) return res;
const parsed = resolve(res);
if (parsed) {
data.value = parsed.records || [];
total.value = parsed.total || 0;
if (onSuccess) onSuccess(res, parsed);
}
return res;
})
.finally(() => {
if (current === requestId) loading.value = false;
});
}
/** 以当前条件从第 1 页重新查询 */
function handleSearch() {
searchForm.pageNumber = 1;
return getDataList();
}
/** 恢复查询条件默认值(保留当前每页条数)并从第 1 页查询 */
function resetSearch() {
Object.assign(searchForm, initialForm);
return handleSearch();
}
/** 保持当前页刷新(如删除/修改后) */
function refresh() {
return getDataList();
}
if (immediate) {
onMounted(getDataList);
}
return {
loading,
data,
total,
searchForm,
getDataList,
handleSearch,
resetSearch,
refresh,
};
}
function defaultResolve(res) {
if (res && res.success && res.result) {
return { records: res.result.records, total: res.result.total };
}
return null;
}

View File

@@ -2,7 +2,7 @@
<div class="forget-password" @click="hideVerify">
<div style="height: 50px"></div>
<div class="logo-box">
<img :src="logoImg" width="150" @click="$router.push('/')" />
<img alt="店铺 Logo" :src="logoImg" width="150" @click="$router.push('/')" />
<div>修改密码</div>
</div>
<div class="login-container">
@@ -119,7 +119,7 @@
import { computed, onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { Iphone, Message, Lock } from "@element-plus/icons-vue";
import { Message as $Message } from "@/utils/message";
import { ElMessage } from "element-plus";
import { md5 } from "@/utils/md5.js";
import * as RegExp from "@/utils/regular.js";
import * as apiLogin from "@/api/index.js";
@@ -188,7 +188,7 @@ function next() {
if (res.success) {
step.value = 1;
} else {
$Message.warning(res.message);
ElMessage.warning(res.message);
}
})
.catch(() => {
@@ -203,7 +203,7 @@ function handleSubmit() {
if (valid) {
const params = JSON.parse(JSON.stringify(form));
if (params.password !== params.oncePasd) {
$Message.warning("两次输入密码不一致");
ElMessage.warning("两次输入密码不一致");
return;
}
params.mobile = formFirst.mobile;
@@ -216,7 +216,7 @@ function handleSubmit() {
.then((res) => {
loading1.value = false;
if (res.success) {
$Message.success("修改密码成功");
ElMessage.success("修改密码成功");
router.push("login");
}
})
@@ -230,11 +230,11 @@ function handleSubmit() {
function sendCode() {
if (time.value === 60) {
if (formFirst.mobile === "") {
$Message.warning("请先填写手机号");
ElMessage.warning("请先填写手机号");
return;
}
if (!verifyStatus.value) {
$Message.warning("请先完成安全验证");
ElMessage.warning("请先完成安全验证");
return;
}
const params = {
@@ -244,7 +244,7 @@ function sendCode() {
sendSms(params)
.then((res) => {
if (res.success) {
$Message.success("验证码发送成功");
ElMessage.success("验证码发送成功");
interval.value = setInterval(() => {
time.value--;
if (time.value === 0) {
@@ -257,7 +257,7 @@ function sendCode() {
}
}, 1000);
} else {
$Message.warning(res.message);
ElMessage.warning(res.message);
}
})
.catch(() => {});

View File

@@ -6,7 +6,7 @@
<div class="main" :class="{ 'menu-collapsed': !showSubMenu }">
<div class="sidebar-menu-con menu-bar">
<div class="logo-con">
<img :src="storeSideLogo" key="max-logo" />
<img alt="店铺 Logo" :src="storeSideLogo" key="max-logo" />
</div>
<shrinkable-menu />
</div>

View File

@@ -175,7 +175,7 @@
<el-table-column prop="id" label="商品ID" width="200" show-overflow-tooltip />
<el-table-column label="商品图片" width="100" align="center">
<template #default="{ row }">
<img
<img alt="商品图片"
v-if="row && row.original"
:src="row.original"
style="height: 50px; width: 50px; object-fit: cover"

View File

@@ -1,7 +1,7 @@
<template>
<div ref="rootEl">
<el-dialog v-model="visible" title="预览图片">
<img v-if="visible" :src="previewPicture" style="width: 100%">
<img alt="预览图片" v-if="visible" :src="previewPicture" style="width: 100%">
</el-dialog>
<div class="goods-publish-page">
<div ref="sectionNav" class="goods-section-nav">
@@ -111,7 +111,7 @@
>
<template #item="{ element }">
<div class="demo-upload-list">
<img :src="element" @error="handleGoodsPictureError($event, element)" />
<img alt="商品图片" data-no-fallback :src="element" @error="handleGoodsPictureError($event, element)" />
<div class="demo-upload-list-cover">
<div>
<el-button link type="primary" @click="handleViewGoodsPicture(element)">预览</el-button>
@@ -139,7 +139,7 @@
<el-button @click="handleCLickImg('goodsGalleryFiles')" type="primary">上传图片</el-button>
</div>
<el-dialog v-model="goodsPictureVisible" title="View Image">
<img v-if="goodsPictureVisible" :src="previewGoodsPicture" style="width: 100%" />
<img alt="商品图片预览" v-if="goodsPictureVisible" :src="previewGoodsPicture" style="width: 100%" />
</el-dialog>
</el-form-item>
<el-form-item>
@@ -186,7 +186,7 @@
<el-form-item :label="contentImage" class="form-item-view-el">
<!-- {{item.url}} -->
<div v-for="(item, index) in listImages.images" :key="index" style="width:100%;display:flex;">
<img :src="item.url" style="width:100px;flex:1;margin-top:10px;cursor:pointer;"
<img alt="图片" :src="item.url" style="width:100px;flex:1;margin-top:10px;cursor:pointer;"
@click="handleView(item.url)" />
</div>
</el-form-item>
@@ -415,7 +415,7 @@
class="sku-upload-list"
style="width: 180px;height: 140px"
>
<img :src="element" style="width: 180px;height: 140px" />
<img alt="商品图片" :src="element" style="width: 180px;height: 140px" />
<div class="sku-upload-list-cover">
<div style="margin-top: 50px">
<el-icon :size="25" style="cursor:pointer;margin-right:8px" @click="handleView(element)"><ZoomIn /></el-icon>

View File

@@ -126,7 +126,7 @@ import { sendSms } from "@/api/common.js";
import { login, storeSmsLogin, userMsg } from "@/api/index";
import util from "@/libs/util.js";
import { setStore } from "@/libs/storage";
import { Message as $Message } from "@/utils/message";
import { ElMessage } from "element-plus";
import { md5 } from "@/utils/md5.js";
import Footer from "@/views/main-components/footer";
import Header from "@/views/main-components/header";
@@ -238,7 +238,7 @@ function afterLogin(res) {
function sendCode() {
if (!isMobileValid.value) {
$Message.warning("请输入正确的手机号");
ElMessage.warning("请输入正确的手机号");
return;
}
if (!verifyStatus.value) {
@@ -255,7 +255,7 @@ function sendCode() {
.then((res) => {
if (res.success) {
smsCodeSent.value = true;
$Message.success("验证码发送成功");
ElMessage.success("验证码发送成功");
interval = setInterval(() => {
time.value--;
if (time.value === 0) {
@@ -269,7 +269,7 @@ function sendCode() {
}
}, 1000);
} else {
$Message.warning(res.message);
ElMessage.warning(res.message);
}
})
.catch(() => {

View File

@@ -1,7 +1,7 @@
<template>
<div>
<el-row class="header">
<img class="logo" :src="storeSideLogo" />
<img alt="店铺 Logo" class="logo" :src="storeSideLogo" />
</el-row>
</div>
</template>

View File

@@ -105,7 +105,8 @@
</template>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { onMounted, ref, watch } from "vue";
import { useTable } from "@/utils/useTable";
import { useRoute } from "vue-router";
import * as API_Order from "@/api/order";
import vueQr from "vue-qr";
@@ -115,15 +116,14 @@ defineOptions({ name: "orderComplaint" });
const route = useRoute();
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
const { loading, data, total, searchForm, getDataList } = useTable(API_Order.getOrderComplain, {
pageSize: 20,
sort: "createTime",
order: "desc",
immediate: false,
form: {
sort: "createTime",
order: "desc",
},
});
const data = ref([]);
const total = ref(0);
// searchFormRef:模板 ref 改名避免与 searchForm 数据互相覆盖
const searchFormRef = ref(null);
@@ -178,20 +178,6 @@ function handleSearch() {
searchForm.pageSize = 20;
getDataList();
}
function getDataList() {
loading.value = true;
API_Order.getOrderComplain(searchForm)
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
})
.catch(() => {
loading.value = false;
});
}
function detail(v) {
customRouterPush({
name: "order-complaint-detail",

View File

@@ -154,7 +154,8 @@
</template>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import { useTable } from "@/utils/useTable";
import { useRoute } from "vue-router";
import * as API_Order from "@/api/order";
import { customRouterPush } from "@/utils/filters";
@@ -163,25 +164,24 @@ defineOptions({ name: "after-sale-order" });
const route = useRoute();
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
const { loading, data, total, searchForm, getDataList } = useTable(API_Order.getAfterSaleOrderPage, {
pageSize: 20,
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
serviceType: "RETURN_GOODS",
orderSn: "",
memberName: "",
serviceStatus: "",
storeName: "",
sn: "",
keywords: "",
immediate: false,
form: {
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
serviceType: "RETURN_GOODS",
orderSn: "",
memberName: "",
serviceStatus: "",
storeName: "",
sn: "",
keywords: "",
},
});
const selectDate = ref(null);
const data = ref([]);
const total = ref(0);
const currentStatus = ref("");
const afterSaleNumData = ref({});
@@ -284,16 +284,6 @@ function selectDateRange(v) {
searchForm.endDate = "";
}
}
function getDataList() {
loading.value = true;
API_Order.getAfterSaleOrderPage(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function getAfterSaleNumData() {
const { serviceStatus, ...searchParams } = searchForm;
API_Order.getAfterSaleNumVO(searchParams).then((res) => {

View File

@@ -9,7 +9,7 @@
<dt>{{ isRefundOnly ? '退款商品' : '退货商品' }}</dt>
<dd>
<div>
<img :src="afterSaleInfo.goodsImage" style="height: 60px">
<img alt="商品图片" :src="afterSaleInfo.goodsImage" style="height: 60px">
</div>
<a @click="linkTo(afterSaleInfo.goodsId, afterSaleInfo.skuId)">{{ afterSaleInfo.goodsName }}
</a><br>
@@ -59,7 +59,7 @@
<dd v-if="afterSaleImage == ''">暂无凭证</dd>
<dd v-else>
<div class="div-img" v-for="(item, index) in afterSaleImage" :key="index">
<img class="complain-img" :src="item" />
<img alt="投诉凭证" class="complain-img" :src="item" />
</div>
</dd>
</dl>

View File

@@ -237,7 +237,7 @@
<tr v-for="row in data" :key="row.sn || row.id">
<td>
<div class="order-goods-cell">
<img :src="row.image" class="order-goods-img" />
<img alt="商品图片" :src="row.image" class="order-goods-img" />
<div class="order-goods-meta">
<div class="div-zoom">
<a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>

View File

@@ -157,7 +157,8 @@
</template>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import { useTable } from "@/utils/useTable";
import { useRoute } from "vue-router";
import { CopyDocument } from "@element-plus/icons-vue";
import * as API_Order from "@/api/order";
@@ -169,25 +170,24 @@ defineOptions({ name: "orderList" });
const route = useRoute();
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
const { loading, data, total, searchForm, getDataList } = useTable(API_Order.getOrderList, {
pageSize: 20,
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
orderType: "",
orderSn: "",
keywords: "",
goodsType: "",
orderStatus: "",
paymentMethod: "",
orderPromotionType: "",
immediate: false,
form: {
sort: "createTime",
order: "desc",
startDate: "",
endDate: "",
orderType: "",
orderSn: "",
keywords: "",
goodsType: "",
orderStatus: "",
paymentMethod: "",
orderPromotionType: "",
},
});
const selectDate = ref(null);
const data = ref([]);
const total = ref(0);
const orderNumData = ref({});
const currentStatus = ref("ALL");
@@ -362,20 +362,6 @@ function selectDateRange(v) {
searchForm.endDate = "";
}
}
function getDataList() {
loading.value = true;
API_Order.getOrderList(searchForm)
.then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
})
.catch(() => {
loading.value = false;
});
}
function detail(v, openFulfill = false) {
customRouterPush({
name: "order-detail",

View File

@@ -127,29 +127,29 @@
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { onMounted, ref } from "vue";
import { useTable } from "@/utils/useTable";
import * as API_Order from "@/api/order";
import { Message, Modal } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
defineOptions({ name: "fictitiousOrderList" });
const loading = ref(true);
const searchForm = reactive({
pageNumber: 1,
const { loading, data, total, searchForm, getDataList } = useTable(API_Order.getOrderList, {
pageSize: 20,
sort: "",
order: "",
startDate: "",
endDate: "",
orderType: "VIRTUAL",
orderSn: "",
buyerName: "",
orderStatus: "",
immediate: false,
form: {
sort: "",
order: "",
startDate: "",
endDate: "",
orderType: "VIRTUAL",
orderSn: "",
buyerName: "",
orderStatus: "",
},
});
const selectDate = ref(null);
const data = ref([]);
const total = ref(0);
const orderStatus = ref([
{ title: "全部", value: "" },
{ title: "未付款", value: "UNPAID" },
@@ -194,16 +194,6 @@ function selectDateRange(v) {
searchForm.endDate = v[1];
}
}
function getDataList() {
loading.value = true;
API_Order.getOrderList(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function confirmPrice(v) {
Modal.confirm({
title: "确认收款",

View File

@@ -88,6 +88,7 @@
<script setup>
import { computed, onMounted, ref } from "vue";
import { useTable } from "@/utils/useTable";
import { ElMessageBox } from "element-plus";
import {
getDamageReportPage,
@@ -112,16 +113,21 @@ defineOptions({ name: "procurement-damage-report-list" });
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const dateRange = ref([]);
const currentStatus = ref("ALL");
const statusCountData = ref({});
const searchForm = ref({
pageNumber: 1,
pageSize: 10,
status: "",
const {
loading,
data,
total,
searchForm,
getDataList,
handleSearch: search,
resetSearch: reset,
} = useTable(getDamageReportPage, {
form: { status: "" },
immediate: false,
beforeRequest: (p) => buildSearchParams(true, p),
});
const statusTabs = computed(() => {
@@ -146,8 +152,8 @@ function init() {
getDataList();
}
function buildSearchParams(includeStatus = true) {
const params = { ...searchForm.value };
function buildSearchParams(includeStatus = true, base = searchForm) {
const params = { ...base };
if (!includeStatus) {
delete params.status;
}
@@ -159,17 +165,15 @@ function buildSearchParams(includeStatus = true) {
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getStatusCount();
getDataList();
search();
}
function resetSearch() {
searchForm.value = { pageNumber: 1, pageSize: 10, status: "" };
dateRange.value = [];
currentStatus.value = "ALL";
getStatusCount();
getDataList();
reset();
}
function onStatusTabClick(tab) {
@@ -178,13 +182,13 @@ function onStatusTabClick(tab) {
function statusClick(name) {
if (name === "ALL" || name === "" || name === undefined) {
searchForm.value.status = "";
searchForm.status = "";
currentStatus.value = "ALL";
} else {
searchForm.value.status = name;
searchForm.status = name;
currentStatus.value = name;
}
searchForm.value.pageNumber = 1;
searchForm.pageNumber = 1;
getDataList();
}
@@ -196,20 +200,6 @@ function getStatusCount() {
});
}
function getDataList() {
loading.value = true;
getDamageReportPage(buildSearchParams(true))
.then((res) => {
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
function refreshList() {
getStatusCount();
getDataList();

View File

@@ -49,56 +49,27 @@
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { ref } from "vue";
import { useTable } from "@/utils/useTable";
import { getProcurementInboundPage } from "@/api/procurement";
import { formatMoney } from "../constants";
import { customRouterPush } from "@/utils/filters";
defineOptions({ name: "procurement-inbound-list" });
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const searchForm = reactive({
pageNumber: 1,
pageSize: 10,
inboundSn: "",
sort: "createTime",
order: "desc",
});
const {
loading,
data,
total,
searchForm,
getDataList,
handleSearch,
resetSearch,
} = useTable(getProcurementInboundPage, { form: { inboundSn: "" }, sort: "createTime", order: "desc" });
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖
const searchFormRef = ref(null);
onMounted(() => {
getDataList();
});
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function resetSearch() {
searchForm.inboundSn = "";
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
getProcurementInboundPage(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
function goCreate() {
customRouterPush({ name: "procurement-inbound-operation" });
}

View File

@@ -34,39 +34,16 @@
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { ref } from "vue";
import { useTable } from "@/utils/useTable";
import { createInventoryCount, getInventoryCountPage } from "@/api/procurement";
import { Message, Modal } from "@/utils/message";
import { customRouterPush } from "@/utils/filters";
defineOptions({ name: "procurement-inventory-count-list" });
const loading = ref(false);
const creating = ref(false);
const data = ref([]);
const total = ref(0);
const searchForm = reactive({
pageNumber: 1,
pageSize: 10,
});
onMounted(() => {
getDataList();
});
function getDataList() {
loading.value = true;
getInventoryCountPage(searchForm)
.then((res) => {
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
const { loading, data, total, searchForm, getDataList } = useTable(getInventoryCountPage);
function handleCreate() {
Modal.confirm({

View File

@@ -96,6 +96,7 @@
<script setup>
import { computed, onMounted, ref } from "vue";
import { useTable } from "@/utils/useTable";
import { useRoute } from "vue-router";
import {
getProcurementOrderPage,
@@ -117,20 +118,21 @@ defineOptions({ name: "procurement-purchase-order-list" });
const route = useRoute();
const loading = ref(false);
const data = ref([]);
const total = ref(0);
const createRange = ref([]);
const currentStatus = ref("ALL");
const statusCountData = ref({});
const searchForm = ref({
pageNumber: 1,
pageSize: 10,
orderSn: "",
goodsName: "",
status: "",
sort: "createTime",
order: "desc",
const {
loading,
data,
total,
searchForm,
getDataList,
handleSearch: search,
resetSearch: reset,
} = useTable(getProcurementOrderPage, {
form: { orderSn: "", goodsName: "", status: "" }, sort: "createTime", order: "desc",
immediate: false,
beforeRequest: (p) => buildSearchParams(true, p),
});
// searchFormRef:模板 ref 改名避免与顶层变量互相覆盖
@@ -153,7 +155,7 @@ onMounted(() => {
const status = route.query.status;
if (status) {
currentStatus.value = status;
searchForm.value.status = status;
searchForm.status = status;
}
init();
});
@@ -163,8 +165,8 @@ function init() {
getDataList();
}
function buildSearchParams(includeStatus = true) {
const params = { ...searchForm.value };
function buildSearchParams(includeStatus = true, base = searchForm) {
const params = { ...base };
if (!includeStatus) {
delete params.status;
}
@@ -176,25 +178,15 @@ function buildSearchParams(includeStatus = true) {
}
function handleSearch() {
searchForm.value.pageNumber = 1;
getStatusCount();
getDataList();
search();
}
function resetSearch() {
searchForm.value = {
pageNumber: 1,
pageSize: 10,
orderSn: "",
goodsName: "",
status: "",
sort: "createTime",
order: "desc",
};
createRange.value = [];
currentStatus.value = "ALL";
getStatusCount();
getDataList();
reset();
}
function onStatusTabClick(tab) {
@@ -203,13 +195,13 @@ function onStatusTabClick(tab) {
function statusClick(name) {
if (name === "ALL" || name === "" || name === undefined) {
searchForm.value.status = "";
searchForm.status = "";
currentStatus.value = "ALL";
} else {
searchForm.value.status = name;
searchForm.status = name;
currentStatus.value = name;
}
searchForm.value.pageNumber = 1;
searchForm.pageNumber = 1;
getDataList();
}
@@ -221,20 +213,6 @@ function getStatusCount() {
});
}
function getDataList() {
loading.value = true;
getProcurementOrderPage(buildSearchParams(true))
.then((res) => {
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
})
.finally(() => {
loading.value = false;
});
}
function refreshList() {
getStatusCount();
getDataList();

View File

@@ -51,45 +51,32 @@
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { ref } from "vue";
import { useTable } from "@/utils/useTable";
import { useRouter } from "vue-router";
import { getFlashDiscountList, updateFlashDiscountStatus } from "@/api/promotion.js";
import { Message } from "@/utils/message";
const router = useRouter();
const loading = ref(false);
const total = ref(0);
const data = ref([]);
const searchForm = reactive({ pageNumber: 1, pageSize: 20, sort: "createTime", order: "desc" });
const {
loading,
data,
total,
searchForm,
getDataList,
handleSearch,
} = useTable(getFlashDiscountList, { pageSize: 20, sort: "createTime", order: "desc" });
// searchFormRef:模板 ref 改名避免与顶层变量 searchForm 互相覆盖
const searchFormRef = ref(null);
onMounted(() => {
getDataList();
});
function statusText(s) {
return { NEW: "未开始", START: "进行中", END: "已结束", CLOSE: "已关闭" }[s] || s;
}
function statusType(s) {
return { NEW: "info", START: "success", END: "danger", CLOSE: "danger" }[s] || "info";
}
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
getFlashDiscountList(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function goAdd() {
router.push({ name: "flash-discount-add" });
}

View File

@@ -40,7 +40,7 @@
<el-form-item label="分享卡片封面" prop="feedsImg">
<div class="form-field-block">
<div class="upload-list" v-if="liveForm.feedsImg">
<img :src="liveForm.feedsImg">
<img alt="直播间列表图" :src="liveForm.feedsImg">
<div class="upload-list-cover" @click="handleView(liveForm.feedsImg)">
<span class="view-icon">查看</span>
</div>
@@ -54,7 +54,7 @@
<el-form-item label="直播间背景墙" prop="coverImg">
<div class="form-field-block">
<div class="upload-list" v-if="liveForm.coverImg">
<img :src="liveForm.coverImg">
<img alt="直播间背景图" :src="liveForm.coverImg">
<div class="upload-list-cover" @click="handleView(liveForm.coverImg)">
<span class="view-icon">查看</span>
</div>
@@ -66,7 +66,7 @@
<el-form-item label="直播间分享图" prop="shareImg">
<div class="form-field-block">
<div class="upload-list" v-if="liveForm.shareImg">
<img :src="liveForm.shareImg">
<img alt="直播间分享图" :src="liveForm.shareImg">
<div class="upload-list-cover" @click="handleView(liveForm.shareImg)">
<span class="view-icon">查看</span>
</div>
@@ -82,7 +82,7 @@
<template #default="{ row, $index }">
<div v-if="row" class="flex-goods">
<el-badge v-if="$index == 0 || $index == 1" is-dot type="danger" />
<img class="thumbnail" :src="row.thumbnail || row.goodsImage">
<img alt="商品图片" class="thumbnail" :src="row.thumbnail || row.goodsImage">
{{ row.goodsName || row.name }}
</div>
</template>
@@ -114,7 +114,7 @@
</el-card>
<el-dialog v-model="imageVisible" title="查看图片" width="600px">
<img :src="imageSrc" v-if="imageVisible" style="width: 100%">
<img alt="预览图片" :src="imageSrc" v-if="imageVisible" style="width: 100%">
</el-dialog>
</div>
</template>

View File

@@ -55,25 +55,26 @@
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { ref } from "vue";
import { useTable } from "@/utils/useTable";
import { useRouter } from "vue-router";
import { getNthItemDiscountList, updateNthItemDiscountStatus } from "@/api/promotion.js";
import { Message } from "@/utils/message";
const router = useRouter();
const loading = ref(false);
const total = ref(0);
const data = ref([]);
const searchForm = reactive({ pageNumber: 1, pageSize: 20, sort: "createTime", order: "desc" });
const {
loading,
data,
total,
searchForm,
getDataList,
handleSearch,
} = useTable(getNthItemDiscountList, { pageSize: 20, sort: "createTime", order: "desc" });
// searchFormRef:模板 ref 改名避免与顶层变量 searchForm 互相覆盖
const searchFormRef = ref(null);
onMounted(() => {
getDataList();
});
function statusText(s) {
return { NEW: "未开始", START: "进行中", END: "已结束", CLOSE: "已关闭" }[s] || s;
}
@@ -83,20 +84,6 @@ function statusType(s) {
function discountTypeText(t) {
return { HALF: "半价", FREE: "免单", RATE: "打折" }[t] || t;
}
function handleSearch() {
searchForm.pageNumber = 1;
getDataList();
}
function getDataList() {
loading.value = true;
getNthItemDiscountList(searchForm).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records;
total.value = res.result.total;
}
});
}
function goAdd() {
router.push({ name: "nth-item-discount-add" });
}

View File

@@ -13,11 +13,12 @@
<el-button type="primary" @click="createTemp">添加页面</el-button>
</div>
<el-table v-loading="loading" :data="list" style="width: 100%">
<el-table-column prop="name" label="页面名称" min-width="200" show-overflow-tooltip>
<el-table-column prop="name" label="页面名称" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row">{{ row.name || "暂无模板昵称" }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" align="center" />
<el-table-column
v-if="searchForm.pageType !== 'SPECIAL'"
label="状态"

View File

@@ -2,7 +2,7 @@
<div class="hotzone-box">
<div class="hotzone-item">
<div ref="content" class="hz-m-wrap">
<img class="hz-u-img" :src="image" />
<img alt="热区背景图" class="hz-u-img" :src="image" />
<ul class="hz-m-area" v-add-item>
<zone
class="hz-m-item"

View File

@@ -36,10 +36,10 @@
@mouseup.stop
@mousemove.stop
>
<img width="17" height="17" src="../assets/styles/icons8-edit-64.png" />
<img alt="编辑" width="17" height="17" src="../assets/styles/icons8-edit-64.png" />
</li>
<li class="hz-flex-img">
<img class="hz-u-img" :src="zoneForm.img" />
<img alt="热区图片" class="hz-u-img" :src="zoneForm.img" />
</li>
<li class="hz-u-square hz-u-square-tl" data-pointer="dealTL"></li>
<li class="hz-u-square hz-u-square-tc" data-pointer="dealTC"></li>

View File

@@ -8,7 +8,7 @@
<el-carousel autoplay height="340px">
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
<div style="overflow: hidden">
<img :src="item.img" width="790" height="340" />
<img alt="轮播图" :src="item.img" width="790" height="340" />
</div>
</el-carousel-item>
</el-carousel>

View File

@@ -8,7 +8,7 @@
<el-carousel autoplay @change="autoChange">
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
<div style="overflow: hidden">
<img :src="item.img" width="1200" height="470" />
<img alt="轮播图" :src="item.img" width="1200" height="470" />
</div>
</el-carousel-item>
</el-carousel>

View File

@@ -8,7 +8,7 @@
<el-carousel autoplay height="340px">
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
<div style="overflow: hidden">
<img :src="item.img" width="590" height="470" />
<img alt="轮播图" :src="item.img" width="590" height="470" />
</div>
</el-carousel-item>
</el-carousel>
@@ -26,13 +26,13 @@
:key="index"
>
<div class="mb_10">
<img :src="item[0].img" width="190" height="150" />
<img alt="轮播图" :src="item[0].img" width="190" height="150" />
</div>
<div class="mb_10">
<img :src="item[1].img" width="190" height="150" />
<img alt="轮播图" :src="item[1].img" width="190" height="150" />
</div>
<div>
<img :src="item[2].img" width="190" height="150" />
<img alt="轮播图" :src="item[2].img" width="190" height="150" />
</div>
</el-carousel-item>
</el-carousel>

View File

@@ -100,7 +100,7 @@
class="draggable"
>
<div class="flex">
<img :src="item.img" class="column-img" />
<img alt="楼层图片" :src="item.img" class="column-img" />
<div class="column-goods-list">
<div><el-input v-model="item.title" /></div>
<div><el-input v-model="item.desc" /></div>
@@ -139,7 +139,7 @@
class="draggable"
>
<div class="flex">
<img :src="item.img" class="column-img" />
<img alt="楼层图片" :src="item.img" class="column-img" />
<div class="column-goods-list">
<el-button
size="small"

View File

@@ -25,7 +25,7 @@
:key="index"
>
<div class="goods-img">
<img :src="item.img" />
<img alt="楼层图片" :src="item.img" />
</div>
<div>
<div class="goods-name wes-2">{{ item.title }}</div>
@@ -93,7 +93,7 @@
class="draggable"
>
<div class="flex">
<img :src="item.img" class="column-img" />
<img alt="楼层图片" :src="item.img" class="column-img" />
<div class="flex column-goods-config">
<div class="column-config">
<div>

View File

@@ -102,7 +102,7 @@
class="draggable"
>
<div class="flex">
<img :src="item.img" class="column-img" />
<img alt="楼层图片" :src="item.img" class="column-img" />
<el-button
size="small"
class="ml_10"

View File

@@ -2,7 +2,7 @@
<div class="line flex flex-j-sb">
<div class="column" v-for="(item,index) in data.options.list" :key="index">
<div v-if="!item.img" class="placeholder">占位符</div>
<img v-else :src="item.img" class="three-column-img">
<img alt="楼层图片" v-else :src="item.img" class="three-column-img">
</div>
<div class="setup-box">
<div>
@@ -23,7 +23,7 @@
append-to-body destroy-on-close>
<div class="modal-tab-bar">
<div v-for="(item,index) in data.options.list" :key="index">
<img :src="item.img" class="three-column-img">
<img alt="楼层图片" :src="item.img" class="three-column-img">
<div>推荐尺寸:{{item.size}}</div>
<el-button
size="small"

View File

@@ -12,7 +12,7 @@
<div class="goods-desc">{{ item.desc }}</div>
</div>
<div class="goods-img">
<img :src="item.img" />
<img alt="楼层图片" :src="item.img" />
</div>
</div>
<div class="setup-box">
@@ -47,7 +47,7 @@
class="draggable"
>
<div class="flex">
<img :src="item.img" class="column-img" />
<img alt="楼层图片" :src="item.img" class="column-img" />
<div class="flex column-goods-config">
<div class="column-config">
<div>

View File

@@ -3,7 +3,7 @@
<el-carousel class="carousel" v-if="res" autoplay :interval="5000" height="150px">
<el-carousel-item v-for="(item, index) in res.list" :key="index">
<div>
<img class="image-mode" :src="item.img" />
<img alt="轮播图" class="image-mode" :src="item.img" />
</div>
</el-carousel-item>
</el-carousel>

View File

@@ -9,13 +9,8 @@
</div>
</template>
<script>
export default {
title: "五列单行图片模块",
};
</script>
<script setup>
defineOptions({ title: "五列单行图片模块" });
defineProps(["res"]);
</script>
<style lang="scss" scoped>

View File

@@ -10,13 +10,8 @@
</div>
</div>
</template>
<script>
// 自定义选项(组件在 <component :is> 场景下按 title 展示名称),需保留在普通 script 块
export default {
title: "两张横图",
};
</script>
<script setup>
defineOptions({ title: "两张横图" });
defineProps(["res"]);
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<template>
<div class="layout">
<div class="view-height-150">
<img class="image-mode" :src="res.list[0].img" />
<img alt="模板图片" class="image-mode" :src="res.list[0].img" />
</div>
<div class="view-height-150">
<div class="view-height-75">

View File

@@ -10,7 +10,7 @@
</div>
</div>
<div class="view-height-150">
<img class="image-mode" style="height:150px;" :src="res.list[2].img" />
<img alt="模板图片" class="image-mode" style="height:150px;" :src="res.list[2].img" />
</div>
</div>
</template>

View File

@@ -32,13 +32,8 @@
</div>
</template>
<script>
// 自定义选项(组件在 <component :is> 场景下按 title 展示名称),需保留在普通 script 块
export default {
title: "文字图片模板",
};
</script>
<script setup>
defineOptions({ title: "文字图片模板" });
defineProps(["res"]);
</script>
<style lang="scss" scoped>

View File

@@ -2,7 +2,7 @@
<template>
<div class="layout">
<div class="view-width-100">
<img class="image-mode" :src="res.list[0].img" />
<img alt="模板图片" class="image-mode" :src="res.list[0].img" />
</div>
<div class="view-width-100">
<div class="view-height-85">

View File

@@ -9,7 +9,7 @@
</div>
</div>
<div class="view-width-100">
<img class="image-mode" :src="res.list[2].img" />
<img alt="模板图片" class="image-mode" :src="res.list[2].img" />
</div>
</div>
</template>

View File

@@ -13,11 +13,12 @@
<el-button type="primary" @click="handleAdd()">添加页面</el-button>
</div>
<el-table v-loading="loading" :data="list" style="width: 100%">
<el-table-column prop="name" label="页面名称" min-width="200" show-overflow-tooltip>
<el-table-column prop="name" label="页面名称" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row">{{ row.name || "暂无模板昵称" }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" align="center" />
<el-table-column label="状态" width="120" align="center">
<template #default="{ row }">
<el-switch
@@ -30,7 +31,7 @@
/>
</template>
</el-table-column>
<el-table-column label="操作" width="140" align="center" fixed="right">
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<template v-if="row">
<a class="link-text" @click="handleEdit(row)">修改</a>

View File

@@ -1,17 +1,27 @@
<template>
<div class="data-report">
<el-card>
<h4 class="page-title">数据报表</h4>
<p class="page-desc">选择报表模块查看本店销售数据统计与分析</p>
<div class="module-grid">
<div
v-for="item in modules"
:key="item.name"
class="module-card"
@click="goReport(item.name)"
>
<div class="report-header">
<div>
<h4 class="page-title">数据报表</h4>
<p class="page-desc">选择报表模块,查看本店销售数据统计与分析</p>
</div>
<span class="module-count">共 {{ modules.length }} 个报表</span>
</div>
<div class="module-grid">
<div
v-for="item in modules"
:key="item.name"
class="module-card"
:style="{ '--accent': item.color, '--accent-bg': item.bg }"
role="link"
tabindex="0"
@click="goReport(item.name)"
@keyup.enter="goReport(item.name)"
>
<div class="module-top">
<div class="module-icon">
<el-icon :size="32">
<el-icon :size="28">
<component :is="item.icon" />
</el-icon>
</div>
@@ -19,15 +29,22 @@
<div class="module-title">{{ item.title }}</div>
<div class="module-desc">{{ item.desc }}</div>
</div>
</div>
<div class="module-tags">
<span v-for="tag in item.tags" :key="tag" class="module-tag">{{ tag }}</span>
</div>
<div class="module-footer">
<span>查看报表</span>
<el-icon class="module-arrow"><ArrowRight /></el-icon>
</div>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import {
ArrowRight,
@@ -46,29 +63,42 @@ const REPORT_MODULES = [
name: "storePerformanceReport",
title: "店铺业绩报表",
desc: "支付金额、营业额、转化率及同环比分析",
tags: ["支付金额", "转化率", "同环比"],
icon: TrendCharts,
color: "#409eff",
bg: "#ecf5ff",
},
{
name: "goodsComparisonReport",
title: "商品同比环比报表",
desc: "商品销售金额、销量占比及同环比对比",
tags: ["销售金额", "销量占比", "同环比"],
icon: DataAnalysis,
color: "#67c23a",
bg: "#f0f9eb",
},
{
name: "salesOrderDetailReport",
title: "销售订单明细报表",
desc: "订单流水、收款明细与优惠明细",
tags: ["订单流水", "收款明细", "优惠明细"],
icon: Document,
color: "#e6a23c",
bg: "#fdf6ec",
},
{
name: "goodsSalesSummaryReport",
title: "商品销售汇总报表",
desc: "商品销量、净销售额及优惠汇总",
tags: ["商品销量", "净销售额", "优惠汇总"],
icon: Goods,
color: "#9b6dff",
bg: "#f4effe",
},
];
const modules = ref(REPORT_MODULES);
// 图标组件为静态常量,不能放进 ref/reactive(否则 Vue 会警告组件被转成响应式对象)
const modules = REPORT_MODULES;
function goReport(name) {
router.push({ name });
@@ -77,41 +107,89 @@ function goReport(name) {
<style scoped lang="scss">
.data-report {
padding: 4px 0;
.report-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 20px 24px;
margin-bottom: 16px;
border-radius: 8px;
background: #fff;
border-left: 4px solid var(--el-color-primary);
}
.page-title {
margin: 0 0 8px;
font-size: 18px;
margin: 0 0 6px;
font-size: 20px;
font-weight: 600;
color: #303133;
}
.page-desc {
margin: 0 0 20px;
margin: 0;
font-size: 14px;
color: #909399;
}
.module-count {
font-size: 13px;
color: #909399;
}
.module-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.module-card {
position: relative;
display: flex;
align-items: center;
flex-direction: column;
gap: 16px;
padding: 20px;
padding: 24px 24px 18px;
overflow: hidden;
border: 1px solid #ebeef5;
border-radius: 8px;
background: #fafafa;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
&:hover {
border-color: var(--el-color-primary-light-5);
background: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
// 顶部强调色条
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--accent);
opacity: 0.85;
}
&:hover,
&:focus-visible {
transform: translateY(-3px);
border-color: var(--accent);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
.module-arrow {
transform: translateX(4px);
}
}
&:active {
transform: translateY(-1px);
}
}
.module-top {
display: flex;
align-items: flex-start;
gap: 16px;
}
.module-icon {
@@ -121,8 +199,8 @@ function goReport(name) {
width: 56px;
height: 56px;
border-radius: 12px;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
color: var(--accent);
background: var(--accent-bg);
flex-shrink: 0;
}
@@ -140,13 +218,51 @@ function goReport(name) {
.module-desc {
font-size: 13px;
line-height: 1.5;
line-height: 1.6;
color: #909399;
}
.module-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.module-tag {
padding: 2px 10px;
font-size: 12px;
line-height: 20px;
border-radius: 10px;
color: var(--accent);
background: var(--accent-bg);
}
.module-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 12px;
border-top: 1px dashed #ebeef5;
font-size: 13px;
color: var(--accent);
}
.module-arrow {
color: #c0c4cc;
flex-shrink: 0;
transition: transform 0.2s ease;
}
}
@media (max-width: 640px) {
.data-report {
.report-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.module-grid {
grid-template-columns: 1fr;
}
}
}
</style>

View File

@@ -41,11 +41,6 @@
style="width: 200px"
/>
</el-form-item>
<el-form-item label="品牌">
<el-select v-model="params.brandId" clearable filterable placeholder="全部品牌" style="width: 160px">
<el-option v-for="item in brandList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="商品名称">
<el-input v-model="params.keyword" clearable placeholder="商品名称" style="width: 160px" />
</el-form-item>
@@ -134,7 +129,7 @@
<script setup>
import { onMounted, reactive, ref } from "vue";
import * as API_Statistics from "@/api/statistics";
import { getBrandListData, getCategoryTree } from "@/api/goods";
import { getCategoryTree } from "@/api/goods";
import { downloadBlob } from "@/utils/downloadBlob";
import { unitPrice } from "@/utils/filters";
@@ -157,14 +152,12 @@ const params = reactive({
pageNumber: 1,
pageSize: 20,
categoryId: "",
brandId: "",
clientType: "",
keyword: "",
sortType: "PRICE",
});
const categoryList = ref([]);
const categoryProps = ref({ value: "id", label: "name", children: "children", checkStrictly: true, emitPath: false });
const brandList = ref([]);
const clientTypeOptions = ref(CLIENT_TYPE_OPTIONS);
const data = ref([]);
const total = ref(0);
@@ -200,11 +193,6 @@ function loadOptions() {
if (res.success) categoryList.value = res.result || [];
})
.catch(() => {});
getBrandListData()
.then((res) => {
if (res.success) brandList.value = res.result || [];
})
.catch(() => {});
}
function formatDate(date, startOfDay) {
const y = date.getFullYear();

View File

@@ -76,19 +76,20 @@
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { onMounted, ref } from "vue";
import { useTable } from "@/utils/useTable";
import * as API_Statistics from "@/api/statistics";
import { downloadBlob } from "@/utils/downloadBlob";
import * as filters from "@/utils/filters";
defineOptions({ name: "seller-goods-sales-summary-report" });
const loading = ref(false);
const dateRange = ref([]);
const defaultTime = ref([new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]);
const params = reactive({ pageNumber: 1, pageSize: 20, keyword: "", sortType: "PRICE" });
const data = ref([]);
const total = ref(0);
const { loading, data, total, searchForm: params, getDataList: loadData, handleSearch } = useTable(
API_Statistics.getGoodsSalesSummaryReport,
{ form: { keyword: "", sortType: "PRICE" }, pageSize: 20, immediate: false, beforeRequest: buildParams }
);
onMounted(() => {
initDefaultRange();
@@ -109,8 +110,8 @@ function formatDate(date, startOfDay) {
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
}
function buildParams() {
const p = { ...params };
function buildParams(base = params) {
const p = { ...base };
if (dateRange.value && dateRange.value.length === 2) {
p.startTime = dateRange.value[0];
p.endTime = dateRange.value[1];
@@ -118,22 +119,6 @@ function buildParams() {
return p;
}
function handleSearch() {
params.pageNumber = 1;
loadData();
}
function loadData() {
loading.value = true;
API_Statistics.getGoodsSalesSummaryReport(buildParams()).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
});
}
function handleExport() {
API_Statistics.exportGoodsSalesSummaryReport(buildParams()).then((blob) => {
downloadBlob(blob, "商品销售汇总报表.xlsx");

View File

@@ -68,25 +68,20 @@
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { onMounted, ref } from "vue";
import { useTable } from "@/utils/useTable";
import * as API_Statistics from "@/api/statistics";
import { downloadBlob } from "@/utils/downloadBlob";
import { unitPrice } from "@/utils/filters";
defineOptions({ name: "seller-sales-order-detail-report" });
const loading = ref(false);
const dateRange = ref([]);
const defaultTime = ref([new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]);
const params = reactive({
pageNumber: 1,
pageSize: 20,
orderSn: "",
memberName: "",
keyword: "",
});
const data = ref([]);
const total = ref(0);
const { loading, data, total, searchForm: params, getDataList: loadData, handleSearch } = useTable(
API_Statistics.getSalesOrderDetailReport,
{ form: { orderSn: "", memberName: "", keyword: "" }, pageSize: 20, immediate: false, beforeRequest: buildParams }
);
onMounted(() => {
initDefaultRange();
@@ -105,28 +100,14 @@ function formatDate(date, startOfDay) {
const d = String(date.getDate()).padStart(2, "0");
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
}
function buildParams() {
const p = { ...params };
function buildParams(base = params) {
const p = { ...base };
if (dateRange.value && dateRange.value.length === 2) {
p.startTime = dateRange.value[0];
p.endTime = dateRange.value[1];
}
return p;
}
function handleSearch() {
params.pageNumber = 1;
loadData();
}
function loadData() {
loading.value = true;
API_Statistics.getSalesOrderDetailReport(buildParams()).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
});
}
function handleExport() {
API_Statistics.exportSalesOrderDetailReport(buildParams()).then((blob) => {
downloadBlob(blob, "销售订单明细报表.xlsx");

View File

@@ -86,19 +86,20 @@
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { onMounted, ref } from "vue";
import { useTable } from "@/utils/useTable";
import * as API_Statistics from "@/api/statistics";
import { downloadBlob } from "@/utils/downloadBlob";
import { unitPrice } from "@/utils/filters";
defineOptions({ name: "seller-store-performance-report" });
const loading = ref(false);
const dateRange = ref([]);
const defaultTime = ref([new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]);
const params = reactive({ pageNumber: 1, pageSize: 20 });
const data = ref([]);
const total = ref(0);
const { loading, data, total, searchForm: params, getDataList: loadData, handleSearch } = useTable(
API_Statistics.getStorePerformanceReport,
{ pageSize: 20, immediate: false, beforeRequest: buildParams }
);
onMounted(() => {
initDefaultRange();
@@ -117,28 +118,14 @@ function formatDate(date, startOfDay) {
const d = String(date.getDate()).padStart(2, "0");
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
}
function buildParams() {
const p = { ...params };
function buildParams(base = params) {
const p = { ...base };
if (dateRange.value && dateRange.value.length === 2) {
p.startTime = dateRange.value[0];
p.endTime = dateRange.value[1];
}
return p;
}
function handleSearch() {
params.pageNumber = 1;
loadData();
}
function loadData() {
loading.value = true;
API_Statistics.getStorePerformanceReport(buildParams()).then((res) => {
loading.value = false;
if (res.success) {
data.value = res.result.records || [];
total.value = res.result.total || 0;
}
});
}
function handleExport() {
API_Statistics.exportStorePerformanceReport(buildParams()).then((blob) => {
downloadBlob(blob, "店铺业绩报表.xlsx");