Compare commits

...

5 Commits

Author SHA1 Message Date
王泽彦 25e9db780b Merge branch 'develop' of ssh://gitlab.frkj.cc:222/php/zhjwxt into develop 11 months ago
王泽彦 a580c2580a 修改 bug 11 months ago
王泽彦 51987f8b10 修改 bug 11 months ago
王泽彦 2324c32458 修改 bug 11 months ago
王泽彦 cdc449472b 修改 bug 11 months ago
  1. 9
      admin/src/app/api/order_table.ts
  2. 381
      admin/src/app/views/order_table/components/order-detail-dialog.vue
  3. 26
      admin/src/app/views/order_table/order_table.vue
  4. 5
      niucloud/app/adminapi/controller/campus_person_role/CampusPersonRole.php
  5. 9
      niucloud/app/adminapi/controller/order_table/OrderTable.php
  6. 2
      niucloud/app/adminapi/controller/personnel/Personnel.php
  7. 3
      niucloud/app/adminapi/route/order_table.php
  8. 16
      niucloud/app/api/controller/apiController/OrderTable.php
  9. 43
      niucloud/app/api/controller/apiController/StudentManager.php
  10. 2
      niucloud/app/api/controller/student/StudentController.php
  11. 1
      niucloud/app/api/route/file.php
  12. 4
      niucloud/app/api/route/route.php
  13. 86
      niucloud/app/job/schedule/HandleCourseSchedule.php
  14. 14
      niucloud/app/model/order_table/OrderTable.php
  15. 11
      niucloud/app/service/admin/campus_person_role/CampusPersonRoleService.php
  16. 39
      niucloud/app/service/admin/order_table/OrderTableService.php
  17. 69
      niucloud/app/service/api/apiService/OrderTableService.php
  18. 3
      niucloud/app/service/api/apiService/StudentService.php
  19. 1
      niucloud/app/service/api/student/StudentService.php
  20. 1467
      uniapp/api/apiRoute.js
  21. 2
      uniapp/components/client-info-card/client-info-card.vue
  22. 225
      uniapp/components/fitness-record-list-popup/index.vue
  23. 27
      uniapp/components/fitness-record-popup/fitness-record-popup.less
  24. 46
      uniapp/components/fitness-record-popup/fitness-record-popup.vue
  25. 376
      uniapp/components/order-form-popup/index.vue
  26. 807
      uniapp/components/order-list-card/index.vue
  27. 505
      uniapp/components/order-list-card/payment-voucher-popup.vue
  28. 207
      uniapp/components/order-list-card/qrcode-payment-dialog.vue
  29. 320
      uniapp/components/schedule/ScheduleDetail.vue
  30. 47
      uniapp/components/student-edit-popup/student-edit-popup.less
  31. 66
      uniapp/components/student-edit-popup/student-edit-popup.vue
  32. 2
      uniapp/components/student-info-card/student-info-card.vue
  33. 61
      uniapp/components/study-plan-popup/study-plan-popup.vue
  34. 48
      uniapp/pages-coach/coach/schedule/schedule_table.vue
  35. 1
      uniapp/pages-market/clue/class_arrangement_detail.vue
  36. 699
      uniapp/pages-market/clue/clue_info.vue
  37. 23
      uniapp/pages-market/clue/edit_clues.vue
  38. 1292
      uniapp/pages/market/clue/class_arrangement_detail.vue
  39. 431
      uniapp/pages/market/clue/clue_info.less
  40. 1567
      uniapp/pages/market/clue/clue_info.vue
  41. 164
      uniapp/pages/market/clue/clue_table.vue
  42. 1806
      uniapp/pages/market/clue/edit_clues.vue
  43. 365
      uniapp/pages/market/clue/edit_clues_log.vue
  44. 1749
      uniapp/pages/market/clue/index.vue
  45. 343
      uniapp/pages/market/data/statistics.vue
  46. 427
      uniapp/pages/market/index/index.vue
  47. 495
      uniapp/pages/market/my/campus_data.vue
  48. 445
      uniapp/pages/market/my/dept_data.vue
  49. 523
      uniapp/pages/market/my/index.vue
  50. 505
      uniapp/pages/market/my/info.vue
  51. 292
      uniapp/pages/market/my/my_data.vue
  52. 105
      uniapp/pages/market/my/set_up.vue
  53. 275
      uniapp/pages/market/my/signed_client_list.vue
  54. 225
      uniapp/pages/market/my/update_pass.vue
  55. 136
      uniapp/pages/market/reimbursement/detail.vue
  56. 144
      uniapp/pages/market/reimbursement/list.vue

9
admin/src/app/api/order_table.ts

@ -69,4 +69,13 @@ export function getWithPersonnelList(params: Record<string, any>) {
return request.get('order_table/personnel_all', { params })
}
/**
* 获取订单详情(带完整关联数据)
* @param id 订单id
* @returns
*/
export function getOrderDetail(id: number) {
return request.get(`order_table/order_detail/${id}`)
}
// USER_CODE_END -- order_table

381
admin/src/app/views/order_table/components/order-detail-dialog.vue

@ -0,0 +1,381 @@
<template>
<el-dialog
v-model="showDialog"
:title="dialogTitle"
width="700px"
:close-on-click-modal="false"
>
<el-descriptions
v-if="orderDetail"
:column="2"
border
v-loading="loading"
>
<el-descriptions-item label="订单编号">
{{ orderDetail.id }}
</el-descriptions-item>
<el-descriptions-item label="订单类型">
{{ getOrderType(orderDetail.order_type) }}
</el-descriptions-item>
<el-descriptions-item label="订单状态">
{{ getOrderStatus(orderDetail.order_status) }}
</el-descriptions-item>
<el-descriptions-item label="支付类型">
{{ getPaymentType(orderDetail.payment_type) }}
</el-descriptions-item>
<el-descriptions-item label="订单金额">
<span class="text-red-500 font-bold">¥{{ orderDetail.order_amount }}</span>
</el-descriptions-item>
<el-descriptions-item label="优惠金额" v-if="orderDetail.discount_amount">
<span class="text-green-500">¥{{ orderDetail.discount_amount }}</span>
</el-descriptions-item>
<el-descriptions-item label="课程名称">
{{ orderDetail.course_id_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="班级名称">
{{ orderDetail.class_id_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="学员姓名">
{{ orderDetail.student_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="所属校区">
{{ orderDetail.campus_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="客户姓名">
{{ orderDetail.resource_id_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="业务员">
{{ orderDetail.staff_id_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="赠品信息" v-if="orderDetail.gift_info && orderDetail.gift_info.gift_name">
{{ orderDetail.gift_info.gift_name }}
<el-tag size="small" :type="getGiftStatusType(orderDetail.gift_info.gift_status)" style="margin-left: 8px;">
{{ getGiftStatus(orderDetail.gift_info.gift_status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="支付时间" v-if="orderDetail.payment_time">
{{ orderDetail.payment_time }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ orderDetail.created_at }}
</el-descriptions-item>
<el-descriptions-item label="订单备注" :span="2" v-if="orderDetail.remark">
{{ orderDetail.remark }}
</el-descriptions-item>
</el-descriptions>
<!-- 确认支付时的支付凭证上传区域 -->
<div v-if="isPaymentAction && orderDetail?.order_status === 'pending'" class="payment-voucher-section">
<el-divider content-position="left">支付凭证</el-divider>
<el-form :model="paymentForm" label-width="100px" style="margin-top: 20px;">
<el-form-item label="上传凭证">
<div class="upload-wrapper">
<el-upload
:action="uploadAction"
:headers="uploadHeaders"
:show-file-list="true"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:before-upload="beforeUpload"
list-type="picture-card"
accept="image/*"
class="voucher-upload"
>
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="upload-tip">
支持 jpg/png/jpeg 格式,单个文件不超过 5MB
</div>
</div>
</el-form-item>
<el-form-item label="支付备注">
<el-input
v-model="paymentForm.remark"
type="textarea"
:rows="3"
placeholder="请输入支付备注信息(可选)"
/>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">关闭</el-button>
<el-button
v-if="isPaymentAction && orderDetail?.order_status === 'pending'"
type="primary"
@click="handleConfirmPayment"
:disabled="paymentForm.vouchers.length === 0"
>
确认支付
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { getOrderDetail } from '@/app/api/order_table'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getToken } from '@/utils/common'
const showDialog = ref(false)
const loading = ref(false)
const orderDetail = ref<any>(null)
const isPaymentAction = ref(false)
// 支付表单数据
const paymentForm = ref({
vouchers: [] as string[], // 支付凭证URL数组
remark: '' // 支付备注
})
// 上传配置
const uploadAction = ref(import.meta.env.VITE_APP_BASE_URL + 'sys/image')
const uploadHeaders = ref({
token: getToken()
})
const dialogTitle = computed(() => {
return isPaymentAction.value ? '确认支付' : '订单详情'
})
// 订单类型映射
const getOrderType = (type: number) => {
const typeMap: Record<number, string> = {
1: '新订单',
2: '续费订单',
3: '内部员工订单',
4: '转校',
5: '客户内转课订单'
}
return typeMap[type] || '-'
}
// 订单状态映射
const getOrderStatus = (status: string) => {
const statusMap: Record<string, string> = {
pending: '待支付',
paid: '已支付',
signed: '待签约',
completed: '已完成',
transfer: '转学'
}
return statusMap[status] || '-'
}
// 支付类型映射
const getPaymentType = (type: string) => {
const typeMap: Record<string, string> = {
cash: '现金支付',
scan_code: '扫码支付',
subscription: '订阅支付',
wxpay_online: '微信在线代付',
client_wxpay: '客户端微信支付',
deposit: '定金'
}
return typeMap[type] || '-'
}
// 赠品状态映射
const getGiftStatus = (status: number) => {
const statusMap: Record<number, string> = {
1: '未使用',
2: '已使用',
3: '已过期',
4: '已作废'
}
return statusMap[status] || '-'
}
// 赠品状态标签类型
const getGiftStatusType = (status: number) => {
const typeMap: Record<number, string> = {
1: 'success',
2: 'info',
3: 'warning',
4: 'danger'
}
return typeMap[status] || 'info'
}
// 打开弹窗
const openDialog = async (orderId: number, forPayment: boolean = false) => {
showDialog.value = true
loading.value = true
isPaymentAction.value = forPayment
// 重置支付表单
paymentForm.value = {
vouchers: [],
remark: ''
}
try {
const res = await getOrderDetail(orderId)
orderDetail.value = res.data
} catch (error) {
ElMessage.error('获取订单详情失败')
showDialog.value = false
} finally {
loading.value = false
}
}
// 上传前校验
const beforeUpload = (file: any) => {
const isImage = file.type.startsWith('image/')
const isLt5M = file.size / 1024 / 1024 < 5
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB!')
return false
}
return true
}
// 上传成功回调
const handleUploadSuccess = (response: any, file: any) => {
if (response.code === 1 && response.data && response.data.url) {
paymentForm.value.vouchers.push(response.data.url)
ElMessage.success('上传成功')
} else {
ElMessage.error(response.msg || '上传失败')
}
}
// 上传失败回调
const handleUploadError = () => {
ElMessage.error('上传失败,请重试')
}
// 确认支付
const handleConfirmPayment = () => {
if (paymentForm.value.vouchers.length === 0) {
ElMessage.warning('请上传支付凭证')
return
}
// 这里可以调用支付接口
console.log('订单ID:', orderDetail.value.id)
console.log('支付凭证:', paymentForm.value.vouchers)
console.log('支付备注:', paymentForm.value.remark)
ElMessage.success('支付确认成功,待后端接口实现')
showDialog.value = false
}
// 暴露方法供父组件调用
defineExpose({
openDialog
})
</script>
<style scoped>
.text-red-500 {
color: #ef4444;
}
.text-green-500 {
color: #10b981;
}
.font-bold {
font-weight: bold;
}
.payment-voucher-section {
margin-top: 20px;
padding: 20px;
background-color: #f9fafb;
border-radius: 8px;
}
/* 上传容器布局 */
.upload-wrapper {
width: 100%;
display: block;
overflow: visible;
}
/* 上传提示文字 */
.upload-tip {
margin-top: 10px;
padding-top: 10px;
display: block;
font-size: 12px;
color: #999;
line-height: 1.5;
clear: both;
}
/* 上传组件样式 - 强制显示为块级元素 */
.voucher-upload {
width: 100%;
display: block !important;
overflow: visible !important;
}
/* el-upload 组件内部样式穿透 */
:deep(.el-upload--picture-card) {
width: 120px !important;
height: 120px !important;
display: inline-block !important;
vertical-align: top !important;
margin: 0 8px 8px 0 !important;
}
:deep(.el-upload-list) {
display: block !important;
overflow: visible !important;
position: relative !important;
}
:deep(.el-upload-list--picture-card) {
display: block !important;
vertical-align: top !important;
width: 100% !important;
overflow: visible !important;
position: relative !important;
}
:deep(.el-upload-list__item) {
width: 120px !important;
height: 120px !important;
display: inline-block !important;
margin: 0 8px 8px 0 !important;
vertical-align: top !important;
position: relative !important;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

26
admin/src/app/views/order_table/order_table.vue

@ -95,14 +95,18 @@
<el-table-column prop="payment_time" :label="t('paymentTime')" min-width="120"
:show-overflow-tooltip="true" />
<el-table-column :label="t('operation')" fixed="right" min-width="120">
<el-table-column :label="t('operation')" fixed="right" min-width="180">
<template #default="{ row }">
<el-button type="primary" link @click="startPayment(row.id)" v-if="row.order_status == 'pending' && row.payment_type == 'scan_code'">支付</el-button>
<el-button type="primary" link @click="startPayment(row.id)" v-if="row.order_status == 'pending' && row.payment_type == 'scan_code'">扫码支付</el-button>
<el-button type="primary" link @click="editEvent(row)">{{
t('edit')
}}</el-button>
<el-button type="primary" link @click="viewOrderDetail(row.id, false)">查看详情</el-button>
<el-button type="warning" link @click="viewOrderDetail(row.id, true)" v-if="row.order_status == 'pending' && row.payment_type != 'scan_code'">确认支付</el-button>
<!-- <el-button type="primary" link @click="editEvent(row)">{{-->
<!-- t('edit')-->
<!-- }}</el-button>-->
<el-button type="primary" link @click="deleteEvent(row.id)">{{
t('delete')
}}</el-button>
@ -118,6 +122,7 @@
<edit ref="editOrderTableDialog" @complete="loadOrderTableList" />
<order-detail-dialog ref="orderDetailDialog" />
</el-card>
@ -154,6 +159,7 @@
import { img } from '@/utils/common'
import { ElMessageBox, FormInstance } from 'element-plus'
import Edit from '@/app/views/order_table/components/order-table-edit.vue'
import OrderDetailDialog from '@/app/views/order_table/components/order-detail-dialog.vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const pageName = route.meta.title
@ -277,6 +283,7 @@
loadOrderTableList()
const editOrderTableDialog : Record<string, any> | null = ref(null)
const orderDetailDialog : Record<string, any> | null = ref(null)
/**
* 添加订单
@ -295,6 +302,15 @@
editOrderTableDialog.value.showDialog = true
}
/**
* 查看订单详情
* @param orderId 订单ID
* @param forPayment 是否为确认支付操作
*/
const viewOrderDetail = (orderId: number, forPayment: boolean = false) => {
orderDetailDialog.value.openDialog(orderId, forPayment)
}
/**
* 删除订单
*/

5
niucloud/app/adminapi/controller/campus_person_role/CampusPersonRole.php

@ -95,7 +95,10 @@ class CampusPersonRole extends BaseAdminController
}
public function getPersonnelAll(){
return success(( new CampusPersonRoleService())->getPersonnelAll());
$data = $this->request->params([
["campus_id",0]
]);
return success(( new CampusPersonRoleService())->getPersonnelAll($data['campus_id']));
}
public function getSysRoleAll(){

9
niucloud/app/adminapi/controller/order_table/OrderTable.php

@ -109,4 +109,13 @@ class OrderTable extends BaseAdminController
public function getPersonnelAll(){
return success(( new OrderTableService())->getPersonnelAll());
}
/**
* 获取订单详情(带完整关联数据)
* @param int $id
* @return \think\Response
*/
public function getOrderDetail(int $id){
return success((new OrderTableService())->getOrderDetail($id));
}
}

2
niucloud/app/adminapi/controller/personnel/Personnel.php

@ -68,6 +68,7 @@ class Personnel extends BaseAdminController
["id_card_back",""],
["status",0],
["is_sys_user",0],
["campus_id",0],
["info",[]],
["use_approval", 0], // 是否使用审批流程
["approval_config_id", 0], // 审批配置ID
@ -110,6 +111,7 @@ class Personnel extends BaseAdminController
["id_card_front",""],
["id_card_back",""],
["status",0],
["campus_id",0],
["is_sys_user",0],
["info",[]],

3
niucloud/app/adminapi/route/order_table.php

@ -39,6 +39,9 @@ Route::group('order_table', function () {
Route::get('personnel_all','order_table.OrderTable/getPersonnelAll');
// 获取订单详情(带完整关联数据)
Route::get('order_detail/:id','order_table.OrderTable/getOrderDetail');
})->middleware([
AdminCheckToken::class,
AdminCheckRole::class,

16
niucloud/app/api/controller/apiController/OrderTable.php

@ -229,4 +229,20 @@ class OrderTable extends BaseApiService
'updated_at' => $orderData['updated_at']
]);
}
public function updateOrderPaymentVoucher(Request $request)
{
$params = $request->params([
["order_id", ""], // 订单ID必填
["payment_voucher", ""], // 支付凭证
]);
$res = (new OrderTableService())->updateOrderPaymentVoucher($params);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data']);
}
}

43
niucloud/app/api/controller/apiController/StudentManager.php

@ -147,4 +147,47 @@ class StudentManager extends BaseApiService
return fail('添加学员失败:' . $e->getMessage());
}
}
/**
* 获取学员基本信息(员工端)
* @param int $id 学员ID
* @return \think\Response
*/
public function info($id)
{
// 验证参数
if (empty($id) || !is_numeric($id)) {
return fail('学员ID无效');
}
try {
// 直接查询学员基本信息,不需要权限验证
$student = \think\facade\Db::table('school_student')
->where('id', $id)
->where('deleted_at', 0)
->find();
if (!$student) {
return fail('学员信息不存在');
}
// 返回学员基本信息
$studentInfo = [
'id' => $student['id'],
'name' => $student['name'],
'gender' => $student['gender'],
'gender_text' => $student['gender'] == 1 ? '男' : '女',
'birthday' => $student['birthday'],
'campus_id' => $student['campus_id'] ?? null,
'headimg' => $student['headimg'] ? get_image_url($student['headimg']) : '',
'emergency_contact' => $student['emergency_contact'],
'contact_phone' => $student['contact_phone']
];
return success($studentInfo, '获取学员信息成功');
} catch (\Exception $e) {
return fail('获取学员信息失败:' . $e->getMessage());
}
}
}

2
niucloud/app/api/controller/student/StudentController.php

@ -374,4 +374,6 @@ class StudentController extends BaseController
}
return success($res['data']);
}
}

1
niucloud/app/api/route/file.php

@ -27,6 +27,7 @@ Route::group('file', function() {
Route::post('video', 'upload.Upload/video');
//拉取图片
Route::post('image/fetch', 'upload.Upload/imageFetch');
//
})->middleware(ApiChannel::class)
// ->middleware(ApiCheckToken::class, true)

4
niucloud/app/api/route/route.php

@ -277,6 +277,7 @@ Route::group(function () {
Route::post('student/edit', 'apiController.StudentManager/edit');
//销售端-学员-列表
Route::get('student/list', 'apiController.StudentManager/list');
Route::get('student/info/:id', 'apiController.StudentManager/info');
//教练端-学员-我的学员列表
Route::get('coach/students/my', 'apiController.CoachStudent/getMyStudents');
@ -316,7 +317,8 @@ Route::group(function () {
Route::post('orderTable/add', 'apiController.OrderTable/add');
//员工端-查询订单支付状态
Route::get('checkOrderPaymentStatus', 'apiController.OrderTable/checkOrderPaymentStatus');
//更新订单支付凭证payment_voucher
Route::post('updateOrderPaymentVoucher', 'apiController.OrderTable/updateOrderPaymentVoucher');
//员工端-更新学员课程人员配置
Route::post('updateStudentCoursePersonnel', 'apiController.Course/updateStudentCoursePersonnel');

86
niucloud/app/job/schedule/HandleCourseSchedule.php

@ -53,29 +53,13 @@ class HandleCourseSchedule extends BaseJob
$currentDate = date('Y-m-d');
$currentTime = date('H:i:s');
$currentDateTime = date('Y-m-d H:i:s');
// 先处理time_slot,解析并更新start_time和end_time字段
$this->updateTimeFields();
$completedCount = 0;
$upcomingCount = 0;
$ongoingCount = 0;
$pendingCount = 0;
// 1. 首先重置未来日期的课程为pending状态(待开始)
// 这是核心修正:course_date > 当前日期的所有课程状态改为pending
$futureRows = CourseSchedule::where('course_date', '>', $currentDate)
->where('status', '<>', 'pending')
->update([
'status' => 'pending',
'updated_at' => time()
]);
$pendingCount += $futureRows;
Log::write("重置未来课程状态完成 - 重置为pending(待开始): {$futureRows}个");
// 2. 更新已完成课程:course_date < 当天的课程
// 1. 更新已过期课程:course_date < 当天的课程标记为 completed
$completedRows = CourseSchedule::where('course_date', '<', $currentDate)
->where('status', '<>', 'completed')
->update([
@ -84,17 +68,19 @@ class HandleCourseSchedule extends BaseJob
]);
$completedCount = $completedRows;
// 3. 处理今天的课程,需要根据时间段判断状态
Log::write("更新过期课程完成 - 已完成: {$completedRows}个");
// 2. 处理今天的课程,需要根据时间段判断状态
// 注意:只处理当天课程,不处理未来课程(course_date > 今天)
$todaySchedules = CourseSchedule::where('course_date', '=', $currentDate)
->whereIn('status', ['pending', 'upcoming', 'ongoing'])
->select();
foreach ($todaySchedules as $schedule) {
$startTime = $schedule['start_time'];
$endTime = $schedule['end_time'];
// 如果 start_time 或 end_time 为空,尝试从 time_slot 解析
if (empty($startTime) || empty($endTime)) {
// 如果没有解析出时间,尝试从time_slot解析
$timeData = $this->parseTimeSlot($schedule['time_slot']);
if ($timeData) {
$startTime = $timeData['start_time'];
@ -106,7 +92,9 @@ class HandleCourseSchedule extends BaseJob
'end_time' => $endTime
]);
} else {
continue; // 无法解析时间,跳过
// 无法解析时间(time_slot 为 null 或空字符串),跳过该记录
Log::write("课程ID {$schedule['id']} 无法解析时间,time_slot: {$schedule['time_slot']}");
continue;
}
}
@ -136,7 +124,7 @@ class HandleCourseSchedule extends BaseJob
}
}
Log::write("课程状态更新完成 - 已完成: {$completedCount}个, 即将开始: {$upcomingCount}个, 进行中: {$ongoingCount}个, 待开始: {$pendingCount}个");
Log::write("当天课程状态更新完成 - 已完成: {$completedCount}个, 即将开始: {$upcomingCount}个, 进行中: {$ongoingCount}个, 待开始: {$pendingCount}个");
Db::commit();
@ -149,34 +137,6 @@ class HandleCourseSchedule extends BaseJob
}
}
/**
* 更新课程安排表的start_time和end_time字段
*/
private function updateTimeFields()
{
try {
// 查询所有没有start_time或end_time的记录
$schedules = CourseSchedule::where(function($query) {
$query->whereNull('start_time')
->whereOr('end_time', null)
->whereOr('start_time', '')
->whereOr('end_time', '');
})->select();
foreach ($schedules as $schedule) {
$timeData = $this->parseTimeSlot($schedule['time_slot']);
if ($timeData) {
CourseSchedule::where('id', $schedule['id'])->update([
'start_time' => $timeData['start_time'],
'end_time' => $timeData['end_time']
]);
}
}
} catch (\Exception $e) {
Log::write('更新时间字段失败:' . $e->getMessage());
}
}
/**
* 解析time_slot字符串,提取开始和结束时间
* @param string $timeSlot 格式如 "09:00-10:30"
@ -201,6 +161,12 @@ class HandleCourseSchedule extends BaseJob
/**
* 根据当前时间和课程时间段判断课程状态
* 逻辑说明:
* 1. 如果当前时间 > 结束时间 → completed(已结束)
* 2. 如果当前时间在 [开始时间, 结束时间] 之间 → ongoing(进行中)
* 3. 如果当前时间 + 6小时 >= 开始时间 → upcoming(即将开始)
* 4. 其他情况 → pending(待开始)
*
* @param string $currentTime 当前时间 H:i:s
* @param string $startTime 开始时间 H:i:s
* @param string $endTime 结束时间 H:i:s
@ -212,23 +178,23 @@ class HandleCourseSchedule extends BaseJob
$startTimestamp = strtotime($startTime);
$endTimestamp = strtotime($endTime);
// 如果当前时间在课程时间段内,状态为进行中
if ($currentTimestamp >= $startTimestamp && $currentTimestamp <= $endTimestamp) {
return 'ongoing';
}
// 如果课程已结束,状态为已完成
// 1. 如果课程已结束(当前时间 > 结束时间),状态为已完成
if ($currentTimestamp > $endTimestamp) {
return 'completed';
}
// 如果距离开始时间不足6小时,状态为即将开始
$timeDiff = $startTimestamp - $currentTimestamp;
if ($timeDiff <= 6 * 3600 && $timeDiff > 0) { // 6小时 = 6 * 60 * 60 秒
// 2. 如果当前时间在课程时间段内,状态为进行中
if ($currentTimestamp >= $startTimestamp && $currentTimestamp <= $endTimestamp) {
return 'ongoing';
}
// 3. 如果距离开始时间在6小时以内(当前时间 + 6小时 >= 开始时间),状态为即将开始
$sixHoursLater = $currentTimestamp + (6 * 3600); // 6小时 = 6 * 60 * 60 秒
if ($sixHoursLater >= $startTimestamp) {
return 'upcoming';
}
// 其他情况为待安排
// 4. 其他情况为待开始
return 'pending';
}
}

14
niucloud/app/model/order_table/OrderTable.php

@ -26,6 +26,10 @@ use app\model\personnel\Personnel;
use app\model\student_courses\StudentCourses;
use app\model\student\Student;
use app\model\campus\Campus;
/**
* 订单模型
* Class OrderTable
@ -105,4 +109,14 @@ class OrderTable extends BaseModel
return $this->hasOne(StudentCourses::class, 'id', 'course_plan_id')->joinType('left')->withField('total_hours,gift_hours,use_total_hours,use_gift_hours')->bind(['total_hours'=>'total_hours','gift_hours'=>'gift_hours','use_total_hours'=>'use_total_hours','use_gift_hours'=>'use_gift_hours']);
}
//学员表-学员信息
public function student(){
return $this->hasOne(Student::class, 'id', 'student_id')->joinType('left')->withField('name,id')->bind(['student_name'=>'name']);
}
//校区表-校区信息
public function campus(){
return $this->hasOne(Campus::class, 'id', 'campus_id')->joinType('left')->withField('campus_name,id')->bind(['campus_name'=>'campus_name']);
}
}

11
niucloud/app/service/admin/campus_person_role/CampusPersonRoleService.php

@ -163,12 +163,13 @@ class CampusPersonRoleService extends BaseAdminService
return $campusModel->select()->toArray();
}
public function getPersonnelAll(){
$person_ids = $this->model->column("person_id");
public function getPersonnelAll($campus_id){
$personnelModel = new Personnel();
return $personnelModel->where([['id','not in',$person_ids]])->select()->toArray();
return $personnelModel->when($campus_id, function ($query) use ($campus_id) {
$query->where('campus_id', $campus_id);
})->where('status',2)
->select()
->toArray();
}
public function getSysRoleAll($data){

39
niucloud/app/service/admin/order_table/OrderTableService.php

@ -151,5 +151,44 @@ class OrderTableService extends BaseAdminService
return $personnelModel->select()->toArray();
}
/**
* 获取订单详情(带完整关联数据)
* @param int $id
* @return array
*/
public function getOrderDetail(int $id)
{
// 查询订单基础信息及关联数据
$order = $this->model
->where([['id', '=', $id]])
->with([
'customerResources', // 客户资源
'course', // 课程
'classGrade', // 班级
'personnel', // 员工
'student', // 学员
'campus' // 校区
])
->findOrEmpty()
->toArray();
if (empty($order)) {
return [];
}
// 如果有赠品ID,单独查询赠品信息
if (!empty($order['gift_id'])) {
$giftInfo = db('shcool_resources_gift')
->where('id', $order['gift_id'])
->field('id,gift_name,gift_type,gift_status')
->find();
$order['gift_info'] = $giftInfo ?: [];
} else {
$order['gift_info'] = [];
}
return $order;
}
}

69
niucloud/app/service/api/apiService/OrderTableService.php

@ -1022,4 +1022,73 @@ class OrderTableService extends BaseApiService
// TODO: 具体的赠课逻辑实现
return true;
}
public function updateOrderPaymentVoucher($params)
{
try {
// 验证必要参数
if (empty($params['order_id'])) {
return [
'code' => 0,
'msg' => '缺少订单ID参数',
'data' => []
];
}
if (empty($params['payment_voucher'])) {
return [
'code' => 0,
'msg' => '缺少支付凭证参数',
'data' => []
];
}
// 查询订单
$order = OrderTable::where('id', $params['order_id'])->find();
if (!$order) {
return [
'code' => 0,
'msg' => '订单不存在',
'data' => []
];
}
// 更新支付凭证
$result = $order->save([
'payment_voucher' => $params['payment_voucher'],
'updated_at' => date('Y-m-d H:i:s')
]);
if ($result) {
\think\facade\Log::info('订单支付凭证更新成功', [
'order_id' => $params['order_id'],
'payment_voucher' => $params['payment_voucher']
]);
return [
'code' => 1,
'msg' => '支付凭证提交成功',
'data' => $order->toArray()
];
} else {
return [
'code' => 0,
'msg' => '支付凭证提交失败',
'data' => []
];
}
} catch (\Exception $e) {
\think\facade\Log::error('更新订单支付凭证异常', [
'params' => $params,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'code' => 0,
'msg' => '更新订单支付凭证异常: ' . $e->getMessage(),
'data' => []
];
}
}
}

3
niucloud/app/service/api/apiService/StudentService.php

@ -146,8 +146,7 @@ class StudentService extends BaseApiService
// 查询该学员的课程安排记录,按日期排序获取一访和二访信息
$visitRecords = Db::table('school_person_course_schedule')
->where([
['person_id', '=', $student['id']],
['person_type', '=', 'student']
['student_id', '=', $student['id']]
])
->order('course_date', 'asc')
->select()

1
niucloud/app/service/api/student/StudentService.php

@ -184,6 +184,7 @@ class StudentService extends BaseService
'contact_phone' => $student['contact_phone'],
'note' => $student['note'],
'headimg' => $student['headimg'] ? get_image_url($student['headimg']) : '',
'campus_id' => $student['campus_id'] ?? null, // 添加校区ID
];
// 处理体测信息

