Browse Source

修改 bug

master
王泽彦 5 months ago
parent
commit
51987f8b10
  1. 9
      admin/src/app/api/order_table.ts
  2. 381
      admin/src/app/views/order_table/components/order-detail-dialog.vue
  3. 36
      admin/src/app/views/order_table/order_table.vue
  4. 9
      niucloud/app/adminapi/controller/order_table/OrderTable.php
  5. 3
      niucloud/app/adminapi/route/order_table.php
  6. 16
      niucloud/app/api/controller/apiController/OrderTable.php
  7. 1
      niucloud/app/api/route/file.php
  8. 3
      niucloud/app/api/route/route.php
  9. 122
      niucloud/app/job/schedule/HandleCourseSchedule.php
  10. 14
      niucloud/app/model/order_table/OrderTable.php
  11. 39
      niucloud/app/service/admin/order_table/OrderTableService.php
  12. 69
      niucloud/app/service/api/apiService/OrderTableService.php
  13. 3
      uniapp/api/apiRoute.js
  14. 225
      uniapp/components/fitness-record-list-popup/index.vue
  15. 27
      uniapp/components/fitness-record-popup/fitness-record-popup.less
  16. 56
      uniapp/components/fitness-record-popup/fitness-record-popup.vue
  17. 299
      uniapp/components/order-form-popup/index.vue
  18. 184
      uniapp/components/order-list-card/index.vue
  19. 505
      uniapp/components/order-list-card/payment-voucher-popup.vue
  20. 87
      uniapp/components/study-plan-popup/study-plan-popup.vue
  21. 76
      uniapp/pages-market/clue/clue_info.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>

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

@ -18,8 +18,8 @@
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="订单类型" prop="order_type">
<el-select class="w-[280px]" v-model="orderTableTable.searchParam.order_type" clearable
placeholder="请选择订单类型">
@ -27,10 +27,10 @@
<el-option label="新订单" :value="1"></el-option>
<el-option label="续费订单" :value="2"></el-option>
<el-option label="内部员工订单" :value="3"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="t('paymentType')" prop="payment_type">
<el-select class="w-[280px]" v-model="orderTableTable.searchParam.payment_type" clearable
@ -59,7 +59,7 @@
</template>
<el-table-column prop="resource_id_name" :label="t('resourceId')" min-width="120"
:show-overflow-tooltip="true" />
<el-table-column prop="type" label="订单类型" min-width="120" />
<el-table-column :label="t('orderStatus')" min-width="180" align="center"
@ -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)
}
/**
* 删除订单
*/

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

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']);
}
}

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)

3
niucloud/app/api/route/route.php

@ -317,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');

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

