fix:修改

This commit is contained in:
liangdong
2026-01-13 20:32:44 +08:00
parent 05496ae4c4
commit 772e35b35b
9 changed files with 538 additions and 159 deletions

View File

@@ -7,8 +7,10 @@
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const ElButton: typeof import('element-plus/es').ElButton
const ElMessage: typeof import('element-plus/es').ElMessage
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
const ElTag: typeof import('element-plus/es').ElTag
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp

View File

@@ -4,123 +4,368 @@
ref="tableRef"
v-bind="$attrs"
:columns="adaptedColumns"
:data="data"
:data="innerData"
:width="tableSize.width"
:height="tableSize.height"
:fixed="true"
@rows-rendered="handleRowsRendered"
>
<template #overlay v-if="loading">
<div class="v2-loading-overlay">
<el-icon class="is-loading" :size="26"><Loading /></el-icon>
<template #overlay v-if="loading && innerData.length === 0">
<div class="v2-initial-loading">
<div class="loading-content">
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
</div>
</div>
</template>
</el-table-v2>
<div class="pro-table-v2-footer">
<div class="footer-left">
<span>已加载 {{ data.length }} / {{ total }} </span>
<template #footer>
<div class="v2-infinite-footer">
<div v-if="loading && innerData.length > 0" class="loading-more">
<el-icon class="is-loading"><Loading /></el-icon>
</div>
<div class="footer-right">
<span v-if="loading">加载中...</span>
<span v-else-if="noMore">已加载全部</span>
<div v-else-if="noMore" class="no-more">
<span class="line"></span>
<span class="text">已经到底了~</span>
<span class="line"></span>
</div>
</div>
</template>
<template #empty>
<el-empty description="暂无相关数据" :image-size="100" />
</template>
</el-table-v2>
</div>
</template>
<script setup lang="tsx">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { debounce } from 'lodash-es';
import { ElTag, ElText } from 'element-plus';
import NameAvatar from "@/components/NameAvatar/index.vue";
import dayjs from 'dayjs';
import { ref, computed, onMounted, onUnmounted, nextTick } from "vue";
import { ElTag, ElButton, ElText } from "element-plus";
import { debounce } from "lodash-es";
import dayjs from "dayjs";
import { DictManage } from "@/dict";
// 组件属性
const props = defineProps({
columns: { type: Array, required: true },
data: { type: Array, required: true },
total: { type: Number, default: 0 },
loading: { type: Boolean, default: false },
requestApi: { type: Function, required: true },
initParam: { type: Object, default: () => ({}) },
pageSize: { type: Number, default: 20 },
});
const emit = defineEmits(["load-more"]);
// 内部数据状态
const tableRef = ref(null);
const tableContainerRef = ref(null);
const loading = ref(false);
const innerData = ref<any[]>([]);
const total = ref(0);
const pageNo = ref(1);
const tableSize = ref({ width: 0, height: 400 });
// --- 内置渲染逻辑工厂 ---
const builtInRenderers = {
// 1. 人员头像渲染器 (默认读取 row.name 和 row.avatar)
member: (scope: any, col: any) => (
<div style="display:flex; align-items:center; gap:8px;">
<NameAvatar
name={scope.rowData[col.prop]}
src={scope.rowData[col.avatarKey || 'avatar']}
size={28}
/>
<span>{scope.rowData[col.prop]}</span>
</div>
),
// 2. 状态标签渲染器 (支持自定义映射)
const noMore = computed(
() => innerData.value.length >= total.value && total.value > 0
);
// --- 1. 核心渲染工厂 (内置常用业务组件) ---
const RenderFactory = {
// 状态点/标签
status: (scope: any, col: any) => {
const val = scope.rowData[col.prop];
const option = col.options?.find((opt: any) => opt.value === val) || { label: val, type: 'info' };
return <ElTag type={option.type} size="small">{option.label}</ElTag>;
},
let rawOptions = col.options;
// 如果是 ref 或 computed先取 .value
if (rawOptions && typeof rawOptions === "object" && "value" in rawOptions) {
rawOptions = rawOptions.value;
}
// 如果是函数则执行
if (typeof rawOptions === "function") {
rawOptions = rawOptions();
}
// 3. 日期格式化
// 强制转为数组,防止 find 报错
const currentOptions = Array.isArray(rawOptions) ? rawOptions : [];
const target = currentOptions.find((opt: any) => opt.value == val);
return (
<div
class="mj-status-dot"
style={{
"--data-status-color": DictManage.statusDictColor[target?.value],
cursor: "pointer",
}}
onClick={() => {
if (col.onClick) {
col.onClick({ cellValue: val, rowData: scope.rowData });
}
}}
>
{target?.label || "-"}
</div>
);
},
// 日期格式化
date: (scope: any, col: any) => {
const val = scope.rowData[col.prop];
return <span>{val ? dayjs(val).format(col.format || 'YYYY-MM-DD HH:mm') : '-'}</span>;
return (
<span>
{val ? dayjs(val).format(col.format || "YYYY-MM-DD HH:mm") : "-"}
</span>
);
},
// 文本自动省略
ellipsis: (scope: any, col: any) => {
const val = scope.rowData[col.prop] || "-";
return (
<div class="mj-ellipsis" title={val}>
{val}
</div>
);
},
};
// 4. 金额格式化
money: (scope: any, col: any) => {
const val = scope.rowData[col.prop];
return <ElText type="warning">¥ {Number(val || 0).toLocaleString()}</ElText>;
/**
* 局部更新某一行数据
* @param id 唯一标识
* @param rowData 新的数据对象(可以是部分属性)
*/
const updateRow = (id: string | number, rowData: object) => {
const index = innerData.value.findIndex((item) => item.id === id);
if (index !== -1) {
// 使用合并方式,保留原有的其他字段
innerData.value[index] = { ...innerData.value[index], ...rowData };
}
};
/**
* 删除某一行数据
* @param id 唯一标识
*/
const removeRow = (id: string | number) => {
const index = innerData.value.findIndex(item => item.id == id);
if (index !== -1) {
innerData.value.splice(index, 1);
total.value = Math.max(0, total.value - 1);
// 💡 关键:删除后如果当前显示的数据太少(比如不足一屏),自动去接下一页
if (innerData.value.length < 15 && !noMore.value) {
fetchTableData(false); // 这里的 false 表示“追加”数据来补位
}
// 如果删除后这一页空了,且还有数据,也去拉取
if (innerData.value.length === 0 && total.value > 0) {
fetchTableData(true);
}
}
};
/**
* 插入一条新数据(通常用于新增成功后插到最前面)
* @param rowData 完整的数据对象
*/
const addRow = (rowData: object) => {
innerData.value.unshift(rowData);
total.value++;
};
// --- 适配 Columns 配置 ---
const adaptedColumns = computed(() => {
return props.columns.map((col: any) => ({
// 1. 获取容器当前实际宽度
const containerWidth = tableSize.value.width;
if (containerWidth <= 0) return [];
// 2. 找出没有设置宽度的列
const autoColumns = props.columns.filter((col: any) => !col.width);
// 3. 计算已经固定了宽度的列总和
const fixedWidthTotal = props.columns
.filter((col: any) => col.width)
.reduce((prev, curr: any) => prev + Number(curr.width), 0);
// 4. 计算剩余可用宽度
const remainingWidth = Math.max(containerWidth - fixedWidthTotal, 0);
// 5. 计算每个自适应列应该分配到的平均宽度
const perAutoWidth = autoColumns.length > 0
? Math.floor(remainingWidth / autoColumns.length)
: 0;
return props.columns.map((col: any) => {
const isAuto = !col.width;
const finalWidth = isAuto ? Math.max(perAutoWidth, 100) : Number(col.width);
return {
key: col.prop,
dataKey: col.prop,
title: col.label,
width: col.width || 150,
width: finalWidth,
flexGrow: isAuto ? 1 : 0,
flexShrink: 1,
fixed: col.fixed,
align: col.align || 'left',
align: col.align || "left",
cellRenderer: (scope: any) => {
// 优先级 1: 用户自定义了 render 函数
if (typeof col.render === 'function') return col.render(scope);
// 优先级 2: 使用内置的 valueType 渲染器
if (col.valueType && builtInRenderers[col.valueType]) {
return builtInRenderers[col.valueType](scope, col);
if (typeof col.render === "function") return col.render(scope);
if (col.valueType && RenderFactory[col.valueType]) {
return RenderFactory[col.valueType](scope, col);
}
// 优先级 3: 默认展示
return scope.rowData[col.prop] ?? '-';
return <span class="v2-cell-text">{scope.rowData[col.prop] ?? "-"}</span>;
},
}));
};
});
});
// --- 容器与滚动逻辑 (保持之前的一致) ---
const tableContainerRef = ref(null);
const tableSize = ref({ width: 0, height: 400 });
const noMore = computed(() => props.data.length >= props.total && props.total > 0);
// --- 请求逻辑 ---
const fetchTableData = async (isReset = false) => {
if (loading.value) return;
if (!isReset && noMore.value) return;
if (isReset) {
pageNo.value = 1;
noMore.value = false;
// 重置时不立即清空数据,防止闪烁
}
loading.value = true;
try {
const params = {
pageNo: pageNo.value,
pageSize: props.pageSize,
...props.initParam,
};
const res = await props.requestApi(params);
const records = res?.records || [];
innerData.value = isReset ? records : [...innerData.value, ...records];
total.value = res?.total || 0;
if (innerData.value.length >= total.value || newList.length < props.pageSize) {
noMore.value = true;
}
pageNo.value++;
} finally {
loading.value = false;
}
};
// --- 交互逻辑 ---
const updateSize = () => {
if (tableContainerRef.value) {
// 1. offsetWidth 包含 border 和 padding是最准确的外盒宽度
const containerWidth = tableContainerRef.value.offsetWidth;
// 不要减 2让表格填满
tableSize.value.width = containerWidth;
const rect = tableContainerRef.value.getBoundingClientRect();
tableSize.value.width = rect.width;
tableSize.value.height = window.innerHeight - rect.top - 45;
}
};
const handleResize = debounce(updateSize, 200);
const handleRowsRendered = ({ endRowIndex }: any) => {
if (endRowIndex >= props.data.length - 5 && !props.loading && !noMore.value) {
emit("load-more");
const remainingHeight = window.innerHeight - rect.top - 24;
tableSize.value.height = Math.max(remainingHeight, 200);
}
};
onMounted(() => { updateSize(); window.addEventListener('resize', handleResize); });
onUnmounted(() => { window.removeEventListener('resize', handleResize); });
const handleRowsRendered = ({ endRowIndex }: any) => {
// 阈值调大一点10保证滚动流畅用户无感知加载
if (endRowIndex >= innerData.value.length - 10) {
fetchTableData();
}
};
const handleResize = debounce(updateSize, 200);
onMounted(() => {
updateSize();
window.addEventListener("resize", handleResize);
fetchTableData(true);
});
onUnmounted(() => window.removeEventListener("resize", handleResize));
// 暴露 API
defineExpose({
refresh: () => fetchTableData(true),
updateRow,
removeRow,
addRow,
getCurrentParams: () => ({
pageNo: pageNo.value - 1, // 因为 fetch 完后 pageNo 会自增,所以要减 1 才是当前页
pageSize: props.pageSize,
...props.initParam
}),
innerData
});
</script>
<style scoped lang="scss">
.pro-table-v2-container {
width: 100%;
height: 100%;
background-color: #fff;
border-radius: 4px;
/* 移除外边框,让样式更融入页面 */
// 1. 初始全屏 Loading
.v2-initial-loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(4px);
z-index: 100;
.loading-content {
text-align: center;
color: var(--el-color-primary);
p {
margin-top: 10px;
font-size: 14px;
}
}
}
// 2. 底部流式加载样式
.v2-infinite-footer {
padding: 20px 0;
color: #909399;
font-size: 13px;
.loading-more {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
.is-loading {
animation: rotating 2s linear infinite;
}
}
.no-more {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
opacity: 0.6;
.line {
width: 30px;
height: 1px;
background-color: #dcdfe6;
}
.text {
font-style: italic;
}
}
}
}
// 适配 el-table-v2 核心样式
:deep(.el-table-v2__main) {
background: #fff;
}
:deep(.el-table-v2__header-wrapper) {
border-bottom: 1px solid #f2f3f5;
.el-table-v2__header-cell {
background-color: #f8fafc;
color: #303133;
font-weight: 600;
}
}
:deep(.el-table-v2__row) {
border-bottom: 1px solid #f9fafb;
&:hover {
background-color: #f5f7fa;
}
}
</style>

View File

@@ -0,0 +1,65 @@
import { ElMessage } from "element-plus";
/**
* @param tableRef ProTableV2 的组件实例引用
*/
interface ActionOptions {
showMsg?: boolean; // 是否显示操作成功提示
}
export const useTableAction = (tableRef: any) => {
/**
* 执行操作并静默更新单行数据
* @param apiPromise 调用的操作接口(如状态切换、编辑提交)
* @param id 当前行 ID
* @param fetchDataApi 可选:列表请求函数,若传入则在成功后自动拉取最新行数据
*/
const handleAction = async (
api: Promise<any> | (() => Promise<any>),
id?: string | number,
fetchDataApi?: Function,
options = { showMsg: true }
) => {
try {
const res = typeof api === 'function' ? await api() : await api;
if (options.showMsg) ElMessage.success("操作成功");
// 局部更新逻辑... (同上)
if (id && fetchDataApi) {
const currentParams = tableRef.value?.getCurrentParams();
const listRes = await fetchDataApi(currentParams);
const records = listRes?.records || listRes?.data?.records || [];
const latestRowData = records.find((item: any) => item.id == id);
if (latestRowData) tableRef.value.updateRow(id, latestRowData);
}
return res;
} catch (error) {
console.error(error);
throw error; // 抛出错误让外部 catch
}
};
/**
* 封装删除逻辑
* @param id 行 ID
* @param deleteApi 删除接口函数
*/
const handleDelete = async (
id: string | number,
deleteApi: (id: any) => Promise<any>
) => {
try {
const res = await deleteApi(id);
tableRef.value?.removeRow(id);
return res;
} catch (error) {
// 捕获取消行为或接口错误
throw error;
}
};
return {
handleAction,
handleDelete,
};
};

View File

@@ -0,0 +1,13 @@
<template>
<div class="">
商机管理
</div>
</template>
<script setup lang="ts">
import {reactive,ref,onMounted} from "vue"
defineOptions({})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,13 @@
<template>
<div class="">
客户管理
</div>
</template>
<script setup lang="ts">
import {reactive,ref,onMounted} from "vue"
defineOptions({})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,13 @@
<template>
<div class="">
游戏和工作室
</div>
</template>
<script setup lang="ts">
import {reactive,ref,onMounted} from "vue"
defineOptions({})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -115,7 +115,7 @@ const onConfirm = async (formEl: FormInstance | undefined) => {
onCancel();
emit('confirm-success');
} catch (error) {
console.log('error',error);
console.log('error',row.id);
} finally{
loading.value = false;
}

View File

@@ -33,7 +33,9 @@
<el-option
:label="item.label"
:value="item.value"
v-for="(item, index) in dicts.permission_list_enable_disable"
v-for="(
item, index
) in dicts.permission_list_enable_disable"
:key="index"
/>
</el-select>
@@ -64,34 +66,10 @@
<CommonTable
ref="dictTableRef"
:columns="columns"
v-model:data="dataValue"
v-model:total="total"
pagination
:data="dataValue"
:total="total"
:request-api="getTableData"
>
<!-- 编号内容显示 -->
<template #number="{ row, index }">
<span>#{{ formatIndex(index) }}</span>
</template>
<template #name="{ row }">
<span style="font-weight: 600">{{ row.name }}</span>
</template>
<!-- 编码标识 -->
<template #code="{ row }">
<el-tag size="small" type="info">{{ row.key }}</el-tag>
</template>
<!-- 状态插槽 -->
<template #status="{ row }">
<div
class="mj-status-dot"
:style="{
'--data-status-color': DictManage.statusDictColor[row.status],
}"
@click="handleDictStatus(row)"
>
{{ dicts.permission_list_enable_disable.find(item=>item.value == row.status)?.label }}
</div>
</template>
</CommonTable>
<!-- 新增-编辑字典弹窗 -->
@@ -106,7 +84,8 @@
</div>
</template>
<script setup lang="ts">
import CommonTable from "@/components/proTable/index.vue";
import { h } from "vue";
import CommonTable from "@/components/proTable/proTablev2.vue";
import dictFieldConfig from "./dictFieldConfig.vue";
import dictManageModules from "./dictManage.vue";
import dayjs from "dayjs";
@@ -119,13 +98,16 @@ import {
import { DictManage } from "@/dict";
import { formatIndex } from "@/utils/utils";
import { ElMessage } from "element-plus";
import { useTableAction } from "@/hooks/useTableAction";
import { useDict } from '@/hooks/useDictData';
const { dicts,refresh } = useDict('permission_list_enable_disable');
import { useDict } from "@/hooks/useDictData";
import { render } from "vue";
const { dicts, refresh } = useDict("permission_list_enable_disable");
defineOptions({ name: "Dictionary" });
const fieldsConfigRef = ref(null);
const dictTableRef = ref(null);
const { handleAction, handleDelete: runDelete } = useTableAction(dictTableRef);
const dictVisible = ref<boolean>(false);
const searchVal = ref<string>("");
const total = ref<number>(0);
@@ -138,70 +120,100 @@ const selectItem = reactive({});
// 列表columns数据
const columns = [
{ prop: "id", label: "编号", width: "80", align: "center", slot: "number" },
{ prop: "name", label: "字典名称", align: "center", slot: "name" },
{
prop: "id",
label: "编号",
width: "80",
align: "center",
render: ({ rowData, rowIndex }) => {
return h("span", `#${formatIndex(rowIndex)}`);
},
},
{
prop: "name",
label: "字典名称",
align: "center",
render: ({ rowData }: any) => {
return h("span", { style: { fontWeight: "600" } }, rowData.name);
},
},
{
prop: "key",
label: "编码标识",
align: "center",
slot: "code",
width: 300,
render: ({ rowData }: any) => {
return h(ElTag, { size: "small", type: "info" }, () => rowData.key);
},
},
{
prop: "status",
label: "状态",
align: "center",
slot: "status",
width: 150,
valueType: "status",
options: computed(() => dicts.value.permission_list_enable_disable),
onClick: ({ cellValue, rowData }) => {
handleDictStatus(rowData);
},
},
{
prop: "remark",
label: "备注说明",
align: "center",
showOverflowTooltip: true,
valueType: "ellipsis",
width: 150,
},
{
prop: "createTime",
label: "创建时间",
align: "center",
showOverflowTooltip: true,
formatter: (val) => {
return val.createTime
? dayjs(val.createTime).format("YYYY-MM-DD HH:mm")
: "-";
},
valueType: "date",
format: "YYYY-MM-DD HH:mm",
width: 150,
},
{
prop: "updateByName",
label: "最后修改人",
align: "center",
width: 150,
},
{
prop: "actions",
label: "操作",
align: "right",
width: "300",
actions: [
render: ({ rowData }: any) => {
return h("div", { class: "space-x-2" }, [
h(
ElButton,
{
label: "编辑",
type: "primary",
link: true,
permission: ["edit"],
onClick: (row) => handleEdit(row),
},
{
label: "字段配置",
type: "primary",
link:true,
permission: ["config"],
onClick: (row) => handlefieldsConfig(row),
onClick: () => handleEdit(rowData),
},
() => "编辑"
),
h(
ElButton,
{
label: "删除",
link: true,
type: "primary",
onClick: () => handlefieldsConfig(rowData),
},
() => "字段配置"
),
h(
ElButton,
{
link: true,
type: "danger",
link:true,
permission: ["delete"],
onClick: (row) => handleDelete(row),
}
],
onClick: () => handleDelete(rowData),
},
() => "删除"
),
]);
},
},
];
@@ -223,7 +235,7 @@ const getTableData = async (params) => {
const response = await getDictValues({
...params,
...(searchVal.value && { keyword: searchVal.value }),
...(filterForm.status && { status: filterForm.status })
...(filterForm.status && { status: filterForm.status }),
});
return response;
} catch (error) {
@@ -235,7 +247,13 @@ const fetchTableData = () => {
onConfirmSuccess();
};
const clearSelectItem = () => {
Object.assign(selectItem,{id:null,name:'',key:'',status:1,remark:''})
Object.assign(selectItem, {
id: null,
name: "",
key: "",
status: 1,
remark: "",
});
};
// 新增字典信息
const addDict = () => {
@@ -251,17 +269,27 @@ const handleEdit = (item) => {
// 启用-禁用事件
const handleDictStatus = async (row) => {
try {
row.status === 1 ? await disableDict(row.id) : await enableDict(row.id);
ElMessage.success("操作成功");
onConfirmSuccess();
const apiCall = row.status === 1 ? disableDict(row.id) : enableDict(row.id);
// row.status === 1 ? await disableDict(row.id) : await enableDict(row.id);
await handleAction(apiCall, row.id, getTableData);
} catch (error) {
console.log("error", error);
}
};
// 刷新Table数据信息
const onConfirmSuccess = () => {
dictTableRef.value && dictTableRef.value.reset();
const onConfirmSuccess = async () => {
if (selectItem.id) {
handleAction(
Promise.resolve(true),
selectItem.id,
getTableData,
{ showMsg: false, silentUpdate: true }
);
}
else {
dictTableRef.value?.refresh();
}
};
// TODO:字段配置
const handlefieldsConfig = (ite) => {
@@ -273,8 +301,8 @@ const handleDelete = async (item) => {
ElMessageBox.confirm("确定要删除吗?", "提示", { type: "warning" })
.then(async () => {
try {
await deleteDictValue(item.id);
onConfirmSuccess();
// await deleteDictValue(item.id);
await runDelete(item.id, deleteDictValue);
} catch (error) {
console.log("fetch error", error);
}

View File

@@ -10,9 +10,9 @@ const stageRoute = {
const businessRoute = {
business: "businessManage",
"business.customer": "customerManage",
"business.game_studio":"customerManage",
"business.opportunity":"customerManage",
"business.customer": "customer",
"business.game_studio":"gameStudios",
"business.opportunity":"businessOpport",
}