Compare commits

...

5 Commits

Author SHA1 Message Date
王泽彦 25e9db780b Merge branch 'develop' of ssh://gitlab.frkj.cc:222/php/zhjwxt into develop 5 months ago
王泽彦 a580c2580a 修改 bug 5 months ago
王泽彦 51987f8b10 修改 bug 5 months ago
王泽彦 2324c32458 修改 bug 5 months ago
王泽彦 cdc449472b 修改 bug 5 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-Typeuni-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)
// formDatapayment_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)
// formDatapayment_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();
}
}
// : scheduleIdwatchvisiblewatch
// scheduleIdvisible=truevisible
},
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 @@
// 使APIadmin
if (data.schedule_info) {
// schedule_infoformal_studentswaiting_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) => {
// ,fileDataurlextnamename
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;
}
//
// propsScheduleDetail
// ScheduleDetailwatchapi.getCourseArrangementDetail()
this.selectedScheduleId = scheduleId;
this.showScheduleDetail = true;
// 使APIadmin
console.log('调用统一API获取课程安排详情:', scheduleId);
// APIScheduleDetailfetchScheduleDetail
// ScheduleDetailapi.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