Browse Source

修改 bug

develop
王泽彦 5 months ago
parent
commit
1d3fffba6a
  1. 380
      admin/src/app/views/personnel/components/BaseInfoForm.vue
  2. 350
      admin/src/app/views/personnel/components/DetailInfoForm.vue
  3. 794
      admin/src/app/views/personnel/components/personnel-edit.vue
  4. 20
      niucloud/app/api/controller/apiController/PersonCourseSchedule.php
  5. 1
      niucloud/app/api/controller/apiController/StudentCourse.php
  6. 2
      niucloud/app/api/route/route.php
  7. 40
      niucloud/app/common.php
  8. 1
      niucloud/app/service/api/apiService/CoachStudentService.php
  9. 235
      niucloud/app/service/api/apiService/CourseScheduleService.php
  10. 5
      niucloud/app/service/api/apiService/CourseService.php
  11. 237
      niucloud/app/service/api/apiService/PersonCourseScheduleService.php
  12. 3
      niucloud/app/service/api/student/ContractService.php
  13. 5
      uniapp/api/apiRoute.js
  14. 52
      uniapp/components/course-info-card/index.vue
  15. 18
      uniapp/pages-common/contract/staff-contract-sign.vue
  16. 1614
      uniapp/pages-market/course/course_detail.vue
  17. 10
      uniapp/pages.json

380
admin/src/app/views/personnel/components/BaseInfoForm.vue

@ -0,0 +1,380 @@
<template>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="base-info-form"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="t('name')" prop="name">
<el-input
v-model="formData.name"
clearable
:placeholder="t('namePlaceholder')"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="t('gender')">
<el-radio-group
v-model="formData.gender"
:placeholder="t('genderPlaceholder')"
>
<el-radio
v-for="(item, index) in genderList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="t('phone')" prop="phone">
<el-input
v-model="formData.phone"
clearable
:placeholder="t('phonePlaceholder')"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="t('address')">
<el-input
v-model="formData.address"
clearable
:placeholder="t('addressPlaceholder')"
class="input-width"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="t('nativePlace')">
<el-input
v-model="formData.native_place"
clearable
:placeholder="t('nativePlacePlaceholder')"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="头像">
<upload-image v-model="formData.head_img" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="t('education')">
<el-select
class="input-width"
v-model="formData.education"
clearable
:placeholder="t('educationPlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in educationList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="t('status')" prop="status">
<el-select
class="input-width"
v-model="formData.status"
clearable
:placeholder="t('statusPlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in statusList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item :label="t('profile')">
<el-input
v-model="formData.profile"
type="textarea"
rows="3"
clearable
:placeholder="t('profilePlaceholder')"
class="input-width"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="t('emergencyContactPhone')">
<el-input
v-model="formData.emergency_contact_phone"
clearable
:placeholder="t('emergencyContactPhonePlaceholder')"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="t('isSysUser')" prop="is_sys_user">
<el-radio-group
v-model="formData.is_sys_user"
:placeholder="t('isSysUserPlaceholder')"
>
<el-radio
v-for="(item, index) in is_sys_userList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="t('idCardFront')">
<upload-image v-model="formData.id_card_front" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="t('idCardBack')">
<upload-image v-model="formData.id_card_back" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="所属校区" prop="campus_id">
<el-select
class="input-width"
v-model="formData.campus_id"
clearable
placeholder="请选择所属校区"
style="width: 300px;"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="item in campusList"
:key="item.id"
:label="item.campus_name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getWithCampusList } from '@/app/api/campus_person_role'
// Props
interface Props {
id?: string | number
}
const props = withDefaults(defineProps<Props>(), {
id: ''
})
//
const initialFormData = {
name: '',
gender: '',
head_img: '',
phone: '',
address: '',
native_place: '',
education: '',
profile: '',
emergency_contact_phone: '',
id_card_front: '',
id_card_back: '',
status: '',
is_sys_user: '',
campus_id: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
name: [{ required: true, message: t('namePlaceholder'), trigger: 'blur' }],
gender: [
{ required: true, message: t('genderPlaceholder'), trigger: 'blur' },
],
phone: [
{ required: true, message: t('phonePlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
is_sys_user: [
{ required: true, message: t('isSysUserPlaceholder'), trigger: 'blur' },
],
}
})
//
let genderList = ref([])
let educationList = ref([])
let statusList = ref([])
let is_sys_userList = ref([])
let campusList = ref([])
//
const genderDictList = async () => {
genderList.value = await (await useDictionary('gender')).data.dictionary
}
const educationDictList = async () => {
educationList.value = await (await useDictionary('education')).data.dictionary
}
const statusDictList = async () => {
statusList.value = await (await useDictionary('personnel_status')).data.dictionary
}
const is_sys_userDictList = async () => {
is_sys_userList.value = await (await useDictionary('global_true_or_false')).data.dictionary
}
const getCampusList = async () => {
const res = await getWithCampusList({})
if (res.data) {
campusList.value = res.data
}
}
//
watch(
() => genderList.value,
() => {
if (genderList.value.length > 0 && !formData.gender) {
formData.gender = genderList.value[0].value
}
}
)
watch(
() => educationList.value,
() => {
if (educationList.value.length > 0 && !formData.education) {
formData.education = educationList.value[0].value
}
}
)
watch(
() => statusList.value,
() => {
if (statusList.value.length > 0 && !formData.status) {
formData.status = statusList.value[0].value
}
}
)
watch(
() => is_sys_userList.value,
() => {
if (is_sys_userList.value.length > 0 && !formData.is_sys_user) {
formData.is_sys_user = is_sys_userList.value[0].value
}
}
)
//
const setFormData = async (data: any = null) => {
Object.assign(formData, initialFormData)
if (data) {
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) {
formData[key] = data[key]
}
})
}
}
//
const getFormData = () => {
return { ...formData }
}
//
const validateForm = async () => {
if (!formRef.value) return false
return await formRef.value.validate()
}
//
onMounted(() => {
genderDictList()
educationDictList()
statusDictList()
is_sys_userDictList()
getCampusList()
})
//
defineExpose({
setFormData,
getFormData,
validateForm,
formData
})
</script>
<style lang="scss" scoped>
.base-info-form {
.input-width {
width: 100%;
}
.el-form-item {
margin-bottom: 18px;
}
.el-textarea {
:deep(.el-textarea__inner) {
resize: vertical;
}
}
}
</style>

350
admin/src/app/views/personnel/components/DetailInfoForm.vue