@ -50,32 +50,16 @@ class HandleCourseSchedule extends BaseJob
{
try {
Db::startTrans();
$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([
@ -83,42 +67,46 @@ class HandleCourseSchedule extends BaseJob
'updated_at' => time()
]);
$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'];
$endTime = $timeData['end_time'];
// 更新数据库中的时间字段
CourseSchedule::where('id', $schedule['id'])->update([
'start_time' => $startTime,
'end_time' => $endTime
]);
} else {
continue; // 无法解析时间,跳过
// 无法解析时间(time_slot 为 null 或空字符串),跳过该记录
Log::write("课程ID {$schedule['id']} 无法解析时间,time_slot: {$schedule['time_slot']}");
continue;
}
}
// 判断课程状态
$newStatus = $this->determineStatus($currentTime, $startTime, $endTime);
if ($newStatus !== $schedule['status']) {
CourseSchedule::where('id', $schedule['id'])->update([
'status' => $newStatus,
'updated_at' => time()
]);
switch ($newStatus) {
case 'upcoming':
$upcomingCount++;
@ -135,48 +123,20 @@ class HandleCourseSchedule extends BaseJob
}
}
}
Log::write("课程状态更新完成 - 已完成: {$completedCount}个, 即将开始: {$upcomingCount}个, 进行中: {$ongoingCount}个, 待开始: {$pendingCount}个");
Log::write("当天课程状态更新完成 - 已完成: {$completedCount}个, 即将开始: {$upcomingCount}个, 进行中: {$ongoingCount}个, 待开始: {$pendingCount}个");
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
Log::write('更新课程状态失败:' . $e->getMessage());
return false;
}
}
/**
* 更新课程安排表的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
@ -211,24 +177,24 @@ class HandleCourseSchedule extends BaseJob
$currentTimestamp = strtotime($currentTime);
$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']);
}
}

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
uniapp/api/apiRoute.js

@ -525,6 +525,9 @@ export default {
return await http.get('/checkOrderPaymentStatus', data)
},
async updateOrderPaymentVoucher(data = {}) {
return await http.post('/updateOrderPaymentVoucher', data)
},
//↓↓↓↓↓↓↓↓↓↓↓↓-----课程预约相关接口-----↓↓↓↓↓↓↓↓↓↓↓↓
// 获取可预约课程列表
async getAvailableCourses(data = {}) {

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 {

56
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,16 +108,19 @@ 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() {
@ -371,27 +374,24 @@ export default {
return false
},
// PDF使
// PDF使PDF
async uploadPdfFile(file) {
const { Api_url } = require('@/common/config.js')
const token = uni.getStorageSync('token') || ''
console.log('开始上传PDF文件:', {
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,27 +415,22 @@ 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) {
console.error('PDF上传认证失败:', response)
uni.showToast({
title: response.msg || '登录已过期',
icon: 'none'
uni.showToast({
title: response.msg || '登录已过期',
icon: 'none'
})
setTimeout(() => {
uni.navigateTo({ url: '/pages/student/login/login' })
@ -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上传失败'
})
}
},

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

@ -14,7 +14,17 @@
<text class="student-name">{{ studentInfo.name || '未选择学生' }}</text>
</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">
@ -47,7 +57,7 @@
<view class="form-item">
<text class="label">订单金额 <text class="required">*</text></text>
<input
<input
class="form-input readonly"
type="digit"
v-model="formData.order_amount"
@ -56,7 +66,35 @@
disabled
/>
</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">
@ -127,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',
@ -148,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: '',
@ -168,14 +209,16 @@ export default {
selectedIndex: 0,
//
campusSelectedIndex: -1,
courseSelectedIndex: -1,
paymentSelectedIndex: -1,
orderTypeSelectedIndex: -1,
giftSelectedIndex: -1,
giftTypeSelectedIndex: -1,
classSelectedIndex: -1,
//
campusList: [], //
courseList: [],
paymentTypes: [], //
orderTypes: [], //
@ -184,10 +227,17 @@ export default {
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)
},
@ -205,6 +255,12 @@ export default {
},
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: {
@ -213,6 +269,7 @@ export default {
if (newVal) {
console.log('开始初始化表单和加载数据')
this.initForm()
this.loadCampusList() //
this.loadCourseList()
this.loadDictionaries() //
this.loadGiftList() //
@ -223,8 +280,10 @@ export default {
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()
}
}
@ -238,6 +297,7 @@ export default {
// visible true
if (this.visible) {
console.log('组件挂载时 visible 为 true,开始加载数据')
this.loadCampusList()
this.loadCourseList()
this.loadDictionaries()
this.loadGiftList()
@ -248,8 +308,10 @@ export default {
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: '',
@ -259,16 +321,49 @@ export default {
remark: '',
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() {
console.log('loadCourseList 方法被调用')
try {
@ -413,18 +508,20 @@ export default {
/**
* 加载班级列表
* @param {Number} campusId - 校区ID如果不传则使用formData.campus_id
*/
async loadClassList() {
console.log('开始加载班级列表,校区ID:', this.studentInfo?.campus_id)
async loadClassList(campusId) {
const targetCampusId = campusId || this.formData.campus_id
console.log('开始加载班级列表,校区ID:', targetCampusId)
try {
if (!this.studentInfo?.campus_id) {
if (!targetCampusId) {
console.warn('缺少校区ID,无法加载班级列表')
this.classList = []
return
}
const res = await apiRoute.getClassListForSchedule({
campus_id: this.studentInfo.campus_id
campus_id: targetCampusId
})
console.log('班级列表API响应:', res)
@ -445,7 +542,25 @@ export default {
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 = '选择课程'
@ -571,8 +686,18 @@ export default {
confirmPicker() {
const selectedOption = this.pickerOptions[this.selectedIndex]
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
@ -591,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
@ -625,12 +754,85 @@ export default {
this.$refs.pickerPopup.close()
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
@ -827,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;
}
}
}
}

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

@ -127,6 +127,16 @@
@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>
@ -140,7 +150,8 @@ export default {
name: 'OrderListCard',
components: {
OrderFormPopup,
QRCodePaymentDialog
QRCodePaymentDialog,
PaymentVoucherPopup: () => import('./payment-voucher-popup.vue')
},
props: {
@ -180,10 +191,14 @@ export default {
//
showOrderForm: false,
showQRCodeModal: false,
showPaymentVoucherPopup: false,
//
currentOrder: null,
//
paymentPollingTimer: null,
//
qrCodePaymentData: null,
@ -422,7 +437,9 @@ export default {
switch(paymentType) {
case 'cash':
this.processCashPayment(order)
case 'client_wxpay':
case 'deposit':
this.processVoucherPayment(order)
break
case 'scan_code':
this.processQRCodePayment(order)
@ -431,7 +448,7 @@ export default {
this.processSubscriptionPayment(order)
break
case 'wxpay_online':
this.processWechatPayment(order)
this.processWechatOnlinePayment(order)
break
default:
uni.showToast({ title: '不支持的支付方式', icon: 'none' })
@ -439,18 +456,12 @@ export default {
},
/**
* 现金支付处理
* 上传支付凭证的支付方式处理(cash, client_wxpay, deposit)
*/
async processCashPayment(order) {
uni.showModal({
title: '现金支付确认',
content: `确认已收到现金支付 ¥${order.total_amount}`,
success: async (res) => {
if (res.confirm) {
await this.updateOrderPaymentStatus(order, 'paid', `CASH${Date.now()}`)
}
}
})
processVoucherPayment(order) {
this.currentOrder = order
this.showPaymentVoucherPopup = true
this.$refs.paymentVoucherPopup.open()
},
/**
@ -474,6 +485,9 @@ export default {
}
this.showQRCodeModal = true
this.$refs.qrCodePopup.open()
//
this.startPaymentPolling(order.order_no)
} else {
uni.showToast({ title: res.msg || '获取支付二维码失败', icon: 'none' })
}
@ -485,55 +499,67 @@ export default {
},
/**
* 订阅支付处理
* 开始轮询支付状态
*/
processSubscriptionPayment(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.updateOrderPaymentStatus(order, 'partial', `SUB${Date.now()}`)
}
}
})
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
}
},
/**
* 微信支付处理
* 订阅支付处理
*/
processWechatPayment(order) {
processSubscriptionPayment(order) {
uni.showModal({
title: '微信支付',
content: `将调用微信支付 ¥${order.total_amount}`,
confirmText: '确认支付',
cancelText: '取消',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '正在调用微信支付...' })
setTimeout(async () => {
uni.hideLoading()
uni.showModal({
title: '支付结果',
content: '微信支付完成,请确认是否已收到款项?',
success: async (confirmRes) => {
if (confirmRes.confirm) {
await this.updateOrderPaymentStatus(order, 'paid', `WX${Date.now()}`)
}
}
})
}, 1500)
}
}
title: '订阅服务说明',
content: '每日支付超过3000笔后解锁订阅服务',
showCancel: false,
confirmText: '知道了'
})
},
/**
* 微信在线代付处理(本阶段只定义函数)
*/
processWechatOnlinePayment(order) {
// TODO: 线
uni.showToast({
title: '微信在线代付功能开发中',
icon: 'none'
})
},
@ -580,6 +606,9 @@ export default {
* 关闭二维码支付弹窗
*/
closeQRCodeModal() {
//
this.stopPaymentPolling()
this.showQRCodeModal = false
this.qrCodePaymentData = null
this.$refs.qrCodePopup.close()
@ -609,6 +638,51 @@ export default {
})
},
/**
* 关闭支付凭证弹窗
*/
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 })
}
},
/**
* 查看订单详情
*/

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>