1467
uniapp/api/apiRoute.js

File diff suppressed because it is too large

2
uniapp/components/client-info-card/client-info-card.vue

@ -35,7 +35,7 @@
</view>
<view class="detail-row">
<text class="info-label">分配顾问:</text>
<text class="info-value">{{ $util.safeGet(clientInfo, 'customerResource.consultant_name', '未知顾问') }}</text>
<text class="info-value">{{ $util.safeGet(clientInfo, 'customerResource.consultant_name', '---') }}</text>
</view>
<view class="detail-row">
<text class="info-label">性别:</text>

225
uniapp/components/fitness-record-list-popup/index.vue

@ -0,0 +1,225 @@
<!-- 体测记录列表弹窗组件 -->
<template>
<view class="fitness-record-list-popup" v-if="visible" @click.stop="handleMaskClick">
<view class="popup-container" @click.stop>
<!-- 标题栏 -->
<view class="popup-header">
<text class="popup-title">体测记录</text>
<view class="close-btn" @click.stop="handleClose">
<text>✕</text>
</view>
</view>
<!-- 体测记录列表 -->
<view class="fitness-records-container">
<!-- 空状态提示 -->
<view v-if="!records || records.length === 0" class="empty-state">
<view class="empty-icon">📊</view>
<view class="empty-text">暂无体测记录</view>
<view class="empty-tip">点击下方"新增"按钮添加体测记录</view>
</view>
<!-- 体测记录列表 -->
<FitnessRecordCard
v-for="record in records"
:key="record.id"
:record="record"
@edit="handleEdit"
/>
</view>
<!-- 底部操作按钮 -->
<view class="popup-footer">
<view class="footer-btn cancel-btn" @click.stop="handleClose">关闭</view>
<view class="footer-btn confirm-btn" @click.stop="handleAddNew">新增</view>
</view>
</view>
</view>
</template>
<script>
import FitnessRecordCard from '@/components/fitness-record-card/fitness-record-card.vue'
export default {
name: 'FitnessRecordListPopup',
components: {
FitnessRecordCard
},
props: {
// 是否显示弹窗
visible: {
type: Boolean,
default: false
},
// 体测记录列表
records: {
type: Array,
default: () => []
}
},
methods: {
// 关闭弹窗
handleClose() {
this.$emit('close')
},
// 点击遮罩关闭
handleMaskClick() {
this.handleClose()
},
// 新增体测记录
handleAddNew() {
this.$emit('add')
},
// 编辑体测记录
handleEdit(record) {
this.$emit('edit', record)
}
}
}
</script>
<style lang="less" scoped>
.fitness-record-list-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 999; /* 比FitnessRecordPopup的z-index低 */
}
.popup-container {
width: 100%;
max-height: 80vh;
background: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 32rpx 24rpx;
border-bottom: 1rpx solid #F0F0F0;
}
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #333333;
}
.close-btn {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background: #F5F5F5;
border-radius: 50%;
text {
font-size: 40rpx;
color: #999999;
line-height: 1;
}
}
.fitness-records-container {
flex: 1;
overflow-y: auto;
padding: 24rpx 32rpx;
max-height: 50vh;
}
/* 滚动条样式 */
.fitness-records-container::-webkit-scrollbar {
width: 6rpx;
}
.fitness-records-container::-webkit-scrollbar-track {
background: transparent;
}
.fitness-records-container::-webkit-scrollbar-thumb {
background: #29D3B4;
border-radius: 3rpx;
}
.fitness-records-container::-webkit-scrollbar-thumb:hover {
background: #24B89E;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
}
.empty-icon {
font-size: 120rpx;
line-height: 1;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
margin-bottom: 16rpx;
}
.empty-tip {
font-size: 24rpx;
color: #CCCCCC;
}
.popup-footer {
display: flex;
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #F0F0F0;
gap: 24rpx;
}
.footer-btn {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 500;
}
.cancel-btn {
background: #F5F5F5;
color: #666666;
}
.confirm-btn {
background: linear-gradient(135deg, #29D3B4 0%, #24B89E 100%);
color: #FFFFFF;
}
</style>

27
uniapp/components/fitness-record-popup/fitness-record-popup.less

@ -1,4 +1,19 @@
// 体测记录弹窗样式
// 弹窗容器 - 使用view类型
.fitness-record-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000; // 高于体测记录列表弹窗(999)
}
.popup-container {
width: 90vw;
max-width: 600rpx;
@ -6,6 +21,18 @@
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.2);
animation: popupFadeIn 0.3s ease-out;
}
@keyframes popupFadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.popup-header {

46
uniapp/components/fitness-record-popup/fitness-record-popup.vue

@ -1,6 +1,7 @@
<template>
<uni-popup ref="popup" type="center">
<view class="popup-container">
<!-- 体测记录编辑弹窗 - 使用view类型 -->
<view v-if="isVisible" class="fitness-record-popup" @click.stop="handleMaskClick">
<view class="popup-container" @click.stop>
<view class="popup-header">
<view class="popup-title">{{ isEditing ? '编辑体测记录' : '新增体测记录' }}</view>
<view class="popup-close" @click="close">✕</view>
@ -58,7 +59,7 @@
<view class="popup-btn confirm-btn" @click="confirm">确认</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
@ -94,7 +95,6 @@ export default {
this.resetData()
this.recordData.test_date = this.getCurrentDate()
this.isVisible = true
this.$refs.popup.open()
},
// 打开编辑弹窗
@ -108,17 +108,20 @@ export default {
pdf_files: [...(record.pdf_files || [])]
}
this.isVisible = true
this.$refs.popup.open()
},
// 关闭弹窗
close() {
this.isVisible = false
this.$refs.popup.close()
this.resetData()
this.$emit('close')
},
// 点击遮罩关闭
handleMaskClick() {
this.close()
},
// 重置数据
resetData() {
this.recordData = {
@ -371,7 +374,7 @@ export default {
return false
},
// 上传PDF文件到服务器(使用通用文档上传接口)
// 上传PDF文件到服务器(使用体测专用PDF上传接口)
async uploadPdfFile(file) {
const { Api_url } = require('@/common/config.js')
const token = uni.getStorageSync('token') || ''
@ -380,18 +383,15 @@ export default {
fileName: file.name,
fileSize: file.size,
filePath: file.path,
uploadUrl: Api_url + '/memberUploadDocument',
uploadUrl: Api_url + '/xy/physicalTest/uploadPdf',
hasToken: !!token
})
return new Promise((resolve, reject) => {
uni.uploadFile({
url: Api_url + '/memberUploadDocument', // 使用学生端文档上传接口
url: Api_url + '/xy/physicalTest/uploadPdf', // 使用体测专用PDF上传接口
filePath: file.path,
name: 'file',
formData: {
'type': 'document' // 指定为文档类型
},
header: {
'token': token
// 不设置 Content-Type,让uni-app自动设置为 multipart/form-data
@ -415,20 +415,15 @@ export default {
if (response.code === 1) {
console.log('PDF上传成功:', response.data)
// 根据文档上传接口的标准响应格式处理
let fileUrl = ''
if (response.data) {
fileUrl = response.data.url || response.data.file_path || response.data
}
// 根据体测PDF上传接口的响应格式处理
resolve({
code: 1,
msg: response.msg || '上传成功',
data: {
url: fileUrl,
name: response.data.name || response.data.original_name || file.name,
ext: response.data.ext || 'pdf',
size: file.size
url: response.data.file_url, // 后端返回的 file_url 字段
name: response.data.file_name || file.name,
ext: 'pdf',
size: response.data.file_size || file.size
}
})
} else if (response.code === 401) {
@ -443,14 +438,9 @@ export default {
reject(response)
} else {
console.error('PDF上传失败响应:', response)
// 提供更详细的错误信息
let errorMsg = response.msg || 'PDF上传失败'
if (response.code === 0 && response.msg.includes('路由未定义')) {
errorMsg = '文件上传接口暂不可用,请联系技术支持'
}
reject({
code: response.code || 0,
msg: errorMsg
msg: response.msg || 'PDF上传失败'
})
}
},

376
uniapp/components/order-form-popup/index.vue

@ -15,6 +15,16 @@
</view>
</view>
<view class="form-item">
<text class="label">校区选择 <text class="required">*</text></text>
<view class="picker-wrapper" @click="showCampusPicker">
<text class="picker-text" :class="{ 'placeholder': !formData.campus_id }">
{{ selectedCampus && selectedCampus.label || '请选择校区' }}
</text>
<text class="picker-arrow">▼</text>
</view>
</view>
<view class="form-item">
<text class="label">课程选择 <text class="required">*</text></text>
<view class="picker-wrapper" @click="showCoursePicker">
@ -57,6 +67,34 @@
/>
</view>
<view class="form-item" v-if="needPaymentVoucher">
<text class="label">支付凭证 <text class="required">*</text></text>
<view class="voucher-upload">
<view class="voucher-grid">
<!-- 已上传的图片 -->
<view
class="voucher-item"
v-for="(img, index) in voucherImages"
:key="index"
>
<image :src="img" class="voucher-image" mode="aspectFill"></image>
<view class="voucher-delete" @click="deleteVoucherImage(index)">×</view>
</view>
<!-- 上传按钮 -->
<view
class="voucher-item voucher-upload-btn"
v-if="voucherImages.length < maxVoucherImages"
@click="chooseVoucherImage"
>
<text class="upload-icon">+</text>
<text class="upload-text">上传凭证</text>
</view>
</view>
<text class="voucher-tip">最多上传{{ maxVoucherImages }}张图片</text>
</view>
</view>
<view class="form-item" v-if="giftList.length > 0">
<text class="label">选择赠品</text>
<view class="picker-wrapper" @click="showGiftPicker">
@ -77,6 +115,16 @@
</view>
</view>
<view class="form-item">
<text class="label">班级选择</text>
<view class="picker-wrapper" @click="showClassPicker">
<text class="picker-text" :class="{ 'placeholder': !formData.class_id }">
{{ selectedClass && selectedClass.label || '请选择班级(可选)' }}
</text>
<text class="picker-arrow">▼</text>
</view>
</view>
<view class="form-item">
<text class="label">备注</text>
<textarea
@ -117,6 +165,7 @@
<script>
import apiRoute from '@/api/apiRoute.js'
import dictUtilSimple from '@/common/dictUtilSimple.js'
import { uploadFile } from '@/common/util.js'
export default {
name: 'OrderFormPopup',
@ -138,8 +187,10 @@ export default {
return {
formData: {
student_id: '',
campus_id: '', // 校区ID,必填字段
course_id: '',
payment_type: '',
payment_voucher: '', // 支付凭证,多张图片URL用逗号分隔
order_type: '',
order_amount: '',
total_hours: '',
@ -147,7 +198,7 @@ export default {
gift_id: '', // 赠品ID
gift_type: '', // 赠品核销类型:1-减现, 2-赠课
remark: '',
class_id: '1' // 临时设置默认班级ID,后续需要添加班级选择功能
class_id: '' // 班级ID,可选字段
},
// 选择器相关
@ -158,24 +209,35 @@ export default {
selectedIndex: 0,
// 为每个选择器维护独立的索引状态
campusSelectedIndex: -1,
courseSelectedIndex: -1,
paymentSelectedIndex: -1,
orderTypeSelectedIndex: -1,
giftSelectedIndex: -1,
giftTypeSelectedIndex: -1,
classSelectedIndex: -1,
// 数据选项
campusList: [], // 校区列表
courseList: [],
paymentTypes: [], // 从字典接口获取
orderTypes: [], // 从字典接口获取
giftList: [], // 可用赠品列表
classList: [], // 班级列表
giftTypes: [ // 赠品核销类型
{ value: '1', label: '减现' },
{ value: '2', label: '赠课' }
]
],
// 支付凭证相关
voucherImages: [], // 支付凭证图片数组,最多9张
maxVoucherImages: 9 // 最多上传9张图片
}
},
computed: {
selectedCampus() {
return this.campusList.find(item => item.value == this.formData.campus_id)
},
selectedCourse() {
return this.courseList.find(item => item.id == this.formData.course_id)
},
@ -190,6 +252,15 @@ export default {
},
selectedGiftType() {
return this.giftTypes.find(item => item.value === this.formData.gift_type)
},
selectedClass() {
return this.classList.find(item => item.value == this.formData.class_id)
},
// 是否需要上传支付凭证
needPaymentVoucher() {
// 扫码支付、订阅支付、微信在线代付不需要上传凭证
const noVoucherTypes = ['scan_code', 'subscription', 'wxpay_online']
return this.formData.payment_type && !noVoucherTypes.includes(this.formData.payment_type)
}
},
watch: {
@ -198,15 +269,23 @@ export default {
if (newVal) {
console.log('开始初始化表单和加载数据')
this.initForm()
this.loadCampusList() // 加载校区列表
this.loadCourseList()
this.loadDictionaries() // 加载字典数据
this.loadGiftList() // 加载可用赠品列表
this.loadClassList() // 加载班级列表
}
},
studentInfo: {
handler(newVal) {
if (newVal && newVal.id) {
this.formData.student_id = newVal.id
// 同步校区ID
if (newVal.campus_id) {
this.formData.campus_id = newVal.campus_id
// 当学生信息变化时,重新加载班级列表
this.loadClassList()
}
}
},
immediate: true,
@ -218,17 +297,21 @@ export default {
// 如果组件挂载时 visible 为 true,主动加载数据
if (this.visible) {
console.log('组件挂载时 visible 为 true,开始加载数据')
this.loadCampusList()
this.loadCourseList()
this.loadDictionaries()
this.loadGiftList()
this.loadClassList()
}
},
methods: {
initForm() {
this.formData = {
student_id: this.studentInfo && this.studentInfo.id || '',
campus_id: this.studentInfo && this.studentInfo.campus_id || '', // 校区ID,必填字段,从studentInfo回显
course_id: '',
payment_type: '',
payment_voucher: '', // 支付凭证,多张图片URL用逗号分隔
order_type: '',
order_amount: '',
total_hours: '',
@ -236,15 +319,49 @@ export default {
gift_id: '',
gift_type: '',
remark: '',
class_id: '1' // 临时设置默认班级ID
class_id: '' // 班级ID,可选字段
}
// 重置选择器索引状态
this.campusSelectedIndex = -1
this.courseSelectedIndex = -1
this.paymentSelectedIndex = -1
this.orderTypeSelectedIndex = -1
this.giftSelectedIndex = -1
this.giftTypeSelectedIndex = -1
this.classSelectedIndex = -1
// 重置支付凭证
this.voucherImages = []
},
/**
* 加载校区列表
*/
async loadCampusList() {
console.log('开始加载校区列表')
try {
const res = await apiRoute.common_getCampusesList({})
console.log('校区列表API响应:', res)
if (res.code === 1) {
// 过滤状态为1的校区,并格式化数据结构
this.campusList = (res.data || [])
.filter(campus => campus.campus_status === 1)
.map(campus => ({
value: campus.id,
label: campus.campus_name,
...campus
}))
console.log('校区列表加载成功:', this.campusList)
} else {
console.error('获取校区列表失败:', res.msg)
this.campusList = []
}
} catch (error) {
console.error('获取校区列表异常:', error)
this.campusList = []
}
},
async loadCourseList() {
@ -300,7 +417,9 @@ export default {
{ value: 'cash', label: '现金支付' },
{ value: 'scan_code', label: '扫码支付' },
{ value: 'subscription', label: '订阅支付' },
{ value: 'wxpay_online', label: '微信在线代付' }
{ value: 'wxpay_online', label: '微信在线代付' },
{ value: 'client_wxpay', label: '客户自行付款' },
{ value: 'deposit', label: '定金' }
]
}
@ -317,7 +436,9 @@ export default {
this.orderTypes = [
{ value: '1', label: '新订单' },
{ value: '2', label: '续费订单' },
{ value: '3', label: '内部员工订单' }
{ value: '3', label: '内部员工订单' },
{ value: '4', label: '转校' },
{ value: '5', label: '客户内转课订单' }
]
}
@ -329,13 +450,17 @@ export default {
{ value: 'cash', label: '现金支付' },
{ value: 'scan_code', label: '扫码支付' },
{ value: 'subscription', label: '订阅支付' },
{ value: 'wxpay_online', label: '微信在线代付' }
{ value: 'wxpay_online', label: '微信在线代付' },
{ value: 'client_wxpay', label: '客户自行付款' },
{ value: 'deposit', label: '定金' }
]
this.orderTypes = [
{ value: '1', label: '新订单' },
{ value: '2', label: '续费订单' },
{ value: '3', label: '内部员工订单' }
{ value: '3', label: '内部员工订单' },
{ value: '4', label: '转校' },
{ value: '5', label: '客户内转课订单' }
]
}
},
@ -381,6 +506,61 @@ export default {
}
},
/**
* 加载班级列表
* @param {Number} campusId - 校区ID,如果不传则使用formData.campus_id
*/
async loadClassList(campusId) {
const targetCampusId = campusId || this.formData.campus_id
console.log('开始加载班级列表,校区ID:', targetCampusId)
try {
if (!targetCampusId) {
console.warn('缺少校区ID,无法加载班级列表')
this.classList = []
return
}
const res = await apiRoute.getClassListForSchedule({
campus_id: targetCampusId
})
console.log('班级列表API响应:', res)
if (res.code === 1) {
// 格式化班级数据结构,使用 id 和 class_name
this.classList = (res.data || []).map(cls => ({
value: cls.id,
label: cls.class_name || `班级${cls.id}`,
...cls
}))
console.log('班级列表加载成功:', this.classList)
} else {
console.error('获取班级列表失败:', res.msg)
this.classList = []
}
} catch (error) {
console.error('获取班级列表异常:', error)
this.classList = []
}
},
showCampusPicker() {
this.currentPicker = 'campus'
this.pickerTitle = '选择校区'
this.pickerOptions = this.campusList
// 根据当前选中的校区设置初始索引
if (this.formData.campus_id) {
const currentIndex = this.campusList.findIndex(item => item.value == this.formData.campus_id)
this.campusSelectedIndex = currentIndex >= 0 ? currentIndex : 0
} else {
this.campusSelectedIndex = 0
}
this.pickerValue = [this.campusSelectedIndex]
this.selectedIndex = this.campusSelectedIndex
this.$refs.pickerPopup.open()
},
showCoursePicker() {
this.currentPicker = 'course'
this.pickerTitle = '选择课程'
@ -476,6 +656,29 @@ export default {
this.$refs.pickerPopup.open()
},
showClassPicker() {
this.currentPicker = 'class'
this.pickerTitle = '选择班级'
// 添加一个"不选择"的选项
this.pickerOptions = [
{ value: '', label: '不选择班级' },
...this.classList
]
// 根据当前选中的班级设置初始索引
if (this.formData.class_id) {
const currentIndex = this.pickerOptions.findIndex(item => item.value == this.formData.class_id)
this.classSelectedIndex = currentIndex >= 0 ? currentIndex : 0
} else {
this.classSelectedIndex = 0
}
this.pickerValue = [this.classSelectedIndex]
this.selectedIndex = this.classSelectedIndex
this.$refs.pickerPopup.open()
},
onPickerChange(e) {
this.selectedIndex = e.detail.value[0]
},
@ -485,6 +688,16 @@ export default {
if (!selectedOption) return
switch (this.currentPicker) {
case 'campus':
this.campusSelectedIndex = this.selectedIndex
this.formData.campus_id = selectedOption.value
// 清空已选择的班级
this.formData.class_id = ''
this.classSelectedIndex = -1
// 重新加载该校区的班级列表
this.loadClassList(selectedOption.value)
console.log('校区选择后更新表单数据,已清空班级选择:', this.formData)
break
case 'course':
this.courseSelectedIndex = this.selectedIndex
this.formData.course_id = selectedOption.id
@ -503,6 +716,10 @@ export default {
case 'payment':
this.paymentSelectedIndex = this.selectedIndex
this.formData.payment_type = selectedOption.value
// 切换支付方式时,清空支付凭证
this.voucherImages = []
this.formData.payment_voucher = ''
console.log('支付方式选择后更新表单数据:', this.formData)
break
case 'orderType':
this.orderTypeSelectedIndex = this.selectedIndex
@ -523,6 +740,11 @@ export default {
this.formData.gift_type = selectedOption.value
console.log('赠品类型选择后更新表单数据:', this.formData)
break
case 'class':
this.classSelectedIndex = this.selectedIndex
this.formData.class_id = selectedOption.value
console.log('班级选择后更新表单数据:', this.formData)
break
}
this.closePicker()
@ -533,11 +755,84 @@ export default {
this.currentPicker = ''
},
/**
* 选择支付凭证图片
*/
chooseVoucherImage() {
const remainCount = this.maxVoucherImages - this.voucherImages.length
if (remainCount <= 0) {
uni.showToast({ title: '最多上传9张图片', icon: 'none' })
return
}
uni.chooseImage({
count: remainCount,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
console.log('选择的图片:', res.tempFilePaths)
// 逐个上传图片
res.tempFilePaths.forEach(filePath => {
this.uploadVoucherImage(filePath)
})
}
})
},
/**
* 上传单张支付凭证图片
*/
uploadVoucherImage(filePath) {
uni.showLoading({ title: '上传中...' })
uploadFile(
filePath,
(fileData) => {
// 上传成功
console.log('图片上传成功:', fileData)
this.voucherImages.push(fileData.url)
// 更新formData中的payment_voucher字段
this.formData.payment_voucher = this.voucherImages.join(',')
console.log('支付凭证已更新:', this.formData.payment_voucher)
uni.hideLoading()
uni.showToast({ title: '上传成功', icon: 'success' })
},
(error) => {
// 上传失败
console.error('图片上传失败:', error)
uni.hideLoading()
}
)
},
/**
* 删除支付凭证图片
*/
deleteVoucherImage(index) {
uni.showModal({
title: '提示',
content: '确定删除这张图片吗?',
success: (res) => {
if (res.confirm) {
this.voucherImages.splice(index, 1)
// 更新formData中的payment_voucher字段
this.formData.payment_voucher = this.voucherImages.join(',')
console.log('支付凭证已更新:', this.formData.payment_voucher)
uni.showToast({ title: '删除成功', icon: 'success' })
}
}
})
},
validateForm() {
if (!this.formData.student_id) {
uni.showToast({ title: '请选择学生', icon: 'none' })
return false
}
if (!this.formData.campus_id) {
uni.showToast({ title: '请选择校区', icon: 'none' })
return false
}
if (!this.formData.course_id) {
uni.showToast({ title: '请选择课程', icon: 'none' })
return false
@ -734,6 +1029,73 @@ export default {
height: 120rpx;
resize: none;
}
// 支付凭证上传样式
.voucher-upload {
.voucher-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.voucher-item {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
position: relative;
background: #2a2a2a;
border: 1px solid #444;
.voucher-image {
width: 100%;
height: 100%;
}
.voucher-delete {
position: absolute;
top: 0;
right: 0;
width: 50rpx;
height: 50rpx;
background: rgba(255, 71, 87, 0.8);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
border-radius: 0 12rpx 0 12rpx;
font-weight: bold;
}
&.voucher-upload-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px dashed #666;
background: transparent;
.upload-icon {
font-size: 60rpx;
color: #888;
margin-bottom: 10rpx;
}
.upload-text {
font-size: 24rpx;
color: #888;
}
}
}
}
.voucher-tip {
display: block;
margin-top: 20rpx;
font-size: 24rpx;
color: #888;
}
}
}
}

807
uniapp/components/order-list-card/index.vue

@ -1,9 +1,17 @@
<!--订单列表内容组件-->
<!--订单列表组件 - 独立业务组件-->
<template>
<view class="order-list-card">
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<text class="loading-icon">⏳</text>
<text class="loading-text">加载中...</text>
</view>
<!-- 订单内容 -->
<view v-else>
<!-- 操作按钮区域 -->
<view class="action-header" v-if="orderList && orderList.length > 0">
<view class="add-order-btn" @click.stop="handleAddOrder">
<view class="action-header" v-if="showAddButton && orderList.length > 0">
<view class="add-order-btn" @click.stop="openAddOrder">
<text class="add-icon">+</text>
<text class="add-text">新增订单</text>
</view>
@ -12,28 +20,27 @@
<!-- 订单列表 -->
<scroll-view
class="order-list"
v-if="orderList && orderList.length > 0"
v-if="orderList.length > 0"
scroll-y="true"
:enhanced="true"
style="height: 70vh;"
:style="{height: maxHeight}"
>
<view
class="order-item"
v-for="(order, index) in orderList"
:key="order && order.id ? order.id : `order-${index}`"
:class="{ 'pending-payment': order && order.status === 'pending' }"
@click.stop="order ? handleOrderClick(order) : null"
v-if="order"
v-for="order in orderList"
:key="order.id"
:class="{ 'pending-payment': order.status === 'pending' }"
@click.stop="handleOrderClick(order)"
>
<view class="order-header">
<view class="order-info">
<view class="order-no">订单号:{{ order.order_no || 'N/A' }}</view>
<view class="order-no">订单号:{{ order.order_no }}</view>
<view class="order-time" v-if="order.create_time">
{{ formatTime(order.create_time) }}
</view>
</view>
<view :class="['order-status',getStatusClass(order && order.status)]">
{{ getStatusText(order && order.status) }}
<view :class="['order-status', getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
</view>
</view>
@ -48,17 +55,17 @@
<view class="order-details">
<!-- 价格信息 -->
<view class="detail-row" v-if="order.total_amount">
<view class="detail-row">
<text class="detail-label">订单金额:</text>
<text class="detail-value price">¥{{ order.total_amount }}</text>
</view>
<view class="detail-row" v-if="order.paid_amount !== undefined">
<view class="detail-row">
<text class="detail-label">已付金额:</text>
<text class="detail-value paid">¥{{ order.paid_amount }}</text>
</view>
<view class="detail-row" v-if="order.unpaid_amount !== undefined">
<view class="detail-row">
<text class="detail-label">未付金额:</text>
<text class="detail-value unpaid">¥{{ order.unpaid_amount }}</text>
</view>
@ -81,7 +88,7 @@
</view>
<!-- 备注信息 -->
<view class="order-remark">
<view class="order-remark" v-if="order.remark">
<view class="remark-label">备注:</view>
<view class="remark-content">{{ order.remark }}</view>
</view>
@ -94,57 +101,734 @@
<view class="empty-icon">📋</view>
<view class="empty-text">暂无订单记录</view>
<view class="empty-tip">客户还未产生任何订单</view>
<view class="empty-add-btn" @click.stop="handleAddOrder">
<view class="empty-add-btn" v-if="showAddButton" @click.stop="openAddOrder">
<text>新增订单</text>
</view>
</view>
</view>
<!-- 新增订单弹窗 -->
<uni-popup ref="orderFormPopup" type="bottom">
<OrderFormPopup
:visible="showOrderForm"
:student-info="studentInfo"
:resource-id="resourceId"
@cancel="closeOrderForm"
@confirm="handleOrderCreated"
/>
</uni-popup>
<!-- 二维码支付弹窗 -->
<uni-popup ref="qrCodePopup" type="center" @close="closeQRCodeModal">
<QRCodePaymentDialog
v-if="qrCodePaymentData"
:payment-data="qrCodePaymentData"
@close="closeQRCodeModal"
@confirm="confirmQRCodePayment"
/>
</uni-popup>
<!-- 支付凭证上传弹窗 -->
<uni-popup ref="paymentVoucherPopup" type="bottom" :safe-area="true" :z-index="9998">
<PaymentVoucherPopup
v-if="showPaymentVoucherPopup"
:order-info="currentOrder"
@cancel="closePaymentVoucherPopup"
@confirm="confirmPaymentVoucher"
/>
</uni-popup>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
import dictUtilSimple from '@/common/dictUtilSimple.js'
import OrderFormPopup from '@/components/order-form-popup/index.vue'
import QRCodePaymentDialog from './qrcode-payment-dialog.vue'
export default {
name: 'OrderListCard',
components: {
OrderFormPopup,
QRCodePaymentDialog,
PaymentVoucherPopup: () => import('./payment-voucher-popup.vue')
},
props: {
// 订单列表数据
orderList: {
type: Array,
default: () => []
// 学员ID
studentId: {
type: [Number, String],
default: null
},
// 资源ID
resourceId: {
type: [Number, String],
default: null
},
// 是否自动加载数据
autoLoad: {
type: Boolean,
default: true
},
// 是否显示新增按钮
showAddButton: {
type: Boolean,
default: true
},
// 列表最大高度
maxHeight: {
type: String,
default: '70vh'
}
},
data() {
return {
// 订单数据
orderList: [],
loading: false,
// 弹窗状态
showOrderForm: false,
showQRCodeModal: false,
showPaymentVoucherPopup: false,
// 当前操作的订单
currentOrder: null,
// 二维码支付轮询定时器
paymentPollingTimer: null,
// 二维码支付数据
qrCodePaymentData: null,
// 字典数据
paymentTypeDict: [],
// 学生信息对象(用于传递给订单表单组件)
studentInfo: {}
}
},
mounted() {
// 加载字典数据
this.loadDictData()
// 构建学生信息对象
if (this.studentId) {
this.buildStudentInfo()
}
// 自动加载订单列表
if (this.autoLoad && (this.studentId || this.resourceId)) {
this.loadOrderList()
}
},
watch: {
// 监听学员ID变化,自动刷新
studentId(newVal) {
if (newVal && this.autoLoad) {
this.loadOrderList()
this.buildStudentInfo()
}
}
},
methods: {
// 查看订单详情
viewOrderDetail(order) {
this.$emit('view-detail', order)
/**
* 构建学生信息对象
* 通过员工端API获取学生详情包含 campus_id
*/
async buildStudentInfo() {
if (!this.studentId) {
this.studentInfo = {}
return
}
try {
// 使用员工端学生信息接口
const res = await apiRoute.getStudentBasicInfo({ student_id: this.studentId })
if (res.code === 1 && res.data) {
this.studentInfo = {
id: res.data.id,
name: res.data.name,
campus_id: res.data.campus_id,
gender: res.data.gender,
gender_text: res.data.gender_text,
birthday: res.data.birthday,
headimg: res.data.headimg,
emergency_contact: res.data.emergency_contact,
contact_phone: res.data.contact_phone
}
console.log('学生信息加载成功:', this.studentInfo)
} else {
console.error('获取学生信息失败:', res.msg)
// 降级方案: 使用基本信息
this.studentInfo = {
id: this.studentId,
name: '学员',
campus_id: null
}
}
} catch (error) {
console.error('获取学生信息异常:', error)
// 降级方案
this.studentInfo = {
id: this.studentId,
name: '学员',
campus_id: null
}
}
},
// 新增订单
handleAddOrder() {
this.$emit('add-order')
/**
* 加载字典数据
*/
async loadDictData() {
try {
const dictResult = await dictUtilSimple.getBatchDict(['payment_type'])
if (dictResult.payment_type && Array.isArray(dictResult.payment_type)) {
this.paymentTypeDict = dictResult.payment_type
} else {
// 使用备用数据
this.paymentTypeDict = [
{ name: '现金支付', value: 'cash' },
{ name: '扫码支付', value: 'scan_code' },
{ name: '订阅支付', value: 'subscription' },
{ name: '微信在线代付', value: 'wxpay_online' },
{ name: '客户自行付款', value: 'client_wxpay' },
{ name: '定金', value: 'deposit' }
]
}
} catch (error) {
console.error('加载支付方式字典失败:', error)
// 使用备用数据
this.paymentTypeDict = [
{ name: '现金支付', value: 'cash' },
{ name: '扫码支付', value: 'scan_code' },
{ name: '订阅支付', value: 'subscription' },
{ name: '微信在线代付', value: 'wxpay_online' },
{ name: '客户自行付款', value: 'client_wxpay' },
{ name: '定金', value: 'deposit' }
]
}
},
// 订单点击处理(根据状态决定是查看详情还是支付)
handleOrderClick(order) {
console.log('点击订单:', order)
// 防护检查:确保order对象存在
if (!order) {
console.warn('订单数据不存在,无法处理点击事件')
/**
* 加载订单列表
*/
async loadOrderList() {
if (!this.studentId && !this.resourceId) {
console.warn('OrderListCard: 缺少必要参数 studentId 或 resourceId')
return
}
this.loading = true
try {
const params = {}
if (this.studentId) {
params.student_id = this.studentId
} else if (this.resourceId) {
params.resource_id = this.resourceId
}
const res = await apiRoute.xs_orderTableList(params)
if (res.code === 1) {
this.orderList = this.processOrderData(res.data?.data || [])
this.$emit('loaded', this.orderList)
} else {
console.error('获取订单列表失败:', res.msg)
this.orderList = []
this.$emit('error', { type: 'load', message: res.msg })
}
} catch (error) {
console.error('获取订单列表异常:', error)
uni.showToast({ title: '加载失败', icon: 'none' })
this.$emit('error', { type: 'load', error })
} finally {
this.loading = false
}
},
/**
* 处理订单数据格式
*/
processOrderData(orders) {
if (!Array.isArray(orders)) return []
return orders.map(order => ({
id: order.id,
student_id: order.student_id,
order_no: order.payment_id || `ORD${order.id}`,
product_name: order.course_id_name || '课程',
total_amount: order.order_amount || '0.00',
paid_amount: order.order_status === 'paid' ? order.order_amount : '0.00',
unpaid_amount: order.order_status === 'paid' ? '0.00' : order.order_amount,
payment_method: this.formatPaymentType(order.payment_type),
salesperson_name: order.staff_id_name || '未指定',
course_count: order.total_hours || 0,
status: this.mapOrderStatus(order.order_status),
create_time: order.created_at,
contract_id: order.contract_id,
contract_sign_id: order.contract_sign_id,
remark: order.remark || '',
_raw: order
}))
},
/**
* 格式化支付类型
*/
formatPaymentType(paymentType) {
if (!paymentType) return '未知'
const paymentItem = this.paymentTypeDict.find(item => item.value === paymentType)
if (paymentItem) return paymentItem.name
const fallbackMap = {
'cash': '现金支付',
'scan_code': '扫码支付',
'subscription': '订阅支付',
'wxpay_online': '微信在线代付',
'client_wxpay': '客户自行付款',
'deposit': '定金'
}
return fallbackMap[paymentType] || paymentType || '未知'
},
/**
* 映射订单状态
*/
mapOrderStatus(orderStatus) {
const statusMap = {
'pending': 'pending',
'paid': 'paid',
'signed': 'completed',
'completed': 'completed',
'transfer': 'partial'
}
return statusMap[orderStatus] || 'pending'
},
/**
* 订单点击处理
*/
handleOrderClick(order) {
this.currentOrder = order
if (order.status === 'pending') {
// 未支付订单,触发支付
this.$emit('pay-order', order)
// 待支付订单,触发支付
this.handleOrderPay(order)
} else {
// 已支付订单,查看详情
this.$emit('view-detail', order)
// 已支付订单,查看详情
this.viewOrderDetail(order)
}
},
// 获取状态样式类
getStatusClass(status) {
if (!status) return 'status-default'
/**
* 处理订单支付
*/
handleOrderPay(order) {
const paymentType = order._raw?.payment_type
switch(paymentType) {
case 'cash':
case 'client_wxpay':
case 'deposit':
this.processVoucherPayment(order)
break
case 'scan_code':
this.processQRCodePayment(order)
break
case 'subscription':
this.processSubscriptionPayment(order)
break
case 'wxpay_online':
this.processWechatOnlinePayment(order)
break
default:
uni.showToast({ title: '不支持的支付方式', icon: 'none' })
}
},
/**
* 上传支付凭证的支付方式处理(cash, client_wxpay, deposit)
*/
processVoucherPayment(order) {
this.currentOrder = order
this.showPaymentVoucherPopup = true
this.$refs.paymentVoucherPopup.open()
},
/**
* 二维码支付处理
*/
async processQRCodePayment(order) {
uni.showLoading({ title: '生成支付二维码...' })
try {
const res = await apiRoute.getOrderPayQrcode({
order_id: order._raw?.id || order.id
})
uni.hideLoading()
if (res.code === 1 && res.data) {
this.qrCodePaymentData = {
order: order,
qrcode: res.data.code_url,
qrcodeImage: res.data.qrcode_base64 || res.data.qrcode_url
}
this.showQRCodeModal = true
this.$refs.qrCodePopup.open()
// 开始轮询支付状态
this.startPaymentPolling(order.order_no)
} else {
uni.showToast({ title: res.msg || '获取支付二维码失败', icon: 'none' })
}
} catch (error) {
uni.hideLoading()
console.error('获取支付二维码失败:', error)
uni.showToast({ title: '获取支付二维码失败', icon: 'none' })
}
},
/**
* 开始轮询支付状态
*/
startPaymentPolling(order_no) {
// 清除之前的定时器
this.stopPaymentPolling()
// 每3秒轮询一次
this.paymentPollingTimer = setInterval(async () => {
try {
const res = await apiRoute.checkOrderPaymentStatus({ order_no })
if (res.code === 1 && res.data) {
// 如果订单状态为已支付,停止轮询并关闭弹窗
if (res.data.order_status === 'paid') {
this.stopPaymentPolling()
this.closeQRCodeModal()
uni.showToast({ title: '支付成功', icon: 'success' })
// 刷新订单列表
await this.refresh()
// 触发支付成功事件
this.$emit('payment-success', this.currentOrder)
}
}
} catch (error) {
console.error('查询支付状态失败:', error)
}
}, 3000)
},
/**
* 停止轮询支付状态
*/
stopPaymentPolling() {
if (this.paymentPollingTimer) {
clearInterval(this.paymentPollingTimer)
this.paymentPollingTimer = null
}
},
/**
* 订阅支付处理
*/
processSubscriptionPayment(order) {
uni.showModal({
title: '订阅服务说明',
content: '每日支付超过3000笔后解锁订阅服务',
showCancel: false,
confirmText: '知道了'
})
},
/**
* 微信在线代付处理(本阶段只定义函数)
*/
processWechatOnlinePayment(order) {
// TODO: 后续实现微信在线代付功能
uni.showToast({
title: '微信在线代付功能开发中',
icon: 'none'
})
},
/**
* 更新订单支付状态
*/
async updateOrderPaymentStatus(order, status, paymentId = '') {
try {
const updateData = {
order_id: order._raw?.id || order.id,
order_status: status,
payment_id: paymentId
}
const result = await apiRoute.xs_orderTableUpdatePaymentStatus(updateData)
if (result.code === 1) {
const statusText = {
'paid': '支付成功',
'partial': '分期支付确认成功',
'cancelled': '支付已取消'
}
uni.showToast({
title: statusText[status] || '状态更新成功',
icon: 'success'
})
// 刷新订单列表
await this.refresh()
// 触发支付成功事件
this.$emit('payment-success', order)
} else {
uni.showToast({ title: result.msg || '状态更新失败', icon: 'none' })
}
} catch (error) {
console.error('更新订单状态失败:', error)
uni.showToast({ title: '状态更新失败', icon: 'none' })
this.$emit('error', { type: 'payment', error })
}
},
/**
* 关闭二维码支付弹窗
*/
closeQRCodeModal() {
// 停止轮询
this.stopPaymentPolling()
this.showQRCodeModal = false
this.qrCodePaymentData = null
this.$refs.qrCodePopup.close()
},
/**
* 确认二维码支付完成
*/
async confirmQRCodePayment() {
if (!this.qrCodePaymentData?.order) return
const order = this.qrCodePaymentData.order
uni.showModal({
title: '支付确认',
content: '请确认是否已完成扫码支付?',
success: async (res) => {
if (res.confirm) {
try {
await this.updateOrderPaymentStatus(order, 'paid', `QR${Date.now()}`)
this.closeQRCodeModal()
} catch (error) {
console.error('支付确认失败:', error)
}
}
}
})
},
/**
* 关闭支付凭证弹窗
*/
closePaymentVoucherPopup() {
this.showPaymentVoucherPopup = false
this.$refs.paymentVoucherPopup?.close()
this.$refs.paymentVoucherPopup?.reset()
},
/**
* 确认提交支付凭证
*/
async confirmPaymentVoucher(data) {
try {
uni.showLoading({ title: '提交中...' })
const res = await apiRoute.updateOrderPaymentVoucher({
order_id: data.order_id,
payment_voucher: data.payment_voucher
})
uni.hideLoading()
if (res.code === 1) {
uni.showToast({ title: '提交成功', icon: 'success' })
// 关闭弹窗
this.closePaymentVoucherPopup()
// 刷新订单列表
await this.refresh()
// 触发支付成功事件
this.$emit('payment-success', this.currentOrder)
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (error) {
uni.hideLoading()
console.error('提交支付凭证失败:', error)
uni.showToast({ title: '提交失败', icon: 'none' })
this.$emit('error', { type: 'payment-voucher', error })
}
},
/**
* 查看订单详情
*/
viewOrderDetail(order) {
const orderInfo = order._raw || order
const isOrderPaid = order.status === 'paid'
const detailText = `
订单号:${order.order_no}
课程:${order.product_name}
金额:¥${order.total_amount}
已付:¥${order.paid_amount}
未付:¥${order.unpaid_amount}
支付方式:${order.payment_method}
销售顾问:${order.salesperson_name}
课时数:${order.course_count}节
状态:${this.getStatusText(order.status)}
创建时间:${this.formatTime(order.create_time)}
${orderInfo.paid_at ? '支付时间:' + this.formatTime(orderInfo.paid_at) : ''}
`.trim()
uni.showModal({
title: '订单详情',
content: detailText,
showCancel: isOrderPaid,
cancelText: isOrderPaid ? '知道了' : '',
confirmText: isOrderPaid ? '合同签署' : '知道了',
success: (res) => {
if (res.confirm && isOrderPaid) {
this.goToContractSign(order)
}
}
})
},
/**
* 跳转到合同签署页面
*/
goToContractSign(order) {
const studentId = order.student_id || this.studentId
const contractId = order.contract_id || order._raw?.contract_id
const contractSignId = order.contract_sign_id || order._raw?.contract_sign_id
if (!studentId) {
uni.showToast({ title: '缺少学生信息', icon: 'none' })
return
}
if (!contractId) {
this.getContractByOrder(order, studentId)
return
}
let url = `/pages-student/contracts/sign?contract_id=${contractId}&student_id=${studentId}&contract_name=${encodeURIComponent(order.product_name + '合同')}&user_role=staff`
if (contractSignId) {
url += `&contract_sign_id=${contractSignId}`
}
uni.navigateTo({ url })
},
/**
* 根据订单获取合同信息
*/
async getContractByOrder(order, studentId) {
try {
uni.showLoading({ title: '获取合同信息...' })
const res = await apiRoute.getContractByOrder({
order_id: order._raw?.id || order.id,
student_id: studentId
})
uni.hideLoading()
if (res.code === 1 && res.data) {
const contractInfo = res.data
uni.navigateTo({
url: `/pages-student/contracts/sign?contract_id=${contractInfo.contract_id}&student_id=${studentId}&contract_name=${encodeURIComponent(contractInfo.contract_name || order.product_name + '合同')}&user_role=staff`
})
} else {
uni.showToast({ title: res.msg || '未找到相关合同', icon: 'none' })
}
} catch (error) {
uni.hideLoading()
console.error('获取合同信息失败:', error)
uni.showToast({ title: '获取合同信息失败', icon: 'none' })
}
},
/**
* 新增订单
*/
openAddOrder() {
if (!this.studentId) {
uni.showToast({ title: '缺少学员信息', icon: 'none' })
return
}
this.showOrderForm = true
this.$refs.orderFormPopup.open()
},
/**
* 关闭新增订单弹窗
*/
closeOrderForm() {
this.showOrderForm = false
this.$refs.orderFormPopup.close()
},
/**
* 订单创建成功回调
*/
async handleOrderCreated() {
this.closeOrderForm()
uni.showToast({ title: '订单创建成功', icon: 'success' })
// 刷新订单列表
await this.refresh()
// 触发订单创建事件
this.$emit('order-created')
},
/**
* 刷新订单列表(对外暴露方法)
*/
async refresh() {
await this.loadOrderList()
this.$emit('refreshed')
},
/**
* 获取当前订单列表(对外暴露方法)
*/
getOrderList() {
return this.orderList
},
/**
* 获取状态样式类
*/
getStatusClass(status) {
const statusMap = {
'pending': 'status-pending',
'paid': 'status-paid',
@ -156,10 +840,10 @@ export default {
return statusMap[status] || 'status-default'
},
// 获取状态文本
/**
* 获取状态文本
*/
getStatusText(status) {
if (!status) return '未知状态'
const statusMap = {
'pending': '待支付',
'paid': '已支付',
@ -171,7 +855,9 @@ export default {
return statusMap[status] || '未知状态'
},
// 格式化时间
/**
* 格式化时间
*/
formatTime(timeStr) {
if (!timeStr) return ''
try {
@ -190,6 +876,24 @@ export default {
padding: 0;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
}
.loading-icon {
font-size: 60rpx;
margin-bottom: 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #999999;
}
.order-list {
display: flex;
flex-direction: column;
@ -211,19 +915,6 @@ export default {
&.pending-payment {
border-color: #FFC107;
background: rgba(255, 193, 7, 0.05);
&::after {
position: absolute;
bottom: 16rpx;
right: 16rpx;
background: #FFC107;
color: #000000;
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-weight: 500;
z-index: 10;
}
}
}

505
uniapp/components/order-list-card/payment-voucher-popup.vue

@ -0,0 +1,505 @@
<!--支付凭证上传弹窗组件-->
<template>
<view class="payment-voucher-wrapper">
<!-- 遮罩层 -->
<view class="popup-mask" @click="handleCancel"></view>
<!-- 弹窗内容 -->
<view class="payment-voucher-popup">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">上传支付凭证</text>
<view class="close-btn" @click="handleCancel">
<text class="close-icon">✕</text>
</view>
</view>
<view class="popup-body">
<!-- 订单信息展示 -->
<view class="order-info-section">
<view class="info-row">
<text class="info-label">订单号:</text>
<text class="info-value">{{ orderInfo.order_no }}</text>
</view>
<view class="info-row">
<text class="info-label">课程类型:</text>
<text class="info-value">{{ orderInfo.product_name }}</text>
</view>
<view class="info-row">
<text class="info-label">支付金额:</text>
<text class="info-value amount">¥{{ orderInfo.total_amount }}</text>
</view>
</view>
<!-- 图片上传区域 -->
<view class="upload-section">
<view class="section-title">支付凭证</view>
<view class="upload-tip">最多上传9张图片</view>
<view class="image-list">
<view
v-for="(image, index) in imageList"
:key="index"
class="image-item"
>
<image :src="image" class="preview-image" mode="aspectFill"></image>
<view class="delete-btn" @click="deleteImage(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view
v-if="imageList.length < 9"
class="upload-btn"
@click="chooseImage"
>
<text class="upload-icon">+</text>
<text class="upload-text">上传图片</text>
</view>
</view>
</view>
</view>
<view class="popup-footer">
<view class="footer-btn cancel-btn" @click="handleCancel">
<text>取消</text>
</view>
<view
class="footer-btn confirm-btn"
:class="{ disabled: !canSubmit }"
@click="handleConfirm"
>
<text>提交</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { uploadFile } from '@/common/util.js'
export default {
name: 'PaymentVoucherPopup',
props: {
// 订单信息
orderInfo: {
type: Object,
default: () => ({
order_no: '',
product_name: '',
total_amount: '0.00'
})
}
},
data() {
return {
imageList: [], // 图片列表
uploading: false
}
},
computed: {
// 是否可以提交
canSubmit() {
return this.imageList.length > 0 && !this.uploading
}
},
methods: {
/**
* 选择图片
*/
chooseImage() {
const remainCount = 9 - this.imageList.length
uni.chooseImage({
count: remainCount,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths
// 上传图片
this.uploadImages(tempFilePaths)
},
fail: (err) => {
console.error('选择图片失败:', err)
uni.showToast({ title: '选择图片失败', icon: 'none' })
}
})
},
/**
* 上传图片
*/
async uploadImages(filePaths) {
this.uploading = true
uni.showLoading({ title: '上传中...' })
try {
const uploadPromises = filePaths.map(filePath => this.uploadSingleImage(filePath))
const results = await Promise.all(uploadPromises)
// 将上传成功的图片URL添加到列表
results.forEach(url => {
if (url) {
this.imageList.push(url)
}
})
uni.hideLoading()
if (results.some(url => url)) {
uni.showToast({ title: '上传成功', icon: 'success' })
}
} catch (error) {
uni.hideLoading()
console.error('上传图片异常:', error)
uni.showToast({ title: '上传失败', icon: 'none' })
} finally {
this.uploading = false
}
},
/**
* 上传单张图片
*/
uploadSingleImage(filePath) {
return new Promise((resolve, reject) => {
uploadFile(
filePath,
(fileData) => {
// 成功回调: fileData = { url, extname, name }
if (fileData && fileData.url) {
resolve(fileData.url)
} else {
console.error('上传成功但未返回URL')
resolve(null)
}
},
(error) => {
// 失败回调
console.error('上传请求失败:', error)
resolve(null)
}
)
})
},
/**
* 删除图片
*/
deleteImage(index) {
uni.showModal({
title: '确认删除',
content: '确定要删除这张图片吗?',
success: (res) => {
if (res.confirm) {
this.imageList.splice(index, 1)
}
}
})
},
/**
* 取消
*/
handleCancel() {
this.$emit('cancel')
},
/**
* 确认提交
*/
handleConfirm() {
if (!this.canSubmit) {
uni.showToast({ title: '请先上传支付凭证', icon: 'none' })
return
}
// 将图片数组转为逗号分隔的字符串
const payment_voucher = this.imageList.join(',')
this.$emit('confirm', {
payment_voucher: payment_voucher,
order_id: this.orderInfo.id || this.orderInfo._raw?.id
})
},
/**
* 重置数据
*/
reset() {
this.imageList = []
this.uploading = false
}
}
}
</script>
<style lang="scss" scoped>
.payment-voucher-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9998;
display: flex;
align-items: flex-end;
}
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.payment-voucher-popup {
position: relative;
left: 0;
right: 0;
bottom: 0;
width: 100%;
background: #2A2A2A;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
z-index: 2;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.3);
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.popup-content {
display: flex;
flex-direction: column;
max-height: 90vh;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 40rpx;
border-bottom: 1px solid #404040;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
.close-btn {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
transition: all 0.3s ease;
&:active {
background: rgba(255, 255, 255, 0.15);
}
}
.close-icon {
font-size: 40rpx;
color: #ffffff;
line-height: 1;
}
.popup-body {
flex: 1;
padding: 32rpx 40rpx;
overflow-y: auto;
}
.order-info-section {
background: rgba(41, 211, 180, 0.05);
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 32rpx;
border-left: 4rpx solid #29D3B4;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.info-label {
font-size: 28rpx;
color: #999999;
min-width: 140rpx;
}
.info-value {
font-size: 28rpx;
color: #ffffff;
flex: 1;
text-align: right;
&.amount {
color: #FFC107;
font-weight: 600;
font-size: 32rpx;
}
}
.upload-section {
margin-bottom: 32rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 12rpx;
}
.upload-tip {
font-size: 24rpx;
color: #999999;
margin-bottom: 24rpx;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.image-item {
position: relative;
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
}
.preview-image {
width: 100%;
height: 100%;
}
.delete-btn {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 44rpx;
height: 44rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&:active {
background: rgba(0, 0, 0, 0.8);
}
}
.delete-icon {
font-size: 32rpx;
color: #ffffff;
line-height: 1;
}
.upload-btn {
width: 200rpx;
height: 200rpx;
border: 2px dashed #404040;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.02);
transition: all 0.3s ease;
&:active {
background: rgba(255, 255, 255, 0.05);
border-color: #29D3B4;
}
}
.upload-icon {
font-size: 48rpx;
color: #666666;
line-height: 1;
margin-bottom: 8rpx;
}
.upload-text {
font-size: 24rpx;
color: #999999;
}
.popup-footer {
display: flex;
gap: 24rpx;
padding: 24rpx 40rpx;
border-top: 1px solid #404040;
background: #2A2A2A;
}
.footer-btn {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 500;
transition: all 0.3s ease;
}
.cancel-btn {
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
&:active {
background: rgba(255, 255, 255, 0.15);
}
}
.confirm-btn {
background: #29D3B4;
color: #ffffff;
&:active {
background: #1fb396;
}
&.disabled {
background: #404040;
color: #666666;
}
}
</style>

207
uniapp/components/order-list-card/qrcode-payment-dialog.vue

@ -0,0 +1,207 @@
<!--二维码支付弹窗组件-->
<template>
<view class="qrcode-payment-modal">
<!-- 弹窗头部 -->
<view class="modal-header">
<text class="modal-title">扫码支付</text>
<view class="close-btn" @click="handleClose">
<text>✕</text>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info">
<view class="info-row">
<text class="label">订单号:</text>
<text class="value">{{ paymentData.order.order_no }}</text>
</view>
<view class="info-row">
<text class="label">支付金额:</text>
<text class="amount">¥{{ paymentData.order.total_amount }}</text>
</view>
</view>
<!-- 二维码区域 -->
<view class="qrcode-container">
<image
v-if="paymentData.qrcodeImage"
:src="paymentData.qrcodeImage"
class="qrcode-image"
mode="aspectFit"
/>
<text v-else class="qrcode-placeholder">二维码加载中...</text>
<text class="qrcode-tip">请使用微信扫码完成支付</text>
</view>
<!-- 操作按钮 -->
<view class="modal-buttons">
<view class="btn secondary" @click="handleClose">取消支付</view>
<view class="btn primary" @click="handleConfirm">确认已支付</view>
</view>
</view>
</template>
<script>
export default {
name: 'QRCodePaymentDialog',
props: {
paymentData: {
type: Object,
required: true
}
},
methods: {
handleClose() {
this.$emit('close')
},
handleConfirm() {
this.$emit('confirm')
}
}
}
</script>
<style lang="scss" scoped>
.qrcode-payment-modal {
background: #2A2A2A;
border-radius: 24rpx;
padding: 48rpx;
width: 600rpx;
max-width: 90vw;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
}
.modal-title {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
font-size: 32rpx;
color: #ffffff;
transition: all 0.3s ease;
&:active {
background: rgba(255, 255, 255, 0.2);
}
}
.order-info {
background: rgba(255, 255, 255, 0.05);
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 40rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
}
.label {
font-size: 28rpx;
color: #999999;
}
.value {
font-size: 28rpx;
color: #ffffff;
}
.amount {
font-size: 40rpx;
font-weight: 600;
color: #FFC107;
}
.qrcode-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx;
background: #ffffff;
border-radius: 16rpx;
margin-bottom: 40rpx;
}
.qrcode-image {
width: 400rpx;
height: 400rpx;
margin-bottom: 24rpx;
}
.qrcode-placeholder {
width: 400rpx;
height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #999999;
margin-bottom: 24rpx;
}
.qrcode-tip {
font-size: 26rpx;
color: #666666;
text-align: center;
}
.modal-buttons {
display: flex;
gap: 24rpx;
}
.btn {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 44rpx;
font-size: 30rpx;
font-weight: 500;
transition: all 0.3s ease;
&.secondary {
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
&:active {
background: rgba(255, 255, 255, 0.15);
}
}
&.primary {
background: #29D3B4;
color: #ffffff;
&:active {
background: #1fb396;
}
}
}
</style>

320
uniapp/components/schedule/ScheduleDetail.vue

@ -1,14 +1,19 @@
<template>
<fui-modal :show="visible" width="700" @cancel="closePopup" :buttons="[{text: '关闭', type: 'default'}]" :showClose="true" @close="closePopup" @click="handleModalClick">
<!-- 自定义关闭按钮 -->
<template #header>
<!-- 组件根容器 - Vue2要求只有一个根元素 -->
<view>
<!-- 自定义弹窗遮罩 -->
<view class="modal-mask" v-if="visible" @click="closePopup">
<!-- 弹窗内容 -->
<view class="modal-container" @click.stop>
<!-- 自定义头部 -->
<view class="custom-header">
<text class="modal-title">课程安排详情</text>
<view class="close-btn" @click="closePopup">
<text class="close-icon">✕</text>
</view>
</view>
</template>
<!-- 弹窗主体内容 -->
<view class="schedule-detail" v-if="scheduleInfo">
<!-- 课程基本信息 -->
<view class="section basic-info">
@ -77,6 +82,10 @@
<view class="student-info">
<view class="student-name">{{ student.name }}</view>
<view class="student-age">年龄:{{ student.age }}岁</view>
<view class="attendance-status">
<text class="status-label">签到状态:</text>
<text :class="['status-text', getStatusClass(student.status)]">{{ getStatusText(student.status) }}</text>
</view>
<view class="course-status">课程状态:{{ student.courseStatus }}</view>
<view class="course-arrangement">课程安排:{{ student.courseType === 'fixed' ? '固定课' : '临时课' }}</view>
@ -136,6 +145,10 @@
<view class="student-info">
<view class="student-name">{{ student.name }}</view>
<view class="student-age">年龄:{{ student.age }}岁</view>
<view class="attendance-status">
<text class="status-label">签到状态:</text>
<text :class="['status-text', getStatusClass(student.status)]">{{ getStatusText(student.status) }}</text>
</view>
<view class="course-status">课程状态:{{ student.courseStatus }}</view>
<view class="course-arrangement">课程安排:等待位</view>
@ -180,11 +193,13 @@
</view>
<!-- 加载状态 -->
<view class="loading" v-if="loading && !scheduleInfo">
<fui-loading></fui-loading>
<text class="loading-text">加载中...</text>
</view>
<!-- 错误信息 -->
<view class="error-message" v-if="error && !scheduleInfo">
<text>{{ errorMessage }}</text>
<view class="retry-btn" @click="fetchScheduleDetail">
@ -192,8 +207,17 @@
</view>
</view>
<!-- 底部关闭按钮 -->
<view class="modal-footer">
<view class="footer-btn close-footer-btn" @click="closePopup">
<text class="btn-text">关闭</text>
</view>
</view>
</view>
</view>
<!-- 学员点名底部弹窗 -->
<fui-modal :show="showAttendanceModal" title="学员点名" @cancel="closeAttendanceModal" :buttons="[]">
<fui-modal :show="showAttendanceModal" title="学员点名" @cancel="closeAttendanceModal" :buttons="[]" :zIndex="10001">
<view class="attendance-modal" v-if="selectedStudent">
<view class="student-info">
<view class="student-avatar-large">
@ -218,7 +242,7 @@
</fui-modal>
<!-- 升级确认弹窗 -->
<fui-modal :show="showUpgradeConfirm" title="升级确认" @cancel="cancelUpgrade" :buttons="[]" :zIndex="10000">
<fui-modal :show="showUpgradeConfirm" title="升级确认" @cancel="cancelUpgrade" :buttons="[]" :zIndex="10002">
<view class="upgrade-confirm-modal" v-if="upgradeStudent">
<view class="confirm-content">
<view class="upgrade-icon">⚡</view>
@ -242,7 +266,7 @@
</fui-modal>
<!-- 删除课程安排确认弹窗 -->
<fui-modal :show="showDeleteSchedulesModal" title="删除确认" @cancel="closeDeleteSchedulesModal" :buttons="[]" :zIndex="10001">
<fui-modal :show="showDeleteSchedulesModal" title="删除确认" @cancel="closeDeleteSchedulesModal" :buttons="[]" :zIndex="10003">
<view class="delete-confirm-modal" v-if="studentToDelete">
<view class="confirm-content">
<view class="error-icon">⚠️</view>
@ -267,7 +291,7 @@
</view>
</view>
</fui-modal>
</fui-modal>
</view>
</template>
<script>
@ -288,12 +312,20 @@
computed: {
// 分离正式学员和等待位学员
formalStudents() {
if (!this.scheduleInfo || !this.scheduleInfo.students) return [];
return this.scheduleInfo.students.filter(student => student.schedule_type === 1 || student.schedule_type === null);
if (!this.scheduleInfo) return [];
const students = this.scheduleInfo.formal_students;
if (!students || !Array.isArray(students)) {
return [];
}
return students;
},
waitingStudents() {
if (!this.scheduleInfo || !this.scheduleInfo.students) return [];
return this.scheduleInfo.students.filter(student => student.schedule_type === 2);
if (!this.scheduleInfo) return [];
const students = this.scheduleInfo.waiting_students;
if (!students || !Array.isArray(students)) {
return [];
}
return students;
},
statusClass() {
const statusMap = {
@ -361,24 +393,12 @@
if (newVal && this.scheduleId) {
this.fetchScheduleDetail();
}
},
scheduleId(newVal, oldVal) {
// 只有在弹窗可见且scheduleId真正发生变化时才重新获取数据
if (newVal && this.visible && newVal !== oldVal) {
this.fetchScheduleDetail();
}
}
// 注意: 已移除scheduleId的watch监听,避免与visible的watch重复触发
// 父组件总是先设置scheduleId再设置visible=true,所以只需监听visible即可
},
methods: {
// 处理弹窗按钮点击
handleModalClick(e) {
// 如果点击的是关闭按钮,关闭弹窗
if (e.index === 0) {
this.closePopup();
}
},
// 获取课程安排详情(使用统一API - 对接admin端)
// 获取课程安排详情(使用统一API - 对接admin端)
async fetchScheduleDetail() {
if (!this.scheduleId) {
this.error = true;
@ -402,8 +422,33 @@
// 使用新的统一API数据结构,与admin端保持一致
if (data.schedule_info) {
// 处理新的统一数据结构(包含schedule_info、formal_students、waiting_students)
const allStudents = [...(data.formal_students || []), ...(data.waiting_students || [])];
// 处理正式学员数据
const processStudents = (students) => {
return (students || []).map(student => ({
...student,
status_text: this.getStatusText(student.status || 0),
// 确保包含课程进度数据
course_progress: student.course_progress || {
total: student.totalHours || 0,
used: student.usedHours || 0,
remaining: student.remainingHours || 0,
percentage: student.totalHours > 0 ? Math.round((student.usedHours / student.totalHours) * 100) : 0
},
// 确保包含续费和体验课标识
needsRenewal: student.needsRenewal || false,
isTrialStudent: student.isTrialStudent || student.person_type !== 'student',
// 确保包含课程状态和类型
courseStatus: student.courseStatus || (student.person_type === 'student' ? '正式课' : '体验课'),
courseType: student.schedule_type === 2 ? 'waiting' : 'formal',
// 确保包含年龄信息
age: student.age || 0,
// 确保包含体验课时信息
trialClassCount: student.trialClassCount || 0,
// 确保包含剩余课时和到期时间
remainingHours: student.remainingHours || student.course_progress?.remaining || 0,
expiryDate: student.expiryDate || ''
}));
};
this.scheduleInfo = {
// 基本课程信息从schedule_info获取
@ -425,8 +470,20 @@
time_info: data.schedule_info.time_info || null,
// 课程时长
course_duration: data.schedule_info.time_info?.duration || data.schedule_info.course_duration || 60,
// 合并正式学员和等待位学员数据
students: allStudents.map(student => ({
// 分别存储正式学员和等待位学员
formal_students: processStudents(data.formal_students),
waiting_students: processStudents(data.waiting_students),
// 其他信息
available_capacity: data.schedule_info.available_capacity || 0,
enrolled_count: (data.formal_students?.length || 0) + (data.waiting_students?.length || 0),
remaining_capacity: data.schedule_info.remaining_capacity || 0
};
console.log('课程安排详情加载成功:', this.scheduleInfo);
} else {
// 兼容旧的数据结构
const processStudents = (students) => {
return (students || []).map(student => ({
...student,
status_text: this.getStatusText(student.status || 0),
// 确保包含课程进度数据
@ -438,7 +495,7 @@
},
// 确保包含续费和体验课标识
needsRenewal: student.needsRenewal || false,
isTrialStudent: student.isTrialStudent || student.person_type !== 'student',
isTrialStudent: student.isTrialStudent || false,
// 确保包含课程状态和类型
courseStatus: student.courseStatus || (student.person_type === 'student' ? '正式课' : '体验课'),
courseType: student.schedule_type === 2 ? 'waiting' : 'formal',
@ -447,16 +504,16 @@
// 确保包含体验课时信息
trialClassCount: student.trialClassCount || 0,
// 确保包含剩余课时和到期时间
remainingHours: student.remainingHours || student.course_progress?.remaining || 0,
remainingHours: student.remainingHours || 0,
expiryDate: student.expiryDate || ''
})),
// 其他信息
available_capacity: data.available_capacity || 0,
enrolled_count: allStudents.length,
remaining_capacity: data.remaining_capacity || 0
}));
};
} else {
// 兼容旧的数据结构
// 如果是旧结构但有students数组,需要分离正式学员和等待位学员
const allStudents = data.students || [];
const formalStudents = allStudents.filter(s => s.schedule_type === 1 || s.schedule_type === null);
const waitingStudents = allStudents.filter(s => s.schedule_type === 2);
this.scheduleInfo = {
// 基本课程信息
id: data.id,
@ -477,34 +534,12 @@
time_info: data.time_info || null,
// 课程时长
course_duration: data.time_info?.duration || data.course_duration || 60,
// 学员数据
students: (data.students || []).map(student => ({
...student,
status_text: this.getStatusText(student.status || 0),
// 确保包含课程进度数据
course_progress: student.course_progress || {
total: student.totalHours || 0,
used: student.usedHours || 0,
remaining: student.remainingHours || 0,
percentage: student.totalHours > 0 ? Math.round((student.usedHours / student.totalHours) * 100) : 0
},
// 确保包含续费和体验课标识
needsRenewal: student.needsRenewal || false,
isTrialStudent: student.isTrialStudent || false,
// 确保包含课程状态和类型
courseStatus: student.courseStatus || (student.person_type === 'student' ? '正式课' : '体验课'),
courseType: student.schedule_type === 2 ? 'waiting' : 'formal',
// 确保包含年龄信息
age: student.age || 0,
// 确保包含体验课时信息
trialClassCount: student.trialClassCount || 0,
// 确保包含剩余课时和到期时间
remainingHours: student.remainingHours || 0,
expiryDate: student.expiryDate || ''
})),
// 分别存储正式学员和等待位学员
formal_students: processStudents(formalStudents),
waiting_students: processStudents(waitingStudents),
// 其他信息
available_capacity: data.available_capacity || 0,
enrolled_count: data.enrolled_count || 0,
enrolled_count: allStudents.length,
remaining_capacity: data.remaining_capacity || 0
};
}
@ -554,16 +589,35 @@
// 学员点击处理
handleStudentClick(student, index) {
console.log('点击了学员:', student)
console.log('=== 学员点击事件触发 ===');
console.log('学员信息:', student);
console.log('学员索引:', index);
console.log('学员状态:', student.status);
// 检查学员签到状态,只有状态为0(待上课)才能进行签到操作
if (student.status !== 0) {
uni.showToast({
title: `该学员已${this.getStatusText(student.status)},无法重复操作`,
icon: 'none',
duration: 2000
});
return;
}
// 检查是否是等待位学员
if (student.schedule_type === 2) {
console.log('等待位学员,弹出升级确认');
// 等待位学员 - 询问是否转为正式课
this.handleWaitingStudentClick(student, index);
} else {
console.log('正式学员,弹出签到弹窗');
console.log('设置 selectedStudent:', student);
console.log('设置 selectedStudentIndex:', index);
// 正式学员 - 进行签到/请假操作
this.selectedStudent = student;
this.selectedStudentIndex = index;
this.showAttendanceModal = true;
console.log('showAttendanceModal 已设置为:', this.showAttendanceModal);
}
},
@ -778,8 +832,14 @@
this.selectedStudent.statusClass = this.getStudentStatusClass(actionMap[action].status);
// 更新scheduleInfo中的学员信息
if (this.scheduleInfo && this.scheduleInfo.students && this.selectedStudentIndex >= 0) {
this.$set(this.scheduleInfo.students, this.selectedStudentIndex, this.selectedStudent);
if (this.scheduleInfo) {
// 判断是正式学员还是等待位学员
const isFormal = this.selectedStudent.schedule_type === 1 || this.selectedStudent.schedule_type === null;
const targetArray = isFormal ? this.scheduleInfo.formal_students : this.scheduleInfo.waiting_students;
if (targetArray && this.selectedStudentIndex >= 0) {
this.$set(targetArray, this.selectedStudentIndex, this.selectedStudent);
}
}
// 发射事件给父组件
@ -854,6 +914,16 @@
return statusTextMap[status] || '未知状态';
},
// 获取签到状态样式类
getStatusClass(status) {
const statusClassMap = {
0: 'status-pending', // 待上课
1: 'status-completed', // 已上课
2: 'status-leave' // 请假
};
return statusClassMap[status] || '';
},
// 根据课程状态代码获取状态文本
getStatusTextFromCode(statusCode) {
const statusTextMap = {
@ -921,6 +991,38 @@
</script>
<style lang="scss" scoped>
/* 自定义弹窗样式 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20rpx;
}
.modal-container {
width: 100%;
max-width: 700rpx;
max-height: 90vh;
background-color: #1a1a1a;
border-radius: 16rpx;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.5);
}
/* 移除 fui-modal 的默认 padding */
::v-deep .fui-modal__body {
padding: 0 !important;
}
/* 自定义头部样式 */
.custom-header {
display: flex;
@ -929,6 +1031,7 @@
padding: 20rpx 30rpx;
background: #2a2a2a;
border-bottom: 1px solid #3a3a3a;
flex-shrink: 0;
}
.modal-title {
@ -966,7 +1069,7 @@
.schedule-detail {
padding: 20rpx;
max-height: 80vh;
flex: 1;
overflow-y: auto;
position: relative;
}
@ -1137,6 +1240,44 @@
color: #ff3b30;
}
/* 签到状态相关样式 */
.attendance-status {
display: flex;
align-items: center;
margin: 8rpx 0;
font-size: 24rpx;
}
.status-label {
color: #666;
margin-right: 8rpx;
}
.status-text {
font-weight: 600;
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-size: 22rpx;
}
.status-text.status-pending {
background: rgba(255, 149, 0, 0.1);
color: #ff9500;
border: 1rpx solid rgba(255, 149, 0, 0.2);
}
.status-text.status-completed {
background: rgba(41, 211, 180, 0.1);
color: #29d3b4;
border: 1rpx solid rgba(41, 211, 180, 0.2);
}
.status-text.status-leave {
background: rgba(255, 59, 48, 0.1);
color: #ff3b30;
border: 1rpx solid rgba(255, 59, 48, 0.2);
}
.status-absent {
color: #8e8e93;
}
@ -1658,4 +1799,43 @@
box-shadow: 0 2rpx 8rpx rgba(239, 68, 68, 0.4);
}
}
/* 弹窗底部样式 */
.modal-footer {
padding: 20rpx 30rpx;
background: #2a2a2a;
border-top: 1px solid #3a3a3a;
flex-shrink: 0;
display: flex;
justify-content: center;
}
.footer-btn {
padding: 16rpx 60rpx;
border-radius: 8rpx;
cursor: pointer;
transition: all 0.3s ease;
.btn-text {
font-size: 28rpx;
font-weight: 600;
}
&:active {
transform: scale(0.98);
}
}
.close-footer-btn {
background: #4a4a4a;
border: 1px solid #666;
.btn-text {
color: #ccc;
}
&:hover {
background: #5a5a5a;
}
}
</style>

47
uniapp/components/student-edit-popup/student-edit-popup.less

@ -82,6 +82,53 @@
.form-input {
flex: 1;
// 头像上传样式
.avatar-upload {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
overflow: hidden;
border: 2rpx solid #e9ecef;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:active {
background: #e9ecef;
transform: scale(0.95);
}
.avatar-preview {
width: 100%;
height: 100%;
border-radius: 50%;
}
.avatar-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.avatar-icon {
font-size: 48rpx;
color: #29d3b4;
margin-bottom: 8rpx;
font-weight: 300;
}
.avatar-text {
font-size: 22rpx;
color: #999;
}
}
}
input {
width: 100%;
height: 80rpx;

66
uniapp/components/student-edit-popup/student-edit-popup.vue

@ -14,6 +14,19 @@
<view class="form-group">
<view class="form-group-title">基本信息</view>
<view class="form-item">
<view class="form-label">头像</view>
<view class="form-input">
<view class="avatar-upload" @click="chooseAvatar">
<image v-if="studentData.headimg" :src="studentData.headimg" class="avatar-preview" mode="aspectFill"></image>
<view v-else class="avatar-placeholder">
<text class="avatar-icon">+</text>
<text class="avatar-text">上传头像</text>
</view>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label required">姓名</view>
<view class="form-input">
@ -151,6 +164,7 @@
<script>
import apiRoute from '@/api/apiRoute.js'
import { uploadFile } from '@/common/util.js'
export default {
name: 'StudentEditPopup',
@ -183,7 +197,8 @@ export default {
consultant_id: null,
coach_id: null,
trial_class_count: 2, // 体验课次数|默认2(新增学员赠送)
actionsExpanded: false // 操作面板展开状态
actionsExpanded: false, // 操作面板展开状态
headimg: '' // 学生头像URL
},
// 选项数据
@ -258,7 +273,8 @@ export default {
consultant_id: student.consultant_id,
coach_id: student.coach_id,
trial_class_count: student.trial_class_count,
actionsExpanded: student.actionsExpanded || false
actionsExpanded: student.actionsExpanded || false,
headimg: student.headimg || '' // 头像URL
}
// 解析已有的标签数据
this.parseExistingTags()
@ -291,7 +307,8 @@ export default {
consultant_id: null,
coach_id: null,
trial_class_count: 2,
actionsExpanded: false
actionsExpanded: false,
headimg: '' // 头像URL
}
// 重置标签选择状态
this.selectedTagIds = []
@ -378,7 +395,8 @@ export default {
member_label: this.studentData.member_label,
consultant_id: this.studentData.consultant_id,
coach_id: this.studentData.coach_id,
trial_class_count: this.studentData.trial_class_count
trial_class_count: this.studentData.trial_class_count,
headimg: this.studentData.headimg // 头像URL
}
if (this.isEditing) {
@ -476,6 +494,46 @@ export default {
this.selectedTagIds = []
this.selectedTagNames = []
}
},
// 选择头像
chooseAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
// 显示上传中提示
uni.showLoading({
title: '上传中...',
mask: true
})
// 调用uploadFile方法上传图片
uploadFile(
tempFilePath,
(fileData) => {
// 上传成功,fileData包含url、extname、name等信息
this.studentData.headimg = fileData.url
uni.hideLoading()
uni.showToast({
title: '头像上传成功',
icon: 'success'
})
},
(error) => {
// 上传失败
uni.hideLoading()
console.error('头像上传失败:', error)
}
)
},
fail: (err) => {
console.error('选择图片失败:', err)
}
})
}
}
}

2
uniapp/components/student-info-card/student-info-card.vue

@ -251,7 +251,7 @@ export default {
.info-label {
color: #999;
font-size: 22rpx;
width: 150rpx;
width: 165rpx;
flex-shrink: 0;
}

61
uniapp/components/study-plan-popup/study-plan-popup.vue

@ -1,6 +1,7 @@
<template>
<uni-popup ref="popup" type="center">
<view class="popup-container">
<view v-if="isVisible" class="popup-wrapper" @click="handleMaskClick">
<view class="popup-mask"></view>
<view class="popup-container" @click.stop>
<view class="popup-header">
<view class="popup-title">{{ isEditing ? '编辑学习计划' : '新增学习计划' }}</view>
<view class="popup-close" @click="close">✕</view>
@ -65,7 +66,7 @@
<view class="popup-btn confirm-btn" @click="confirm">确认</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
@ -99,7 +100,6 @@ export default {
this.isEditing = false
this.resetData()
this.isVisible = true
this.$refs.popup.open()
},
// 打开编辑弹窗
@ -118,17 +118,20 @@ export default {
this.statusIndex = this.statusOptions.indexOf(this.planData.status)
this.isVisible = true
this.$refs.popup.open()
},
// 关闭弹窗
close() {
this.isVisible = false
this.$refs.popup.close()
this.resetData()
this.$emit('close')
},
// 点击遮罩层关闭
handleMaskClick() {
this.close()
},
// 重置数据
resetData() {
this.planData = {
@ -253,13 +256,59 @@ export default {
</script>
<style lang="less" scoped>
.popup-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease;
}
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.popup-container {
position: relative;
z-index: 2;
width: 90vw;
max-width: 500rpx;
background-color: #1a1a1a;
border-radius: 20rpx;
border: 1px solid #333;
overflow: hidden;
animation: slideUp 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(100rpx);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.popup-header {

48
uniapp/pages-coach/coach/schedule/schedule_table.vue

@ -1431,8 +1431,7 @@ export default {
},
// 查看课程安排详情(使用统一API)
async viewScheduleDetail(scheduleId) {
try {
viewScheduleDetail(scheduleId) {
if (!scheduleId) {
uni.showToast({
title: '课程ID不能为空',
@ -1441,51 +1440,10 @@ export default {
return;
}
// 显示课程详情弹窗
// 直接设置props,让子组件ScheduleDetail负责获取数据
// ScheduleDetail组件会在watch中调用api.getCourseArrangementDetail()
this.selectedScheduleId = scheduleId;
this.showScheduleDetail = true;
// 使用新的统一API获取课程安排详情,对接admin端功能
console.log('调用统一API获取课程安排详情:', scheduleId);
// 注意:具体的API调用逻辑在ScheduleDetail组件的fetchScheduleDetail方法中实现
// ScheduleDetail组件会调用api.getCourseArrangementDetail()方法,与admin端保持一致
// 这确保了移动端和管理端使用相同的数据结构和业务逻辑
// 预先验证API是否可用(可选的预检查)
try {
const testResponse = await api.getCourseArrangementDetail({
schedule_id: scheduleId
});
if (testResponse.code !== 1) {
console.warn('API预检查警告:', testResponse.msg);
}
} catch (preCheckError) {
console.warn('API预检查失败,将在ScheduleDetail组件中处理:', preCheckError);
}
} catch (error) {
console.error('获取课程安排详情失败:', error);
let errorMessage = '获取课程详情失败';
if (error.message) {
if (error.message.includes('timeout')) {
errorMessage = '请求超时,请重试';
} else if (error.message.includes('Network')) {
errorMessage = '网络连接失败';
}
}
uni.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
});
// 即使出错也要显示弹窗,让ScheduleDetail组件处理错误
this.selectedScheduleId = scheduleId;
this.showScheduleDetail = true;
}
},
// 处理编辑课程事件

1
uniapp/pages-market/clue/class_arrangement_detail.vue

@ -29,6 +29,7 @@
<view class="student-info">
<view class="student-name">{{ stu.name }}</view>
<view class="student-age">年龄:{{ stu.age || '未知' }}岁</view>
<view class="course-status">课程类型:{{ stu.person_type == 'customer_resource' ? '体验课' : '正式课'}}</view>
<view class="course-status">课程状态:{{ stu.courseStatus }}</view>
<view class="course-status">上课情况:{{ stu.course_progress.used }}/{{ stu.course_progress.total }}节</view>
<view class="expiry-date" v-if="stu.student_course_info">到期时间:{{ stu.student_course_info.end_date || '未设置' }}</view>

699
uniapp/pages-market/clue/clue_info.vue

@ -120,26 +120,16 @@
<CourseInfoCard v-if="currentPopup === 'course_info'" :course-list="courseInfo"
@view-detail="viewCourseDetail" />
<!-- 体测记录弹窗 -->
<view class="fitness-records-container" v-if="currentPopup === 'fitness_record'">
<!-- 空状态提示 -->
<view v-if="currentStudentFitnessRecords.length === 0" class="empty-state">
<view class="empty-icon">📊</view>
<view class="empty-text">暂无体测记录</view>
<view class="empty-tip">点击下方"新增"按钮添加体测记录</view>
</view>
<!-- 体测记录列表 -->
<FitnessRecordCard v-for="record in currentStudentFitnessRecords" :key="record.id" :record="record"
@edit="openEditFitnessRecord" />
</view>
<!-- 学习计划弹窗 -->
<StudyPlanCard v-if="currentPopup === 'study_plan'" :plan-list="studyPlanList" @edit="openEditStudyPlan" />
<!-- 订单列表弹窗 -->
<OrderListCard v-if="currentPopup === 'order_list'" :order-list="orderList" @add-order="openAddOrderDialog"
@pay-order="handlePayOrder" @view-detail="viewOrderDetail" />
<OrderListCard
v-if="currentPopup === 'order_list'"
:student-id="currentStudent && currentStudent.id"
:resource-id="clientInfo.resource_id"
@payment-success="handlePaymentSuccess"
/>
<!-- 服务列表弹窗 -->
<ServiceListCard v-if="currentPopup === 'service_list'" :service-list="serviceList" />
@ -164,57 +154,22 @@
</view>
</uni-popup>
<!-- 体测记录列表弹窗 (z-index: 999) -->
<FitnessRecordListPopup
:visible="showFitnessListPopup"
:records="currentStudentFitnessRecords"
@close="closeFitnessListPopup"
@add="openAddFitnessRecord"
@edit="openEditFitnessRecord"
/>
<!-- 体测记录编辑弹窗 (z-index: 1000, 使用view类型,高于列表弹窗) -->
<FitnessRecordPopup ref="fitnessRecordPopup" :resource-id="String(clientInfo.resource_id)"
:student-id="currentStudent && currentStudent.id" @confirm="handleFitnessRecordConfirm" />
<StudentEditPopup ref="studentEditPopup" :resource-id="clientInfo.resource_id"
@confirm="handleStudentEditConfirm" />
<StudyPlanPopup ref="studyPlanPopup" :student-id="currentStudent && currentStudent.id"
@confirm="handleStudyPlanConfirm" />
<!-- 新增订单弹窗 -->
<uni-popup ref="orderFormPopup" type="bottom">
<OrderFormPopup :visible="showOrderForm" :student-info="currentStudent"
:resource-id="clientInfo.resource_id" @cancel="closeOrderForm" @confirm="handleOrderFormConfirm" />
</uni-popup>
<!-- 二维码支付弹窗 -->
<uni-popup ref="qrCodePopup" type="center" @close="closeQRCodeModal">
<view class="qrcode-payment-modal" v-if="qrCodePaymentData">
<!-- 弹窗头部 -->
<view class="modal-header">
<text class="modal-title">扫码支付</text>
<view class="close-btn" @click="closeQRCodeModal">
<text>✕</text>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info">
<view class="info-row">
<text class="label">订单号:</text>
<text class="value">{{ qrCodePaymentData.order.order_no }}</text>
</view>
<view class="info-row">
<text class="label">支付金额:</text>
<text class="amount">¥{{ qrCodePaymentData.order.total_amount }}</text>
</view>
</view>
<!-- 二维码区域 -->
<view class="qrcode-container">
<image v-if="qrCodePaymentData.qrcodeImage" :src="qrCodePaymentData.qrcodeImage"
class="qrcode-image" mode="aspectFit" />
<text v-else class="qrcode-placeholder">二维码加载中...</text>
<text class="qrcode-tip">请使用微信扫码完成支付</text>
</view>
<!-- 操作按钮 -->
<view class="modal-buttons">
<view class="btn secondary" @click="closeQRCodeModal">取消支付</view>
<view class="btn primary" @click="confirmQRCodePayment">发送二维码给客户</view>
</view>
</view>
</uni-popup>
</view>
</template>
@ -233,11 +188,12 @@
import CourseInfoCard from '@/components/course-info-card/index.vue'
import OrderListCard from '@/components/order-list-card/index.vue'
import ServiceListCard from '@/components/service-list-card/index.vue'
import OrderFormPopup from '@/components/order-form-popup/index.vue'
// 编辑弹窗
import StudentEditPopup from '@/components/student-edit-popup/student-edit-popup.vue'
import FitnessRecordPopup from '@/components/fitness-record-popup/fitness-record-popup.vue'
import StudyPlanPopup from '@/components/study-plan-popup/study-plan-popup.vue'
// 体测记录列表弹窗
import FitnessRecordListPopup from '@/components/fitness-record-list-popup/index.vue'
export default {
components: {
@ -252,10 +208,10 @@
CourseInfoCard,
OrderListCard,
ServiceListCard,
OrderFormPopup,
StudentEditPopup,
FitnessRecordPopup,
StudyPlanPopup
StudyPlanPopup,
FitnessRecordListPopup
},
data() {
return {
@ -280,15 +236,10 @@
// 底部弹窗相关
currentPopup: null,
studyPlanList: [],
orderList: [],
serviceList: [],
// 订单表单弹窗
showOrderForm: false,
// 二维码支付弹窗
showQRCodeModal: false,
qrCodePaymentData: null,
// 体测记录列表弹窗
showFitnessListPopup: false,
// 编辑相关
remark_content: '',
@ -386,8 +337,8 @@
},
showAddButton() {
// 只有体测记录和学习计划弹窗显示新增按钮
return ['fitness_record', 'study_plan'].includes(this.currentPopup)
// 只有学习计划弹窗显示新增按钮
return ['study_plan'].includes(this.currentPopup)
},
},
@ -588,14 +539,15 @@
break
case 'fitness_record':
await this.getFitnessRecords(student.id)
this.currentPopup = 'fitness_record'
// 使用新的体测记录列表弹窗组件
this.showFitnessListPopup = true
break
case 'study_plan':
await this.getStudyPlanList(student.id)
this.currentPopup = 'study_plan'
break
case 'order_list':
await this.getOrderList(student.id)
// 订单列表由组件内部自动加载,只需打开弹窗
this.currentPopup = 'order_list'
break
case 'service_list':
@ -610,16 +562,17 @@
this.currentPopup = null
// 重置相关数据
this.studyPlanList = []
this.orderList = []
this.serviceList = []
this.courseInfo = []
this.fitnessRecords = []
// 不重置体测记录,由专门的组件管理
},
closeFitnessListPopup() {
this.showFitnessListPopup = false
},
handleAddAction() {
if (this.currentPopup === 'fitness_record') {
this.openAddFitnessRecord()
} else if (this.currentPopup === 'study_plan') {
if (this.currentPopup === 'study_plan') {
this.openAddStudyPlan()
}
},
@ -1217,563 +1170,12 @@
}
},
// 获取订单列表
async getOrderList(studentId = null) {
if (!this.clientInfo?.resource_id) return
try {
const targetStudentId = studentId || this.currentStudent?.id
// 构建查询参数,优先使用student_id
const params = {}
if (targetStudentId) {
params.student_id = targetStudentId
} else if (this.clientInfo.resource_id) {
params.resource_id = this.clientInfo.resource_id
} else {
console.warn('缺少查询参数')
this.orderList = []
return
}
const res = await apiRoute.xs_orderTableList(params)
if (res.code === 1) {
// 处理API返回的数据格式
this.orderList = this.processOrderData(res.data?.data || [])
} else {
console.error('获取订单列表失败:', res.msg)
this.orderList = []
}
} catch (error) {
console.error('获取订单列表异常:', error)
this.orderList = []
}
},
// 处理订单数据格式,适配前端组件
processOrderData(orders) {
if (!Array.isArray(orders)) return []
return orders.map(order => ({
id: order.id,
student_id: order.student_id,
order_no: order.payment_id || `ORD${order.id}`, // 使用payment_id作为订单号
product_name: order.course_id_name || '课程',
total_amount: order.order_amount || '0.00',
paid_amount: order.order_status === 'paid' ? order.order_amount : '0.00',
unpaid_amount: order.order_status === 'paid' ? '0.00' : order.order_amount,
payment_method: this.formatPaymentType(order.payment_type),
salesperson_name: order.staff_id_name || '未指定',
course_count: order.total_hours || 0, // 使用课时数
status: this.mapOrderStatus(order.order_status),
create_time: order.created_at,
// 新增合同相关字段
contract_id: order.contract_id,
contract_sign_id: order.contract_sign_id,
// 保留原始数据
_raw: order
}))
},
// 格式化支付类型
formatPaymentType(paymentType) {
const typeMap = {
'cash': '现金支付',
'scan_code': '扫码支付',
'subscription': '订阅支付',
'wxpay_online': '微信在线代付'
}
return typeMap[paymentType] || paymentType || '未知'
},
// 映射订单状态
mapOrderStatus(orderStatus) {
const statusMap = {
'pending': 'pending',
'paid': 'paid',
'signed': 'completed',
'completed': 'completed',
'transfer': 'partial'
}
return statusMap[orderStatus] || 'pending'
},
// 获取服务列表
async getServiceList(studentId = null) {
if (!this.clientInfo?.resource_id) return
const targetStudentId = studentId || this.currentStudent?.id
if (!targetStudentId) {
uni.showToast({
title: '请先选择学生',
icon: 'none'
})
return
}
try {
const response = await apiRoute.getStudentServiceList({
student_id: targetStudentId
})
if (response.code === 1) {
this.serviceList = response.data || []
} else {
console.error('获取服务记录失败:', response.msg)
uni.showToast({
title: response.msg || '获取服务记录失败',
icon: 'none'
})
this.serviceList = []
}
} catch (error) {
console.error('获取服务记录异常:', error)
uni.showToast({
title: '网络请求失败',
icon: 'none'
})
this.serviceList = []
}
},
// 打开新增订单弹窗
openAddOrderDialog() {
if (!this.currentStudent) {
uni.showToast({
title: '请先选择学生',
icon: 'none'
})
return
}
// 先关闭订单列表弹窗,避免遮挡新增订单弹窗
this.closePopup()
this.showOrderForm = true
this.$refs.orderFormPopup.open()
},
// 关闭新增订单弹窗
closeOrderForm() {
this.showOrderForm = false
this.$refs.orderFormPopup.close()
},
// 订单表单确认处理
async handleOrderFormConfirm() {
try {
// 关闽弹窗
this.closeOrderForm()
// 刷新订单列表
await this.getOrderList()
uni.showToast({
title: '订单创建成功',
icon: 'success'
})
} catch (error) {
console.error('处理订单确认失败:', error)
}
},
// 处理订单支付
handlePayOrder(order) {
// 关闭当前弹窗
this.closePopup()
// 根据支付类型处理不同的支付流程
this.processPayment(order)
},
// 处理支付流程
async processPayment(order) {
const paymentType = order._raw?.payment_type || order.payment_type
console.log('paymentType', paymentType)
try {
switch (paymentType) {
case 'cash':
// 现金支付 - 直接标记为已支付
await this.confirmCashPayment(order)
break
case 'scan_code':
// 扫码支付 - 显示二维码
this.showQRCodePayment(order)
break
case 'subscription':
// 订阅支付 - 分期支付流程
this.showSubscriptionPayment(order)
break
case 'wxpay_online':
// 微信在线代付 - 调用微信支付
this.showWechatPayment(order)
break
default:
uni.showToast({
title: '不支持的支付方式',
icon: 'none'
})
}
} catch (error) {
console.error('支付处理失败:', error)
uni.showToast({
title: '支付处理失败',
icon: 'none'
})
}
},
// 现金支付确认
async confirmCashPayment(order) {
uni.showModal({
title: '现金支付确认',
content: `确认已收到现金支付 ¥${order.total_amount}?`,
success: async (res) => {
if (res.confirm) {
try {
// 调用API更新订单状态为已支付
const updateData = {
order_id: order._raw?.id || order.id,
order_status: 'paid',
payment_id: `CASH${Date.now()}` // 生成现金支付单号
}
const result = await apiRoute.xs_orderTableUpdatePaymentStatus(updateData)
if (result.code === 1) {
uni.showToast({
title: '支付确认成功',
icon: 'success'
})
// 刷新订单列表
await this.getOrderList()
} else {
uni.showToast({
title: result.msg || '支付确认失败',
icon: 'none'
})
}
} catch (error) {
console.error('现金支付确认失败:', error)
uni.showToast({
title: '支付确认失败',
icon: 'none'
})
}
}
}
})
},
// 扫码支付
async showQRCodePayment(order) {
console.log('扫码支付:', order)
try {
uni.showLoading({
title: '生成支付二维码...'
})
// 调用接口获取支付二维码
const res = await apiRoute.getOrderPayQrcode({
order_id: order._raw?.id || order.id
})
uni.hideLoading()
if (res.code === 1 && res.data) {
// 显示二维码支付弹窗
this.openQRCodeModal({
order: order,
qrcode: res.data.code_url,
// 优先使用base64编码的二维码(解决微信小程序无法访问localhost的问题)
qrcodeImage: res.data.qrcode_base64 || res.data.qrcode_url
})
} else {
uni.showToast({
title: res.msg || '获取支付二维码失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('获取支付二维码失败:', error)
uni.showToast({
title: '获取支付二维码失败',
icon: 'none'
})
}
},
// 打开二维码支付弹窗
openQRCodeModal(paymentData) {
this.qrCodePaymentData = paymentData
this.showQRCodeModal = true
this.$refs.qrCodePopup.open()
},
// 关闭二维码支付弹窗
closeQRCodeModal() {
this.showQRCodeModal = false
this.qrCodePaymentData = null
this.$refs.qrCodePopup.close()
},
// 确认二维码支付完成
async confirmQRCodePayment() {
if (!this.qrCodePaymentData?.order) return
const order = this.qrCodePaymentData.order
uni.showModal({
title: '支付确认',
content: '请确认是否已完成扫码支付?',
success: async (res) => {
if (res.confirm) {
try {
// 更新订单状态为已支付
await this.updateOrderStatus(order, 'paid', `QR${Date.now()}`)
this.closeQRCodeModal()
} catch (error) {
console.error('支付确认失败:', error)
uni.showToast({
title: '支付确认失败',
icon: 'none'
})
}
}
}
})
},
// 订阅支付
showSubscriptionPayment(order) {
uni.showActionSheet({
itemList: ['确认分期支付方案', '取消支付'],
success: async (res) => {
if (res.tapIndex === 0) {
uni.showModal({
title: '分期支付确认',
content: `确认使用分期支付方式支付 ¥${order.total_amount}?`,
success: async (modalRes) => {
if (modalRes.confirm) {
// 这里可以设置为部分支付状态
await this.updateOrderStatus(order, 'partial',
`SUB${Date.now()}`)
}
}
})
}
}
})
},
// 微信支付
showWechatPayment(order) {
uni.showModal({
title: '微信支付',
content: `将调用微信支付 ¥${order.total_amount}`,
confirmText: '确认支付',
cancelText: '取消',
success: async (res) => {
if (res.confirm) {
// 模拟微信支付流程
uni.showLoading({
title: '正在调用微信支付...'
})
// 模拟支付延时
setTimeout(async () => {
uni.hideLoading()
// 模拟支付结果(实际应该调用微信支付API)
uni.showModal({
title: '支付结果',
content: '微信支付完成,请确认是否已收到款项?',
success: async (confirmRes) => {
if (confirmRes.confirm) {
await this.updateOrderStatus(order, 'paid',
`WX${Date.now()}`)
}
}
})
}, 1500)
}
}
})
},
// 统一的订单状态更新方法
async updateOrderStatus(order, status, paymentId = '') {
try {
const updateData = {
order_id: order._raw?.id || order.id,
order_status: status,
payment_id: paymentId
}
const result = await apiRoute.xs_orderTableUpdatePaymentStatus(updateData)
if (result.code === 1) {
const statusText = {
'paid': '支付成功',
'partial': '分期支付确认成功',
'cancelled': '支付已取消'
}
uni.showToast({
title: statusText[status] || '状态更新成功',
icon: 'success'
})
// 刷新订单列表
await this.getOrderList()
} else {
uni.showToast({
title: result.msg || '状态更新失败',
icon: 'none'
})
}
} catch (error) {
console.error('更新订单状态失败:', error)
uni.showToast({
title: '状态更新失败',
icon: 'none'
})
}
},
// 查看订单详情
viewOrderDetail(order) {
const orderInfo = order._raw || order
const detailText = `
订单号:${order.order_no}
课程:${order.product_name}
金额:¥${order.total_amount}
已付:¥${order.paid_amount}
未付:¥${order.unpaid_amount}
支付方式:${order.payment_method}
销售顾问:${order.salesperson_name}
课时数:${order.course_count}节
状态:${this.getOrderStatusText(order.status)}
创建时间:${this.formatOrderTime(order.create_time)}
${orderInfo.paid_at ? '支付时间:' + this.formatOrderTime(orderInfo.paid_at) : ''}
`.trim()
// 检查是否为已支付订单,显示不同的按钮
const isOrderPaid = order.status === 'paid'
const buttons = isOrderPaid ? ['知道了', '合同签署'] : ['知道了']
console.log('订单数据', order)
uni.showModal({
title: '订单详情',
content: detailText,
showCancel: isOrderPaid,
cancelText: isOrderPaid ? '知道了' : '',
confirmText: isOrderPaid ? '合同签署' : '知道了',
success: (res) => {
if (res.confirm && isOrderPaid) {
// 点击了合同签署按钮
this.goToContractSign(order)
}
}
})
},
// 跳转到合同签署页面
goToContractSign(order) {
// 检查必要参数
const studentId = order.student_id || this.currentStudent?.id
const contractId = order.contract_id || order._raw?.contract_id
const contractSignId = order.contract_sign_id || order._raw?.contract_sign_id
if (!studentId) {
uni.showToast({
title: '缺少学生信息',
icon: 'none'
})
return
}
if (!contractId) {
// 如果订单中没有合同模板ID,尝试从课程信息获取
this.getContractByOrder(order, studentId)
return
}
// 构建跳转参数
let url =
`/pages-student/contracts/sign?contract_id=${contractId}&student_id=${studentId}&contract_name=${encodeURIComponent(order.product_name + '合同')}&user_role=staff`
// 如果有合同签署记录ID,也传递过去(用于已存在的签署记录)
if (contractSignId) {
url += `&contract_sign_id=${contractSignId}`
}
// 跳转到学员端合同签署页面
uni.navigateTo({
url: url
})
},
// 根据订单获取合同信息
async getContractByOrder(order, studentId) {
try {
uni.showLoading({
title: '获取合同信息...'
})
// 调用API获取订单对应的合同
const res = await apiRoute.getContractByOrder({
order_id: order._raw?.id || order.id,
student_id: studentId
})
uni.hideLoading()
if (res.code === 1 && res.data) {
const contractInfo = res.data
// 跳转到合同签署页面
uni.navigateTo({
url: `/pages-student/contracts/sign?contract_id=${contractInfo.contract_id}&student_id=${studentId}&contract_name=${encodeURIComponent(contractInfo.contract_name || order.product_name + '合同')}&user_role=staff`
})
} else {
uni.showToast({
title: res.msg || '未找到相关合同',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('获取合同信息失败:', error)
uni.showToast({
title: '获取合同信息失败',
icon: 'none'
})
}
},
// 获取订单状态文本
getOrderStatusText(status) {
const statusMap = {
'pending': '待支付',
'paid': '已支付',
'partial': '部分支付',
'cancelled': '已取消',
'completed': '已完成',
'refunded': '已退款'
}
return statusMap[status] || '未知状态'
},
// 格式化订单时间
formatOrderTime(timeStr) {
if (!timeStr) return '未知'
try {
const date = new Date(timeStr)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
} catch (e) {
return timeStr
}
/**
* 订单支付成功回调
*/
handlePaymentSuccess(order) {
console.log('订单支付成功:', order)
// 可以在这里添加支付成功后的业务逻辑,例如刷新其他相关数据
},
// 获取赠品记录列表
@ -1801,28 +1203,5 @@ ${orderInfo.paid_at ? '支付时间:' + this.formatOrderTime(orderInfo.paid_at
</script>
<style lang="less" scoped>
.fitness-records-container {
max-height: 60vh;
overflow-y: auto;
}
/* 滚动条样式 */
.fitness-records-container::-webkit-scrollbar {
width: 6rpx;
}
.fitness-records-container::-webkit-scrollbar-track {
background: transparent;
}
.fitness-records-container::-webkit-scrollbar-thumb {
background: #29D3B4;
border-radius: 3rpx;
}
.fitness-records-container::-webkit-scrollbar-thumb:hover {
background: #24B89E;
}
@import './clue_info.less';
</style>

23
uniapp/pages-market/clue/edit_clues.vue

@ -684,8 +684,6 @@
//获取资源共享-详情(客户资源详情)
async getInfo() {
try {
console.log('getInfo - 开始获取客户详情, resource_sharing_id:', this.resource_sharing_id);
if (!this.resource_sharing_id) {
console.error('getInfo - resource_sharing_id 为空,无法获取客户详情');
return;
@ -695,9 +693,7 @@
resource_sharing_id: this.resource_sharing_id
};
console.log('getInfo - 发起请求:', params);
let res = await apiRoute.xs_resourceSharingInfo(params); //资源共享-详情(客户资源详情)
console.log('getInfo - 请求响应:', res);
if (res.code != 1) {
console.error('getInfo - 请求失败:', res.msg);
@ -710,8 +706,6 @@
let customerResource = res.data.customerResource || {}; //客户资源详情
let sixSpeed = res.data.customerResource.sixSpeed || {}; //六要素详情
console.log('getInfo - 客户资源详情:', customerResource);
console.log('getInfo - 六要素详情:', sixSpeed);
// 存储原始数据,用于后续回显
this._resourceDetail = res.data;
@ -755,17 +749,18 @@
emotional_stickiness_score: sixSpeed.emotional_stickiness_score || '', // 情感粘度
};
console.log('getInfo - 表单数据设置完成:', this.formData);
console.log('getInfo - 表单数据设置完成:',sixSpeed.promised_visit_time);
// 格式化日期时间
if (sixSpeed.promised_visit_time) {
// if (sixSpeed.promised_visit_time) {
// console.log('getInfo - 开始格式化日期时间',sixSpeed.promised_visit_time.includes(' '));
// 如果包含时间部分,保持原格式;否则只格式化日期部分
if (sixSpeed.promised_visit_time.includes(' ')) {
this.formData.promised_visit_time = sixSpeed.promised_visit_time;
} else {
this.formData.promised_visit_time = this.$util.formatToDateTime(sixSpeed.promised_visit_time, 'Y-m-d');
}
}
// if (sixSpeed.promised_visit_time.includes(' ')) {
// this.formData.promised_visit_time = sixSpeed.promised_visit_time;
// } else {
// this.formData.promised_visit_time = this.$util.formatToDateTime(sixSpeed.promised_visit_time, 'Y-m-d');
// }
// }
// 可选上课时间保持原始文本格式,不做日期格式化
if (sixSpeed.preferred_class_time) {

1292
uniapp/pages/market/clue/class_arrangement_detail.vue

File diff suppressed because it is too large

431
uniapp/pages/market/clue/clue_info.less

@ -1,431 +0,0 @@
// 客户详情页样式文件
.assemble {
background-color: #292929;
min-height: 100vh;
}
.content {
padding: 20rpx;
}
.tab-switcher-container {
margin: 20rpx 0;
}
// 学生信息区域
.student-section {
margin-top: 20rpx;
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
}
.add-student-btn {
display: flex;
align-items: center;
background: #29d3b4;
color: #fff;
border-radius: 20rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
.add-icon {
margin-right: 8rpx;
font-weight: bold;
font-size: 20rpx;
}
&:active {
background: #1ea08e;
transform: scale(0.95);
}
}
}
.student-cards {
width: 100%;
min-height: 600rpx;
.student-swiper {
width: 100%;
height: 560rpx;
.student-swiper-content {
display: flex;
flex-direction: column;
height: 580rpx;
padding: 20rpx;
box-sizing: border-box;
background-color: #434544;
border-radius: 16rpx;
margin: 0 10rpx;
color: #fff;
}
}
}
}
.popup-footer {
display: flex;
padding: 30rpx 40rpx;
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
border-top: 1px solid #333;
gap: 30rpx;
flex-shrink: 0; /* 确保底部按钮区域不被压缩 */
background: #1a1a1a; /* 确保背景色一致 */
.popup-footer-btns {
display: flex;
justify-content: space-between;
.footer-btn {
flex: 1;
height: 88rpx;
display: flex;
width: 45%;
align-items: center;
justify-content: center;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 500;
&.cancel-btn {
background: #cccccc;
}
&.confirm-btn {
background: linear-gradient(45deg, #29D3B4, #1DB584);
color: #ffffff;
}
}
}
}
// 课程、通话、体测记录等区域
.course-section, .call-section, .fitness-section, .study-plan-section {
background-color: #434544;
border-radius: 16rpx;
margin: 20rpx 0;
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
.context-title {
font-size: 28rpx;
font-weight: bold;
color: #fff;
}
.add-record-btn {
display: flex;
align-items: center;
background: #29d3b4;
color: #fff;
border-radius: 20rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
.add-icon {
margin-right: 8rpx;
font-weight: bold;
}
&:active {
background: #1ea08e;
transform: scale(0.95);
}
}
}
}
// 操作按钮区域 - 独立于Swiper外部
.student-section .action-buttons-section {
display: flex;
gap: 8rpx;
margin-top: 15rpx;
padding: 15rpx;
flex-wrap: nowrap;
height: 100rpx;
box-sizing: border-box;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
background-color: #434544;
border-radius: 16rpx;
margin: 15rpx 0;
&::-webkit-scrollbar {
display: none;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(41, 211, 180, 0.3);
border-radius: 8rpx;
padding: 12rpx 8rpx;
min-width: 90rpx;
flex-shrink: 0;
transition: all 0.3s ease;
.action-icon {
font-size: 24rpx;
margin-bottom: 5rpx;
line-height: 1;
}
.action-text {
font-size: 18rpx;
color: #fff;
text-align: center;
line-height: 1.2;
word-break: break-all;
}
&:active {
background: rgba(41, 211, 180, 0.2);
border-color: #29d3b4;
transform: scale(0.95);
.action-text {
color: #29d3b4;
}
}
}
}
// 通用空状态样式
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 20rpx;
min-height: 200rpx;
.empty-icon {
font-size: 60rpx;
opacity: 0.6;
margin-bottom: 20rpx;
}
.empty-text {
color: #ccc;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.empty-add-btn {
background: #29d3b4;
color: #fff;
padding: 12rpx 24rpx;
border-radius: 20rpx;
font-size: 24rpx;
transition: all 0.3s ease;
&:active {
background: #1ea08e;
transform: scale(0.95);
}
}
}
// 备注弹窗样式
.remark-dialog {
width: 600rpx;
padding: 30rpx;
background: #fff;
border-radius: 16rpx;
textarea {
width: 100%;
height: 200rpx;
padding: 20rpx;
border: 2rpx solid #e9ecef;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
background-color: #fff;
box-sizing: border-box;
resize: none;
&::placeholder {
color: #999;
}
&:focus {
border-color: #29d3b4;
outline: none;
box-shadow: 0 0 0 2rpx rgba(41, 211, 180, 0.1);
}
}
.dialog-btns {
display: flex;
gap: 20rpx;
margin-top: 30rpx;
.btn {
flex: 1;
padding: 20rpx;
border-radius: 8rpx;
font-size: 28rpx;
text-align: center;
&.cancel {
background: #f8f9fa;
color: #666;
border: 2rpx solid #e9ecef;
}
&.confirm {
background: #29d3b4;
color: #fff;
}
}
}
}
// 二维码支付弹窗样式
.qrcode-payment-modal {
width: 600rpx;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 40rpx 40rpx 20rpx;
border-bottom: 1px solid #f0f0f0;
.modal-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f5f5f5;
color: #666;
font-size: 32rpx;
&:active {
background: #e5e5e5;
transform: scale(0.95);
}
}
}
.order-info {
padding: 30rpx 40rpx;
.info-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.label {
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
}
.value {
font-size: 28rpx;
color: #333;
flex: 1;
}
.amount {
font-size: 32rpx;
color: #ff4757;
font-weight: 600;
flex: 1;
}
}
}
.qrcode-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx;
background: #fafafa;
.qrcode-image {
width: 300rpx;
height: 300rpx;
border: 1px solid #e5e5e5;
border-radius: 12rpx;
background: #fff;
margin-bottom: 20rpx;
}
.qrcode-tip {
font-size: 24rpx;
color: #666;
text-align: center;
line-height: 1.4;
}
}
.modal-buttons {
display: flex;
padding: 30rpx 40rpx 40rpx;
gap: 20rpx;
.btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 40rpx;
font-size: 28rpx;
font-weight: 500;
&.secondary {
background: #f5f5f5;
color: #666;
&:active {
background: #e5e5e5;
transform: scale(0.98);
}
}
&.primary {
background: #29d3b4;
color: #fff;
&:active {
background: #1ea08e;
transform: scale(0.98);
}
}
}
}
}

1567
uniapp/pages/market/clue/clue_info.vue

File diff suppressed because it is too large

164
uniapp/pages/market/clue/clue_table.vue

@ -1,164 +0,0 @@
<template>
<view class="dark-table-container">
<view v-for="(row, rIdx) in data" :key="rIdx" class="card">
<view class="card-title" @click="toggleExpand(rIdx)">
{{ row.channel }}
<text class="expand-icon">{{ expanded[rIdx] ? '▲' : '▼' }}</text>
</view>
<view class="card-content">
<view
class="card-row"
v-for="(col, cIdx) in getVisibleColumns(rIdx)"
:key="cIdx"
>
<view class="card-label">
{{ col.label }}
<text v-if="col.tip" class="tip" @click.stop="showTip(col.tip)">?</text>
</view>
<view class="card-value">{{ row[col.key] || '-' }}</view>
</view>
</view>
<view v-if="columns.length > 6" class="expand-btn" @click="toggleExpand(rIdx)">
{{ expanded[rIdx] ? '收起' : '展开全部' }}
</view>
</view>
<!-- 说明弹窗 -->
<uni-popup ref="popup" type="center">
<view class="popup-content">{{ tipContent }}</view>
</uni-popup>
<AQTabber></AQTabber>
</view>
</template>
<script>
import AQTabber from "@/components/AQ/AQTabber.vue"
export default {
components: {
AQTabber,
},
data() {
return {
columns: [
{ label: '渠道', key: 'channel' },
{ label: '资源数', key: 'resource', tip: '本月的新资源' },
{ label: '到访数(一访)', key: 'visit1', tip: '本月资源的一访到访数' },
{ label: '到访率(一访)', key: 'visitRate1', tip: '一访到访数/本月新资源数' },
{ label: '到访数(二访)', key: 'visit2', tip: '二访到访数/一访到访数' },
{ label: '到访率(二访)', key: 'visitRate2', tip: '二访到访数/一访到访数' },
{ label: '关单数(一访)', key: 'close1', tip: '本月一访到访的关单数' },
{ label: '关单率(一访)', key: 'closeRate1', tip: '一访关单数/一访到访数' },
{ label: '关单数(二访)', key: 'close2', tip: '二访到访的关单数' },
{ label: '关单率(二访)', key: 'closeRate2', tip: '二访关单数/二访到访数' },
{ label: '月总转', key: 'monthTrans', tip: '关单数(合计)/资源数' },
{ label: '往月到访数', key: 'lastMonthVisit', tip: '本月资源在往月到访的到访数' },
{ label: '月共到访', key: 'monthTotalVisit', tip: '本月资源任在本月到访/关单' },
{ label: '往月关单数', key: 'lastMonthClose', tip: '本月关单-往月关单' },
{ label: '月共关单', key: 'monthTotalClose', tip: '本月关单+往月关单' },
],
data: [
{ channel: '体检包(地推)', resource: 10, visit1: 5, visitRate1: '50%', visit2: 2, visitRate2: '40%', close1: 1, closeRate1: '20%', close2: 1, closeRate2: '50%', monthTrans: '10%', lastMonthVisit: 0, monthTotalVisit: 5, lastMonthClose: 0, monthTotalClose: 1 },
],
tipContent: '',
expanded: {},
}
},
methods: {
showTip(content) {
this.tipContent = content
this.$refs.popup.open()
},
toggleExpand(idx) {
this.$set(this.expanded, idx, !this.expanded[idx])
},
getVisibleColumns(idx) {
// 只展示前5个字段,展开后展示全部(不含渠道)
const cols = this.columns.slice(1)
if (this.expanded[idx]) return cols
return cols.slice(0, 5)
}
}
}
</script>
<style scoped>
.dark-table-container {
background: #18181c;
color: #f1f1f1;
min-height: 100vh;
padding: 24rpx 12rpx;
}
.card {
background: #23232a;
border-radius: 18rpx;
box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.18);
margin-bottom: 32rpx;
padding: 24rpx 20rpx 8rpx 20rpx;
transition: box-shadow 0.2s;
}
.card-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 18rpx;
color: #ffb300;
letter-spacing: 2rpx;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.expand-icon {
font-size: 24rpx;
color: #bdbdbd;
margin-left: 12rpx;
}
.card-content {
display: flex;
flex-direction: column;
gap: 12rpx;
overflow: hidden;
transition: max-height 0.3s;
}
.card-row {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px dashed #333;
padding: 8rpx 0;
}
.card-label {
color: #bdbdbd;
font-size: 26rpx;
display: flex;
align-items: center;
}
.card-value {
color: #f1f1f1;
font-size: 26rpx;
text-align: right;
max-width: 60%;
word-break: break-all;
}
.tip {
color: #ffb300;
margin-left: 8rpx;
font-size: 24rpx;
cursor: pointer;
}
.expand-btn {
color: #ffb300;
text-align: center;
font-size: 26rpx;
margin: 10rpx 0 0 0;
padding-bottom: 8rpx;
cursor: pointer;
}
.popup-content {
background: #23232a;
color: #fff;
padding: 32rpx;
border-radius: 16rpx;
min-width: 300rpx;
text-align: center;
}
</style>

1806
uniapp/pages/market/clue/edit_clues.vue

File diff suppressed because it is too large

365
uniapp/pages/market/clue/edit_clues_log.vue

@ -1,365 +0,0 @@
<!--编辑客户-->
<template>
<view class="assemble">
<!--切换-->
<view class="tab_section">
<fui-segmented-control
:values="optionTable"
:current="(Number(optionTableId))"
type="button"
radius="8"
height="80"
color="#29d3b4"
bold="true"
@click="segmented">
</fui-segmented-control>
</view>
<!-- 客户资源修改记录-->
<scroll-view
v-show="filteredData.type == `resource`"
scroll-y="true"
:lower-threshold="lowerThreshold"
@scrolltolower="loadMoreData"
style="margin-top:40rpx;height: 80vh;"
>
<!--时间轴-->
<view class="table_list">
<fui-timeaxis background="#292929" :padding="['36rpx','16rpx']">
<fui-timeaxis-node
:lineColor="k<3 ? v.activeColor:'#ccc'"
:lined="k!==tableList.length-1"
v-for="(v,k) in tableList"
:key="k"
>
<!--inco-->
<view
class="fui-node"
:style="{borderColor:(k != tableList.length-1 ? '#29d3b4' :'' ) }"
>
</view>
<template v-slot:right>
<view
class="fui-process__node table_itme"
:style="{paddingBottom:(k !== tableList.length-1 ? '50rpx' :'0rpx')}"
:class="{'fui-node__pb':k!==tableList.length-1}"
>
<view class="fui-title">
<!--修改时间-->
{{v.modification_time}}
</view>
<view class="itme_box">
<view class="title_name">修改人:{{v.staff_id_name}} 修改了如下内容</view>
<view class="table_section">
<table class="table_box">
<!-- 自定义表头 -->
<thead>
<tr>
<th style="width: 30%; border: 1px solid #ddd; padding: 12rpx;">字段名称</th>
<th style="width: 34%; border: 1px solid #ddd; padding: 12rpx;">原数据</th>
<th style="width: 34%; border: 1px solid #ddd; padding: 12rpx;">新数据</th>
</tr>
</thead>
<!-- 自定义每一行数据 -->
<tbody>
<tr v-for="(row, index) in v.update_arr" :key="index">
<td style="width: 30%; border: 1px solid #ddd; padding: 12rpx;">{{ row.field_name_zh }}</td>
<td style="width: 34%; border: 1px solid #ddd; padding: 12rpx;">{{ row.old_value }}</td>
<td style="width: 34%; border: 1px solid #ddd; padding: 12rpx;">{{ row.new_value }}</td>
</tr>
</tbody>
</table>
</view>
</view>
</view>
</template>
</fui-timeaxis-node>
</fui-timeaxis>
</view>
</scroll-view>
<!-- 六要素修改记录-->
<scroll-view
v-show="filteredData.type == `six_speed`"
scroll-y="true"
:lower-threshold="lowerThreshold"
@scrolltolower="loadMoreData"
style="margin-top:40rpx;height: 80vh;"
>
<!--时间轴-->
<view class="table_list">
<fui-timeaxis background="#292929" :padding="['36rpx','16rpx']">
<fui-timeaxis-node
:lineColor="k<3?v.activeColor:'#ccc'"
:lined="k!==tableList.length-1"
v-for="(v,k) in tableList"
:key="k"
>
<!--inco-->
<view
class="fui-node"
:style="{borderColor:(k != tableList.length-1 ? '#29d3b4' :'' ) }"
>
</view>
<template v-slot:right>
<view
class="fui-process__node table_itme"
:style="{paddingBottom:(k !== tableList.length-1 ? '50rpx' :'0rpx')}"
:class="{'fui-node__pb':k!==tableList.length-1}"
>
<view class="fui-title">
<!--修改时间-->
{{v.modification_time}}
</view>
<view class="itme_box">
<view class="title_name">修改人:{{v.staff_id_name}} 修改了如下内容</view>
<view class="table_section">
<table class="table_box">
<!-- 自定义表头 -->
<thead>
<tr>
<th style="width: 30%; border: 1px solid #ddd; padding: 12rpx;">字段名称</th>
<th style="width: 34%; border: 1px solid #ddd; padding: 12rpx;">原数据</th>
<th style="width: 34%; border: 1px solid #ddd; padding: 12rpx;">新数据</th>
</tr>
</thead>
<!-- 自定义每一行数据 -->
<tbody>
<tr v-for="(row, index) in v.update_arr" :key="index">
<td style="width: 30%; border: 1px solid #ddd; padding: 12rpx;">{{ row.field_name_zh }}</td>
<td style="width: 34%; border: 1px solid #ddd; padding: 12rpx;">{{ row.old_value }}</td>
<td style="width: 34%; border: 1px solid #ddd; padding: 12rpx;">{{ row.new_value }}</td>
</tr>
</tbody>
</table>
</view>
</view>
</view>
</template>
</fui-timeaxis-node>
</fui-timeaxis>
</view>
</scroll-view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
loading:false,//加载状态
lowerThreshold: 100,//距离底部多远触发
isReachedBottom: false,//防止重复加载|true=不可加载|false=可加载
//筛选条件
filteredData:{
page:1,//当前页码
limit:10,//每页返回数据条数
total:10,//数据总条数
type:'resource',//查询类型|resource=客户资源,six_speed=六要素
customer_resource_id: '',//客户资源表id
},
//数据列表
tableList:[],//表格数据
//tab切换
optionTableId:0,//分段器初始选中项索引
optionTable:[
{
id: 0,
name: '客户资源修改记录',
type:'resource',
},
{
id: 1,
name: '六要素修改记录',
type:'six_speed',
}
]
}
},
onLoad(options) {
this.filteredData.customer_resource_id = options.resource_id//客户资源表id
},
onShow() {
this.init()
},
//下拉刷新
async onPullDownRefresh() {
//重置为第一页
await this.resetFilteredData()
await this.getList()
},
methods: {
//初始化
async init() {
await this.getList()//获取客户修改记录列表
},
//加载更多(下一页)
loadMoreData() {
//判断是否加载
if (!this.isReachedBottom) {
this.isReachedBottom = true;//设置为不可请求状态
this.getList();
}
},
//重置为第一页
async resetFilteredData() {
this.isReachedBottom = false; // 重置状态,以便下次触发加载更多
this.filteredData.page = 1//当前页码
this.filteredData.limit = 10//每页返回数据条数
this.filteredData.total = 10//数据总条数
},
//获取列表-客户信息修改记录
async getList(){
this.loading = true
let data = {...this.filteredData}
//判断是否还有数据
if ((this.filteredData.page - 1) * this.filteredData.limit >= this.filteredData.total) {
this.loading = false
uni.showToast({
title: '暂无更多',
icon: 'none'
})
return
}
if(data.page == 1){
this.tableList = []
}
let res = await apiRoute.xs_customerResourcesGetEditLogList(data)
this.loading = false
this.isReachedBottom = false;
if (res.code != 1){
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
console.log(123123123,res)
this.tableList = this.tableList.concat(res.data.data); // 使用 concat 方法 将新数据追加到数组中
console.log('列表1',this.tableList)
this.filteredData.total = res.data.total
this.filteredData.page++
},
//切换tag列表
async segmented(e) {
console.log('切换',e)
//e.id|0=基础信息 1=六要素
let status = e.id
this.optionTableId = String(status)
this.filteredData.type = e.type//查询类型|resource=客户资源,six_speed=六要素
await this.resetFilteredData()
await this.getList()
},
}
}
</script>
<style lang="less" scoped>
.assemble{
width: 100%;
height: 100%;
background-color: #292929;
overflow: auto;
.tab_section{
padding: 0 20rpx;
padding-top: 30rpx;
}
.table_list{
color: #fff;
::v-deep .fui-timeaxis__line{
background-color: #fff;
}
.itme_box {
margin-top: 30rpx;
display: flex;
flex-direction: column;
gap: 25rpx;
.title_name{
font-size: 28rpx;
}
.table_section{
.table_box{
width: 100%;
border-collapse: collapse;
table-layout: fixed;
tr{
display: flex;
th{}
td{}
}
}
}
}
::v-deep .fui-timeaxis__wrap{
}
.fui-node {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
border: 4px solid #ccc;
box-sizing: border-box;
}
.fui-process__node {
padding-left: 20rpx;
}
.fui-title {
font-size: 34rpx;
line-height: 34rpx;
font-weight: bold;
}
.fui-descr {
font-size: 28rpx;
padding-top: 12rpx;
color: #7F7F7F;
}
.fui-time__left {
font-size: 36rpx;
line-height: 36rpx;
text-align: right;
padding-right: 20rpx;
}
.fui-node__pbtm {
padding-bottom: 88rpx;
}
}
}
</style>

1749
uniapp/pages/market/clue/index.vue

File diff suppressed because it is too large

343
uniapp/pages/market/data/statistics.vue

@ -1,343 +0,0 @@
<!--市场数据统计页面-->
<template>
<view class="main_box" :class="{'has-safe-area': hasSafeArea}">
<!--自定义导航栏-->
<view class="navbar_section">
<view class="title">市场数据统计</view>
</view>
<!-- 顶部筛选 -->
<view class="filter-section">
<view class="filter-item">
<view class="label">统计时间:</view>
<picker mode="date" fields="month" :value="currentDate" @change="dateChange">
<view class="picker-value">{{currentDate}}</view>
</picker>
</view>
</view>
<!-- 市场人员资源量统计 -->
<view class="table-section">
<view class="table-title">市场人员资源量统计</view>
<view class="table-container">
<view class="table-header">
<view class="th th-person">人员</view>
<view class="th th-week">周数量</view>
<view class="th th-month">月数量</view>
<view class="th th-year">年数量</view>
</view>
<view class="table-body">
<view class="table-row" v-for="(item, index) in staffData" :key="index">
<view class="td td-person">{{item.name}}</view>
<view class="td td-week">{{item.weekCount}}</view>
<view class="td td-month">{{item.monthCount}}</view>
<view class="td td-year">{{item.yearCount}}</view>
</view>
</view>
</view>
</view>
<!-- 校区人员资源渠道量 -->
<view class="table-section">
<view class="table-title">{{currentMonth}}月 - 年度校区人员资源渠道量</view>
<view class="table-container">
<view class="table-header">
<view class="th th-channel" style="width: 100px;">渠道</view>
<view class="th" v-for="(school, index) in schoolList" :key="index">{{school.campus_name}}</view>
<view class="th th-total">渠道合计</view>
</view>
<view class="table-body">
<view class="table-row" v-for="(channel, index) in channelList" :key="index">
<view class="td td-channel" style="width: 100px;">{{channel.name}}</view>
<view class="td" v-for="(school, sIndex) in schoolList" :key="sIndex">
{{channel[school.id] || 0}}
</view>
<view class="td td-total">{{channel.total}}</view>
</view>
</view>
</view>
</view>
<!-- 市场人员资源渠道统计 -->
<view class="table-section">
<view class="table-title">{{currentMonth}}月 - 年度市场人员资源渠道统计</view>
<view class="table-container">
<view class="table-header">
<view class="th th-channel" style="width: 100px;">渠道</view>
<view class="th" v-for="(staff, index) in staffData" :key="index">市场{{staff.name}}</view>
<view class="th th-total">资源合计</view>
</view>
<view class="table-body">
<view class="table-row" v-for="(channel, index) in channelList" :key="index">
<view class="td td-channel" style="width: 100px;">{{channel.name}}</view>
<view class="td" v-for="(staff, sIndex) in staffData" :key="sIndex">
{{staff['channel'][channel.value] || 0}}
</view>
<view class="td td-total">{{channel.total}}</view>
</view>
</view>
</view>
</view>
<AQTabber />
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
import AQTabber from "@/components/AQ/AQTabber.vue"
export default {
components: {
AQTabber,
},
data() {
return {
currentDate: this.formatDate(new Date()),
currentMonth: new Date().getMonth() + 1,
// 市场人员资源量统计
staffData: [],
// 校区列表
schoolList: [],
// 是否有安全区域
hasSafeArea: false,
// 渠道列表
channelList: [],
// 市场人员列表
marketStaffList: [
{ id: 'A', name: 'A' },
{ id: 'B', name: 'B' },
{ id: 'C', name: 'C' },
{ id: 'D', name: 'D' }
],
// 校区渠道数据
channelSchoolData: [],
// 人员渠道数据
channelStaffData: []
}
},
onLoad() {
this.initData();
this.checkSafeArea();
},
methods: {
// 格式化日期为 YYYY-MM
formatDate(date) {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
return `${year}-${month}`;
},
// 日期选择器变更
dateChange(e) {
this.currentDate = e.detail.value;
const [year, month] = this.currentDate.split('-');
this.currentMonth = parseInt(month);
this.initData();
},
// 初始化数据
async initData() {
// 模拟获取数据,实际项目中应该调用API
await this.getStaffStatistics();
// await this.getChannelSchoolStatistics();
// this.getChannelStaffStatistics();
},
// 获取人员资源量统计
async getStaffStatistics() {
// 模拟数据,实际项目中应该调用API
let res = await apiRoute.getStaffStatistics({date: this.currentDate})
console.log(res.data.staffData,"================");
this.staffData = res.data.staffData
this.schoolList = res.data.schoolList
this.channelList = res.data.channelList
},
// 获取校区渠道统计
getChannelSchoolStatistics() {
// 模拟数据,实际项目中应该调用API
this.channelSchoolData = this.channelList.map(channel => {
const schools = {};
let total = 0;
this.schoolList.forEach(school => {
const count = Math.floor(Math.random() * 50);
schools[school.id] = count;
total += count;
});
return {
id: channel.id,
name: channel.name,
schools,
total
}
});
},
// 获取人员渠道统计
getChannelStaffStatistics() {
// 模拟数据,实际项目中应该调用API
this.channelStaffData = this.channelList.map(channel => {
const staffs = {};
let total = 0;
this.marketStaffList.forEach(staff => {
const count = Math.floor(Math.random() * 40);
staffs[staff.id] = count;
total += count;
});
return {
id: channel.id,
name: channel.name,
staffs,
total
}
});
},
// 检测设备是否有安全区域
checkSafeArea() {
try {
const systemInfo = uni.getSystemInfoSync();
// 检测是否为iPhone X及以上机型或其他有安全区域的设备
if (systemInfo.safeAreaInsets && systemInfo.safeAreaInsets.bottom > 0) {
this.hasSafeArea = true;
}
} catch (e) {
console.error('获取系统信息失败', e);
}
}
}
}
</script>
<style lang="scss" scoped>
.main_box {
min-height: 100vh;
background-color: #1a1a1a;
color: #ffffff;
padding-bottom: 150rpx; /* 增加底部内边距,防止内容被底部导航栏遮挡 */
}
/* 适配有安全区域的设备(如iPhone X及以上机型) */
.has-safe-area {
padding-bottom: calc(150rpx + constant(safe-area-inset-bottom)); /* iOS 11.0-11.2 */
padding-bottom: calc(150rpx + env(safe-area-inset-bottom)); /* iOS 11.2+ */
}
.navbar_section {
background-color: #1a1a1a;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.title {
font-size: 36rpx;
font-weight: bold;
}
}
.filter-section {
padding: 20rpx;
background-color: #222222;
margin: 20rpx;
border-radius: 10rpx;
.filter-item {
display: flex;
align-items: center;
.label {
font-size: 28rpx;
margin-right: 10rpx;
}
.picker-value {
padding: 10rpx 20rpx;
background-color: #333333;
border-radius: 6rpx;
font-size: 28rpx;
}
}
}
.table-section {
margin: 30rpx 20rpx;
background-color: #222222;
border-radius: 10rpx;
overflow: hidden;
.table-title {
font-size: 32rpx;
font-weight: bold;
padding: 20rpx;
border-bottom: 1px solid #333333;
}
.table-container {
width: 100%;
overflow-x: auto;
}
.table-header {
display: flex;
background-color: #333333;
.th {
padding: 15rpx 10rpx;
font-size: 26rpx;
text-align: center;
flex: 1;
min-width: 100rpx;
font-weight: bold;
}
}
.table-body {
.table-row {
display: flex;
border-bottom: 1px solid #333333;
&:last-child {
border-bottom: none;
}
.td {
padding: 15rpx 10rpx;
font-size: 26rpx;
text-align: center;
flex: 1;
min-width: 100rpx;
}
}
}
.th-person, .td-person {
min-width: 80rpx;
flex: 0.8;
}
.th-week, .td-week,
.th-month, .td-month,
.th-year, .td-year {
flex: 1;
}
.th-channel, .td-channel {
flex: 1.2;
text-align: left;
padding-left: 20rpx;
}
.th-total, .td-total {
flex: 1.2;
font-weight: bold;
}
}
</style>

427
uniapp/pages/market/index/index.vue

@ -1,427 +0,0 @@
<template>
<view class="assemble">
<view style="height: 20rpx;"></view>
<!-- 时间筛选 -->
<view class="filter-section">
<picker mode="date" fields="month" :value="currentDate" @change="onDateChange">
<view class="date-picker">
<text>{{currentDate}}</text>
<image class="drop-image" :src="$util.img('/static/images/drop.png')" mode="aspectFit"></image>
</view>
</picker>
</view>
<!-- 本月提成卡片 -->
<view class="commission-card">
<view class="card-title">本月提成</view>
<view class="commission-amount">¥{{totalCommission}}</view>
</view>
<!-- 续费提成记录 -->
<view class="record-card">
<view class="card-title">续费提成</view>
<view class="table">
<view class="table-header">
<view class="th">时间</view>
<view class="th">到期数</view>
<view class="th">续费数</view>
<view class="th">续费率</view>
<view class="th">提成</view>
</view>
<view class="table-body">
<view class="tr" v-for="(item, index) in renewalRecords" :key="index">
<view class="td">{{item.time}}</view>
<view class="td">{{item.expireCount}}</view>
<view class="td">{{item.renewCount}}</view>
<view class="td">{{item.renewRate}}%</view>
<view class="td">¥{{item.commission}}</view>
</view>
</view>
</view>
</view>
<!-- 新招课包 -->
<view class="record-card">
<view class="card-title">新招课包</view>
<view class="table">
<view class="table-header">
<view class="th">成交数</view>
<view class="th">提成</view>
<view class="th">合计</view>
</view>
<view class="table-body">
<view class="tr">
<view class="td">{{newPackageCount}}</view>
<view class="td">¥{{newPackageCommission}}</view>
<view class="td">¥{{newPackageTotal}}</view>
</view>
</view>
</view>
</view>
<!-- 私教课包 -->
<view class="record-card">
<view class="card-title">私教课包</view>
<view class="table">
<view class="table-header">
<view class="th">数量</view>
<view class="th">提成</view>
</view>
<view class="table-body">
<view class="tr">
<view class="td">{{privatePackageCount}}</view>
<view class="td">¥{{privatePackageCommission}}</view>
</view>
</view>
</view>
</view>
<!-- 时间卡 -->
<view class="record-card">
<view class="card-title">时间卡</view>
<view class="table">
<view class="table-header">
<view class="th">时间卡</view>
<view class="th">数量</view>
<view class="th">提成</view>
</view>
<view class="table-body">
<view class="tr">
<view class="td">{{timeCardCount}}</view>
<view class="td">{{timeCardAmount}}</view>
<view class="td">¥{{timeCardCommission}}</view>
</view>
</view>
</view>
</view>
<!-- 其他提成 -->
<view class="record-card">
<view class="card-title">其他提成</view>
<view class="table">
<view class="table-header">
<view class="th">项目</view>
<view class="th">金额</view>
</view>
<view class="table-body">
<view class="tr" v-for="(item, index) in otherCommissions" :key="index">
<view class="td">{{item.project}}</view>
<view class="td">¥{{item.amount}}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
currentDate: this.formatDate(new Date()),
totalCommission: 0,
renewalRecords: [],
newPackageCount: 0,
newPackageCommission: 0,
newPackageTotal: 0,
privatePackageCount: 0,
privatePackageCommission: 0,
timeCardCount: 0,
timeCardAmount: 0,
timeCardCommission: 0,
otherCommissions: []
}
},
onShow() {
this.getStatisticsData()
},
methods: {
formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
return `${year}-${month}`
},
onDateChange(e) {
this.currentDate = e.detail.value
this.getStatisticsData()
},
async getStatisticsData() {
try {
const res = await apiRoute.xs_statisticsMarketData({
date: this.currentDate
})
if (res && res.code === 1) {
const data = res.data
this.totalCommission = data.totalCommission || 0
this.renewalRecords = data.renewalRecords || []
this.newPackageCount = data.newPackageCount || 0
this.newPackageCommission = data.newPackageCommission || 0
this.newPackageTotal = data.newPackageTotal || 0
this.privatePackageCount = data.privatePackageCount || 0
this.privatePackageCommission = data.privatePackageCommission || 0
this.timeCardCount = data.timeCardCount || 0
this.timeCardAmount = data.timeCardAmount || 0
this.timeCardCommission = data.timeCardCommission || 0
this.otherCommissions = data.otherCommissions || []
}
} catch (error) {
console.error('获取统计数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none'
})
}
}
}
}
</script>
<style lang="less" scoped>
//自定义导航栏
.navbar_section {
border: 1px solid #23262F;
display: flex;
justify-content: center;
align-items: center;
background: #23262F;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.18);
.title {
padding: 40rpx 0rpx;
// #ifdef MP-WEIXIN
padding: 80rpx 0rpx;
// #endif
font-size: 30rpx;
color: #F5F6FA;
font-weight: bold;
letter-spacing: 2rpx;
}
}
.assemble {
width: 100%;
min-height: 100vh;
background: #181A20;
padding-bottom: 100rpx;
}
.div-style {
width: 92%;
height: 85vh;
background: #fff;
border-radius: 16rpx;
margin: auto;
}
.coach-message {
width: 92%;
margin: 10rpx auto;
display: flex;
align-items: center;
padding-top: 20rpx;
}
.drop-image {
width: 50rpx;
height: 50rpx;
filter: brightness(0.8);
}
.title {
font-size: 30rpx;
color: #F5F6FA;
padding-left: 20rpx;
}
.left1 {
width: 48%;
height: 95%;
margin: auto;
}
.right1 {
width: 48%;
height: 95%;
margin: auto;
.statistics_box {
margin: auto;
margin-top: 10rpx;
display: flex;
justify-content: space-between;
.item {
width: 90rpx;
display: flex;
flex-direction: column;
align-items: center;
.box {
width: 100%;
height: 328rpx;
border: 1px solid #ddd;
border-radius: 6rpx;
background: #f5f5f5;
position: relative;
.progress-bar {
width: 100%;
height: 0;
transition: height 0.3s ease;
position: absolute;
bottom: 0;
}
.ratio {
width: 100%;
position: absolute;
bottom: -0rpx;
font-size: 26rpx;
text-align: center;
}
}
.title {
margin-top: 5rpx;
padding: 0;
font-size: 26rpx;
color: #999999;
;
text-align: center;
}
}
}
}
.this_month {
width: 100%;
height: 95%;
margin: auto;
}
.drop-image-x {
width: 20rpx;
height: 20rpx;
}
.title-x {
font-size: 28rpx;
color: #7F7F7F;
padding-left: 20rpx;
}
.title-x1 {
font-size: 28rpx;
color: #333333;
padding-left: 60rpx;
}
.filter-section {
padding: 20rpx 30rpx;
background: #23262F;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.2);
.date-picker {
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #F5F6FA;
.drop-image {
width: 30rpx;
height: 30rpx;
margin-left: 10rpx;
filter: brightness(0.8);
}
}
}
.commission-card {
background: linear-gradient(135deg, #FF6B6B, #FF8E8E);
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
color: #fff;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.25);
.card-title {
font-size: 28rpx;
margin-bottom: 20rpx;
color: #fff;
}
.commission-amount {
font-size: 48rpx;
font-weight: bold;
color: #fff;
}
}
.record-card {
background: #23262F;
margin: 20rpx;
padding: 20rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.18);
.card-title {
font-size: 30rpx;
color: #F5F6FA;
margin-bottom: 20rpx;
font-weight: bold;
}
.table {
width: 100%;
.table-header {
display: flex;
background: #181A20;
padding: 20rpx 0;
border-radius: 8rpx 8rpx 0 0;
.th {
flex: 1;
text-align: center;
font-size: 26rpx;
color: #A0A3B1;
font-weight: 500;
}
}
.table-body {
.tr {
display: flex;
padding: 20rpx 0;
border-bottom: 1px solid #33343A;
transition: background 0.2s;
&:last-child {
border-bottom: none;
}
.td {
flex: 1;
text-align: center;
font-size: 26rpx;
color: #F5F6FA;
font-weight: 400;
}
&:hover {
background: #23262F;
}
}
}
}
}
// 其他细节样式
::-webkit-scrollbar {
display: none;
}
</style>

495
uniapp/pages/market/my/campus_data.vue

@ -1,495 +0,0 @@
<!--校区数据-->
<template>
<view class="main_box">
<!--自定义导航栏-->
<view class="navbar_section">
<view class="navbar_content">
<view class="back_btn" @click="goBack">
<fui-icon name="arrowleft" size="32" color="#fff"></fui-icon>
</view>
<view class="title">校区数据</view>
<view class="placeholder"></view>
</view>
</view>
<view class="content_section">
<!-- 校区选择器 -->
<view class="campus_selector">
<view class="selector_label">选择校区:</view>
<view class="selector_box">
<view class="selected_campus">请选择校区</view>
<fui-icon name="dropdown" size="24" color="#999"></fui-icon>
</view>
</view>
<!-- 数据概览卡片 -->
<view class="overview_cards">
<view class="card_item">
<view class="card_icon">
<fui-icon name="home" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">部门数量</view>
<view class="card_value">--</view>
</view>
</view>
<view class="card_item">
<view class="card_icon">
<fui-icon name="addressbook" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">员工总数</view>
<view class="card_value">--</view>
</view>
</view>
<view class="card_item">
<view class="card_icon">
<fui-icon name="star" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">客户总数</view>
<view class="card_value">--</view>
</view>
</view>
<view class="card_item">
<view class="card_icon">
<fui-icon name="wallet" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">校区业绩</view>
<view class="card_value">--</view>
</view>
</view>
</view>
<!-- 部门对比 -->
<view class="dept_comparison">
<view class="section_title">部门业绩对比</view>
<view class="comparison_list">
<view class="comparison_item">
<view class="dept_info">
<view class="dept_name">销售部</view>
<view class="dept_score">¥120000</view>
</view>
<view class="progress_bar">
<view class="progress_fill" style="width: 80%"></view>
</view>
<view class="dept_percent">80%</view>
</view>
<view class="comparison_item">
<view class="dept_info">
<view class="dept_name">市场部</view>
<view class="dept_score">¥100000</view>
</view>
<view class="progress_bar">
<view class="progress_fill" style="width: 67%"></view>
</view>
<view class="dept_percent">67%</view>
</view>
<view class="comparison_item">
<view class="dept_info">
<view class="dept_name">运营部</view>
<view class="dept_score">¥80000</view>
</view>
<view class="progress_bar">
<view class="progress_fill" style="width: 53%"></view>
</view>
<view class="dept_percent">53%</view>
</view>
<view class="comparison_item">
<view class="dept_info">
<view class="dept_name">客服部</view>
<view class="dept_score">¥60000</view>
</view>
<view class="progress_bar">
<view class="progress_fill" style="width: 40%"></view>
</view>
<view class="dept_percent">40%</view>
</view>
</view>
</view>
<!-- 月度趋势 -->
<view class="trend_section">
<view class="section_title">月度趋势</view>
<view class="trend_chart">
<view class="chart_placeholder">
<fui-icon name="linechart" size="80" color="#ddd"></fui-icon>
<view class="placeholder_text">图表功能待开发</view>
</view>
</view>
</view>
<!-- 功能按钮区域 -->
<view class="function_section">
<view class="section_title">数据分析</view>
<view class="function_grid">
<view class="function_item">
<view class="function_icon">
<fui-icon name="barchart" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">校区对比</view>
</view>
<view class="function_item">
<view class="function_icon">
<fui-icon name="piechart" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">部门分析</view>
</view>
<view class="function_item">
<view class="function_icon">
<fui-icon name="linechart" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">趋势分析</view>
</view>
<view class="function_item">
<view class="function_icon">
<fui-icon name="list" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">详细报表</view>
</view>
</view>
</view>
<!-- 提示信息 -->
<view class="tips_section">
<view class="tips_title">功能说明</view>
<view class="tips_content">
这里将显示校区的各项数据统计,包括部门数量、员工总数、客户数量、校区业绩等。
具体功能待后续开发实现。
</view>
</view>
</view>
</view>
</template>
<script>
import fuiIcon from "@/components/firstui/fui-icon/fui-icon.vue"
export default {
components: {
fuiIcon,
},
data() {
return {
}
},
onLoad() {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
},
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f5f5f5;
min-height: 100vh;
}
// 自定义导航栏
.navbar_section {
background: #29D3B4;
padding-top: 20rpx;
// 小程序端样式
// #ifdef MP-WEIXIN
padding-top: 90rpx;
// #endif
.navbar_content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx 40rpx 24rpx;
.back_btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.title {
font-size: 32rpx;
color: #fff;
font-weight: 500;
}
.placeholder {
width: 60rpx;
}
}
}
// 内容区域
.content_section {
padding: 40rpx 24rpx;
// 校区选择器
.campus_selector {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
margin-bottom: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.selector_label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
}
.selector_box {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background: #f8f8f8;
border-radius: 12rpx;
.selected_campus {
font-size: 26rpx;
color: #666;
}
}
}
// 概览卡片
.overview_cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
margin-bottom: 40rpx;
.card_item {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.card_icon {
width: 64rpx;
height: 64rpx;
background: rgba(41, 211, 180, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.card_content {
flex: 1;
.card_title {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.card_value {
font-size: 32rpx;
color: #333;
font-weight: 600;
}
}
}
}
// 部门对比
.dept_comparison {
margin-bottom: 40rpx;
.section_title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 24rpx;
}
.comparison_list {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.comparison_item {
display: flex;
align-items: center;
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.dept_info {
width: 150rpx;
.dept_name {
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
font-weight: 500;
}
.dept_score {
font-size: 22rpx;
color: #29D3B4;
font-weight: 600;
}
}
.progress_bar {
flex: 1;
height: 16rpx;
background: #f0f0f0;
border-radius: 8rpx;
margin: 0 20rpx;
overflow: hidden;
.progress_fill {
height: 100%;
background: linear-gradient(90deg, #29D3B4, #5CE1E6);
border-radius: 8rpx;
transition: width 0.3s ease;
}
}
.dept_percent {
width: 60rpx;
text-align: right;
font-size: 24rpx;
color: #29D3B4;
font-weight: 600;
}
}
}
}
// 月度趋势
.trend_section {
margin-bottom: 40rpx;
.section_title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 24rpx;
}
.trend_chart {
background: #fff;
border-radius: 16rpx;
padding: 40rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.chart_placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200rpx;
.placeholder_text {
font-size: 24rpx;
color: #999;
margin-top: 16rpx;
}
}
}
}
// 功能区域
.function_section {
margin-bottom: 40rpx;
.section_title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 24rpx;
}
.function_grid {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.function_item {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 16rpx;
border-radius: 12rpx;
transition: all 0.3s ease;
&:active {
background-color: #f5f5f5;
transform: scale(0.95);
}
.function_icon {
width: 64rpx;
height: 64rpx;
background: rgba(41, 211, 180, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.function_text {
font-size: 24rpx;
color: #333;
}
}
}
}
// 提示信息
.tips_section {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.tips_title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 16rpx;
}
.tips_content {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
}
}
</style>

445
uniapp/pages/market/my/dept_data.vue

@ -1,445 +0,0 @@
<!--部门数据-->
<template>
<view class="main_box">
<!--自定义导航栏-->
<view class="navbar_section">
<view class="navbar_content">
<view class="back_btn" @click="goBack">
<fui-icon name="arrowleft" size="32" color="#fff"></fui-icon>
</view>
<view class="title">部门数据</view>
<view class="placeholder"></view>
</view>
</view>
<view class="content_section">
<!-- 部门选择器 -->
<view class="dept_selector">
<view class="selector_label">选择部门:</view>
<view class="selector_box">
<view class="selected_dept">请选择部门</view>
<fui-icon name="dropdown" size="24" color="#999"></fui-icon>
</view>
</view>
<!-- 数据概览卡片 -->
<view class="overview_cards">
<view class="card_item">
<view class="card_icon">
<fui-icon name="addressbook" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">部门人数</view>
<view class="card_value">--</view>
</view>
</view>
<view class="card_item">
<view class="card_icon">
<fui-icon name="star" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">总客户数</view>
<view class="card_value">--</view>
</view>
</view>
<view class="card_item">
<view class="card_icon">
<fui-icon name="wallet" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">部门业绩</view>
<view class="card_value">--</view>
</view>
</view>
<view class="card_item">
<view class="card_icon">
<fui-icon name="check" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">完成率</view>
<view class="card_value">--</view>
</view>
</view>
</view>
<!-- 排行榜 -->
<view class="ranking_section">
<view class="section_title">部门排行榜</view>
<view class="ranking_list">
<view class="ranking_item">
<view class="rank_number first">1</view>
<view class="member_info">
<view class="member_name">张三</view>
<view class="member_score">业绩:¥50000</view>
</view>
<view class="medal">
<fui-icon name="star-fill" size="24" color="#FFD700"></fui-icon>
</view>
</view>
<view class="ranking_item">
<view class="rank_number second">2</view>
<view class="member_info">
<view class="member_name">李四</view>
<view class="member_score">业绩:¥45000</view>
</view>
<view class="medal">
<fui-icon name="star-fill" size="24" color="#C0C0C0"></fui-icon>
</view>
</view>
<view class="ranking_item">
<view class="rank_number third">3</view>
<view class="member_info">
<view class="member_name">王五</view>
<view class="member_score">业绩:¥40000</view>
</view>
<view class="medal">
<fui-icon name="star-fill" size="24" color="#CD7F32"></fui-icon>
</view>
</view>
</view>
</view>
<!-- 功能按钮区域 -->
<view class="function_section">
<view class="section_title">数据分析</view>
<view class="function_grid">
<view class="function_item">
<view class="function_icon">
<fui-icon name="barchart" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">业绩对比</view>
</view>
<view class="function_item">
<view class="function_icon">
<fui-icon name="piechart" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">人员分布</view>
</view>
<view class="function_item">
<view class="function_icon">
<fui-icon name="linechart" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">趋势分析</view>
</view>
<view class="function_item">
<view class="function_icon">
<fui-icon name="list" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">详细报表</view>
</view>
</view>
</view>
<!-- 提示信息 -->
<view class="tips_section">
<view class="tips_title">功能说明</view>
<view class="tips_content">
这里将显示部门的各项数据统计,包括部门人员、客户数量、销售业绩、排行榜等。
具体功能待后续开发实现。
</view>
</view>
</view>
</view>
</template>
<script>
import fuiIcon from "@/components/firstui/fui-icon/fui-icon.vue"
export default {
components: {
fuiIcon,
},
data() {
return {
}
},
onLoad() {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
},
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f5f5f5;
min-height: 100vh;
}
// 自定义导航栏
.navbar_section {
background: #29D3B4;
padding-top: 20rpx;
// 小程序端样式
// #ifdef MP-WEIXIN
padding-top: 90rpx;
// #endif
.navbar_content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx 40rpx 24rpx;
.back_btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.title {
font-size: 32rpx;
color: #fff;
font-weight: 500;
}
.placeholder {
width: 60rpx;
}
}
}
// 内容区域
.content_section {
padding: 40rpx 24rpx;
// 部门选择器
.dept_selector {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
margin-bottom: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.selector_label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
}
.selector_box {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background: #f8f8f8;
border-radius: 12rpx;
.selected_dept {
font-size: 26rpx;
color: #666;
}
}
}
// 概览卡片
.overview_cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
margin-bottom: 40rpx;
.card_item {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.card_icon {
width: 64rpx;
height: 64rpx;
background: rgba(41, 211, 180, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.card_content {
flex: 1;
.card_title {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.card_value {
font-size: 32rpx;
color: #333;
font-weight: 600;
}
}
}
}
// 排行榜
.ranking_section {
margin-bottom: 40rpx;
.section_title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 24rpx;
}
.ranking_list {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.ranking_item {
display: flex;
align-items: center;
padding: 20rpx 16rpx;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.rank_number {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: 600;
color: #fff;
margin-right: 24rpx;
&.first {
background: #FFD700;
}
&.second {
background: #C0C0C0;
}
&.third {
background: #CD7F32;
}
}
.member_info {
flex: 1;
.member_name {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
font-weight: 500;
}
.member_score {
font-size: 24rpx;
color: #666;
}
}
.medal {
margin-left: 16rpx;
}
}
}
}
// 功能区域
.function_section {
margin-bottom: 40rpx;
.section_title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 24rpx;
}
.function_grid {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.function_item {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 16rpx;
border-radius: 12rpx;
transition: all 0.3s ease;
&:active {
background-color: #f5f5f5;
transform: scale(0.95);
}
.function_icon {
width: 64rpx;
height: 64rpx;
background: rgba(41, 211, 180, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.function_text {
font-size: 24rpx;
color: #333;
}
}
}
}
// 提示信息
.tips_section {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.tips_title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 16rpx;
}
.tips_content {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
}
}
</style>

523
uniapp/pages/market/my/index.vue

@ -1,523 +0,0 @@
<!--销售我的-首页-->
<template>
<view class="main_box">
<!--自定义导航栏-->
<view class="navbar_section">
<view class="title">我的</view>
</view>
<view style="background:#29D3B4;">
<!--用户信息-->
<view class="user_section">
<view class="box">
<view class="left" @click="openViewMyInfo()">
<image class="pic" :src="userInfo.head_img"></image>
<view class="name">{{userInfo.name}}</view>
</view>
<view class="right">
<view class="btn"></view>
<!-- <view class="btn">切换身份</view>-->
<view class="btn"></view>
</view>
</view>
<view class="bottom" v-for="(v,k) in userInfo.cameus_dept_arr">
<view class="left">
<view class="title">校区:</view>
<view class="title">{{v.campus_id_name || ''}}</view>
</view>
<view class="division"></view>
<view class="right">
<view class="title">部门:</view>
<view class="title dept">{{v.dept_name_str || ''}}</view>
</view>
</view>
</view>
</view>
<view class="main_section">
<view class="grid_container">
<view class="grid_item" @click="openViewReimbursementList()">
<view class="icon_wrapper">
<fui-icon name="bankcard" size="48" color="#29D3B4"></fui-icon>
</view>
<view class="item_text">报销记录</view>
</view>
<view class="grid_item" @click="openViewMyAttendance()">
<view class="icon_wrapper">
<fui-icon name="calendar" size="48" color="#29D3B4"></fui-icon>
</view>
<view class="item_text">我的考勤</view>
</view>
<view class="grid_item" @click="goCourseSchedule()">
<view class="icon_wrapper">
<fui-icon name="list" size="48" color="#29D3B4"></fui-icon>
</view>
<view class="item_text">课程安排</view>
</view>
<view class="grid_item" @click="openViewMyMessage()">
<view class="icon_wrapper">
<fui-icon name="message" size="48" color="#29D3B4"></fui-icon>
</view>
<view class="item_text">我的消息</view>
</view>
<view class="grid_item" @click="my_contract()">
<view class="icon_wrapper">
<fui-icon name="order" size="48" color="#29D3B4"></fui-icon>
</view>
<view class="item_text">我的合同</view>
</view>
<view class="grid_item" @click="openMyData()">
<view class="icon_wrapper">
<fui-icon name="barchart" size="48" color="#29D3B4"></fui-icon>
</view>
<view class="item_text">我的数据</view>
</view>
<view class="grid_item" @click="openDeptData()">
<view class="icon_wrapper">
<fui-icon name="piechart" size="48" color="#29D3B4"></fui-icon>
</view>
<view class="item_text">部门数据</view>
</view>
<view class="grid_item" @click="openCampusData()">
<view class="icon_wrapper">
<fui-icon name="linechart" size="48" color="#29D3B4"></fui-icon>
</view>
<view class="item_text">校区数据</view>
</view>
<view class="grid_item" @click="openViewSetUp()">
<view class="icon_wrapper">
<fui-icon name="setup" size="48" color="#29D3B4"></fui-icon>
</view>
<view class="item_text">设置</view>
</view>
</view>
</view>
<!-- 底部导航-->
<AQTabber/>
</view>
</template>
<script>
import marketApi from '@/api/market.js';
import apiRoute from '@/api/apiRoute.js';
import {
Api_url
} from "@/common/config.js";
import AQTabber from "@/components/AQ/AQTabber.vue"
import fuiIcon from "@/components/firstui/fui-icon/fui-icon.vue"
export default {
components: {
AQTabber,
fuiIcon,
},
data() {
return {
formData:{},
userInfo:{},//用户信息
//上传图片APi路径
uploadUrl: `${Api_url}/file/image`,
signedClientListCount:0,//已签客户数量
}
},
onLoad() {
},
onShow() {
this.init();
},
methods: {
//初始化
async init(){
await this.getUserInfo()//获取用户详情
// await this.getSignedClientListCount()//获取已签客户数量
},
//获取已签客户数量
async getSignedClientListCount(){
let data = {
page:1,
limit:1,
}
let res = await marketApi.signClient(data);
if (res.code != 1){
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
this.signedClientListCount = res.data.total
},
//获取用户详情
async getUserInfo(){
let data = {}
let res = await apiRoute.getPersonnelInfo(data);
if (res.code != 1){
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
res.data.cameus_dept_arr.forEach((v,k)=>{
let d_arr = []
v.dept_arr.forEach((dv,dk)=>{
d_arr.push(dv.dept_name)
})
//数组转字符串
v.dept_name_str = d_arr.join(',')
})
this.userInfo = res.data
},
//打开到课率统计
openViewArrivalStatistics(){
uni.navigateTo({
url: '/pages-market/my/arrival_statistics'
})
},
//打开即将到期
openViewDueSoon(){
uni.navigateTo({
url: '/pages-market/my/due_soon'
})
},
//打开授课统计
openViewSchoolingStatistics(){
uni.navigateTo({
url: '/pages-market/my/schooling_statistics'
})
},
//打开意见反馈
openViewFeedback(){
uni.navigateTo({
url: '/pages-common/feedback'
})
},
//打开个人资料
openViewMyInfo(){
uni.navigateTo({
url: '/pages-market/my/info'
})
},
//跳转页面-已签客户
openViewSignedClientList(){
uni.navigateTo({
url: '/pages-market/my/signed_client_list'
})
},
//跳转页面-我的考勤
openViewMyAttendance(){
uni.navigateTo({
url: '/pages-common/my_attendance'
})
},
//打开企业信息
openViewFirmInfo(){
uni.navigateTo({
url: '/pages-market/my/firm_info'
})
},
//打开设置
openViewSetUp(){
uni.navigateTo({
url: '/pages-market/my/set_up'
})
},
//跳转页面-我的消息
openViewMyMessage(){
uni.navigateTo({
url: '/pages-common/my_message'
})
},
//跳转页面-报销记录
openViewReimbursementList(){
uni.navigateTo({
url: '/pages-market/reimbursement/list'
})
},
goCourseSchedule(){
uni.navigateTo({
url: '/pages-coach/coach/schedule/schedule_table'
})
},
my_contract(){
uni.navigateTo({
url: '/pages-common/contract/my_contract'
})
},
//打开我的数据
openMyData(){
uni.navigateTo({
url: '/pages-market/my/my_data'
})
},
//打开部门数据
openDeptData(){
uni.navigateTo({
url: '/pages-market/my/dept_data'
})
},
//打开校区数据
openCampusData(){
uni.navigateTo({
url: '/pages-market/my/campus_data'
})
},
}
}
</script>
<style lang="less" scoped>
.main_box{
background: #292929;
//min-height: 28vh;
min-height: 100%;
}
//自定义导航栏
.navbar_section{
border: 1px solid #29D3B4;
display: flex;
justify-content: center;
align-items: center;
background: #29D3B4;
.title{
padding: 40rpx 0rpx;
/* 小程序端样式 */
// #ifdef MP-WEIXIN
padding-top: 110rpx;
padding-bottom: 40rpx;
// #endif
font-size: 30rpx;
color: #fff;
}
}
//用户信息
.user_section {
background-color: #29D3B4;
padding-top: 10rpx;
padding-bottom: 42rpx;
color: #fff;
font-size: 28rpx;
.box{
padding-left: 19rpx;
padding-right: 29rpx;
display: flex;
justify-content: space-between;
align-items: center;
gap: 15rpx;
.left{
display: flex;
align-items: center;
gap: 20rpx;
.pic{
width: 144rpx;
height: 144rpx;
border-radius: 50%;
}
.name{
font-size: 28rpx;
}
}
.right{
display: flex;
flex-direction: column;
gap: 20rpx;
.btn{
min-height: 28rpx;
font-size: 28rpx;
}
}
}
.bottom{
margin-top: 30rpx;
padding: 0rpx 40rpx;
display: flex;
justify-content: space-between;
align-items: flex-start;
.title{
font-size: 28rpx;
}
//分割线段
.division{
width: 2px;
height: 35rpx;
background-color: #fff;
}
.left{
width: 48%;
display: flex;
}
.right{
width: 48%;
display: flex;
.dept{
width: 70%;
}
}
}
}
//统计信息
.count_section{
position: relative;
.main{
position: relative;
z-index: 2;
padding: 0rpx 24rpx;
display: flex;
justify-content: center;
.course_box{
padding: 42rpx 28rpx;
width: 692rpx;
border-radius: 20rpx;
background-color: #fff;
display: flex;
flex-direction: column;
gap: 32rpx;
.top{
display: flex;
justify-content: space-between;
align-items: center;
.item{
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
.num{
color: #29D3B4;
font-size: 56rpx;
}
.intro{
color: #AAAAAA;
font-size: 24rpx;
}
}
}
.bottom{
font-size: 24rpx;
color: #333333;
text{
margin-left: 10rpx;
color: #29D3B4;
}
.reduce{
color: #ef95a0;
}
}
}
}
.bg_box{
z-index: 1;
width: 100%;
height: 150rpx;
}
.bg_top{
position: absolute;
top: 0;
background-color: #29D3B4;
}
.bg_bottom{
top: 50%;
position: absolute;
background-color: #292929;
}
}
.main_section{
background: #292929 100%;
padding: 0 24rpx;
padding-top: 40rpx;
padding-bottom: 150rpx;
font-size: 24rpx;
color: #333333;
.grid_container {
background: #fff;
border-radius: 16rpx;
padding: 40rpx 24rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 40rpx 20rpx;
.grid_item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20rpx 10rpx;
border-radius: 12rpx;
transition: all 0.3s ease;
&:active {
background-color: #f5f5f5;
transform: scale(0.95);
}
.icon_wrapper {
width: 80rpx;
height: 80rpx;
background: rgba(41, 211, 180, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.item_text {
font-size: 24rpx;
color: #333333;
text-align: center;
font-weight: 400;
line-height: 1.2;
}
}
}
}
</style>

505
uniapp/pages/market/my/info.vue

@ -1,505 +0,0 @@
<!--销售-个人资料-详情-->
<template>
<view class="main_box">
<view class="main_section">
<view class="section">
<view class="item">
<image
@click="changeAvatar()"
class="pic"
:src="$util.img(formData.head_img)"
></image>
<view class="btn" @click="changeAvatar()">修改头像</view>
</view>
</view>
<view class="section">
<view class="item">
<view class="title">
姓名 <text class="required">*</text>
</view>
<view class="input">
<input v-model="formData.name" placeholder="请输入姓名" />
</view>
</view>
<view class="item">
<view class="title">
账号 <text class="required"></text>
</view>
<view class="input">
<input v-model="formData.username" disabled placeholder="暂无" />
</view>
</view>
<view class="item">
<view class="title">
部门 <text class="required"></text>
</view>
<view class="input">
<!-- <input disabled :placeholder="formData.department_name_str" />-->
<view class="dept disabled">{{formData.department_name_str || '暂无'}}</view>
</view>
</view>
<view class="item">
<view class="title">
等级 <text class="required"></text>
</view>
<view class="input">
<input v-model="formData.member_level_name" disabled placeholder="暂无" />
</view>
</view>
</view>
<view class="section">
<view class="item">
<view class="title">
性别 <text class="required">*</text>
</view>
<view class="input">
<input placeholder="请选择性别" v-model="formData.gender_str" @click="picker_show_sex=true"/>
<fui-picker
layer="1"
:linkage="true"
:options="options_sex_arr"
:show="picker_show_sex"
@change="changePickerSex"
@cancel="picker_show_sex=false"
></fui-picker>
</view>
</view>
<view class="item">
<view class="title">
生日 <text class="required">*</text>
</view>
<view class="input">
<input placeholder="请选择生日" @click="picker_show_birthday=true" v-model="formData.birthday"/>
<fui-date-picker
:minDate="minDate"
:maxDate="maxDate"
:show="picker_show_birthday"
type="3"
@change="changePickerBirthday"
@cancel="picker_show_birthday=false"
></fui-date-picker>
</view>
</view>
<view class="item">
<view class="title">
邮箱 <text class="required">*</text>
</view>
<view class="input">
<input v-model="formData.email" placeholder="请输入邮箱" />
</view>
</view>
<view class="item">
<view class="title">
手机 <text class="required">*</text>
</view>
<view class="input">
<input v-model="formData.phone" placeholder="请输入手机" />
</view>
</view>
<view class="item">
<view class="title">
微信 <text class="required"></text>
</view>
<view class="input">
<input v-model="formData.wx" placeholder="请输入微信" />
</view>
</view>
</view>
<view class="submet_btn" @click="submit">提交</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
import marketApi from '@/api/market.js';
import {
Api_url
} from "@/common/config.js";
import AQTabber from "@/components/AQ/AQTabber"
export default {
components: {
AQTabber,
},
data() {
return {
formData:{
head_img:'',//头像地址
name:'',//姓名
username:'',//账号
address:'',//住址
gender:'',//性别|1男,2女
gender_str:'',
birthday:'',//生日
email:'',//邮箱
phone:'',//手机
wx:'',//微信号
},
userInfo: {},
//上传图片APi路径
uploadUrl: `${Api_url}/uploadImage`,
//性别选择器 相关
picker_show_sex: false,
sex_name:'请选择',
options_sex_arr: [
{
value: 1,
text: '男'
},
{
value: 2,
text: '女'
},
],
//生日选择器相关
minDate: '',
maxDate: '',
picker_show_birthday: false,
upload_type: 1,
uploadHeadimg: '',
editHeadimg: '',
}
},
onLoad() {
},
onShow() {
this.init()
},
methods: {
async init(){
// this.getBirthday()
this.setDateYear()
await this.getUserInfo()
},
//设置年份
setDateYear() {
let currentYear = new Date().getFullYear();
this.minDate = String(currentYear - 100);
this.maxDate = String(currentYear + 1);
},
//获取用户详情
async getUserInfo(){
let res = await apiRoute.getPersonnelInfo({})
if (res.code != 1){
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
let gender_str = ''
if(res.data.gender == 1){
gender_str = '男'
}else if(res.data.gender == 2){
gender_str = '女'
}
//用户表单
this.formData = {
head_img: res.data.head_img,//头像地址
name: res.data.name,//姓名
username: res.data.phone,//账号
address: res.data.address,//住址
gender: res.data.gender,//性别|1男,2女
gender_str:gender_str,
birthday: res.data.birthday,//生日
email: res.data.email || '',//邮箱
phone: res.data.phone,//手机
wx: res.data.wx || '',//微信号
member_level_name: res.data.member_level_name || '',//等级名称
department_name_str:res.data.department_name_str || '暂无',//部门名称
}
},
// 修改头像按钮
changeAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
// 这里可以调用上传接口
this.uploadFilePromise(tempFilePath)
}
})
},
uploadFilePromise(url) {
let token = uni.getStorageSync('token') || ''
let a = uni.uploadFile({
url: this.uploadUrl, //仅为示例,非真实的接口地址
filePath: url,
name: 'file',
header: {
'token': `${token}`, //请求头设置token
},
success: (e) => {
let res = JSON.parse(e.data.replace(/\ufeff/g, "") || "{}")
console.log('上传成功2', res)
if (res.code == 1) {
this.upload_type = 2
this.formData.head_img = res.data.url
// this.editHeadimg = res.data.path
// this.uploadHeadimg = res.data.url
} else {
uni.showToast({
title: res.msg,
icon: 'none'
})
}
},
});
},
//性别选择相关
changePickerSex(e) {
console.log('监听选择', e)
this.formData.gender = e.value
this.formData.gender_str = e.text
this.picker_show_sex = false
},
//生日选择相关
//获取当前年月日+获取30年前的日期
getBirthday() {
let date = new Date();
let year = date.getFullYear();
let month = date.getMonth() + 1;
let day = date.getDate();
let year_30 = year - 30;
let month_30 = month;
let day_30 = day;
if (month_30 == 2 && day_30 > 28) {
month_30 = 3;
day_30 = 1;
}
if (month_30 == 4 && day_30 > 30) {
month_30 = 5;
day_30 = 1;
}
if (month_30 == 6 && day_30 > 30) {
month_30 = 7;
day_30 = 1;
}
if (month_30 == 9 && day_30 > 30) {
month_30 = 10;
day_30 = 1;
}
if (month_30 == 11 && day_30 > 30) {
month_30 = 12;
day_30 = 1;
}
if (month_30 > 12) {
month_30 = month_30 - 12;
year_30 = year_30 + 1;
}
let minDate = year_30 + "-" + month_30 + "-" + day_30
let maxDate = year + "-" + month + "-" + day
this.minDate = minDate
this.maxDate = maxDate
},
//监听生日选择
changePickerBirthday(e) {
console.log('监听生日选择', e)
this.formData.birthday = e.result
this.picker_show_birthday = false
},
//提交信息
async submit() {
let data = {...this.formData}
if(!data.head_img){
uni.showToast({
title: '请上传头像',
icon: 'none'
})
return
}
if(!data.name){
uni.showToast({
title: '请填写',
icon: 'none'
})
return
}
if(!data.gender){
uni.showToast({
title: '请选择性别',
icon: 'none'
})
return
}
if(!data.birthday){
uni.showToast({
title: '请选择生日',
icon: 'none'
})
return
}
if(!data.email){
uni.showToast({
title: '请填写邮箱',
icon: 'none'
})
return
}
if(!data.phone){
uni.showToast({
title: '请填写手机',
icon: 'none'
})
return
}
let res = await apiRoute.editPersonnelInfo(data)
if(res.code != 1){
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
uni.showToast({
title: res.msg,
icon: 'success'
})
//延迟1s执行
setTimeout(() => {
this.getUserInfo()
}, 1000)
},
}
}
</script>
<style lang="less" scoped>
.main_box{
background: #292929 ;
}
//自定义导航栏
.navbar_section{
display: flex;
justify-content: center;
align-items: center;
background: #29d3b4;
.title{
padding: 40rpx 0rpx;
/* 小程序端样式 */
// #ifdef MP-WEIXIN
padding: 80rpx 0rpx;
// #endif
font-size: 30rpx;
color: #315d55;
}
}
.main_section{
min-height: 100vh;
background: #292929 100%;
padding: 0 0rpx;
padding-top: 32rpx;
padding-bottom: 150rpx;
font-size: 28rpx;
color: #fff;
display: flex;
flex-direction: column;
gap: 20rpx;
.section{
background-color: #434544;
.item{
padding: 20rpx 40rpx;
display: flex;
justify-content: space-between;
align-items: center;
.pic{
width: 82rpx;
height: 82rpx;
border-radius: 50%;
}
.btn{}
.title{
min-width: 100rpx;
display: flex;
align-items: center;
font-size: 26rpx;
color: #D7D7D7;
.required{
margin-left: 10rpx;
color: red;
}
}
.input{
display: flex;
justify-content: flex-end;
input{
text-align: right;
}
.dept{
width: 50%;
}
.disabled{
color: #808080;
//禁止图标
cursor: not-allowed;
}
}
}
}
.submet_btn{
margin: 0 auto;
margin-top: 40rpx;
border: 2px solid #25a18b;
color: #25a18b;
width: 80%;
height: 80rpx;
font-size: 30rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
</style>

292
uniapp/pages/market/my/my_data.vue

@ -1,292 +0,0 @@
<!--我的数据-->
<template>
<view class="main_box">
<!--自定义导航栏-->
<view class="navbar_section">
<view class="navbar_content">
<view class="back_btn" @click="goBack">
<fui-icon name="arrowleft" size="32" color="#fff"></fui-icon>
</view>
<view class="title">我的数据</view>
<view class="placeholder"></view>
</view>
</view>
<view class="content_section">
<!-- 数据概览卡片 -->
<view class="overview_cards">
<view class="card_item">
<view class="card_icon">
<fui-icon name="star" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">总客户数</view>
<view class="card_value">--</view>
</view>
</view>
<view class="card_item">
<view class="card_icon">
<fui-icon name="check" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">已签客户</view>
<view class="card_value">--</view>
</view>
</view>
<view class="card_item">
<view class="card_icon">
<fui-icon name="wallet" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">销售业绩</view>
<view class="card_value">--</view>
</view>
</view>
<view class="card_item">
<view class="card_icon">
<fui-icon name="calendar" size="40" color="#29D3B4"></fui-icon>
</view>
<view class="card_content">
<view class="card_title">本月任务</view>
<view class="card_value">--</view>
</view>
</view>
</view>
<!-- 功能按钮区域 -->
<view class="function_section">
<view class="section_title">数据统计</view>
<view class="function_grid">
<view class="function_item">
<view class="function_icon">
<fui-icon name="barchart" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">客户统计</view>
</view>
<view class="function_item">
<view class="function_icon">
<fui-icon name="piechart" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">业绩统计</view>
</view>
<view class="function_item">
<view class="function_icon">
<fui-icon name="linechart" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">趋势分析</view>
</view>
<view class="function_item">
<view class="function_icon">
<fui-icon name="list" size="32" color="#29D3B4"></fui-icon>
</view>
<view class="function_text">详细报表</view>
</view>
</view>
</view>
<!-- 提示信息 -->
<view class="tips_section">
<view class="tips_title">功能说明</view>
<view class="tips_content">
这里将显示您个人的各项数据统计,包括客户数量、销售业绩、任务完成情况等。
具体功能待后续开发实现。
</view>
</view>
</view>
</view>
</template>
<script>
import fuiIcon from "@/components/firstui/fui-icon/fui-icon.vue"
export default {
components: {
fuiIcon,
},
data() {
return {
}
},
onLoad() {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
},
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f5f5f5;
min-height: 100vh;
}
// 自定义导航栏
.navbar_section {
background: #29D3B4;
padding-top: 20rpx;
// 小程序端样式
// #ifdef MP-WEIXIN
padding-top: 90rpx;
// #endif
.navbar_content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx 40rpx 24rpx;
.back_btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.title {
font-size: 32rpx;
color: #fff;
font-weight: 500;
}
.placeholder {
width: 60rpx;
}
}
}
// 内容区域
.content_section {
padding: 40rpx 24rpx;
// 概览卡片
.overview_cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
margin-bottom: 40rpx;
.card_item {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.card_icon {
width: 64rpx;
height: 64rpx;
background: rgba(41, 211, 180, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.card_content {
flex: 1;
.card_title {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.card_value {
font-size: 32rpx;
color: #333;
font-weight: 600;
}
}
}
}
// 功能区域
.function_section {
margin-bottom: 40rpx;
.section_title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 24rpx;
}
.function_grid {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.function_item {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 16rpx;
border-radius: 12rpx;
transition: all 0.3s ease;
&:active {
background-color: #f5f5f5;
transform: scale(0.95);
}
.function_icon {
width: 64rpx;
height: 64rpx;
background: rgba(41, 211, 180, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.function_text {
font-size: 24rpx;
color: #333;
}
}
}
}
// 提示信息
.tips_section {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.tips_title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 16rpx;
}
.tips_content {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
}
}
</style>

105
uniapp/pages/market/my/set_up.vue

@ -1,105 +0,0 @@
<!--设置页-->
<template>
<view class="assemble">
<view style="height: 30rpx;"></view>
<view class="option" @click="update_pass()">修改密码</view>
<view class="option" @click="privacy_agreement(1)">用户协议</view>
<view class="option" @click="privacy_agreement(2)">隐私策略</view>
<view class="option" @click="clearCache()">清空缓存</view>
<view style="width:90%;margin: 60rpx auto;">
<fui-button background="#29d3b4" @click="loginOut()">退出账号</fui-button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
//退出登陆
loginOut(){
this.$util.loginOut()
},
privacy_agreement(type){
uni.navigateTo({
url: '/pages-common/privacy_agreement?type='+type
})
},
update_pass(){
uni.navigateTo({
url: '/pages-market/my/update_pass'
})
},
// 清空缓存
clearCache() {
uni.showModal({
title: '清空缓存',
content: '确定要清空所有字典缓存吗?',
success: (res) => {
if (res.confirm) {
this.performClearCache();
}
}
});
},
// 执行清空缓存操作
performClearCache() {
try {
// 获取本地存储的所有键
const storageInfo = uni.getStorageInfoSync();
const keys = storageInfo.keys;
// 筛选出dict_开头的键并删除
let clearCount = 0;
keys.forEach(key => {
if (key.startsWith('dict_')) {
uni.removeStorageSync(key);
clearCount++;
}
});
// 显示清理结果
uni.showToast({
title: `已清空${clearCount}个字典缓存`,
icon: 'success',
duration: 2000
});
console.log(`清空缓存完成,共清理${clearCount}个dict_开头的缓存项`);
} catch (error) {
console.error('清空缓存失败:', error);
uni.showToast({
title: '清空缓存失败',
icon: 'none',
duration: 2000
});
}
}
}
}
</script>
<style lang="less" scoped>
.assemble{
width: 100%;
height: 100vh;
background: #333333;
}
.option{
margin-bottom: 20rpx;
background: #404045;
width: 100%;
font-size: 28rpx;
color: #fff;
line-height: 56rpx;
padding: 20rpx 0 20rpx 100rpx;
}
</style>

275
uniapp/pages/market/my/signed_client_list.vue

@ -1,275 +0,0 @@
<!--已签客户-列表-->
<template>
<view class="main_box">
<!--自定义导航栏-->
<!-- <view class="navbar_section">-->
<!-- <view class="title">班级详情</view>-->
<!-- </view>-->
<view class="main_section">
<!-- 班级成员列表-->
<scroll-view
class="section_4"
scroll-y="true"
:lower-threshold="lowerThreshold"
@scrolltolower="loadMoreData"
style="height: 83vh;"
>
<view class="ul">
<view class="li"
v-for="(v,k) in tableList"
:key="k"
@click="openViewStudentInfo(v)">
<view class="left">
<view class="box_1">
<image class="pic"
v-if="v.header" :src="$util.img(v.header)"></image>
<image v-else class="pic" :src="$util.img('/uniapp_src/static/images/index/myk.png')"></image>
<!-- <view class="tag_box">-->
<!-- 即将到期-->
<!-- </view>-->
</view>
<view class="box_2">
<view class="name">{{v.name}}</view>
<view class="date">课程截止时间:{{v.end_time}}</view>
</view>
</view>
<view class="right">
<view class="item">
<view>{{v.have_study_time}}</view>
<view>已上课时</view>
</view>
<view class="item">
<view>{{v.end_study_time ? v.end_study_time:0}}</view>
<view>剩余课时</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部导航-->
<!-- <AQTabber/>-->
</view>
</template>
<script>
import marketApi from '@/api/market.js';
import AQTabber from "@/components/AQ/AQTabber.vue"
export default {
components: {
AQTabber,
},
data() {
return {
loading:false,//加载状态
lowerThreshold: 100,//距离底部多远触发
isReachedBottom: false,//防止重复加载|true=不可加载|false=可加载
//筛选条件
filteredData:{
page:1,//当前页码
limit:10,//每页返回数据条数
total:10,//数据总条数
name: '',//班级名称
},
tableList:[],//聊天数据列表
}
},
onLoad() {
},
onShow() {
this.init();
},
methods: {
//初始化
async init(){
await this.getList()//获取已签客户
},
//加载更多(下一页)
loadMoreData() {
//判断是否加载
if (!this.isReachedBottom) {
this.isReachedBottom = true;//设置为不可请求状态
this.getList();
}
},
//重置为第一页
async resetFilteredData() {
this.isReachedBottom = false; // 重置状态,以便下次触发加载更多
this.filteredData.page = 1//当前页码
this.filteredData.limit = 10//每页返回数据条数
this.filteredData.total = 10//数据总条数
},
//获取列表
async getList(){
this.loading = true
let data = {...this.filteredData}
//判断是否还有数据
if ((this.filteredData.page - 1) * this.filteredData.limit >= this.filteredData.total) {
this.loading = false
uni.showToast({
title: '暂无更多',
icon: 'none'
})
return
}
if(data.page == 1){
this.tableList = []
}
let res = await marketApi.signClient(data)
this.loading = false
this.isReachedBottom = false;
if (res.code != 1){
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
this.tableList = this.tableList.concat(res.data.data); // 使用 concat 方法 将新数据追加到数组中
console.log('列表',this.tableList)
this.filteredData.total = res.data.total
this.filteredData.page++
},
//打开学员详情页
openViewStudentInfo(item){
let students_id= item.id
uni.navigateTo({
url: `/pages-coach/coach/student/info?students_id=${students_id}`
})
},
}
}
</script>
<style lang="less" scoped>
.main_box{
background: #292929 ;
}
//自定义导航栏
.navbar_section{
display: flex;
justify-content: center;
align-items: center;
background: #292929;
.title{
padding: 40rpx 0rpx;
/* 小程序端样式 */
// #ifdef MP-WEIXIN
padding: 80rpx 0rpx;
// #endif
font-size: 30rpx;
color: #fff;
}
}
.main_section{
min-height: 95vh;
background: #292929 100%;
padding: 0 24rpx;
padding-top: 40rpx;
padding-bottom: 150rpx;
font-size: 24rpx;
color: #FFFFFF;
//班级成员列表
.section_4{
.ul{
display: flex;
flex-direction: column;
gap: 10rpx;
.li{
padding: 20rpx 0;
padding-bottom: 40rpx;
border-bottom: 2px solid #D7D7D7;
display: flex;
justify-content: space-between;
.left{
display: flex;
align-items: center;
gap: 30rpx;
.box_1{
padding-left: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
.pic{
width: 84rpx;
height: 84rpx;
border-radius: 50%;
}
.tag_box{
position: absolute;
bottom: -30rpx;
width: 120rpx;
height: 38rpx;
background-color: #F59A23;
border-radius: 4rpx;
line-height: 35rpx;
text-align: center;
font-size: 20rpx;
}
}
.box_2{
display: flex;
flex-direction: column;
gap: 20rpx;
.name{
font-size: 28rpx;
}
.date{
font-size: 24rpx;
}
}
}
.right{
display: flex;
align-items: center;
gap: 14rpx;
.item{
border: 1px solid #00E5BB;
border-radius: 10rpx;
width: 102rpx;
display: flex;
flex-direction: column;
view{
text-align: center;
height: 50rpx;
line-height: 50rpx;
}
view:nth-child(1){
font-size: 32rpx;
background-color: #fff;
color: #00e5bb;
}
view:nth-child(2){
font-size: 20rpx;
background-color: #00e5bb;
}
}
}
}
}
}
}
</style>

225
uniapp/pages/market/my/update_pass.vue

@ -1,225 +0,0 @@
<!--修改密码-->
<template>
<view>
<view class="title">
<view :class="{'green-text': tset_style === 1}">1.验证旧密码</view>
<view :class="{'green-text': tset_style === 2}">2.设置新密码</view>
</view>
<view :style="{'background-color':'#fff','width':'100%','height':'100vh' }">
<view style="width: 95%;height: 30rpx;"></view>
<view v-if="tset_style == 1">
<view class="describe">
为保障您的账号安全,修改密码前请填写原密码
</view>
<view style="width: 95%;margin:30rpx auto;">
<fui-input borderTop placeholder="请输入原登录密码" v-model="old_password"
backgroundColor="#f2f2f2"></fui-input>
</view>
</view>
<view v-if="tset_style == 2">
<view style="width: 95%;margin:30rpx auto;">
<fui-input borderTop placeholder="请设置6-20位新的登录密码" v-model="formData.new_password" @input="input"
backgroundColor="#f2f2f2"></fui-input>
</view>
<view style="width: 95%;margin: auto;">
<fui-input borderTop :padding="['20rpx','32rpx']" v-model="formData.new_password_2" placeholder="请再次输入新的登录密码" @input="input"
backgroundColor="#f2f2f2">
</fui-input>
</view>
</view>
<view style="width: 95%;margin:60rpx auto;">
<view class="btn_box">
<fui-button
background="#465cff"
radius="5rpx"
@click="nextStep(1)"
v-if="tset_style != 1">上一步
</fui-button>
<fui-button background="#00be8c" radius="5rpx" @click="nextStep(2)" v-if="tset_style == 1">下一步</fui-button>
<fui-button background="#00be8c" radius="5rpx" @click="submit" v-if="tset_style == 2">提交</fui-button>
</view>
<view style="width: 95%;margin:60rpx auto;">
<fui-button background="#fff" radius="5rpx" @click="forgot" color="#999999" v-if="tset_style == 1">忘记原密码</fui-button>
</view>
</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
code: '',
user: '',
tset_style: 1,//tab栏目|1=验证旧密码页,2=设置新密码页
old_password:'',//旧密码
formData:{
phone:'',//当前登录用户的手机号
new_password:'',//新密码
new_password_2:'',//二次新密码
key_value:'',//修改密码的key_value
},
}
},
onLoad() {},
onShow() {
this.init()//初始化
},
methods: {
//初始化
async init(){
await this.getUserInfo()
},
//获取用户信息
async getUserInfo() {
let res = await apiRoute.getPersonnelInfo({})
if (res.code != 1) {
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
this.formData.phone = res.data.phone//用户手机号
},
//上/下一步切换
async nextStep(tset_style){
//tset_style|1=第一页,2=第二页
if(tset_style == 2){
if(!this.old_password){
uni.showToast({
title: '请输入原登录密码',
icon: 'none'
})
return
}
//验证旧密码
let params = {
old_password: this.old_password
}
//验证旧密码是否正确
let res = await apiRoute.common_personnelCheckOldPwd(params)
if(!res.code){
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
this.formData.key_value = res.data.key_value
}
this.tset_style = Number(tset_style)
},
//忘记密码
forgot() {
uni.navigateTo({
url: '/pages/student/login/forgot'
})
},
//提交
async submit() {
//验证数据
if (!this.formData.new_password) {
uni.showToast({
title: '请输入新密码',
icon: 'none'
})
return
}
if (this.formData.new_password.length < 6 || this.formData.new_password.length > 20) {
uni.showToast({
title: '新密码长度为6-20位',
icon: 'none'
})
return
}
if (!this.formData.new_password) {
uni.showToast({
title: '请输入新密码',
icon: 'none'
})
return
}
//验证两次密码是否一致
if (this.formData.new_password != this.formData.new_password_2) {
uni.showToast({
title: '两次密码不一致',
icon: 'none'
})
return
}
let res = await apiRoute.common_personnelEdidPassword(this.formData)
if(!res.code){
uni.showToast({
title: res.msg,
icon: 'none'
})
return
}
uni.showToast({
title: res.msg,
icon: 'success'
})
//延迟1s执行
setTimeout(() => {
uni.navigateBack()
}, 1000)
},
}
}
</script>
<style lang="less" scoped>
page {
font-weight: normal;
}
.fui-section__title {
margin-left: 32rpx;
}
.fui-left__icon {
padding-right: 24rpx;
}
.title {
display: flex;
justify-content: space-around;
align-items: center;
height: 100rpx;
width: 100%;
background-color: #fff;
font-size: 26rpx;
border: 4rpx #f5f5f5 solid;
}
.green-text{
color: #36d6b9;
}
.describe{
color: #999999;
padding-left: 30rpx;
}
.btn_box{
display: flex;
flex-direction: column;
gap: 40rpx;
}
</style>

136
uniapp/pages/market/reimbursement/detail.vue

@ -1,136 +0,0 @@
<template>
<view class="reim-detail-page">
<view class="header-bar">
<view class="title">报销详情</view>
</view>
<view class="detail-box">
<view class="row">
<view class="label">报销金额</view>
<view class="value">¥{{ detail.amount }}</view>
</view>
<view class="row">
<view class="label">报销描述</view>
<view class="value">{{ detail.description }}</view>
</view>
<view class="row">
<view class="label">发票/收据</view>
<view class="value">
<image v-if="detail.receipt_url && isImage(detail.receipt_url)" :src="detail.receipt_url" class="receipt-img" mode="aspectFit" />
<view v-else-if="detail.receipt_url" class="file-link">{{ detail.receipt_url }}</view>
<text v-else>无附件</text>
</view>
</view>
<view class="row">
<view class="label">状态</view>
<view :class="['value', 'status-' + detail.status]">{{ statusMap[detail.status] || detail.status }}</view>
</view>
<view class="row">
<view class="label">创建时间</view>
<view class="value">{{ detail.created_at }}</view>
</view>
<view class="row">
<view class="label">修改时间</view>
<view class="value">{{ detail.updated_at }}</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
detail: {},
statusMap: {
pending: '待审批',
approved: '已批准',
rejected: '已拒绝',
},
}
},
onLoad(options) {
if (options.id) {
this.fetchDetail(options.id);
} else {
uni.showToast({ title: '缺少报销ID', icon: 'none' });
uni.navigateBack();
}
},
methods: {
fetchDetail(id) {
// 模拟接口获取详情
uni.showLoading({ title: '加载中...' });
setTimeout(() => {
const mockData = {
id: id,
amount: 300.00,
description: '办公用品采购(已批准)',
receipt_url: 'https://cdn.uviewui.com/uview/swiper/1.jpg',
status: 'approved',
created_at: '2024-06-02 09:30',
updated_at: '2024-06-03 12:00',
};
this.detail = mockData;
uni.hideLoading();
}, 500);
},
isImage(url) {
if (!url) return false;
return /\.(jpg|jpeg|png|gif|bmp)$/i.test(url)
}
}
}
</script>
<style lang="less" scoped>
.reim-detail-page {
min-height: 100vh;
background: #292929;
}
.header-bar {
padding: 32rpx 32rpx 0 32rpx;
.title {
font-size: 36rpx;
color: #24BA9F;
font-weight: bold;
}
}
.detail-box {
margin: 32rpx;
background: #434544;
border-radius: 18rpx;
padding: 32rpx 24rpx 24rpx 24rpx;
}
.row {
display: flex;
align-items: flex-start;
margin-bottom: 32rpx;
.label {
color: #aaa;
font-size: 28rpx;
width: 180rpx;
flex-shrink: 0;
margin-top: 10rpx;
}
.value {
color: #fff;
font-size: 28rpx;
word-break: break-all;
}
.status-pending { color: #f0ad4e; }
.status-approved { color: #24BA9F; }
.status-rejected { color: #e74c3c; }
.receipt-img {
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
background: #222;
border: 1rpx solid #333;
}
.file-link {
color: #24BA9F;
font-size: 26rpx;
word-break: break-all;
}
}
</style>

144
uniapp/pages/market/reimbursement/list.vue

@ -1,144 +0,0 @@
<template>
<view class="reim-list-page">
<view class="header-bar">
<view class="title">报销列表</view>
<fui-button
background="transparent"
color="#24BA9F"
borderColor="#24BA9F"
width="180rpx"
height="64rpx"
radius="32rpx"
@click="goAdd">
新增报销
</fui-button>
</view>
<view v-if="list.length === 0" class="empty-tip">暂无报销记录</view>
<view v-for="item in list" :key="item.id" class="reim-card" @click="goDetail(item)">
<view class="row">
<view class="label">金额:</view>
<view class="value">¥{{ item.amount }}</view>
</view>
<view class="row">
<view class="label">描述:</view>
<view class="value">{{ item.description }}</view>
</view>
<view class="row">
<view class="label">发票/收据:</view>
<view class="value">
<image v-if="item.receipt_url" :src="item.receipt_url" class="receipt-img" mode="aspectFit" />
<text v-else>无附件</text>
</view>
</view>
<view class="row">
<view class="label">状态:</view>
<view :class="['value', 'status-' + item.status]">{{ statusMap[item.status] || item.status }}</view>
</view>
<view class="row">
<view class="label">创建时间:</view>
<view class="value">{{ item.created_at }}</view>
</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
list: [],
statusMap: {
pending: '待审批',
approved: '已批准',
rejected: '已拒绝',
},
}
},
async onShow() {
await this.reimbursementList();
},
methods: {
async reimbursementList(){
let res = await apiRoute.reimbursement_list({})
this.list = res.data;
},
goAdd() {
uni.navigateTo({
url: '/pages-market/reimbursement/add'
});
},
goDetail(item) {
// 待审批状态可编辑,否则只能查看
if (item.status === 'pending') {
this.$navigateToPage(`/pages-market/reimbursement/add`, {
id: item.id
});
} else {
this.$navigateToPage(`/pages-market/reimbursement/detail`, {
id: item.id
});
}
}
}
}
</script>
<style lang="less" scoped>
.reim-list-page {
min-height: 100vh;
background: #292929;
padding-bottom: 120rpx;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 32rpx 0 32rpx;
.title {
font-size: 36rpx;
color: #fff;
font-weight: bold;
}
}
.empty-tip {
color: #888;
text-align: center;
margin: 80rpx 0;
}
.reim-card {
background: #434544;
border-radius: 18rpx;
margin: 32rpx;
margin-bottom: 0;
padding: 32rpx 24rpx 24rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
.row {
display: flex;
align-items: center;
margin-bottom: 18rpx;
.label {
color: #aaa;
font-size: 26rpx;
width: 140rpx;
flex-shrink: 0;
}
.value {
color: #fff;
font-size: 28rpx;
word-break: break-all;
}
.status-pending { color: #f0ad4e; }
.status-approved { color: #24BA9F; }
.status-rejected { color: #e74c3c; }
.receipt-img {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background: #222;
border: 1rpx solid #333;
}
}
}
</style>
Loading…
Cancel
Save