@ -0,0 +1,350 @@
<template>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="detail-info-form"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="花名" prop="name">
<el-input
v-model="formData.name"
placeholder="请填写花名"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="门店" prop="store">
<el-input
v-model="formData.store"
placeholder="请填写门店"
class="input-width"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="民族" prop="ethnicity">
<el-input
v-model="formData.ethnicity"
placeholder="请填写民族"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生日日期" prop="birthday">
<el-date-picker
v-model="formData.birthday"
type="date"
placeholder="请选择生日日期"
class="input-width"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年龄" prop="age">
<el-input
v-model="formData.age"
placeholder="请填写年龄"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="司龄" prop="tenure">
<el-input
v-model="formData.tenure"
placeholder="请填写司龄"
class="input-width"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="转正时间" prop="regular_date">
<el-date-picker
v-model="formData.regular_date"
type="date"
placeholder="请选择转正时间"
class="input-width"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否转正" prop="is_regular">
<el-select
v-model="formData.is_regular"
placeholder="请选择是否转正"
class="input-width"
clearable
>
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="政治面貌" prop="politics">
<el-input
v-model="formData.politics"
placeholder="请填写政治面貌"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="毕业院校" prop="university">
<el-input
v-model="formData.university"
placeholder="请填写毕业院校"
class="input-width"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="专业" prop="major">
<el-input
v-model="formData.major"
placeholder="请填写专业"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="毕业时间" prop="graduation_date">
<el-date-picker
v-model="formData.graduation_date"
type="date"
placeholder="请选择毕业时间"
class="input-width"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="户籍所在地" prop="household_place">
<el-input
v-model="formData.household_place"
placeholder="请填写户籍所在地"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="户籍类型" prop="household_type">
<el-input
v-model="formData.household_type"
placeholder="请填写户籍类型"
class="input-width"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="婚否" prop="marital_status">
<el-select
v-model="formData.marital_status"
placeholder="请选择婚否"
class="input-width"
clearable
>
<el-option label="已婚" value="已婚" />
<el-option label="未婚" value="未婚" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="银行卡号" prop="bank_card">
<el-input
v-model="formData.bank_card"
placeholder="请填写银行卡号"
class="input-width"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开户行" prop="bank_name">
<el-input
v-model="formData.bank_name"
placeholder="请填写开户行"
class="input-width"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同到期时间" prop="contract_expire">
<el-date-picker
v-model="formData.contract_expire"
type="date"
placeholder="请选择合同到期时间"
class="input-width"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否复聘" prop="is_rehired">
<el-select
v-model="formData.is_rehired"
placeholder="请选择是否复聘"
class="input-width"
clearable
>
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
placeholder="请填写备注"
class="input-width"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import type { FormInstance } from 'element-plus'
// Props
interface Props {
id?: string | number
}
const props = withDefaults(defineProps<Props>(), {
id: ''
})
//
const initialFormData = {
name: '',
store: '',
ethnicity: '',
birthday: '',
age: '',
tenure: '',
regular_date: '',
is_regular: '',
politics: '',
university: '',
education: '',
major: '',
graduation_date: '',
native_place: '',
household_place: '',
household_type: '',
household_address: '',
current_address: '',
emergency_contact: '',
emergency_phone: '',
marital_status: '',
bank_card: '',
bank_name: '',
contract_expire: '',
is_rehired: '',
remark: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
//
name: [{ required: false, message: '请填写花名', trigger: 'blur' }],
store: [{ required: false, message: '请填写门店', trigger: 'blur' }],
}
})
//
const setFormData = async (data: any = null) => {
Object.assign(formData, initialFormData)
if (data) {
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) {
formData[key] = data[key]
}
})
}
}
//
const getFormData = () => {
return { ...formData }
}
//
const validateForm = async () => {
if (!formRef.value) return false
return await formRef.value.validate()
}
//
defineExpose({
setFormData,
getFormData,
validateForm,
formData
})
</script>
<style lang="scss" scoped>
.detail-info-form {
.input-width {
width: 100%;
}
.el-form-item {
margin-bottom: 18px;
}
.el-date-picker {
width: 100%;
:deep(.el-input__wrapper) {
width: 100%;
}
:deep(.el-input) {
width: 100%;
}
}
}
</style>

794
admin/src/app/views/personnel/components/personnel-edit.vue

