新模块的第一次测试修复了部分bug

This commit is contained in:
2025-07-16 00:24:31 +08:00
parent de30b60256
commit 6b0562b498
17 changed files with 1695 additions and 378 deletions

1
components.d.ts vendored
View File

@ -27,6 +27,7 @@ declare module 'vue' {
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']

BIN
dist7月14日.zip Normal file

Binary file not shown.

View File

@ -14,7 +14,7 @@ const myAxios = axios.create({
//baseURL:'http://27.30.77.229:9092/'
//baseURL:'http://160.202.242.36:9091/'
// baseURL:'http://160.202.242.36:9092/'
// baseURL:'http://160.202.242.36:9092'
});

View File

@ -17,9 +17,10 @@
</span>
</template>
<a-menu-item key="/adminList">管理员列表</a-menu-item>
<a-menu-item key="staffList">员工列表</a-menu-item>
<a-menu-item key="supervisorList">主管列表</a-menu-item>
<a-menu-item key="userList">普通用户列表</a-menu-item>
<a-menu-item key="/staffList">员工列表</a-menu-item>
<a-menu-item key="/supervisorList">主管列表</a-menu-item>
<a-menu-item key="/userList">普通用户列表</a-menu-item>
<a-menu-item key="/managerInformation">经理信息</a-menu-item>
</a-sub-menu>
<a-sub-menu>
<template #title>
@ -61,11 +62,18 @@
<a-menu-item key="/workList">工作列表</a-menu-item>
<a-menu-item key="/workDetail">工作详情</a-menu-item>
</a-sub-menu>
<a-sub-menu>
<template #title>
<span>
<FieldTimeOutlined />
<span>业绩管理</span>
</span>
</template>
<a-menu-item key="/performanceManagement">业绩报表</a-menu-item>
<a-menu-item key="/supervisorPerformanceRanking">主管绩效排行</a-menu-item>
<a-menu-item key="/employeePerformanceRanking">员工绩效排行</a-menu-item>
<a-menu-item key="/performanceManagement">
<CommentOutlined />
<span>业绩管理</span>
</a-menu-item>
</a-sub-menu>
<a-menu-item key="/employeeApplication">
<CommentOutlined />
<span>员工申请管理</span>

View File

@ -162,6 +162,21 @@ export const routes = [
name:'员工申请详情',
component: ()=> import("../view/employeeApplication/employeeDetail.vue")
},
{
path:'/managerInformation',
name:'经理信息',
component: ()=> import("../view/userList/managerInformation.vue")
},
{
path:'/supervisorPerformanceRanking',
name:'主管绩效排行',
component: ()=> import("../view/performance/supervisorPerformanceRanking.vue")
},
{
path:'/employeePerformanceRanking',
name:'员工绩效排行',
component: ()=> import("../view/performance/employeePerformanceRanking.vue")
},
]
},
]

View File

