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

@@ -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;
}