@ -1,656 +1,274 @@
<template>
<el-dialog v-model="showDialog" :title="formData.id ? t('updatePersonnel') : t('addPersonnel')" width="50%"
class="diy-dialog-wrap" :destroy-on-close="true">
<el-dialog
v-model="showDialog"
:title="formData.id ? t('updatePersonnel') : t('addPersonnel')"
width="70%"
class="personnel-edit-dialog"
:destroy-on-close="true"
:close-on-click-modal="false"
>
<el-tabs v-model="activeTab" class="tab-pane-half">
<el-tabs v-model="activeTab" class="personnel-tabs">
<!-- Tab 1: 基本信息 -->
<el-tab-pane label="基本信息" name="base">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form">
<el-form-item :label="t('name')" prop="name">
<el-input v-model="formData.name" clearable :placeholder="t('namePlaceholder')"
class="input-width" />
</el-form-item>
<el-form-item :label="t('gender')">
<el-radio-group v-model="formData.gender" :placeholder="t('genderPlaceholder')">
<el-radio v-for="(item, index) in genderList" :key="index" :label="item.value">
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('phone')" prop="phone">
<el-input v-model="formData.phone" clearable :placeholder="t('phonePlaceholder')"
class="input-width" />
</el-form-item>
<el-form-item :label="t('address')">
<el-input v-model="formData.address" clearable :placeholder="t('addressPlaceholder')"
class="input-width" />
</el-form-item>
<el-form-item :label="t('nativePlace')">
<el-input v-model="formData.native_place" clearable :placeholder="t('nativePlacePlaceholder')"
class="input-width" />
</el-form-item>
<el-form-item label="头像">
<upload-image v-model="formData.head_img" />
</el-form-item>
<el-form-item :label="t('education')">
<el-select class="input-width" v-model="formData.education" clearable
:placeholder="t('educationPlaceholder')">
<el-option label="请选择" value=""></el-option>
<el-option v-for="(item, index) in educationList" :key="index" :label="item.name"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('profile')">
<el-input v-model="formData.profile" type="textarea" rows="4" clearable
:placeholder="t('profilePlaceholder')" class="input-width" />
</el-form-item>
<el-form-item :label="t('emergencyContactPhone')">
<el-input v-model="formData.emergency_contact_phone" clearable
:placeholder="t('emergencyContactPhonePlaceholder')" class="input-width" />
</el-form-item>
<el-form-item :label="t('idCardFront')">
<upload-image v-model="formData.id_card_front" />
</el-form-item>
<el-form-item :label="t('idCardBack')">
<upload-image v-model="formData.id_card_back" />
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-select class="input-width" v-model="formData.status" clearable
:placeholder="t('statusPlaceholder')">
<el-option label="请选择" value=""></el-option>
<el-option v-for="(item, index) in statusList" :key="index" :label="item.name"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('isSysUser')" prop="is_sys_user">
<el-radio-group v-model="formData.is_sys_user" :placeholder="t('isSysUserPlaceholder')">
<el-radio v-for="(item, index) in is_sys_userList" :key="index" :label="item.value">
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="所属校区" prop="campus_id">
<el-select class="input-width" v-model="formData.campus_id" clearable placeholder="请选择所属校区">
<el-option label="请选择" value=""></el-option>
<el-option v-for="item in campusList" :key="item.id" :label="item.campus_name" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<div class="tab-content">
<BaseInfoForm
ref="baseInfoFormRef"
:id="formData.id"
/>
</div>
</el-tab-pane>
<!-- Tab 2: 详情信息 -->
<el-tab-pane label="详情信息" name="info">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="花名" prop="info.name">
<el-input v-model="formData.info.name" placeholder="请填写花名" class="input-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="门店" prop="info.store">
<el-input v-model="formData.info.store" placeholder="请填写门店" class="input-width" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="民族" prop="info.ethnicity">
<el-input v-model="formData.info.ethnicity" placeholder="请填写民族" class="input-width" />
</el-form-item>
</el-col>
<el-form-item label="生日日期" prop="info.birthday">
<el-date-picker
v-model="formData.info.birthday"
type="date"
placeholder="请选择生日日期"
class="input-width"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年龄" prop="info.age">
<el-input v-model="formData.info.age" placeholder="请填写年龄" class="input-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="司龄" prop="info.tenure">
<el-input v-model="formData.info.tenure" placeholder="请填写司龄" class="input-width" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="转正时间" prop="info.regular_date">
<el-date-picker
v-model="formData.info.regular_date"
type="date"
placeholder="请选择转正时间"
class="input-width"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否转正" prop="info.is_regular">
<el-select
v-model="formData.info.is_regular"
placeholder="请选择是否转正"
class="input-width"
clearable
>
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="政治面貌" prop="info.politics">
<el-input v-model="formData.info.politics" placeholder="请填写政治面貌" class="input-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="毕业院校" prop="info.university">
<el-input v-model="formData.info.university" placeholder="请填写毕业院校" class="input-width" />
</el-form-item>
</el-col>
</el-row>
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学历" prop="info.education">
<el-input v-model="formData.info.education" placeholder="请填写学历" class="input-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="info.major">
<el-input v-model="formData.info.major" placeholder="请填写专业" class="input-width" />
</el-form-item>
</el-col>
</el-row> -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="专业" prop="info.major">
<el-input v-model="formData.info.major" placeholder="请填写专业" class="input-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="毕业时间" prop="info.graduation_date">
<el-date-picker
v-model="formData.info.graduation_date"
type="date"
placeholder="请选择毕业时间"
class="input-width"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="户籍所在地" prop="info.household_place">
<el-input v-model="formData.info.household_place" placeholder="请填写户籍所在地" class="input-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="户籍类型" prop="info.household_type">
<el-input v-model="formData.info.household_type" placeholder="请填写户籍类型" class="input-width" />
</el-form-item>
</el-col>
</el-row>
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="户籍地址" prop="info.household_address">
<el-input v-model="formData.info.household_address" placeholder="请填写户籍地址" class="input-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="现居地址" prop="info.current_address">
<el-input v-model="formData.info.current_address" placeholder="请填写现居地址" class="input-width" />
</el-form-item>
</el-col>
</el-row> -->
<!--
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="紧急联系人" prop="info.emergency_contact">
<el-input v-model="formData.info.emergency_contact" placeholder="请填写紧急联系人" class="input-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="紧急联系人联系电话" prop="info.emergency_phone">
<el-input v-model="formData.info.emergency_phone" placeholder="请填写紧急联系人联系电话" class="input-width" />
</el-form-item>
</el-col>
</el-row> -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="婚否" prop="info.marital_status">
<el-select
v-model="formData.info.marital_status"
placeholder="请选择婚否"
class="input-width"
clearable
>
<el-option label="已婚" value="已婚" />
<el-option label="未婚" value="未婚" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="银行卡号" prop="info.bank_card">
<el-input v-model="formData.info.bank_card" placeholder="请填写银行卡号" class="input-width" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开户行" prop="info.bank_name">
<el-input v-model="formData.info.bank_name" placeholder="请填写开户行" class="input-width" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同到期时间" prop="info.contract_expire">
<el-date-picker
v-model="formData.info.contract_expire"
type="date"
placeholder="请选择合同到期时间"
class="input-width"
value-format="YYYY-MM-DD"
<div class="tab-content">
<DetailInfoForm
ref="detailInfoFormRef"
:id="formData.id"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否复聘" prop="info.is_rehired">
<el-select
v-model="formData.info.is_rehired"
placeholder="请选择是否复聘"
class="input-width"
clearable
>
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="info.remark">
<el-input v-model="formData.info.remark" placeholder="请填写备注" class="input-width" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading"
@click="confirm(formRef)">{{ t('confirm') }}</el-button>
@click="confirm">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import {
import { ref, reactive } from 'vue'
import { t } from '@/lang'
import {
addPersonnel,
editPersonnel,
getPersonnelInfo,
} from '@/app/api/personnel'
import { getWithCampusList } from '@/app/api/campus_person_role'
} from '@/app/api/personnel'
import BaseInfoForm from './BaseInfoForm.vue'
import DetailInfoForm from './DetailInfoForm.vue'
let showDialog = ref(false)
const loading = ref(false)
const activeTab = ref('base')
/**
let showDialog = ref(false)
const loading = ref(false)
const activeTab = ref('base')
//
const baseInfoFormRef = ref()
const detailInfoFormRef = ref()
/**
* 表单数据
*/
const initialFormData = {
const initialFormData = {
id: '',
name: '',
gender: '',
head_img: '',
phone: '',
address: '',
native_place: '',
education: '',
profile: '',
emergency_contact_phone: '',
id_card_front: '',
id_card_back: '',
status: '',
is_sys_user: '',
campus_id: '',
info:{
name:'',
store:'',
ethnicity:'',
birthday:'',
age:'',
tenure:'',
regular_date:'',
is_regular:'',
politics:'',
university:'',
education:'',
major:'',
graduation_date:'',
native_place:'',
household_place:'',
household_type:'',
household_address:'',
current_address:'',
emergency_contact:'',
emergency_phone:'',
marital_status:'',
bank_card:'',
bank_name:'',
contract_expire:'',
is_rehired:'',
remark:''
info: {}
}
}
}
const formData : Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
name: [{ required: true, message: t('namePlaceholder'), trigger: 'blur' }],
gender: [
{ required: true, message: t('genderPlaceholder'), trigger: 'blur' },
],
phone: [
{ required: true, message: t('phonePlaceholder'), trigger: 'blur' },
],
address: [
{ required: true, message: t('addressPlaceholder'), trigger: 'blur' },
],
native_place: [
{ required: true, message: t('nativePlacePlaceholder'), trigger: 'blur' },
],
education: [
{ required: true, message: t('educationPlaceholder'), trigger: 'blur' },
],
profile: [
{ required: true, message: t('profilePlaceholder'), trigger: 'blur' },
],
emergency_contact_phone: [
{
required: true,
message: t('emergencyContactPhonePlaceholder'),
trigger: 'blur',
},
],
id_card_front: [
{ required: true, message: t('idCardFrontPlaceholder'), trigger: 'blur' },
],
id_card_back: [
{ required: true, message: t('idCardBackPlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
is_sys_user: [
{ required: true, message: t('isSysUserPlaceholder'), trigger: 'blur' },
],
}
})
const formData: Record<string, any> = reactive({ ...initialFormData })
const emit = defineEmits(['complete'])
const emit = defineEmits(['complete'])
/**
/**
* 确认
* @param formEl
*/
const confirm = async (formEl : FormInstance | undefined) => {
if (loading.value || !formEl) return
let save = formData.id ? editPersonnel : addPersonnel
const confirm = async () => {
if (loading.value) return
//
const baseInfoValid = await baseInfoFormRef.value?.validateForm()
const detailInfoValid = await detailInfoFormRef.value?.validateForm()
if (!baseInfoValid) {
activeTab.value = 'base'
return
}
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
try {
//
const baseData = baseInfoFormRef.value?.getFormData() || {}
const detailData = detailInfoFormRef.value?.getFormData() || {}
const submitData = {
...baseData,
...formData,
info: detailData
}
const save = formData.id ? editPersonnel : addPersonnel
await save(submitData)
save(data)
.then((res) => {
loading.value = false
showDialog.value = false
emit('complete')
})
.catch((err) => {
} catch (err) {
loading.value = false
})
}
})
}
}
//
let genderList = ref([])
const genderDictList = async () => {
genderList.value = await (await useDictionary('gender')).data.dictionary
}
genderDictList()
watch(
() => genderList.value,
() => {
formData.gender = genderList.value[0].value
}
)
let educationList = ref([])
const educationDictList = async () => {
educationList.value = await (await useDictionary('education')).data.dictionary
/**
* 设置表单数据
*/
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
try {
if (row) {
const data = await (await getPersonnelInfo(row.id)).data
if (data) {
formData.id = data.id
//
await baseInfoFormRef.value?.setFormData(data)
//
await detailInfoFormRef.value?.setFormData(data.info || {})
}
educationDictList()
watch(
() => educationList.value,
() => {
formData.education = educationList.value[0].value
} else {
//
await baseInfoFormRef.value?.setFormData()
await detailInfoFormRef.value?.setFormData()
}
)
let statusList = ref([])
const statusDictList = async () => {
statusList.value = await (
await useDictionary('personnel_status')
).data.dictionary
} catch (error) {
console.error('设置表单数据失败:', error)
} finally {
loading.value = false
}
statusDictList()
watch(
() => statusList.value,
() => {
formData.status = statusList.value[0].value
}
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped>
.personnel-edit-dialog {
:deep(.el-dialog) {
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
)
let is_sys_userList = ref([])
const is_sys_userDictList = async () => {
is_sys_userList.value = await (
await useDictionary('global_true_or_false')
).data.dictionary
:deep(.el-dialog__header) {
padding: 20px 24px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
border-radius: 8px 8px 0 0;
.el-dialog__title {
font-size: 18px;
font-weight: 600;
color: #1f2937;
}
is_sys_userDictList()
watch(
() => is_sys_userList.value,
() => {
formData.is_sys_user = is_sys_userList.value[0].value
}
)
//
let campusList = ref([])
const getCampusList = async () => {
const res = await getWithCampusList({})
if (res.data) {
campusList.value = res.data
:deep(.el-dialog__body) {
padding: 24px;
max-height: 70vh;
overflow-y: auto;
}
:deep(.el-dialog__footer) {
padding: 16px 24px 20px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
border-radius: 0 0 8px 8px;
}
}
onMounted(() => {
getCampusList()
})
.personnel-tabs {
:deep(.el-tabs__header) {
margin-bottom: 24px;
border-bottom: 2px solid #e5e7eb;
const setFormData = async (row : any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getPersonnelInfo(row.id)).data
if (data)
Object.keys(formData).forEach((key : string) => {
if (data[key] != undefined) formData[key] = data[key]
})
.el-tabs__nav-wrap {
&::after {
display: none;
}
loading.value = false
}
//
const mobileVerify = (rule : any, value : any, callback : any) => {
if (value && !/^1[3-9]\d{9}$/.test(value)) {
callback(new Error(t('generateMobile')))
} else {
callback()
}
}
.el-tabs__item {
padding: 12px 24px;
font-size: 15px;
font-weight: 500;
color: #6b7280;
border-bottom: none;
transition: all 0.3s ease;
//
const idCardVerify = (rule : any, value : any, callback : any) => {
if (
value &&
!/^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(
value
)
) {
callback(new Error(t('generateIdCard')))
} else {
callback()
}
&:hover {
color: #3b82f6;
}
//
const emailVerify = (rule : any, value : any, callback : any) => {
if (value && !/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(value)) {
callback(new Error(t('generateEmail')))
} else {
callback()
&.is-active {
color: #3b82f6;
background: #eff6ff;
border: 1px solid #3b82f6;
border-bottom: 1px solid #eff6ff;
border-radius: 8px 8px 0 0;
position: relative;
top: 2px;
}
}
}
//
const numberVerify = (rule : any, value : any, callback : any) => {
if (!Number.isInteger(value)) {
callback(new Error(t('generateNumber')))
} else {
callback()
.tab-content {
padding: 20px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
}
.dialog-footer {
.el-button {
padding: 10px 24px;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
}
}
</style>
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label {
.personnel-edit-dialog .el-form-item__label {
height: auto !important;
line-height: 1.6;
}
.personnel-edit-dialog .el-tabs__content {
overflow: visible;
}
.personnel-edit-dialog .el-dialog__body {
padding: 24px;
}
//
.personnel-edit-dialog .el-dialog__body::-webkit-scrollbar {
width: 6px;
}
.personnel-edit-dialog .el-dialog__body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.personnel-edit-dialog .el-dialog__body::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
</style>

20
niucloud/app/api/controller/apiController/PersonCourseSchedule.php

@ -211,4 +211,24 @@ class PersonCourseSchedule extends BaseApiService
}
return success($res['data']);
}
//获取学生课程详情包含已上课的情况和订单情况
public function getStudentCourseDetail(Request $request)
{
$student_course_id = $request->param('student_course_id', '');
if (empty($student_course_id)) {
return fail('缺少参数student_course_id');
}
$where = [
'student_course_id' => $student_course_id,
];
$res = (new PersonCourseScheduleService())->getStudentCourseDetail($where);
if(!$res['code']){
return fail($res['msg']);
}
return success($res['data']);
}
}

1
niucloud/app/api/controller/apiController/StudentCourse.php

@ -208,4 +208,5 @@ class StudentCourse extends BaseApiService
$customerResource = \app\model\customer_resources\CustomerResources::where('member_id', $member_id)->find();
return $customerResource ? $customerResource->id : '';
}
}

2
niucloud/app/api/route/route.php

@ -582,6 +582,8 @@ Route::group(function () {
//获取学生课程信息列表(包含教练配置)
Route::get('getStudentCourseInfo', 'apiController.PersonCourseSchedule/getStudentCourseInfo');
//获取学生课程详情包含已上课的情况和订单情况
Route::post('getStudentCourseDetail', 'apiController.PersonCourseSchedule/getStudentCourseDetail');
//获取人员列表(教练、教务、助教)
Route::get('getPersonnelList', 'apiController.PersonCourseSchedule/getPersonnelList');
//更新学生课程人员配置

40
niucloud/app/common.php

@ -2305,3 +2305,43 @@ function save_user_signature($data)
return false;
}
}
// ==================== 合同占位符处理函数 ====================
/**
* 将合同内容中的占位符替换为下划线输入框
* 使用正则表达式将 {{占位符}} 格式替换为指定长度的下划线
*
* @param string $contractContent 合同内容
* @param int $underlineLength 下划线长度(默认为10个字符)
* @return string 替换后的合同内容
*/
function replace_placeholders_with_underlines($contractContent, $underlineLength = 10)
{
if (empty($contractContent)) {
return $contractContent;
}
// 使用正则表达式匹配 {{}} 格式的占位符
// \{\{ 匹配 {{
// [^}]+ 匹配一个或多个非 } 字符(占位符内容)
// \}\} 匹配 }}
$pattern = '/\{\{[^}]+\}\}/';
// 生成替换字符串 - 根据占位符内容长度动态调整下划线数量
$replacement = function($matches) use ($underlineLength) {
$placeholder = $matches[0];
// 提取占位符中的内容
$content = preg_replace('/^\{\{|\}\}$/', '', $placeholder);
// 根据内容长度动态决定下划线数量,最少8个,最多20个
$contentLength = mb_strlen($content, 'UTF-8');
$adjustedLength = max(8, min(20, $contentLength + 4));
return str_repeat('_', $adjustedLength); // 使用全角下划线,显示效果更好
};
// 执行替换
return preg_replace_callback($pattern, $replacement, $contractContent);
}

1
niucloud/app/service/api/apiService/CoachStudentService.php

@ -285,7 +285,6 @@ class CoachStudentService extends BaseApiService
// resource_sharing_id 是 school_resource_assignment 表的 id
$resourceAssignment = Db::table('school_resource_assignment')
->where('resource_id', $customerResource['id'])
->where('assignee_type', 'user')
->field('id')
->order('assigned_at', 'desc')
->find();

235
niucloud/app/service/api/apiService/CourseScheduleService.php

@ -2771,6 +2771,231 @@ class CourseScheduleService extends BaseApiService
}
}
/**
* 处理正式学员课程消减逻辑
* @param array $enrollment 学员课程安排记录
* @param Student $student 学员信息
* @throws \Exception
*/
private function handlePaidStudentCourseDeduction($enrollment, $student)
{
try {
$scheduleId = $enrollment['schedule_id'] ?? 0;
if (empty($scheduleId)) {
throw new \Exception('课程安排ID不能为空');
}
// 获取课程安排信息以确定课程日期
$schedule = Db::name('course_schedule')
->where('id', $scheduleId)
->find();
if (!$schedule) {
throw new \Exception('找不到课程安排信息');
}
$courseDate = $schedule['course_date'];
// 根据课程日期动态查找应该核销的课程包
$studentCourse = $this->findApplicableStudentCourse($student->id, $courseDate);
if (!$studentCourse) {
throw new \Exception('找不到该日期有效的学员课程包');
}
// 获取课程信息以计算消耗课时数
$course = Db::name('course')
->where('id', $studentCourse['course_id'])
->find();
if (!$course) {
throw new \Exception('找不到课程信息');
}
// 计算本次消耗的课时数
$deductHours = $this->calculateCourseDeduction($course, $scheduleId);
// 检查剩余课时是否足够
$remainingHours = $studentCourse['total_hours'] - $studentCourse['use_total_hours'];
if ($remainingHours < $deductHours) {
throw new \Exception('剩余课时不足,当前剩余:' . $remainingHours . ',需要消耗:' . $deductHours);
}
// 1. 更新学员课程的已使用课时数
$updateResult = Db::name('student_courses')
->where('id', $studentCourse['id'])
->inc('use_total_hours', $deductHours)
->update(['updated_at' => date('Y-m-d H:i:s')]);
if (!$updateResult) {
throw new \Exception('更新学员课程课时失败');
}
// 2. 插入课程消减记录
$usageData = [
'student_course_id' => $studentCourse['id'],
'student_id' => $student->id,
'resource_id' => $enrollment['resources_id'] ?? null,
'used_hours' => $deductHours,
'usage_date' => date('Y-m-d'),
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
$usageResult = Db::name('student_course_usage')->insert($usageData);
if (!$usageResult) {
throw new \Exception('插入课程消减记录失败');
}
// 记录日志
trace('Paid student course deduction processed', 'info');
trace('Deduction details: ' . json_encode([
'student_id' => $student->id,
'student_course_id' => $studentCourse['id'],
'course_start_date' => $studentCourse['start_date'],
'course_end_date' => $studentCourse['end_date'],
'schedule_date' => $courseDate,
'deducted_hours' => $deductHours,
'remaining_hours' => $remainingHours - $deductHours,
'usage_date' => date('Y-m-d')
]), 'info');
} catch (\Exception $e) {
// 抛出异常以便外层事务回滚
throw new \Exception('处理正式学员课程消减失败:' . $e->getMessage());
}
}
/**
* 根据课程日期查找适用的学员课程包
* 优先选择:
* 1. 在有效期内(开始日期 ≤ 课程日期 ≤ 结束日期)
* 2. 还有剩余课时
* 3. 最早开始的课程包
* @param int $studentId 学员ID
* @param string $courseDate 课程日期
* @return array|null 学员课程记录
*/
private function findApplicableStudentCourse($studentId, $courseDate)
{
try {
// 查找在有效期内且有剩余课时的课程包
$applicableCourses = Db::name('student_courses')
->where('student_id', $studentId)
->where('status', 1) // 有效状态
->where('start_date', '<=', $courseDate) // 开始日期 <= 课程日期
->where('end_date', '>=', $courseDate) // 结束日期 >= 课程日期
->whereRaw('total_hours > use_total_hours') // 有剩余课时
->order('start_date ASC, created_at ASC') // 按开始日期升序,创建时间升序
->select()
->toArray();
if (empty($applicableCourses)) {
return null;
}
// 返回最早开始的有效课程包
return $applicableCourses[0];
} catch (\Exception $e) {
trace('Find applicable student course error: ' . $e->getMessage(), 'error');
return null;
}
}
/**
* 计算课程消耗课时数
* @param array $course 课程信息
* @param int $scheduleId 课程安排ID
* @return float 消耗课时数
*/
private function calculateCourseDeduction($course, $scheduleId)
{
try {
// 获取课程安排信息
$schedule = Db::name('course_schedule')
->where('id', $scheduleId)
->find();
if (!$schedule) {
throw new \Exception('找不到课程安排信息');
}
// 根据课程类型计算消耗课时数
switch ($course['course_type']) {
case 1: // 按课时
// 解析时间段计算实际课时
$timeSlot = $schedule['time_slot'] ?? '';
$deductHours = $this->parseTimeSlotToHours($timeSlot);
break;
case 2: // 按次卡
// 按次卡每次消耗1次
$deductHours = 1;
break;
case 3: // 按周期
// 周期课程每次消耗1次
$deductHours = 1;
break;
case 4: // 按时长
// 按实际时长计算,转换为小时
$duration = $course['duration'] ?? 0; // 分钟
$deductHours = $duration / 60;
break;
default:
// 默认每次消耗1课时
$deductHours = 1;
break;
}
// 确保返回正数
return max(0.01, $deductHours);
} catch (\Exception $e) {
// 计算失败时默认消耗1课时
return 1;
}
}
/**
* 解析时间段为小时数
* @param string $timeSlot 时间段,格式:08:00-08:30
* @return float 小时数
*/
private function parseTimeSlotToHours($timeSlot)
{
try {
if (empty($timeSlot) || strpos($timeSlot, '-') === false) {
return 1; // 默认1小时
}
$times = explode('-', $timeSlot);
if (count($times) !== 2) {
return 1;
}
$startTime = strtotime($times[0]);
$endTime = strtotime($times[1]);
if (!$startTime || !$endTime) {
return 1;
}
$minutes = ($endTime - $startTime) / 60;
$hours = $minutes / 60;
// 如果计算结果小于0.1小时,按0.1小时计算
return max(0.1, $hours);
} catch (\Exception $e) {
return 1; // 解析失败时默认1小时
}
}
/**
* 处理单个学员签到(内部方法,不管理事务)
* @param int $scheduleId 课程安排ID
@ -2856,9 +3081,15 @@ class CourseScheduleService extends BaseApiService
return ['success' => false, 'error' => '更新签到状态失败'];
}
// 处理试听课签到逻辑(仅在签到成功且学员未付费时处理)
if ($status === 1 && $student->pay_status != 1) {
// 处理签到后的课程消减逻辑
if ($status === 1) {
if ($student->pay_status != 1) {
// 处理试听课签到逻辑
$this->handleTrialClassCheckin($student);
} else {
// 处理正式学员的课程消减逻辑
$this->handlePaidStudentCourseDeduction($enrollment, $student);
}
}
return ['success' => true, 'error' => ''];

5
niucloud/app/service/api/apiService/CourseService.php

@ -793,9 +793,12 @@ class CourseService extends BaseApiService
$phone = $student['student']['contact_phone'] ?: '';
$trialClassCount = $student['student']['trial_class_count'] ?: 0;
// 获取学员最新的付费课程信息
// 获取学员最新的有效课程信息
$studentCourseInfo = Db::name('student_courses')
->where('student_id', $student['student_id'])
->where('status', 1) // 只获取状态为1的有效课程
->where('start_date', '<=', date('Y-m-d')) // 开始时间小于等于当前时间
->where('end_date', '>=', date('Y-m-d')) // 结束时间大于等于当前时间
->order('created_at DESC')
->find();

237
niucloud/app/service/api/apiService/PersonCourseScheduleService.php

@ -406,6 +406,31 @@ class PersonCourseScheduleService extends BaseApiService
->where('status', 2) // 2表示请假
->count();
// 获取班级关联信息
$classInfo = null;
try {
$classRel = \app\model\class_resources_rel\ClassResourcesRel::alias('crr')
->join(['school_class' => 'c'], 'crr.class_id = c.id', 'left')
->where([
'crr.resource_id' => $where['resource_id'],
'crr.status' => 1
])
->field([
'crr.class_id',
'c.class_name',
'c.head_coach',
'c.educational_id'
])
->find();
if ($classRel) {
$classInfo = $classRel->toArray();
}
} catch (\Exception $e) {
// 班级查询失败,不影响整体功能
$classInfo = null;
}
// 获取教练配置信息
$mainCoach = null;
$education = null;
@ -471,12 +496,19 @@ class PersonCourseScheduleService extends BaseApiService
'status' => $status, // 课程状态
'db_status' => $dbStatus, // 数据库原始状态
'single_session_count' => $course['single_session_count'] ?? 1, // 单次消课数量
'resource_id' => $course['resource_id'] ?? null, // 添加资源ID
'student_course_id' => $course['id'], // 添加学生课程ID
'main_coach_id' => $course['main_coach_id'] ?? null,
'main_coach_name' => $mainCoach['name'] ?? '未分配',
'education_id' => $course['education_id'] ?? null,
'education_name' => $education['name'] ?? '未分配',
'assistant_ids' => $course['assistant_ids'] ?? '',
'assistant_names' => implode(', ', array_column($assistants, 'name')) ?: '无'
'assistant_names' => implode(', ', array_column($assistants, 'name')) ?: '无',
// 班级相关字段
'class_id' => $classInfo['class_id'] ?? null,
'class_name' => $classInfo['class_name'] ?? null,
'has_class' => !empty($classInfo), // 是否有班级关联
'class_info' => $classInfo // 完整的班级信息
];
}
@ -553,6 +585,209 @@ class PersonCourseScheduleService extends BaseApiService
return $res;
}
//获取学生课程详情(包含课时使用记录和订单信息)
public function getStudentCourseDetail(array $where)
{
$res = [
'code' => 0,
'msg' => '获取失败',
'data' => []
];
try {
$studentCourseId = $where['student_course_id'];
// 1. 获取学员课程基础信息
$studentCourse = StudentCourses::where('id', $studentCourseId)
->with([
'course' => function($query) {
$query->field('id,course_name');
},
'student' => function($query) {
$query->field('id,name');
}
])
->find();
if (!$studentCourse) {
$res['msg'] = '学员课程不存在';
return $res;
}
$studentCourse = $studentCourse->toArray();
// 2. 获取课时使用记录(一对多关联)
$usageRecords = StudentCourseUsage::where('student_course_id', $studentCourseId)
->order('usage_date', 'desc')
->select()
->toArray();
// 3. 获取关联的订单信息(一对一关联,通过course_plan_id)
$orderInfo = \app\model\order_table\OrderTable::where('course_plan_id', $studentCourseId)
->with([
'course' => function($query) {
$query->field('id,course_name');
},
'personnel' => function($query) {
$query->field('id,name');
},
'campus' => function($query) {
$query->field('id,campus_name');
}
])
->find();
// 4. 计算课时统计信息
$totalHours = ($studentCourse['total_hours'] ?? 0) + ($studentCourse['gift_hours'] ?? 0);
$usedHours = ($studentCourse['use_total_hours'] ?? 0) + ($studentCourse['use_gift_hours'] ?? 0);
$remainingHours = $totalHours - $usedHours;
// 5. 获取教练配置信息
$mainCoach = null;
$education = null;
$assistants = [];
if (!empty($studentCourse['main_coach_id'])) {
$mainCoach = Personnel::where('id', $studentCourse['main_coach_id'])->field('id,name')->find();
}
if (!empty($studentCourse['education_id'])) {
$education = Personnel::where('id', $studentCourse['education_id'])->field('id,name')->find();
}
if (!empty($studentCourse['assistant_ids'])) {
$assistantIds = array_filter(explode(',', $studentCourse['assistant_ids']));
if (!empty($assistantIds)) {
$assistants = Personnel::whereIn('id', $assistantIds)->field('id,name')->select()->toArray();
}
}
// 6. 计算课程状态
$status = 'active';
$dbStatus = $studentCourse['status'] ?? 1;
switch ($dbStatus) {
case 1:
$status = 'active';
break;
case 2:
$status = 'expired';
break;
case 3:
$status = 'waiting';
break;
case 4:
$status = 'delayed';
break;
}
if ($status === 'active' && !empty($studentCourse['end_date'])) {
if (strtotime($studentCourse['end_date']) < time()) {
$status = 'expired';
}
}
if ($remainingHours <= 0) {
$status = 'completed';
}
// 7. 组装返回数据
$data = [
// 学员课程基础信息
'student_course_info' => [
'id' => $studentCourse['id'],
'student_id' => $studentCourse['student_id'],
'student_name' => $studentCourse['student']['name'] ?? '未知学员',
'course_id' => $studentCourse['course_id'],
'course_name' => $studentCourse['course']['course_name'] ?? '未知课程',
'total_hours' => $studentCourse['total_hours'] ?? 0,
'gift_hours' => $studentCourse['gift_hours'] ?? 0,
'use_total_hours' => $studentCourse['use_total_hours'] ?? 0,
'use_gift_hours' => $studentCourse['use_gift_hours'] ?? 0,
'start_date' => $studentCourse['start_date'] ?? '',
'end_date' => $studentCourse['end_date'] ?? '',
'status' => $status,
'db_status' => $dbStatus,
'single_session_count' => $studentCourse['single_session_count'] ?? 1,
'resource_id' => $studentCourse['resource_id'] ?? null,
'main_coach_id' => $studentCourse['main_coach_id'] ?? null,
'education_id' => $studentCourse['education_id'] ?? null,
'assistant_ids' => $studentCourse['assistant_ids'] ?? '',
// 课时统计
'total_class_hours' => $totalHours,
'used_class_hours' => $usedHours,
'remaining_class_hours' => $remainingHours,
// 教练信息
'main_coach_name' => $mainCoach['name'] ?? '未分配',
'education_name' => $education['name'] ?? '未分配',
'assistant_names' => implode(', ', array_column($assistants, 'name')) ?: '无',
'coach_details' => [
'main_coach' => $mainCoach,
'education' => $education,
'assistants' => $assistants
]
],
// 课时使用记录列表
'usage_records' => array_map(function($record) {
return [
'id' => $record['id'],
'used_hours' => $record['used_hours'],
'usage_date' => $record['usage_date'],
'created_at' => $record['created_at'],
'updated_at' => $record['updated_at'],
'student_id' => $record['student_id'],
'resource_id' => $record['resource_id']
];
}, $usageRecords),
// 订单信息(如果存在)
'order_info' => $orderInfo ? [
'id' => $orderInfo['id'],
'payment_id' => $orderInfo['payment_id'] ?? '',
'order_type' => $orderInfo['order_type'] ?? '',
'order_status' => $orderInfo['order_status'] ?? '',
'payment_type' => $orderInfo['payment_type'] ?? '',
'order_amount' => $orderInfo['order_amount'] ?? 0,
'course_id' => $orderInfo['course_id'] ?? 0,
'class_id' => $orderInfo['class_id'] ?? null,
'staff_id' => $orderInfo['staff_id'] ?? 0,
'resource_id' => $orderInfo['resource_id'] ?? 0,
'campus_id' => $orderInfo['campus_id'] ?? 0,
'student_id' => $orderInfo['student_id'] ?? null,
'discount_amount' => $orderInfo['discount_amount'] ?? 0,
'remark' => $orderInfo['remark'] ?? '',
'payment_time' => $orderInfo['payment_time'] ?? '',
'created_at' => $orderInfo['created_at'] ?? '',
// 关联信息
'course_name' => $orderInfo['course']['course_name'] ?? '',
'staff_name' => $orderInfo['personnel']['name'] ?? '',
'campus_name' => $orderInfo['campus']['campus_name'] ?? ''
] : null,
// 统计信息
'statistics' => [
'total_usage_records' => count($usageRecords),
'total_used_hours_from_records' => array_sum(array_column($usageRecords, 'used_hours')),
'has_order' => !empty($orderInfo),
'usage_date_range' => $usageRecords ? [
'first_usage' => min(array_column($usageRecords, 'usage_date')),
'last_usage' => max(array_column($usageRecords, 'usage_date'))
] : null
]
];
$res = [
'code' => 1,
'msg' => '获取成功',
'data' => $data
];
} catch (\Exception $e) {
$res['msg'] = '获取异常:' . $e->getMessage();
}
return $res;
}
//更新学生课程人员配置
public function updateStudentCoursePersonnel($studentCourseId, array $data)
{

3
niucloud/app/service/api/student/ContractService.php

@ -1133,4 +1133,5 @@ class ContractService extends BaseService
return (string)$value;
}
}
}

5
uniapp/api/apiRoute.js

@ -1083,6 +1083,11 @@ export default {
return response
},
// 获取学员课程详情
async getStudentCourseDetail(data = {}) {
return await http.post('/getStudentCourseDetail', data)
},

52
uniapp/components/course-info-card/index.vue

@ -7,7 +7,7 @@
class="course-item"
v-for="(course, index) in courseList"
:key="course.id || index"
@tap="viewCourseDetail(course)"
@tap="viewCourseDetail(courseList[index])"
>
<view class="course-header">
<view class="course-title">{{ course.course_name || '未知课程' }}</view>
@ -252,6 +252,11 @@ export default {
methods: {
//
viewCourseDetail(course) {
console.log('viewCourseDetail 被调用,course:', course)
console.log('course 类型:', typeof course)
console.log('course.id:', course?.id)
console.log('course.student_course_id:', course?.student_course_id)
if (!course) {
console.error('viewCourseDetail: course参数为空')
uni.showToast({
@ -260,6 +265,17 @@ export default {
})
return
}
//
if (!course.id && !course.student_course_id) {
console.error('viewCourseDetail: 课程缺少ID信息')
uni.showToast({
title: '课程信息不完整',
icon: 'none'
})
return
}
this.$emit('view-detail', course)
},
@ -603,16 +619,36 @@ export default {
// class_id0
const targetId = String(this.editForm.class_id || 0)
const classIndex = this.classList.findIndex(item => String(item.id) === targetId)
console.log('班级索引设置调试信息:', {
targetId,
targetIdType: typeof targetId,
editFormClassId: this.editForm.class_id,
editFormClassIdType: typeof this.editForm.class_id,
classList: this.classList.map(c => ({ id: c.id, idType: typeof c.id, name: c.class_name })),
classIndex,
classIndexType: typeof classIndex
})
if (classIndex >= 0) {
this.selectedClassIndex = classIndex
this.editForm.class_name = this.classList[classIndex].class_name
console.log('班级设置成功:', this.classList[classIndex].class_name, '索引:', classIndex)
this.selectedClassIndex = Number(classIndex) //
// 使使API
const listClassName = this.classList[classIndex].class_name
this.editForm.class_name = listClassName || this.editForm.class_name || '请选择班级'
console.log('班级设置成功:', {
className: this.editForm.class_name,
listClassName,
classIndex: this.selectedClassIndex,
classIndexType: typeof this.selectedClassIndex
})
} else {
// ""
// API""
this.selectedClassIndex = 0
this.editForm.class_id = 0
this.editForm.class_name = '无班级'
console.log('未找到匹配的班级ID,默认设置为无班级:', this.editForm.class_id, '班级列表:', this.classList.map(c => c.id))
// class_idclass_name0
console.log('未找到匹配的班级ID,保留原有值:', {
classId: this.editForm.class_id,
className: this.editForm.class_name,
classList: this.classList.map(c => c.id)
})
}
} else {
console.log('班级列表为空:', {

18
uniapp/pages-common/contract/staff-contract-sign.vue

@ -36,7 +36,7 @@
<view class="form_title">请填写以下信息</view>
<view class="form_content">
<view
v-for="(field, index) in formFields"
v-for="(field, index) in partyBFormFields"
:key="index"
class="form_field"
>
@ -190,6 +190,11 @@ export default {
})
return content
},
// party_b
partyBFormFields() {
return this.formFields.filter(field => field.sign_party === 'party_b')
}
},
@ -276,7 +281,8 @@ export default {
initFormData() {
const data = {}
this.formFields.forEach(field => {
// party_b
this.partyBFormFields.forEach(field => {
const key = field.placeholder || field.name
//
if (field.data_type === 'database' || field.data_type === 'system') {
@ -400,8 +406,8 @@ export default {
},
validateForm() {
//
for (const field of this.formFields) {
// party_b
for (const field of this.partyBFormFields) {
if (field.is_required) {
const key = field.placeholder || field.name
const value = this.formData[key]
@ -466,8 +472,8 @@ export default {
},
getSignatureImage() {
// signature
for (const field of this.formFields) {
// signatureparty_b
for (const field of this.partyBFormFields) {
if (field.data_type === 'signature') {
const key = field.placeholder || field.name
return this.formData[key] || ''

1614
uniapp/pages-market/course/course_detail.vue

File diff suppressed because it is too large

10
uniapp/pages.json

@ -395,6 +395,14 @@
"navigationBarBackgroundColor": "#292929",
"navigationBarTextStyle": "white"
}
},
{
"path": "course/course_detail",
"style": {
"navigationBarTitleText": "课程详情",
"navigationBarBackgroundColor": "#292929",
"navigationBarTextStyle": "white"
}
}
]
},
@ -469,7 +477,7 @@
"style": {
"navigationBarTitleText": "我的工资",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarBackgroundColor": "#171717",
"navigationBarTextStyle": "white"
}
},

Loading…
Cancel
Save