@ -120,11 +120,15 @@
@input="validatePrices"
>
</label>
<!-- 新增错误提示 -->
<div v-if="priceError" class="price-error">
{{ priceError }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 富文本编辑器区域 - 修改为50%宽度 -->
<div class="rich-text-container">
@ -158,6 +162,7 @@ import RichTextEditor from '../components/RichTextEditor.vue';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import myAxios from "../../api/myAxios.ts";
import router from "../../router";
import {message} from "ant-design-vue";
interface CourseForm {
name: string;
@ -173,6 +178,8 @@ interface CourseForm {
const fileInput = ref<HTMLInputElement | null>(null);
const fileName = ref('');
// 新增价格错误状态
const priceError = ref('');
const handleFileUpload = async (event: Event) => {
const input = event.target as HTMLInputElement;
@ -241,28 +248,27 @@ const encryptValue = (value: any, key: string): any => {
}
};
// 验证函数
// 修改验证函数:添加错误提示
const validatePrices = () => {
priceError.value = '';
if (formData.discountPrice >= formData.originPrice) {
message.warn('折扣价必须小于原价')
priceError.value = '折扣价必须小于原价';
return false;
}
return true;
};
const handleSubmit = async () => {
// 验证价格和佣金比例
// 重置错误状态
priceError.value = '';
// 验证价格
const isPriceValid = validatePrices();
let errorMessage = '';
if (!isPriceValid) {
errorMessage += '折扣价必须小于原价\n';
}
if (errorMessage) {
alert(errorMessage);
return;
return; // 验证失败时不提交
}
try {

View File

@ -456,13 +456,6 @@ const showDetails = (id: string) => {
})
}
// 查看章节详情
// const chapterDetails = (id: string) => {
// router.push({
// path: '/chapterDetail',
// query: { id: String(id) }
// })
// }
</script>
<style scoped>

View File

@ -11,6 +11,7 @@
@search="handleSearch"
v-model:value="searchOrderNumber"
class="custom-search"
@input="filterOrderInput"
/>
</a-form-item>
@ -30,13 +31,6 @@
</a-form-item>
<a-button class="custom-button" @click="reset">重置搜索</a-button>
<a-button
class="custom-button danger"
:disabled="selectedRowKeys.length === 0"
@click="showDeleteConfirm"
>
批量删除
</a-button>
</a-space>
</a-form>
</div>
@ -45,10 +39,9 @@
<a-table
:columns="columns"
:data-source="tableData"
:scroll="{ x: 2000, y: 550 }"
:scroll="{ x: 1800, y: 550 }"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
bordered
rowKey="id"
@change="handleTableChange"
@ -127,13 +120,7 @@ const searchParams = ref({
orderStatus: ""
});
// 行选择配置
const rowSelection = ref({
selectedRowKeys: selectedRowKeys,
onChange: (selectedKeys: number[]) => {
selectedRowKeys.value = selectedKeys;
}
});
// 订单表格列定义
const columns = [
@ -150,62 +137,17 @@ const columns = [
{
title: '订单号',
dataIndex: 'orderNumber',
width: 200,
width: 150,
key: 'orderNumber',
fixed: 'left',
align: 'center'
},
{
title: '课程图片',
dataIndex: 'image',
key: 'image',
width: 75,
align: 'center'
},
{
title: '用户ID',
dataIndex: 'userId',
width: 90,
key: 'userId',
align: 'center'
},{
title: '课程ID',
dataIndex: 'courseId',
width: 90,
key: 'courseId',
align: 'center'
},
{
title: '课程名称',
dataIndex: 'name',
width: 140,
key: 'name',
align: 'center'
},
{
title: '课程类别',
dataIndex: 'type',
width: 75,
key: 'type',
align: 'center'
},
{
title: '课程原价',
dataIndex: 'originPrice',
width: 95,
key: 'originPrice',
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '折扣价格',
dataIndex: 'discountPrice',
key: 'discountPrice',
width: 95,
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '订单金额',
@ -216,13 +158,6 @@ const columns = [
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '交易号',
dataIndex: 'transactionNumber',
key: 'transactionNumber',
width: 150,
align: 'center'
},
{
title: '订单状态',
dataIndex: 'orderStatus',
@ -239,15 +174,6 @@ const columns = [
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
width: 150,
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '操作',
key: 'action',
@ -257,6 +183,18 @@ const columns = [
}
];
const filterOrderInput = (e: Event) => {
const input = e.target as HTMLInputElement;
let value = input.value;
// 使用正则表达式过滤非数字字符(只保留数字)
value = value.replace(/\D/g, '');
// 更新输入框值
input.value = value;
searchOrderNumber.value = value;
};
// 订单搜索方法
const handleSearch = async () => {
searchParams.value.orderNumber = searchOrderNumber.value;
@ -394,44 +332,6 @@ const deleteOrder = (id: number) => {
});
};
// 批量删除确认弹窗
const showDeleteConfirm = () => {
if (selectedRowKeys.value.length === 0) {
message.warning('请至少选择一个订单');
return;
}
Modal.confirm({
title: '确认批量删除',
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个订单吗?删除后数据将无法恢复!`,
okText: '确认',
cancelText: '取消',
onOk: deleteBatchOrders
});
};
// 批量删除方法
const deleteBatchOrders = async () => {
try {
const storedToken = localStorage.getItem('token');
const res: any = await myAxios.post(
"/courseOrder/delBatch",
{ ids: selectedRowKeys.value },
{ headers: { Authorization: storedToken } }
);
if (res.code === 1) {
message.success(`成功删除 ${selectedRowKeys.value.length} 个订单`);
selectedRowKeys.value = []; // 清空选择
await getOrderList(); // 刷新列表
} else {
message.error(res.message || '批量删除失败');
}
} catch (error) {
console.error('批量删除失败:', error);
message.error('批量删除操作失败');
}
};
// 重置搜索条件
const reset = () => {

View File

@ -46,6 +46,13 @@ const columns = [
: '-';
}
},
{
title: '身份证号',
dataIndex: 'idCard',
key: 'idCard',
width: 140,
align: 'center'
},
{
title: '审核状态',
dataIndex: 'reviewStatus',
@ -56,6 +63,7 @@ const columns = [
{ text: '已通过', value: '已通过' },
{ text: '待审核', value: '待审核' },
{ text: '已拒绝', value: '已拒绝' },
{ text: '已撤回', value: '已撤回' },
],
filterMultiple: false,
},
@ -154,20 +162,27 @@ const fetchAdvancementData = async () => {
}
};
// 查看简历
const handleViewResume = (resumeCode: string) => {
// 这里实现查看简历的逻辑
// 例如:打开一个新窗口,显示简历
// window.open(`/api/resume/view?code=${resumeCode}`, '_blank');
const handleViewResume = (fileName: string) => {
const storedToken = localStorage.getItem('token');
const baseURL = myAxios.defaults.baseURL || '';
// 或者调用API获取简历内容
// fetchResumeContent(resumeCode);
// 直接构建下载URL
const downloadUrl = `${baseURL}/file/download/${encodeURIComponent(fileName)}`;
// 暂时使用消息提示
message.info(`查看简历:${resumeCode}`);
// 创建隐藏链接
const link = document.createElement('a');
link.href = downloadUrl;
link.setAttribute('download', fileName);
link.style.display = 'none';
// 添加认证头(注意:这种方法不一定有效)
link.setAttribute('Authorization', `Bearer ${storedToken}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 处理表格变化事件(分页、排序、筛选)
const handleTableChange: TableProps['onChange'] = (pag, filters, sorter) => {
// 更新分页参数

View File

@ -11,12 +11,11 @@
@search="handleSearch"
v-model:value="searchOrderNumber"
class="custom-search"
@input="filterOrderNumber"
/>
</a-form-item>
<a-button class="custom-button" @click="reset">重置搜索</a-button>
<a-button class="custom-button" @click="goBack">返回</a-button>
</a-space>
</a-form>
</div>
@ -25,60 +24,92 @@
<a-table
:columns="columns"
:data-source="tableData"
:scroll="{ x: 2500, y: 550 }"
:scroll="{ x: 1500, y: 550 }"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
bordered
rowKey="id"
>
<template #bodyCell="{ column, record }">
<!-- 图片 -->
<template v-if="column.key === 'image'">
<a-avatar :src="record.image" shape="square" :size="64"/>
</template>
<!-- 金额 -->
<template v-if="column.key === 'amount'">
¥{{record.amount }}
<template v-if="column.key === 'totalAmount'">
¥{{ record.totalAmount }}
</template>
<!-- 状态 -->
<template v-if="column.key === 'status'">
<a-tag :color="record.status === '交易成功' ? 'green' : 'orange'">
{{ record.status }}
<template v-if="column.key === 'orderStatus'">
<a-tag :color="record.orderStatus === '交易成功' ? 'green' : 'orange'">
{{ record.orderStatus }}
</a-tag>
</template>
<!-- 操作列 -->
<template v-if="column.key === 'action'">
<a-space :size="8">
<a-button
size="small"
@click="customerDetail"
>
详情
</a-button>
<a-button
size="small"
danger
@click="deleteOrder(record.id)"
>
删除
</a-button>
</a-space>
<!-- 提成状态 -->
<template v-if="column.key === 'commissionStatus'">
<a-tag :color="getCommissionStatusColor(record.commissionStatus)">
{{ record.commissionStatus }}
</a-tag>
</template>
</template>
</a-table>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import { message, Modal } from "ant-design-vue";
import { onMounted, ref, watch,nextTick } from "vue";
import { message } from "ant-design-vue";
import router from "../../router";
import { useRoute } from "vue-router";
import myAxios from "../../api/myAxios.ts";
const loading = ref(false);
const searchOrderNumber = ref("");
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
showTotal: (total: number) => `${total} 条记录`,
});
const route = useRoute();
// 客户订单明细表
// 修复:确保正确获取路由参数
const employeeId = ref<number>(0);
const filterOrderNumber = (event: Event) => {
const inputElement = event.target as HTMLInputElement;
// 保存当前光标位置
const start = inputElement.selectionStart;
const end = inputElement.selectionEnd;
// 移除非数字字符
searchOrderNumber.value = searchOrderNumber.value.replace(/[^\d]/g, '');
// 恢复光标位置(避免光标跳到末尾)
nextTick(() => {
inputElement.setSelectionRange(start, end);
});
};
// 客户订单接口定义
interface OrderRecord {
id: number;
nickName: string;
phoneNumber: string;
orderNumber: string;
totalAmount: number;
orderStatus: string;
firstRate: number;
secondRate: number;
firstReward: number;
secondReward: number;
commissionStatus: string;
createTime: string;
}
const tableData = ref<OrderRecord[]>([]);
const columns = [
{
title: '编号',
@ -88,6 +119,14 @@ const columns = [
fixed: 'left',
align: 'center'
},
{
title: '用户Id',
dataIndex: 'userId',
width: 80,
key: 'userId',
fixed: 'left',
align: 'center'
},
{
title: '订单号',
dataIndex: 'orderNumber',
@ -98,208 +137,253 @@ const columns = [
},
{
title: '用户名',
dataIndex: 'userName',
key: 'userName',
dataIndex: 'nickName',
key: 'nickName',
width: 120,
align: 'center'
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
dataIndex: 'phoneNumber',
key: 'phoneNumber',
width: 120,
align: 'center'
},
{
title: '课程名称',
dataIndex: 'courseName',
key: 'courseName',
width: 150,
align: 'center'
},
{
title: '类别',
dataIndex: 'category',
key: 'category',
title: '订单金额',
dataIndex: 'totalAmount',
key: 'totalAmount',
width: 100,
align: 'center'
},
{
title: '图片',
dataIndex: 'image',
key: 'image',
title: '订单状态',
dataIndex: 'orderStatus',
key: 'orderStatus',
width: 100,
align: 'center'
},
{
title: '金额',
dataIndex: 'amount',
key: 'amount',
title: '一级提成比例',
dataIndex: 'firstRate',
key: 'firstRate',
width: 120,
align: 'center',
customRender: ({ text }: { text: number }) => `${(text * 100).toFixed(0)}%`
},
{
title: '二级提成比例',
dataIndex: 'secondRate',
key: 'secondRate',
width: 120,
align: 'center',
customRender: ({ text }: { text: number }) => `${(text * 100).toFixed(0)}%`
},
{
title: '一级提成金额',
dataIndex: 'firstReward',
key: 'firstReward',
width: 120,
align: 'center',
customRender: ({ text }: { text: number }) => `¥${text.toFixed(2)}`
},
{
title: '二级提成金额',
dataIndex: 'secondReward',
key: 'secondReward',
width: 120,
align: 'center',
customRender: ({ text }: { text: number }) => `¥${text.toFixed(2)}`
},
{
title: '提成状态',
dataIndex: 'commissionStatus',
key: 'commissionStatus',
width: 100,
align: 'center'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: 'center'
},
{
title: '下单时间',
dataIndex: 'orderTime',
key: 'orderTime',
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180,
align: 'center'
},
{
title: '支付时间',
dataIndex: 'paymentTime',
key: 'paymentTime',
width: 180,
align: 'center'
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 100,
align: 'center'
}
];
// 客户订单接口定义
interface OrderRecord {
id: number;
orderNumber: string;
userName: string;
phone: string;
courseName: string;
category: string;
image: string;
amount: number;
status: string;
orderTime: string;
paymentTime: string;
}
const tableData = ref<OrderRecord[]>([]);
const originalTableData = ref<OrderRecord[]>([]);
// 生成模拟数据
const generateMockData = () => {
const mockData: OrderRecord[] = [];
const names = ['陈新知', '李小明', '王芳', '张伟', '赵敏', '刘洋', '周杰', '吴婷'];
const courses = [
'UI设计实战课程',
'Java高级开发',
'Python数据分析',
'前端框架精讲',
'产品经理实战',
'人工智能入门',
'云计算基础',
'网络安全实战'
];
const categories = ['考证', '技能提升', '职业发展', '兴趣爱好'];
const statuses = ['交易成功', '待支付', '已取消', '退款中'];
// 生成20条订单记录
for (let i = 0; i < 20; i++) {
const nameIndex = i % names.length;
const courseIndex = i % courses.length;
const categoryIndex = i % categories.length;
const statusIndex = i % statuses.length;
// 生成随机订单号
const orderNumber = `2025${(Math.floor(Math.random() * 12) + 1).toString().padStart(2, '0')}${(Math.floor(Math.random() * 28) + 1).toString().padStart(2, '0')}${Math.floor(Math.random() * 1000000).toString().padStart(6, '0')}`;
// 生成随机时间
const orderDate = `2025-${(Math.floor(Math.random() * 6) + 1).toString().padStart(2, '0')}-${(Math.floor(Math.random() * 28) + 1).toString().padStart(2, '0')}`;
const orderTime = `${Math.floor(Math.random() * 24).toString().padStart(2, '0')}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}`;
const paymentTime = `${Math.floor(Math.random() * 24).toString().padStart(2, '0')}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}`;
mockData.push({
id: i + 1,
orderNumber: orderNumber,
userName: names[nameIndex],
phone: `1${Math.floor(Math.random() * 9000000000) + 1000000000}`,
courseName: courses[courseIndex],
category: categories[categoryIndex],
image: `https://picsum.photos/200?random=${i}`, // 随机图片
amount: Math.floor(Math.random() * 1000) + 100,
status: statuses[statusIndex],
orderTime: `${orderDate} ${orderTime}`,
paymentTime: `${orderDate} ${paymentTime}`
});
}
return mockData;
// 获取提成状态颜色
const getCommissionStatusColor = (status: string) => {
const statusColors: Record<string, string> = {
'待提成': 'orange',
'已提成': 'green',
'已取消': 'red'
};
return statusColors[status] || 'blue';
};
// 获取订单数据
const fetchOrderData = async () => {
loading.value = true;
try {
const token = localStorage.getItem('token');
if (!token) {
message.error('未找到登录信息');
return;
}
const params = {
current: pagination.value.current,
pageSize: pagination.value.pageSize,
orderNumber: searchOrderNumber.value,
staffUserId: employeeId.value
};
console.log("发送请求参数:", params); // 调试日志
const response:any = await myAxios.post(
'/perform/user/page',
params,
{ headers: { Authorization: token } }
);
if (response.code === 1) {
const data = response.data;
tableData.value = data.records || [];
pagination.value.total = data.total;
} else {
message.error(response.message || '获取订单数据失败');
}
} catch (error) {
console.error('获取订单数据失败:', error);
message.error('获取订单数据失败,请稍后再试');
} finally {
loading.value = false;
}
};
// 搜索功能
const handleSearch = () => {
if (!searchOrderNumber.value.trim()) {
tableData.value = [...originalTableData.value];
return;
}
const filtered = originalTableData.value.filter(item =>
item.orderNumber.includes(searchOrderNumber.value)
);
if (filtered.length === 0) {
message.warning("未找到匹配的订单");
tableData.value = [];
} else {
tableData.value = filtered;
}
pagination.value.current = 1;
fetchOrderData();
};
// 删除订单
const deleteOrder = (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该订单吗?删除后数据将无法恢复!',
okText: '确认',
cancelText: '取消',
onOk: async () => {
// 前端删除
const index = tableData.value.findIndex(item => item.id === id);
if (index !== -1) {
tableData.value.splice(index, 1);
message.success('删除成功');
}
}
});
};
// 重置按钮
// 重置搜索
const reset = () => {
searchOrderNumber.value = "";
tableData.value = [...originalTableData.value];
pagination.value.current = 1;
fetchOrderData();
};
// 加载模拟数据
// 处理分页变化
const handleTableChange = (pag: any) => {
pagination.value = { ...pagination.value, ...pag };
fetchOrderData();
};
// 返回员工绩效页面
const goBack = () => {
router.push('/performanceManagement');
};
// 安全解析路由参数 - 修复版
const parseEmployeeId = () => {
// 1. 直接使用 route.params.employeeId 可能不准确
// 2. 使用 route.params 对象中的具体键名
const idParam = route.params.employeeId || route.params.id;
// 调试信息
console.log("路由参数对象:", route.params);
console.log("原始员工ID参数:", idParam);
// if (!idParam) {
// console.error("未找到员工ID参数");
// message.error("未找到员工ID参数请返回重试");
// employeeId.value = 0;
// return;
// }
if (Array.isArray(idParam)) {
// 处理数组类型的参数(如多个匹配)
employeeId.value = parseInt(idParam[0]) || 0;
} else if (typeof idParam === 'string') {
// 处理字符串类型的参数
employeeId.value = parseInt(idParam) || 0;
} else if (typeof idParam === 'number') {
// 直接是数字类型
employeeId.value = idParam;
} else {
// 处理其他情况
employeeId.value = 0;
}
console.log("解析后的员工ID:", employeeId.value);
// if (employeeId.value <= 0) {
// console.error("无效的员工ID参数:", idParam);
// message.error("无效的员工ID请返回重试");
// }
};
// 初始加载数据 - 修复版
onMounted(() => {
loading.value = true;
setTimeout(() => {
const mockData = generateMockData();
tableData.value = mockData;
originalTableData.value = mockData;
loading.value = false;
}, 800);
console.log("组件挂载,当前路由:", route.path);
console.log("路由参数:", route.params);
parseEmployeeId();
// 如果员工ID有效则获取数据
if (employeeId.value > 0) {
fetchOrderData();
} else {
// 尝试从路由查询参数中获取
const queryId = route.query.employeeId || route.query.id;
if (queryId) {
console.log("尝试从查询参数获取员工ID:", queryId);
employeeId.value = parseInt(queryId.toString()) || 0;
if (employeeId.value > 0) {
fetchOrderData();
}
}
}
});
const customerDetail =()=>{
router.push('/customerDetail')
}
// 监听路由参数变化 - 修复版
watch(
() => route.params,
(newParams) => {
console.log("路由参数变化:", newParams);
parseEmployeeId();
if (employeeId.value > 0) {
pagination.value.current = 1;
fetchOrderData();
}
},
{ deep: true, immediate: false }
);
const goBack = () => {
router.push('/employeePerformaince');
};
// 监听路由查询参数变化
watch(
() => route.query,
(newQuery) => {
console.log("路由查询参数变化:", newQuery);
const queryId = newQuery.employeeId || newQuery.id;
if (queryId) {
employeeId.value = parseInt(queryId.toString()) || 0;
if (employeeId.value > 0) {
pagination.value.current = 1;
fetchOrderData();
}
}
},
{ deep: true, immediate: false }
);
</script>
<style scoped>
/* 样式保持不变 */
:deep(.ant-divider-vertical) {
border-color: rgba(0, 0, 0, 0.15);
height: 1.2em;

View File

@ -299,6 +299,7 @@ const updateProportionRate = async () => {
};
onMounted(() => {
fetchProportionRate();
// 确保有managerId参数
if (!managerId.value) {
message.warning("未获取到主管ID");
@ -307,7 +308,7 @@ onMounted(() => {
}
fetchEmployeePerformance();
fetchProportionRate();
});

View File

@ -0,0 +1,383 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import myAxios from "../../api/myAxios.ts";
import {
message,
Table as ATable,
Pagination as APagination,
Input,
Button,
Select,
DatePicker
} from 'ant-design-vue';
interface PerformanceRank {
nickName: string;
phoneNumber: string;
empCount: number;
promoCount: number;
orderCount: number;
totalAmount: number;
netAmount: number;
}
interface PerformanceRankResponse {
code: number;
data: {
records: PerformanceRank[];
total: number;
size: number;
current: number;
pages: number;
};
message: string;
}
// 排序选项
const sortOptions = [
{ label: '员工数量', value: 'empCount' },
{ label: '推广人数', value: 'promoCount' },
{ label: '下单数量', value: 'orderCount' },
{ label: '订单总金额', value: 'totalAmount' },
{ label: '净成交金额', value: 'netAmount' }
];
// 排序方向选项
const orderOptions = [
{ label: '升序', value: 'asc' },
{ label: '降序', value: 'desc' }
];
// 查询参数
const queryParams = reactive({
current: 1,
pageSize: 10,
sortField: "netAmount", // 默认排序字段为净成交金额
sortOrder: "asc", // 默认排序方向为升序
nickName: "",
phoneNumber: "",
startDate: "",
endDate: ""
});
// 绩效排行数据
const performanceRanks = ref<PerformanceRank[]>([]);
const loading = ref(false);
const total = ref(0);
// 表格列定义
const columns = [
{ title: '用户昵称', dataIndex: 'nickName', key: 'nickName' },
{ title: '手机号', dataIndex: 'phoneNumber', key: 'phoneNumber' },
{ title: '员工数量', dataIndex: 'empCount', key: 'empCount' },
{ title: '推广人数', dataIndex: 'promoCount', key: 'promoCount' },
{ title: '下单数量', dataIndex: 'orderCount', key: 'orderCount' },
{
title: '订单总金额',
dataIndex: 'totalAmount',
key: 'totalAmount',
customRender: ({ text }: { text: number }) => `¥${text.toFixed(2)}`
},
{
title: '净成交',
dataIndex: 'netAmount',
key: 'netAmount',
customRender: ({ text }: { text: number }) => `¥${text.toFixed(2)}`
}
];
onMounted(async () => {
await fetchPerformanceRank();
});
// 获取绩效排行数据
const fetchPerformanceRank = async () => {
try {
loading.value = true;
const token = localStorage.getItem('token');
// 准备请求参数 - 确保金额参数为数字类型
const params = {
current: queryParams.current,
pageSize: queryParams.pageSize,
sortField: queryParams.sortField,
sortOrder: queryParams.sortOrder,
nickName: queryParams.nickName,
phoneNumber: queryParams.phoneNumber,
startDate: queryParams.startDate,
endDate: queryParams.endDate
};
const response:any = await myAxios.post<PerformanceRankResponse>(
'/perform/rank/staff/page',
params,
{ headers: { Authorization: token } }
);
if (response.code === 1) {
// 确保金额字段为数字类型
performanceRanks.value = response.data.records.map((record:any) => ({
...record,
totalAmount: Number(record.totalAmount),
netAmount: Number(record.netAmount)
}));
total.value = response.data.total;
} else {
throw new Error(response.message || '获取绩效排行失败');
}
} catch (err: any) {
console.error('获取绩效排行出错:', err);
message.error(err.message || '获取绩效排行失败,请稍后再试');
} finally {
loading.value = false;
}
};
// 处理分页变化
const handlePageChange = async (page: number, pageSize: number) => {
queryParams.current = page;
queryParams.pageSize = pageSize;
await fetchPerformanceRank();
};
// 处理排序变化
const handleSortChange = async () => {
queryParams.current = 1; // 重置到第一页
await fetchPerformanceRank();
};
// 重置搜索条件
const resetSearch = async () => {
queryParams.nickName = '';
queryParams.phoneNumber = '';
queryParams.startDate = '';
queryParams.endDate = '';
queryParams.current = 1;
await fetchPerformanceRank();
};
// 处理搜索
const handleSearch = async () => {
queryParams.current = 1; // 搜索时重置到第一页
await fetchPerformanceRank();
};
// 处理日期范围变化
// 处理日期范围变化
const handleDateChange = (_: any, dateStrings: [string, string]) => {
queryParams.startDate = dateStrings[0] || '';
queryParams.endDate = dateStrings[1] || '';
};
</script>
<template>
<div class="performance-container">
<div class="header">
<h2>员工绩效排行</h2>
<div class="filter-container">
<!-- 排序条件区域 -->
<div class="sort-section">
<div class="sort-header">排序条件</div>
<div class="sort-controls">
<Select
v-model:value="queryParams.sortField"
placeholder="选择排序字段"
style="width: 150px; margin-right: 10px;"
@change="handleSortChange"
>
<Select.Option v-for="option in sortOptions" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
<Select
v-model:value="queryParams.sortOrder"
placeholder="排序方式"
style="width: 120px; margin-right: 10px;"
@change="handleSortChange"
>
<Select.Option v-for="option in orderOptions" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</div>
</div>
<!-- 搜索条件区域 -->
<div class="search-section">
<div class="search-header">搜索条件</div>
<div class="search-controls">
<Input
v-model:value="queryParams.phoneNumber"
placeholder="手机号"
style="width: 180px; margin-right: 10px;"
allow-clear
/>
<Input
v-model:value="queryParams.nickName"
placeholder="用户昵称"
style="width: 180px; margin-right: 10px;"
allow-clear
/>
<DatePicker.RangePicker
@change="handleDateChange"
style="margin-right: 10px; width: 220px;"
/>
<Button type="primary" @click="handleSearch" style="margin-right: 10px;">
搜索
</Button>
<Button @click="resetSearch">
清空
</Button>
</div>
</div>
</div>
</div>
<a-table
:dataSource="performanceRanks"
:columns="columns"
:pagination="false"
:loading="loading"
:rowKey="(record:any) => record.phoneNumber"
bordered
>
<!-- 金额格式化 -->
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'totalAmount'">
¥{{ record.totalAmount.toFixed(2) }}
</template>
<template v-else-if="column.dataIndex === 'netAmount'">
¥{{ record.netAmount.toFixed(2) }}
</template>
</template>
</a-table>
<a-pagination
class="pagination"
v-model:current="queryParams.current"
v-model:pageSize="queryParams.pageSize"
:total="total"
:pageSizeOptions="['10', '20', '50', '100']"
show-size-changer
show-quick-jumper
@change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>
</template>
<style scoped>
.performance-container {
padding: 20px;
/* 温馨的乳白色背景,带一点暖色调 */
background: #fffaf0;
border-radius: 12px;
/* 轻柔的暖色阴影 */
box-shadow: 0 4px 12px rgba(255, 164, 64, 0.2);
transition: transform 0.3s;
}
.performance-container:hover {
transform: translateY(-2px);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
h2 {
margin: 0;
/* 使用主题色 */
color: #ffa940;
font-size: 20px;
font-weight: 600;
}
.filter-container {
width: 100%;
margin-top: 15px;
/* 柔和的卡片背景 */
background: #fff5e6;
border: 1px solid #ffd591;
border-radius: 8px;
padding: 18px;
}
.sort-section, .search-section {
margin-bottom: 18px;
}
.sort-header, .search-header {
font-weight: 600;
margin-bottom: 12px;
color: #333;
font-size: 15px;
}
.sort-controls, .search-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.pagination {
margin-top: 25px;
text-align: right;
}
/* 单元格边框去掉横线,更显高级 */
:deep(.ant-table-cell) {
border-bottom: none;
padding: 12px 8px;
}
/* 分页器主题色 */
:deep(.ant-pagination-item-active) a {
color: #ffa940 !important;
}
:deep(.ant-pagination-item-active) {
border-color: #ffa940 !important;
}
:deep(.ant-pagination-prev .ant-pagination-item-link),
:deep(.ant-pagination-next .ant-pagination-item-link) {
color: #ffa940;
}
/* 按钮主题色 */
:deep(.ant-btn-primary) {
background-color: #ffa940;
border-color: #ffa940;
}
:deep(.ant-btn-primary:hover),
:deep(.ant-btn-primary:focus) {
background-color: #ff9d24;
border-color: #ff9d24;
}
/* 下拉选择框主题色 */
:deep(.ant-select-selector) {
border-color: #ffd591 !important;
}
:deep(.ant-select-focused .ant-select-selector) {
box-shadow: 0 0 0 2px rgba(255, 164, 64, 0.2) !important;
}
/* 日期选择控件 */
:deep(.ant-picker-range) {
border-color: #ffd591 !important;
}
:deep(.ant-picker-focused) {
box-shadow: 0 0 0 2px rgba(255, 164, 64, 0.2) !important;
}
</style>

View File

@ -230,7 +230,7 @@ const fetchProportionRate = async () => {
{ level: "first" }, // 按照接口要求发送level参数
{ headers: { Authorization: storedToken } }
);
console.log(response)
if (response.code === 1) {
proportionRate.value = response.data;
} else {
@ -294,8 +294,9 @@ const updateProportionRate = async () => {
}
};
onMounted(() => {
fetchManagerPerformance();
fetchProportionRate(); // 调用获取抽成比例
fetchManagerPerformance();
});
const filterNameInput = (e: Event) => {
@ -409,7 +410,7 @@ const settlement = async () => {
<a-space>
<a-form-item label="主管姓名">
<a-input
style="width: 300px;"
style="width: 300px;border: 1px solid #ffa940;"
placeholder="请输入主管姓名"
v-model:value="searchName"
@click="handleSearch"
@ -419,7 +420,7 @@ const settlement = async () => {
</a-form-item>
<a-form-item label="手机号">
<a-input
style="width: 200px"
style="width: 300px;border: 1px solid #ffa940;"
placeholder="请输入手机号"
v-model:value="searchPhone"
class="custom-search"
@ -486,8 +487,9 @@ const settlement = async () => {
</template>
</a-table>
</template>
<style scoped>
/* 样式保持不变 */
/* 搜索框样式 */
.search-box {
margin-bottom: 20px;
padding: 16px;
@ -496,54 +498,41 @@ const settlement = async () => {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
:deep(.ant-table-thead) > tr > th {
background-color: #f0f5ff !important;
font-weight: 600;
}
:deep(.ant-table-row:hover) {
background-color: #fafafa !important;
}
.search-box .ant-form-item {
margin-bottom: 0;
}
.search-box .ant-space {
width: 100%;
/* 搜索框输入框聚焦和悬浮状态边框为橙色 */
.search-box .custom-search :deep(.ant-input:hover),
.search-box .custom-search :deep(.ant-input:focus) {
border-color: #ffa940 !important;
box-shadow: 0 0 0 2px rgba(255, 169, 64, 0.2) !important;
}
/* 按钮样式 */
.custom-button {
background-color: #ffa940;
border-color: #ffa940;
color: #fff;
border: 1px solid #ffa940;
}
.custom-button:hover,
.custom-button:focus {
background-color: #ffa940;
border-color: #ffa940;
color:#fff;
color: #fff;
opacity: 0.9;
}
/* 搜索按钮样式 */
.custom-search :deep(.ant-input-search-button) {
background-color: #ffa940;
border-color: #ffa940;
border: 1px solid #ffa940;
}
.custom-search :deep(.ant-input-search-button:hover),
.custom-search :deep(.ant-input-search-button:focus) {
background-color: #fa8c16;
border-color: #fa8c16;
border: 1px solid #ffa940;
}
.custom-search :deep(.ant-input) {
border-right-color: #ffa940;
}
.custom-search :deep(.ant-input-search-button) {
background-color: #ffa940;
border-color: #ffa940;
}
</style>

View File

@ -0,0 +1,378 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import myAxios from "../../api/myAxios.ts";
import {
message,
Table as ATable,
Pagination as APagination,
Input,
Button,
Select,
DatePicker
} from 'ant-design-vue';
interface PerformanceRank {
nickName: string;
phoneNumber: string;
empCount: number;
promoCount: number;
orderCount: number;
totalAmount: number;
netAmount: number;
}
interface PerformanceRankResponse {
code: number;
data: {
records: PerformanceRank[];
total: number;
size: number;
current: number;
pages: number;
};
message: string;
}
// 排序选项
const sortOptions = [
{ label: '员工数量', value: 'empCount' },
{ label: '推广人数', value: 'promoCount' },
{ label: '下单数量', value: 'orderCount' },
{ label: '订单总金额', value: 'totalAmount' },
{ label: '净成交金额', value: 'netAmount' }
];
// 排序方向选项
const orderOptions = [
{ label: '升序', value: 'asc' },
{ label: '降序', value: 'desc' }
];
// 查询参数
const queryParams = reactive({
current: 1,
pageSize: 10,
sortField: "",
sortOrder: "",
nickName: "",
phoneNumber: "",
startDate: "",
endDate: ""
});
// 绩效排行数据
const performanceRanks = ref<PerformanceRank[]>([]);
const loading = ref(false);
const total = ref(0);
// 表格列定义
const columns = [
{ title: '用户昵称', dataIndex: 'nickName', key: 'nickName' },
{ title: '手机号', dataIndex: 'phoneNumber', key: 'phoneNumber' },
{ title: '员工数量', dataIndex: 'empCount', key: 'empCount' },
{ title: '推广人数', dataIndex: 'promoCount', key: 'promoCount' },
{ title: '下单数量', dataIndex: 'orderCount', key: 'orderCount' },
{
title: '订单总金额',
dataIndex: 'totalAmount',
key: 'totalAmount',
customRender: ({ text }: { text: number }) => `¥${text.toFixed(2)}`
},
{
title: '净成交',
dataIndex: 'netAmount',
key: 'netAmount',
customRender: ({ text }: { text: number }) => `¥${text.toFixed(2)}`
}
];
onMounted(async () => {
await fetchPerformanceRank();
});
// 获取绩效排行数据
const fetchPerformanceRank = async () => {
loading.value = true;
const token = localStorage.getItem('token');
try {
// 准备请求参数
const params = {
current: queryParams.current,
pagesize: queryParams.pageSize,
sortField: queryParams.sortField,
sortOrder: queryParams.sortOrder,
nickName: queryParams.nickName,
phoneNumber: queryParams.phoneNumber,
startDate: queryParams.startDate,
endDate: queryParams.endDate
};
const response:any = await myAxios.post<PerformanceRankResponse>(
'/perform/rank/supervisor/page',
params,
{ headers: { Authorization: token } }
);
if (response.code === 1) {
performanceRanks.value = response.data.records;
total.value = response.data.total;
} else {
throw new Error(response.message || '获取绩效排行失败');
}
} catch (err: any) {
console.error('获取绩效排行出错:', err);
message.error(err.message || '获取绩效排行失败,请稍后再试');
} finally {
loading.value = false;
}
};
// 处理分页变化
const handlePageChange = (page: number, pageSize: number) => {
queryParams.current = page;
queryParams.pageSize = pageSize;
fetchPerformanceRank();
};
// 处理排序变化
const handleTableChange = (_: any, __: any, sorter: any) => {
if (sorter.field) {
queryParams.sortField = sorter.field;
queryParams.sortOrder = sorter.order === 'ascend' ? 'asc' : 'desc';
} else {
queryParams.sortField = '';
queryParams.sortOrder = '';
}
fetchPerformanceRank();
};
// 重置搜索条件
const resetSearch = () => {
queryParams.nickName = '';
queryParams.phoneNumber = '';
queryParams.startDate = '';
queryParams.endDate = '';
queryParams.sortField = '';
queryParams.sortOrder = '';
fetchPerformanceRank();
};
// 处理日期范围变化
const handleDateChange = (dates: any, dateStrings: [string, string]) => {
if (dates) {
queryParams.startDate = dateStrings[0];
queryParams.endDate = dateStrings[1];
} else {
queryParams.startDate = '';
queryParams.endDate = '';
}
};
</script>
<template>
<div class="performance-container">
<div class="header">
<h2>主管绩效排行</h2>
<div class="filter-container">
<!-- 排序条件区域 -->
<div class="sort-section">
<div class="sort-header">排序条件</div>
<div class="sort-controls">
<Select
v-model:value="queryParams.sortField"
placeholder="选择排序字段"
style="width: 120px; margin-right: 10px;"
>
<Select.Option v-for="option in sortOptions" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
<Select
v-model:value="queryParams.sortOrder"
placeholder="排序方式"
style="width: 100px; margin-right: 10px;"
>
<Select.Option v-for="option in orderOptions" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</div>
</div>
<!-- 搜索条件区域 -->
<div class="search-section">
<div class="search-header">搜索条件</div>
<div class="search-controls">
<Input
v-model:value="queryParams.phoneNumber"
placeholder="手机号"
style="width: 150px; margin-right: 10px;"
/>
<Input
v-model:value="queryParams.nickName"
placeholder="用户昵称"
style="width: 150px; margin-right: 10px;"
/>
<DatePicker.RangePicker
@change="handleDateChange"
style="margin-right: 10px;"
/>
<Button type="primary" @click="fetchPerformanceRank" style="margin-right: 10px;">
搜索
</Button>
<Button @click="resetSearch">
清空
</Button>
</div>
</div>
</div>
</div>
<a-table
:dataSource="performanceRanks"
:columns="columns"
:pagination="false"
:loading="loading"
:rowKey="(record:any) => record.phoneNumber"
bordered
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'totalAmount'">
¥{{ record.totalAmount.toFixed(2) }}
</template>
<template v-else-if="column.dataIndex === 'netAmount'">
¥{{ record.netAmount.toFixed(2) }}
</template>
</template>
</a-table>
<a-pagination
class="pagination"
v-model:current="queryParams.current"
v-model:pageSize="queryParams.pageSize"
:total="total"
:pageSizeOptions="['10', '20', '50', '100']"
show-size-changer
show-quick-jumper
@change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>
</template>
<style scoped>
.performance-container {
padding: 20px;
/* 温馨的乳白色背景,带一点暖色调 */
background: #fffaf0;
border-radius: 12px;
/* 轻柔的暖色阴影 */
box-shadow: 0 4px 12px rgba(255, 164, 64, 0.2);
transition: transform 0.3s;
}
.performance-container:hover {
transform: translateY(-2px);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
h2 {
margin: 0;
/* 使用主题色 */
color: #ffa940;
font-size: 20px;
font-weight: 600;
}
.filter-container {
width: 100%;
margin-top: 15px;
/* 柔和的卡片背景 */
background: #fff5e6;
border: 1px solid #ffd591;
border-radius: 8px;
padding: 18px;
}
.sort-section, .search-section {
margin-bottom: 18px;
}
.sort-header, .search-header {
font-weight: 600;
margin-bottom: 12px;
color: #333;
font-size: 15px;
}
.sort-controls, .search-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.pagination {
margin-top: 25px;
text-align: right;
}
/* 单元格边框去掉横线,更显高级 */
:deep(.ant-table-cell) {
border-bottom: none;
padding: 12px 8px;
}
/* 分页器主题色 */
:deep(.ant-pagination-item-active) a {
color: #ffa940 !important;
}
:deep(.ant-pagination-item-active) {
border-color: #ffa940 !important;
}
:deep(.ant-pagination-prev .ant-pagination-item-link),
:deep(.ant-pagination-next .ant-pagination-item-link) {
color: #ffa940;
}
/* 按钮主题色 */
:deep(.ant-btn-primary) {
background-color: #ffa940;
border-color: #ffa940;
}
:deep(.ant-btn-primary:hover),
:deep(.ant-btn-primary:focus) {
background-color: #ff9d24;
border-color: #ff9d24;
}
/* 下拉选择框主题色 */
:deep(.ant-select-selector) {
border-color: #ffd591 !important;
}
:deep(.ant-select-focused .ant-select-selector) {
box-shadow: 0 0 0 2px rgba(255, 164, 64, 0.2) !important;
}
/* 日期选择控件 */
:deep(.ant-picker-range) {
border-color: #ffd591 !important;
}
:deep(.ant-picker-focused) {
box-shadow: 0 0 0 2px rgba(255, 164, 64, 0.2) !important;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<!-- 搜索框 -->
<div class="search-box">
<div class="search-box" v-if="hasPermission">
<div class="search-container">
<a-form layout="inline">
<a-space>
@ -762,8 +762,46 @@ const getUserList = async () => {
loading.value = false;
}
};
// 新增权限控制变量
const hasPermission = ref(true);
onMounted(async () => {
// 检查用户权限
await checkPermission();
// 如果有权限才加载数据
if (hasPermission.value) {
getUserList();
}
});
// 检查用户权限
const checkPermission = async () => {
try {
// 获取当前用户角色
const userRole = store.loginUser.userRole;
// 如果是管理员,显示提示并隐藏内容
if (userRole === 'admin') {
hasPermission.value = false;
// 使用Modal显示提示
Modal.warning({
title: '权限提示',
content: '管理员无权限查看管理员列表',
okText: '确定',
onOk() {
// 可以添加额外的处理逻辑
}
});
} else {
hasPermission.value = true;
}
} catch (error) {
console.error('权限检查失败:', error);
message.error('权限检查失败');
hasPermission.value = false;
}
};
onMounted(getUserList);
// ID查询方法
interface User {

View File

@ -0,0 +1,506 @@
<script setup lang="ts">
import { ref, onMounted} from 'vue';
import myAxios from "../../api/myAxios.ts";
import { message } from 'ant-design-vue';
import { downLoadImage } from '../../api/ImageUrl.ts';
import { EditOutlined } from '@ant-design/icons-vue';
interface ManagerInfo {
id: number;
nickName: string;
userAvatar: string;
phoneNumber: string;
userPassword: string;
}
const managerInfo = ref<Partial<ManagerInfo>>({});
const loading = ref(true);
const error = ref(false);
const avatarError = ref(false);
const editModalVisible = ref(false);
const editForm = ref<Partial<ManagerInfo>>({});
const formLoading = ref(false);
const fileInput = ref<HTMLInputElement | null>(null);
const previewAvatar = ref<string | null>(null); // 预览图URL带完整前缀
const isUploading = ref(false);
// 表单验证规则
const rules = {
nickName: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 2, max: 16, message: '昵称长度应为2-16个字符', trigger: 'blur' }
],
phoneNumber: [
{ required: true, message: '请输入11位手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
userPassword: [
{
min: 6,
max: 20,
message: '密码长度应为6-20个字符',
trigger: 'blur'
},
{
pattern: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[a-zA-Z0-9]+$/,
message: '密码需同时包含数字、小写字母、大写字母,且不能包含空格和特殊字符',
trigger: 'blur'
}
]
};
const fetchManagerInfo = async () => {
loading.value = true;
error.value = false;
const storedToken = localStorage.getItem('token');
if (!storedToken) {
message.error('未找到登录信息');
error.value = true;
loading.value = false;
return;
}
try {
const response: any = await myAxios.post(
'/userInfo/query/manager',
{},
{ headers: { Authorization: storedToken } }
);
console.log('API响应:', response);
if (response.code === 1) {
const data = response.data;
managerInfo.value = {
id: data.id,
nickName: data.nickName,
userAvatar: data.userAvatar,
phoneNumber: data.phoneNumber,
userPassword: data.userPassword
};
console.log(managerInfo.value)
} else {
throw new Error(response.data?.message || '获取经理信息失败');
}
} catch (err: any) {
console.error('获取经理信息出错:', err);
message.error(err.message || '获取经理信息失败,请稍后再试');
error.value = true;
} finally {
loading.value = false;
}
};
const openEditModal = () => {
editForm.value = {
...managerInfo.value,
userPassword: managerInfo.value.userPassword || '' // 回显原始密码
};
previewAvatar.value = managerInfo.value.userAvatar
? `${downLoadImage}${managerInfo.value.userAvatar}`
: null;
editModalVisible.value = true;
};
const handleFileChange = async () => {
if (!fileInput.value?.files || fileInput.value.files.length === 0) {
return;
}
const file = fileInput.value.files[0];
const storedToken = localStorage.getItem('token');
if (!storedToken) {
message.error('未找到登录信息');
return;
}
isUploading.value = true;
try {
// 临时预览图本地URL
previewAvatar.value = URL.createObjectURL(file);
// 上传文件
const formData = new FormData();
formData.append('biz', 'avatar');
formData.append('file', file);
const uploadResponse: any = await myAxios.post(
'/file/upload',
formData,
{ headers: { Authorization: storedToken, 'Content-Type': 'multipart/form-data' } }
);
if (uploadResponse.code === 1) {
// 接口返回的 data 直接是头像标识,这里直接作为相对路径使用
let newAvatarUrl = uploadResponse.data;
// 若需要拼接前缀,根据实际场景调整,比如:
// newAvatarUrl = `自定义前缀/${uploadResponse.data}`;
// 按之前逻辑,若返回路径带 downLoadImage 前缀则处理(根据实际接口返回决定是否保留)
if (newAvatarUrl.startsWith(downLoadImage)) {
newAvatarUrl = newAvatarUrl.replace(downLoadImage, '');
}
editForm.value.userAvatar = newAvatarUrl;
message.success('头像上传成功');
} else {
throw new Error(uploadResponse.message || '上传头像失败');
}
} catch (err: any) {
console.error('上传头像出错:', err);
message.error(err.message || '上传头像失败,请稍后再试');
// 恢复原有预览图
previewAvatar.value = managerInfo.value.userAvatar
? `${downLoadImage}${managerInfo.value.userAvatar}`
: null;
} finally {
isUploading.value = false;
// 清除临时URL避免内存泄漏
if (previewAvatar.value?.startsWith('blob:')) {
URL.revokeObjectURL(previewAvatar.value);
}
}
};
// 提交修改
const submitEdit = async () => {
formLoading.value = true;
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) {
message.error('未找到登录信息');
return;
}
const finalUserAvatar = editForm.value.userAvatar || managerInfo.value.userAvatar;
if (!finalUserAvatar) {
throw new Error('头像信息缺失');
}
const submitData = {
id: editForm.value.id,
nickName: editForm.value.nickName,
userAvatar: finalUserAvatar,
phoneNumber: editForm.value.phoneNumber,
userPassword: editForm.value.userPassword || undefined // 不传空字符串
};
const modifyResponse: any = await myAxios.post(
'/userInfo/modify/manager',
submitData,
{ headers: { Authorization: storedToken } }
);
if (modifyResponse.code === 1) {
message.success('经理信息更新成功');
managerInfo.value = {
...managerInfo.value,
nickName: submitData.nickName,
userAvatar: submitData.userAvatar,
phoneNumber: submitData.phoneNumber,
userPassword: submitData.userPassword || managerInfo.value.userPassword // 更新密码字段
};
editModalVisible.value = false;
} else {
throw new Error(modifyResponse.message || '更新经理信息失败');
}
} catch (err: any) {
console.error('更新经理信息出错:', err);
message.error(err.message || '更新经理信息失败,请稍后再试');
} finally {
formLoading.value = false;
}
};
const handleAvatarError = () => {
avatarError.value = true;
};
onMounted(() => {
fetchManagerInfo();
});
const editFormRef = ref();
const handleOk = async () => {
// 手动校验表单
const valid = await editFormRef.value?.validate();
if (valid) {
// 校验通过,执行提交
await submitEdit();
} else {
// 校验失败,不提交
message.warning('表单校验未通过,请检查输入内容');
}
};
const handlePhoneInput = (value: string) => {
const numericValue = value.replace(/[^0-9]/g, '');
editForm.value.phoneNumber = numericValue;
};
</script>
<template>
<div class="manager-container">
<h2 class="manager-title">经理信息</h2>
<div v-if="loading" class="loading-container">
<a-spin size="large" />
<p>加载经理信息中...</p>
</div>
<div v-else-if="error" class="error-container">
<a-alert type="error" message="获取经理信息失败" show-icon />
<a-button type="primary" @click="fetchManagerInfo" class="retry-btn">重新加载</a-button>
</div>
<div v-else-if="managerInfo" class="manager-info-card">
<div class="avatar-container">
<a-avatar
:size="120"
:src="downLoadImage + managerInfo.userAvatar"
@error="handleAvatarError"
/>
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">ID:</span>
<span class="info-value">{{ managerInfo.id || '未知' }}</span>
</div>
<div class="info-item">
<span class="info-label">昵称:</span>
<span class="info-value">{{ managerInfo.nickName || '未知' }}</span>
</div>
<div class="info-item">
<span class="info-label">手机号:</span>
<span class="info-value">{{ managerInfo.phoneNumber || '未知' }}</span>
</div>
</div>
<a-button
type="primary"
@click="openEditModal"
class="edit-button"
>
<EditOutlined /> 编辑信息
</a-button>
</div>
<div v-else class="no-data">
<a-empty description="暂无经理信息" />
</div>
<!-- 编辑模态框 -->
<a-modal
v-model:visible="editModalVisible"
title="编辑经理信息"
:confirm-loading="formLoading"
@ok="handleOk"
@cancel="editModalVisible = false"
width="600px"
>
<a-form
layout="vertical"
:model="editForm"
:rules="rules"
ref="editFormRef"
>
<a-form-item label="昵称" name="nickName" :rules="rules.nickName">
<a-input v-model:value="editForm.nickName" placeholder="请输入昵称" />
</a-form-item>
<a-form-item label="手机号" name="phoneNumber" :rules="rules.phoneNumber">
<a-input
v-model:value="editForm.phoneNumber"
placeholder="请输入11位手机号"
@input="handlePhoneInput"
maxlength="11"
/>
</a-form-item>
<a-form-item label="头像">
<div class="avatar-upload-container">
<div v-if="previewAvatar" class="avatar-preview">
<img :src="previewAvatar" alt="头像预览" class="preview-image" />
</div>
<input
type="file"
ref="fileInput"
@change="handleFileChange"
accept="image/*"
class="file-input"
/>
<div v-if="isUploading" class="upload-status">
<a-spin size="small" />
<span>上传中...</span>
</div>
</div>
</a-form-item>
<a-form-item label="修改密码" name="userPassword" :rules="rules.userPassword">
<a-input-password
v-model:value="editForm.userPassword"
placeholder="如需修改密码请在此输入新密码6-20位"
/>
<small style="color: #8c8c8c">留空则不更改原密码</small>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.manager-container {
max-width: 100%;
margin: 30px auto;
padding: 20px;
background: #f9fafb;
border-radius: 12px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
}
.manager-title {
text-align: center;
color: #57b8ff;
margin-bottom: 30px;
padding-bottom: 15px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
border-bottom: 2px solid #e0e0e0;
}
.manager-info-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px;
border: 1px solid #e0e0e0;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
position: relative;
}
.avatar-container {
margin-bottom: 25px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
width: 100%;
margin-bottom: 20px;
}
.info-item {
display: flex;
padding: 15px;
background: #fafafa;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.info-label {
font-weight: bold;
color: #57606f;
min-width: 80px;
}
.info-value {
color: #333;
margin-left: 10px;
}
.loading-container, .error-container, .no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
padding: 30px;
text-align: center;
}
.error-container {
gap: 20px;
}
.retry-btn {
margin-top: 15px;
background-color: #57b8ff;
border-color: #57b8ff;
}
.edit-button {
margin-top: 20px;
background-color: #57b8ff;
border-color: #57b8ff;
display: flex;
align-items: center;
gap: 8px;
}
.form-tip {
color: #999;
font-size: 12px;
margin-top: 4px;
}
/* 新增头像上传样式 */
.avatar-upload-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.avatar-preview {
width: 100px;
height: 100px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-input {
display: block;
width: 100%;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
}
.upload-status {
display: flex;
align-items: center;
gap: 8px;
color: #57b8ff;
}
@media (max-width: 576px) {
.manager-container {
padding: 15px;
margin: 15px;
}
.info-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1 +1 @@
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/imageurl.ts","./src/api/myaxios.ts","./src/router/index.ts","./src/router/routes.ts","./src/store/index.ts","./src/store/userstore.ts","./src/types/wangeditor.d.ts","./src/app.vue","./src/layout/managelayout.vue","./src/layout/manage/manageheader.vue","./src/layout/manage/managesidebar.vue","./src/view/index.vue","./src/view/login.vue","./src/view/test.vue","./src/view/community/community.vue","./src/view/components/richtexteditor.vue","./src/view/course/addcourse.vue","./src/view/course/chapterdetail.vue","./src/view/course/coursedetail.vue","./src/view/course/coursemanagement.vue","./src/view/course/courseorder.vue","./src/view/project/addproject.vue","./src/view/project/addprojectnotice.vue","./src/view/project/moneydetail.vue","./src/view/project/noticedetail.vue","./src/view/project/project.vue","./src/view/project/projectdetail.vue","./src/view/project/projectnotice.vue","./src/view/project/promotioncode.vue","./src/view/settlement/applicationrecord.vue","./src/view/settlement/moneyrecord.vue","./src/view/settlement/withdrawalapplicationrecord.vue","./src/view/userlist/userlist.vue","./src/view/work/workdetail.vue","./src/view/work/worklist.vue"],"version":"5.6.3"}
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/imageurl.ts","./src/api/myaxios.ts","./src/router/index.ts","./src/router/routes.ts","./src/store/index.ts","./src/store/userstore.ts","./src/types/wangeditor.d.ts","./src/app.vue","./src/layout/managelayout.vue","./src/layout/manage/manageheader.vue","./src/layout/manage/managesidebar.vue","./src/view/index.vue","./src/view/login.vue","./src/view/test.vue","./src/view/components/richtexteditor.vue","./src/view/course/addcourse.vue","./src/view/course/chapterdetail.vue","./src/view/course/coursedetail.vue","./src/view/course/coursemanagement.vue","./src/view/course/courseorder.vue","./src/view/employeeapplication/employeeapplication.vue","./src/view/employeeapplication/employeedetail.vue","./src/view/performance/customerdetail.vue","./src/view/performance/customerorder.vue","./src/view/performance/employeeperformaince.vue","./src/view/performance/employeeperformanceranking.vue","./src/view/performance/performancemanagement.vue","./src/view/performance/supervisorperformanceranking.vue","./src/view/project/addproject.vue","./src/view/project/addprojectnotice.vue","./src/view/project/moneydetail.vue","./src/view/project/noticedetail.vue","./src/view/project/project.vue","./src/view/project/projectdetail.vue","./src/view/project/projectnotice.vue","./src/view/project/promotioncode.vue","./src/view/settlement/applicationrecord.vue","./src/view/settlement/moneyrecord.vue","./src/view/settlement/withdrawalapplicationrecord.vue","./src/view/userlist/adminlist.vue","./src/view/userlist/managerinformation.vue","./src/view/userlist/stafflist.vue","./src/view/userlist/supervisorlist.vue","./src/view/userlist/userlist.vue","./src/view/work/workdetail.vue","./src/view/work/worklist.vue"],"version":"5.6.3"}