87
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>
@ -24,7 +25,7 @@
</picker>
</view>
</view>
<view class="form-item">
<view class="form-label">结束日期</view>
<view class="form-input">
@ -35,19 +36,19 @@
</picker>
</view>
</view>
<view class="form-item">
<view class="form-label">计划内容</view>
<view class="form-input">
<textarea
v-model="planData.plan_content"
<textarea
v-model="planData.plan_content"
placeholder="请输入计划详细内容"
maxlength="500"
:show-confirm-bar="false"
></textarea>
</view>
</view>
<!-- <view class="form-item">-->
<!-- <view class="form-label">状态</view>-->
<!-- <view class="form-input">-->
@ -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()
},
//
@ -113,22 +113,25 @@ export default {
plan_content: plan.plan_content || '',
status: plan.status || 'pending'
}
//
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 {
@ -286,7 +335,7 @@ export default {
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: #ffffff;
}
@ -321,7 +370,7 @@ export default {
border-radius: 12rpx;
border: 1px solid #404040;
overflow: hidden;
input, textarea {
width: 100%;
padding: 20rpx;
@ -330,12 +379,12 @@ export default {
font-size: 28rpx;
border: none;
outline: none;
&::placeholder {
color: #999999;
}
}
textarea {
min-height: 120rpx;
resize: none;
@ -376,7 +425,7 @@ export default {
background-color: #333333;
color: #ffffff;
border: 1px solid #404040;
&:active {
background-color: #404040;
}
@ -385,7 +434,7 @@ export default {
.confirm-btn {
background-color: #29D3B4;
color: #ffffff;
&:active {
background-color: #24B89E;
}

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

@ -120,20 +120,6 @@
<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" />
@ -168,6 +154,16 @@
</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"
@ -196,6 +192,8 @@
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: {
@ -212,7 +210,8 @@
ServiceListCard,
StudentEditPopup,
FitnessRecordPopup,
StudyPlanPopup
StudyPlanPopup,
FitnessRecordListPopup
},
data() {
return {
@ -238,6 +237,9 @@
currentPopup: null,
studyPlanList: [],
serviceList: [],
//
showFitnessListPopup: false,
//
remark_content: '',
@ -335,8 +337,8 @@
},
showAddButton() {
//
return ['fitness_record', 'study_plan'].includes(this.currentPopup)
//
return ['study_plan'].includes(this.currentPopup)
},
},
@ -536,9 +538,10 @@
this.currentPopup = 'course_info'
break
case 'fitness_record':
await this.getFitnessRecords(student.id)
this.currentPopup = 'fitness_record'
break
await this.getFitnessRecords(student.id)
// 使
this.showFitnessListPopup = true
break
case 'study_plan':
await this.getStudyPlanList(student.id)
this.currentPopup = 'study_plan'
@ -561,13 +564,15 @@
this.studyPlanList = []
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()
}
},
@ -1198,28 +1203,5 @@
</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>

Loading…
Cancel
Save