Browse Source

修改 bug

master
王泽彦 8 months ago
parent
commit
27ff575e86
  1. 65
      admin/src/app/api/lesson_course_teaching.ts
  2. 305
      admin/src/app/views/binding_personnel/binding_personnel.vue
  3. 359
      admin/src/app/views/customer_resources/components/Messages.vue
  4. 6
      admin/src/app/views/customer_resources/components/UserProfile.vue
  5. 364
      admin/src/app/views/jlyj/jlyj.vue
  6. 284
      admin/src/app/views/lesson_course_teaching/components/components_backup/Jump-lesson-library-edit.vue
  7. 284
      admin/src/app/views/lesson_course_teaching/components/components_backup/basketball-course-teaching-edit.vue
  8. 280
      admin/src/app/views/lesson_course_teaching/components/components_backup/en-course-teaching-edit.vue
  9. 284
      admin/src/app/views/lesson_course_teaching/components/components_backup/lesson-course-teaching-edit.vue
  10. 280
      admin/src/app/views/lesson_course_teaching/components/components_backup/ninja-teaching-edit.vue
  11. 284
      admin/src/app/views/lesson_course_teaching/components/components_backup/physical-teaching-edit.vue
  12. 284
      admin/src/app/views/lesson_course_teaching/components/components_backup/security-teaching-edit.vue
  13. 282
      admin/src/app/views/lesson_course_teaching/components/components_backup/strengthen-course-teaching-edit.vue
  14. 344
      admin/src/app/views/lesson_course_teaching/components/unified-data-table.vue
  15. 371
      admin/src/app/views/lesson_course_teaching/components/unified-edit-dialog.vue
  16. 2501
      admin/src/app/views/lesson_course_teaching/lesson_course_teaching.vue
  17. 2236
      admin/src/app/views/lesson_course_teaching/lesson_course_teaching_backup.vue
  18. BIN
      doc/副本(时间卡)体能课学员课程协议.docx
  19. 156
      niucloud/app/adminapi/controller/lesson_course_teaching/LessonCourseTeaching.php
  20. 206
      niucloud/app/adminapi/controller/performance/PerformanceConfig.php
  21. 1
      niucloud/app/adminapi/controller/sys/System.php
  22. 18
      niucloud/app/adminapi/route/lesson_course_teaching.php
  23. 41
      niucloud/app/adminapi/route/performance_config.php
  24. 64
      niucloud/app/api/controller/student/MessageController.php
  25. 12
      niucloud/app/api/route/student.php
  26. 214
      niucloud/app/command/TeachingSyncCommand.php
  27. 13
      niucloud/app/dict/schedule/schedule.php
  28. 112
      niucloud/app/job/schedule/TeachingPersonnelSync.php
  29. 18
      niucloud/app/model/personnel/Personnel.php
  30. 773
      niucloud/app/service/admin/lesson_course_teaching/LessonCourseTeachingService.php
  31. 233
      niucloud/app/service/admin/sys/SystemService.php
  32. 212
      niucloud/app/service/api/apiService/ChatService.php
  33. 3
      niucloud/app/service/api/apiService/PersonCourseScheduleService.php
  34. 18
      niucloud/app/service/api/apiService/TeachingResearchService.php
  35. 161
      niucloud/app/service/api/login/UnifiedLoginService.php
  36. 246
      niucloud/app/service/api/student/MessageService.php
  37. 298
      niucloud/app/service/core/performance/PerformanceConfigService.php
  38. 3
      niucloud/config/console.php
  39. 203
      niucloud/config/performance_config.php
  40. 137
      niucloud/config/teaching_management.php
  41. 84
      uniapp/api/apiRoute.js
  42. 4
      uniapp/common/config.js
  43. 503
      uniapp/pages-coach/coach/my/teaching_management_info.vue
  44. 749
      uniapp/pages-student/messages/conversation.vue
  45. 337
      uniapp/pages-student/messages/index.vue
  46. 18
      uniapp/pages.json
  47. 55
      uniapp/pages/common/home/index.vue
  48. 294
      uniapp/utils/messageRouter.js
  49. 1878
      学员端消息管理数据库分析报告.md

65
admin/src/app/api/lesson_course_teaching.ts

@ -339,4 +339,69 @@ export function setBindingTestPaperModule(params: Record<string, any>) {
)
}
/**
* Tab
* @returns
*/
export function getModuleConfigs() {
return request.get(`lesson_course_teaching/module_configs`)
}
/**
* table_type
* @param params
* @returns
*/
export function getUnifiedList(params: Record<string, any>) {
return request.get(`lesson_course_teaching/unified_list`, {
params,
})
}
/**
* table_type
* @param params
* @returns
*/
export function addUnified(params: Record<string, any>) {
return request.post('lesson_course_teaching/unified', params, {
showErrorMessage: true,
showSuccessMessage: true,
})
}
/**
* table_type
* @param params
* @returns
*/
export function editUnified(params: Record<string, any>) {
return request.put(
`lesson_course_teaching/unified/${params.id}`,
params,
{ showErrorMessage: true, showSuccessMessage: true }
)
}
/**
* table_type
* @param id
* @returns
*/
export function deleteUnified(id: number) {
return request.delete(`lesson_course_teaching/unified/${id}`, {
showErrorMessage: true,
showSuccessMessage: true,
})
}
/**
* table_type
* @param id
* @returns
*/
export function getUnifiedInfo(id: number) {
return request.get(`lesson_course_teaching/unified/${id}`)
}
// USER_CODE_END -- lesson_course_teaching

305
admin/src/app/views/binding_personnel/binding_personnel.vue

@ -193,6 +193,7 @@ import {
addLessonCourseTeaching,
editLessonCourseTeaching,
getLessonCourseTeachingInfo,
getUnifiedInfo,
getLessonCourseTeachingList,
getWithPersonnelDataList,
setBindingModule,
@ -223,18 +224,71 @@ let lessonCourseTeachingTable = reactive({
const lessonCourseTableRef = ref()
const boundUserIds = ref<number[]>([]) // ID
const selectedUserIds = ref<Set<number>>(new Set()) // ID
//
const disableAutoSelect = ref(false)
// 访
const visitedPages = ref(new Map<number, Set<number>>()) // -> ID
//
const isRestoringSelection = ref(false)
watch(
() => lessonCourseTeachingTable.data,
async (newData) => {
if (newData.length > 0) {
await nextTick()
newData.forEach((row) => {
lessonCourseTableRef.value.toggleRowSelection(row, false)
if(lessonCourseTeachingTable.searchParam.dept_id || lessonCourseTeachingTable.searchParam.role_id){
lessonCourseTableRef.value.toggleRowSelection(row, true)
}
console.log('数据变更 - 开始处理页面:', lessonCourseTeachingTable.page)
console.log('数据变更 - 恢复前全局状态:', Array.from(selectedUserIds.value))
//
isRestoringSelection.value = true
console.log('数据变更 - 设置恢复模式标志为 true')
// isRestoringSelectiontrue
if (lessonCourseTableRef.value) {
console.log('数据变更 - 执行clearSelection前,恢复标志:', isRestoringSelection.value)
lessonCourseTableRef.value.clearSelection()
console.log('数据变更 - clearSelection执行完成')
}
// DOMclearSelection
await nextTick()
await nextTick()
await nextTick()
//
if(!disableAutoSelect.value && (lessonCourseTeachingTable.searchParam.dept_id || lessonCourseTeachingTable.searchParam.role_id)){
newData.forEach((row) => {
lessonCourseTableRef.value.toggleRowSelection(row, true)
selectedUserIds.value.add(row.sys_user_id) //
})
})
//
const currentPageIds = new Set(newData.map(row => row.sys_user_id))
visitedPages.value.set(lessonCourseTeachingTable.page, currentPageIds)
//
disableAutoSelect.value = true
} else {
//
console.log('数据变更 - 开始恢复跨页选中状态,恢复标志:', isRestoringSelection.value)
restorePageSelections(newData)
}
console.log('数据变更 - 恢复后全局状态:', Array.from(selectedUserIds.value))
//
await nextTick()
await nextTick()
await nextTick()
//
console.log('数据变更 - 准备重置恢复标志')
isRestoringSelection.value = false
console.log('数据变更 - 恢复标志重置完成,最终全局状态:', Array.from(selectedUserIds.value))
}
},
{ deep: true }
@ -256,10 +310,66 @@ const multipleSelection = ref<[]>([])
const binding_module = ref('')
const handleSelectionChange = (val: []) => {
multipleSelection.value = val
// ID
const currentPageUserIds = lessonCourseTeachingTable.data.map((row: any) => row.sys_user_id)
// ID
const currentSelectedIds = val.map((row: any) => row.sys_user_id)
console.log('选择变更事件 - 页面:', lessonCourseTeachingTable.page, '恢复标志:', isRestoringSelection.value)
console.log('选择变更事件 - 选中ID:', currentSelectedIds)
console.log('选择变更事件 - 变更前全局状态:', Array.from(selectedUserIds.value))
// 使
if (isRestoringSelection.value) {
console.log('恢复模式 - 安全模式,只添加不删除')
//
currentSelectedIds.forEach(userId => {
selectedUserIds.value.add(userId)
})
// 访
const currentPageSelections = new Set(currentSelectedIds)
visitedPages.value.set(lessonCourseTeachingTable.page, currentPageSelections)
console.log('恢复模式 - 全局选中状态:', Array.from(selectedUserIds.value))
console.log('恢复模式 - 页面访问记录:', Object.fromEntries(visitedPages.value))
return
}
//
console.log('正常模式 - 精确更新全局状态')
// ID
currentPageUserIds.forEach(userId => {
if (!currentSelectedIds.includes(userId)) {
console.log('正常模式 - 删除用户ID:', userId)
selectedUserIds.value.delete(userId)
}
})
// ID
currentSelectedIds.forEach(userId => {
console.log('正常模式 - 添加用户ID:', userId)
selectedUserIds.value.add(userId)
})
// 访
const currentPageSelections = new Set(currentSelectedIds)
visitedPages.value.set(lessonCourseTeachingTable.page, currentPageSelections)
console.log('正常模式 - 最终状态:')
console.log(' 当前页面:', lessonCourseTeachingTable.page)
console.log(' 当前页面用户ID:', currentPageUserIds)
console.log(' 当前页面选中ID:', currentSelectedIds)
console.log(' 访问过的页面状态:', Object.fromEntries(visitedPages.value))
console.log(' 全局选中状态:', Array.from(selectedUserIds.value))
}
const loadLessonCourseTeachingList = (page: number = 1) => {
multipleSelection.value = [];
// watch
lessonCourseTeachingTable.loading = true
lessonCourseTeachingTable.page = page
@ -282,6 +392,12 @@ loadLessonCourseTeachingList()
const resetForm = (page: number = 1) => {
lessonCourseTeachingTable.searchParam.name = ''
lessonCourseTeachingTable.searchParam.phone = ''
lessonCourseTeachingTable.searchParam.dept_id = ''
lessonCourseTeachingTable.searchParam.role_id = ''
//
disableAutoSelect.value = false
visitedPages.value.clear() // 访
isRestoringSelection.value = false //
loadLessonCourseTeachingList()
}
@ -322,34 +438,77 @@ const formRules = computed(() => {
const emit = defineEmits(['complete'])
//
const getAllSelectedUsers = async () => {
if (selectedUserIds.value.size === 0) {
return []
}
const selectedIds = Array.from(selectedUserIds.value)
console.log('准备获取所有选中用户信息,ID列表:', selectedIds)
try {
//
const res = await getWithPersonnelDataList({ limit: 100 })
const allUsers = res.data.data || []
//
const selectedUsers = allUsers.filter(user => selectedIds.includes(user.sys_user_id))
console.log('获取到的选中用户信息:', selectedUsers.map(u => ({name: u.name, id: u.sys_user_id})))
return selectedUsers
} catch (error) {
console.error('获取用户信息失败:', error)
return []
}
}
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if(multipleSelection.value.length == 0) {
if(selectedUserIds.value.size === 0) {
ElMessage.error('请选择数据');
return;
}
let data = {
id: BindingId.value,
user_permission: multipleSelection.value
.map((item) => item.sys_user_id)
.join(','),
user_permission_name: multipleSelection.value
.map((item) => item.name)
.join(','),
table_type: binding_module.value,
}
setBindingModule(data)
.then((res) => {
loading.value = true
try {
//
const allSelectedUsers = await getAllSelectedUsers()
if (allSelectedUsers.length === 0) {
ElMessage.error('获取选中用户信息失败');
loading.value = false
showDialog.value = false
emit('complete')
})
.catch((err) => {
loading.value = false
})
return;
}
const data = {
id: BindingId.value,
user_permission: allSelectedUsers
.map((item) => item.sys_user_id)
.join(','),
user_permission_name: allSelectedUsers
.map((item) => item.name)
.join(','),
table_type: binding_module.value,
}
console.log('提交的绑定数据:', data)
await setBindingModule(data)
loading.value = false
showDialog.value = false
emit('complete')
} catch (err) {
console.error('保存失败:', err)
loading.value = false
}
}
//
@ -387,6 +546,100 @@ setUserPermissionList()
const setFormData = async (row: any = null) => {
BindingId.value = row.id
binding_module.value = row.table_type
//
boundUserIds.value = []
selectedUserIds.value.clear()
visitedPages.value.clear() // 访
disableAutoSelect.value = false
isRestoringSelection.value = false //
//
if (row.id) {
try {
// 使
let res
try {
res = await getUnifiedInfo(row.id)
} catch (e) {
// 使
res = await getLessonCourseTeachingInfo(row.id)
}
const detailInfo = res.data
// ID
if (detailInfo.user_permission) {
boundUserIds.value = detailInfo.user_permission.split(',').map(id => parseInt(id.trim()))
//
boundUserIds.value.forEach(id => selectedUserIds.value.add(id))
console.log('已绑定的用户ID:', boundUserIds.value)
console.log('初始选中状态:', Array.from(selectedUserIds.value))
//
if (lessonCourseTeachingTable.data.length > 0) {
setTimeout(() => {
preSelectBoundUsers(boundUserIds.value)
}, 100)
}
}
} catch (error) {
console.error('获取人员绑定信息失败:', error)
}
}
}
//
const restorePageSelections = (pageData: any[]) => {
if (!lessonCourseTableRef.value || !pageData.length) return
let restoredCount = 0
pageData.forEach((row: any) => {
// ID
if (selectedUserIds.value.has(row.sys_user_id)) {
lessonCourseTableRef.value.toggleRowSelection(row, true)
restoredCount++
console.log('恢复选中用户:', row.name, 'ID:', row.sys_user_id)
}
})
// 访
const currentPageSelections = new Set(
pageData
.filter(row => selectedUserIds.value.has(row.sys_user_id))
.map(row => row.sys_user_id)
)
visitedPages.value.set(lessonCourseTeachingTable.page, currentPageSelections)
console.log('恢复选中状态完成,当前页面:', lessonCourseTeachingTable.page)
console.log('恢复的选中用户数:', restoredCount)
console.log('当前页面选中状态:', Array.from(currentPageSelections))
}
//
const preSelectBoundUsers = (userIds: number[]) => {
console.log('开始预选用户,用户ID列表:', userIds)
console.log('表格数据:', lessonCourseTeachingTable.data)
if (lessonCourseTableRef.value && lessonCourseTeachingTable.data.length > 0) {
let selectedCount = 0
lessonCourseTeachingTable.data.forEach((row: any) => {
// IDID
if (userIds.includes(row.sys_user_id)) {
console.log('选中用户:', row.name, 'ID:', row.sys_user_id)
lessonCourseTableRef.value.toggleRowSelection(row, true)
selectedCount++
}
})
console.log('预选完成,共选中', selectedCount, '个用户')
console.log('当前selectedUserIds状态:', Array.from(selectedUserIds.value))
} else {
console.log('表格引用或数据不存在,无法进行预选')
}
}
//

359
admin/src/app/views/customer_resources/components/Messages.vue

@ -0,0 +1,359 @@
<template>
<div class="messages-container">
<!-- 消息统计 -->
<el-row :gutter="16" class="mb-4">
<el-col :span="6">
<el-statistic title="总消息数" :value="messageStats.total" />
</el-col>
<el-col :span="6">
<el-statistic title="未读消息" :value="messageStats.unread" />
</el-col>
<el-col :span="6">
<el-statistic title="已读消息" :value="messageStats.read" />
</el-col>
<el-col :span="6">
<el-statistic title="最后消息时间" :value="messageStats.lastTime" />
</el-col>
</el-row>
<!-- 消息类型筛选 -->
<el-row class="mb-4">
<el-col>
<el-radio-group v-model="filterType" @change="loadMessages">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="system">系统消息</el-radio-button>
<el-radio-button label="notification">通知公告</el-radio-button>
<el-radio-button label="homework">作业任务</el-radio-button>
<el-radio-button label="feedback">反馈评价</el-radio-button>
<el-radio-button label="reminder">课程提醒</el-radio-button>
<el-radio-button label="order">订单消息</el-radio-button>
</el-radio-group>
</el-col>
</el-row>
<!-- 搜索功能 -->
<el-row class="mb-4">
<el-col>
<el-form :inline="true" :model="searchForm" @submit.prevent="searchMessages">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="搜索消息标题或内容"
clearable
style="width: 200px;"
/>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 300px;"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchMessages">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
<!-- 消息列表 -->
<el-table :data="messageList" v-loading="loading" stripe>
<el-table-column prop="message_type" label="消息类型" width="120" align="center">
<template #default="{ row }">
<el-tag :type="getMessageTypeColor(row.message_type)">
{{ getMessageTypeText(row.message_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="title" label="消息标题" width="200" show-overflow-tooltip />
<el-table-column prop="content" label="消息内容" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ row.content.length > 50 ? row.content.substring(0, 50) + '...' : row.content }}</span>
</template>
</el-table-column>
<el-table-column prop="from_name" label="发送人" width="120" align="center" />
<el-table-column prop="is_read" label="已读状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.is_read ? 'success' : 'danger'">
{{ row.is_read ? '已读' : '未读' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="发送时间" width="160" />
<el-table-column prop="read_time" label="阅读时间" width="160">
<template #default="{ row }">
<span>{{ row.read_time || '未读' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="viewMessageDetail(row)">
查看
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="mt-4 flex justify-end">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadMessages"
@size-change="loadMessages"
/>
</div>
<!-- 消息详情弹窗 -->
<el-dialog v-model="detailDialogVisible" title="消息详情" width="60%">
<div v-if="selectedMessage">
<el-descriptions :column="2" border>
<el-descriptions-item label="消息类型">
<el-tag :type="getMessageTypeColor(selectedMessage.message_type)">
{{ getMessageTypeText(selectedMessage.message_type) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="发送时间">
{{ selectedMessage.created_at }}
</el-descriptions-item>
<el-descriptions-item label="发送人">
{{ selectedMessage.from_name }}
</el-descriptions-item>
<el-descriptions-item label="已读状态">
<el-tag :type="selectedMessage.is_read ? 'success' : 'danger'">
{{ selectedMessage.is_read ? '已读' : '未读' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="阅读时间">
{{ selectedMessage.read_time || '未读' }}
</el-descriptions-item>
<el-descriptions-item label="业务关联">
{{ selectedMessage.business_type ? `${selectedMessage.business_type}:${selectedMessage.business_id}` : '无' }}
</el-descriptions-item>
<el-descriptions-item label="消息标题" :span="2">
{{ selectedMessage.title }}
</el-descriptions-item>
</el-descriptions>
<div class="mt-4">
<h4>消息内容</h4>
<div class="message-content p-4 bg-gray-50 rounded">
{{ selectedMessage.content }}
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
// API
// import { getCustomerMessages, getCustomerMessageStats } from '@/app/api/customer_resources'
const props = defineProps({
customer_resource_id: {
type: Number,
required: true
}
})
const loading = ref(false)
const messageList = ref([])
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const filterType = ref('all')
const detailDialogVisible = ref(false)
const selectedMessage = ref(null)
const searchForm = reactive({
keyword: '',
dateRange: []
})
const messageStats = reactive({
total: 0,
read: 0,
unread: 0,
lastTime: ''
})
const loadMessages = async () => {
loading.value = true
try {
// APIAPI
console.log('Loading messages for customer:', props.customer_resource_id)
//
const mockResponse = {
code: 1,
data: {
list: [
{
id: 1,
message_type: 'system',
title: '欢迎加入系统',
content: '欢迎您使用我们的服务,如有问题请联系客服。',
from_name: '系统',
is_read: true,
read_time: '2024-01-15 10:30:00',
created_at: '2024-01-15 09:00:00',
business_type: null,
business_id: null
},
{
id: 2,
message_type: 'order',
title: '订单状态变更通知',
content: '您的订单已确认,正在处理中。',
from_name: '张老师',
is_read: false,
read_time: null,
created_at: '2024-01-16 14:20:00',
business_type: 'order',
business_id: 123
}
],
total: 2
}
}
if (mockResponse.code === 1) {
messageList.value = mockResponse.data.list
total.value = mockResponse.data.total
}
/*
// API
const response = await getCustomerMessages({
customer_resource_id: props.customer_resource_id,
message_type: filterType.value === 'all' ? '' : filterType.value,
keyword: searchForm.keyword,
start_date: searchForm.dateRange?.[0],
end_date: searchForm.dateRange?.[1],
page: currentPage.value,
limit: pageSize.value
})
if (response.code === 1) {
messageList.value = response.data.list
total.value = response.data.total
}
*/
} catch (error) {
console.error('获取消息列表失败:', error)
} finally {
loading.value = false
}
}
const loadMessageStats = async () => {
try {
//
Object.assign(messageStats, {
total: 15,
unread: 3,
read: 12,
lastTime: '2024-01-16 14:20:00'
})
/*
// API
const response = await getCustomerMessageStats({
customer_resource_id: props.customer_resource_id
})
if (response.code === 1) {
Object.assign(messageStats, response.data)
}
*/
} catch (error) {
console.error('获取消息统计失败:', error)
}
}
const searchMessages = () => {
currentPage.value = 1
loadMessages()
}
const resetSearch = () => {
searchForm.keyword = ''
searchForm.dateRange = []
currentPage.value = 1
loadMessages()
}
const getMessageTypeText = (type) => {
const typeMap = {
'text': '文本消息',
'img': '图片消息',
'system': '系统消息',
'notification': '通知公告',
'homework': '作业任务',
'feedback': '反馈评价',
'reminder': '课程提醒',
'order': '订单消息',
'student_courses': '课程变动',
'person_course_schedule': '课程安排'
}
return typeMap[type] || type
}
const getMessageTypeColor = (type) => {
const colorMap = {
'system': 'primary',
'notification': 'warning',
'homework': 'danger',
'feedback': 'success',
'reminder': 'info',
'order': 'warning',
'student_courses': 'success',
'person_course_schedule': 'info'
}
return colorMap[type] || 'default'
}
const viewMessageDetail = (message) => {
selectedMessage.value = message
detailDialogVisible.value = true
}
onMounted(() => {
loadMessages()
loadMessageStats()
})
</script>
<style scoped>
.messages-container {
.message-content {
white-space: pre-wrap;
word-break: break-all;
}
}
.search-form {
background: #f8f9fa;
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
</style>

6
admin/src/app/views/customer_resources/components/UserProfile.vue

@ -30,6 +30,7 @@
<el-tab-pane label="订单列表" name="orders" />
<el-tab-pane label="沟通记录列表" name="communication_records" />
<el-tab-pane label="赠品记录" name="gift_records" />
<el-tab-pane label="消息记录" name="messages" />
</el-tabs>
<!-- 六要素信息卡片 -->
@ -90,6 +91,10 @@
<GiftRecords :customer_resource_id="user.id"/>
</el-card>
<el-card v-if="activeTab === 'messages'">
<Messages :customer_resource_id="user.id"/>
</el-card>
</el-dialog>
</template>
@ -102,6 +107,7 @@
import Orders from '@/app/views/customer_resources/components/order_table.vue'
import CommunicationRecords from '@/app/views/communication_records/communication_records.vue'
import GiftRecords from '@/app/views/customer_resources/components/gift_records.vue'
import Messages from '@/app/views/customer_resources/components/Messages.vue'
let showDialog = ref(false)

364
admin/src/app/views/jlyj/jlyj.vue

@ -3,200 +3,246 @@
<el-card class="box-card !border-none" shadow="never" v-loading="loading">
<div class="flex justify-between items-center">
<span class="text-lg">{{ pageName }}</span>
<el-button type="primary" @click="addStage"> 新增阶段 </el-button>
</div>
</el-card>
<!-- 课时提成配置 -->
<el-card class="box-card !border-none" shadow="never" style="margin-bottom: 20px">
<template #header>
<div class="flex justify-between items-center">
<div>
<h3>课时提成配置</h3>
<p class="text-sm text-gray-500">教练上课的课时提成</p>
</div>
<el-button type="primary" @click="addCourseCommission">新增课程提成</el-button>
</div>
</template>
<el-table :data="formData.course_commission" border>
<el-table-column prop="course_name" label="课程名称" width="200">
<template #default="{ row }">
<el-select v-model="row.course_id" placeholder="选择课程" @change="updateCourseName(row)">
<el-option
v-for="course in availableCourses"
:key="course.course_id"
:label="course.course_name"
:value="course.course_id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column prop="sales_commission" label="销售提成(元)">
<template #default="{ row }">
<el-input-number
v-model="row.sales_commission"
:min="0"
placeholder="销售提成"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column prop="coach_commission" label="教练提成(元)">
<template #default="{ row }">
<el-input-number
v-model="row.coach_commission"
:min="0"
placeholder="教练提成"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ $index }">
<el-button
type="danger"
size="small"
@click="removeCourseCommission($index)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 订单提成配置 -->
<el-card class="box-card !border-none" shadow="never">
<div>
基础绩效配置
</div>
<div
class="flex items-center justify-between p-[10px] table-item-border bg"
>
<span class="text-base w-[230px]">阶段名称</span>
<span class="text-base w-[110px] text-center">底薪</span>
</div>
<template #header>
<div class="flex justify-between items-center">
<div>
<h3>订单提成配置</h3>
<p class="text-sm text-gray-500">教练作为销售时的订单提成</p>
</div>
<el-button type="primary" @click="addOrderCommission">新增续费率规则</el-button>
</div>
</template>
<el-collapse v-model="activeNames" accordion>
<el-collapse-item
v-for="(stage, index) in stages"
:key="stage.id"
:name="stage.id"
>
<template #title>
<div class="collapse-title">
<span class="title-name">{{ stage.name }}</span>
<span class="arrow">{{ stage.price }} </span>
<!-- <span class="arrow">&gt;</span> -->
</div>
<el-table :data="formData.order_commission" border>
<el-table-column prop="min_rate" label="最低续费率(%)">
<template #default="{ row }">
<el-input-number
v-model="row.min_rate"
:min="0"
:max="100"
placeholder="最低续费率"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column prop="max_rate" label="最高续费率(%)">
<template #default="{ row }">
<el-input-number
v-model="row.max_rate"
:min="0"
:max="100"
placeholder="最高续费率"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column prop="commission" label="绩效单价(元/单)">
<template #default="{ row }">
<el-input-number
v-model="row.commission"
:min="0"
placeholder="绩效单价"
style="width: 100%"
/>
</template>
</el-table-column>
<el-form label-width="100px" style="margin-bottom: 10px">
<el-form-item label="阶段名称">
<el-input v-model="stage.name" placeholder="请输入阶段名称" />
</el-form-item>
</el-form>
<el-form label-width="100px" style="margin-bottom: 10px">
<el-form-item label="阶段底薪">
<el-input v-model="stage.price" placeholder="请输入阶段底薪" />
</el-form-item>
</el-form>
<el-button type="success" size="small" @click="addRule(stage)"
>新增规则</el-button
>
<el-table :data="stage.rules" border style="margin-top: 10px">
<el-table-column prop="renewal_standard_min" label="续费上限">
<template #default="{ row }">
<el-input
v-model="row.renewal_standard_min"
placeholder="请输入续费上限"
/>
</template>
</el-table-column>
<el-table-column prop="renewal_standard_max" label="续费下限">
<template #default="{ row }">
<el-input
v-model="row.renewal_standard_max"
placeholder="请输入续费下限"
/>
</template>
</el-table-column>
<el-table-column prop="renewal_commission" label="续费提成">
<template #default="{ row }">
<el-input v-model="row.renewal_commission" placeholder="%" />
</template>
</el-table-column>
<el-table-column prop="new_count_min" label="新单成交数上限">
<template #default="{ row }">
<el-input v-model="row.new_count_min" />
</template>
</el-table-column>
<el-table-column prop="new_count_max" label="新单成交数下限">
<template #default="{ row }">
<el-input v-model="row.new_count_max" />
</template>
</el-table-column>
<el-table-column prop="new_move_5" label="新招(5+1)x3">
<template #default="{ row }">
<el-input v-model="row.new_move_5" />
</template>
</el-table-column>
<el-table-column prop="new_move_7" label="新招(7+1)x3">
<template #default="{ row }">
<el-input v-model="row.new_move_7" />
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ $index }">
<el-button
type="danger"
size="small"
@click="removeRule(stage, $index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-button
type="danger"
size="small"
style="margin-top: 10px"
@click="removeStage(index)"
>删除该阶段</el-button
>
</el-collapse-item>
</el-collapse>
<div style="text-align: right; margin-top: 20px">
<el-button type="primary" @click="onSave">提交保存</el-button>
</div>
<el-table-column prop="description" label="规则说明">
<template #default="{ row }">
<el-input
v-model="row.description"
placeholder="规则说明"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ $index }">
<el-button
type="danger"
size="small"
@click="removeOrderCommission($index)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<div style="text-align: right; margin-top: 20px">
<el-button type="primary" @click="onSave">提交保存</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { jlyjConfig, getJlyjConfig } from '@/app/api/sys'
import { jlyjConfig, getJlyjConfig, getXsyjConfig } from '@/app/api/sys'
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
const route = useRoute()
const pageName = route.meta.title
const loading = ref(true)
const stages = ref([])
const activeNames = ref(null)
function addStage() {
const newStage = {
name: '默认阶段',
price: 0,
rules: [
{
renewal_standard_min: '',
renewal_standard_max: '',
renewal_commission: '',
new_count_min: '',
new_count_max: '',
new_move_5: '',
new_move_7: '',
},
],
const formData = ref({
course_commission: [],
order_commission: []
})
const availableCourses = ref([])
// API
const getAvailableCourses = async () => {
try {
const response = await getXsyjConfig()
if (response.data?.course_type) {
availableCourses.value = response.data.course_type
}
} catch (error) {
console.error('获取课程列表失败:', error)
}
stages.value.push(newStage)
activeNames.value = newStage.name
}
function removeStage(index) {
stages.value.splice(index, 1)
//
function addCourseCommission() {
formData.value.course_commission.push({
course_id: null,
course_name: '',
sales_commission: 0,
coach_commission: 0
})
}
function addRule(stage) {
stage.rules.push({
renewal_standard_min: '',
renewal_standard_max: '',
renewal_commission: '',
new_count_min: '',
new_count_max: '',
new_move_5: '',
new_move_7: '',
})
//
function removeCourseCommission(index) {
formData.value.course_commission.splice(index, 1)
}
function removeRule(stage, ruleIndex) {
if (stage.rules.length === 1) {
ElMessage.warning('至少保留一条规则')
return
//
function updateCourseName(row) {
const selectedCourse = availableCourses.value.find(course => course.course_id === row.course_id)
if (selectedCourse) {
row.course_name = selectedCourse.course_name
}
stage.rules.splice(ruleIndex, 1)
}
//
function addOrderCommission() {
formData.value.order_commission.push({
min_rate: 0,
max_rate: 100,
commission: 0,
description: ''
})
}
//
function removeOrderCommission(index) {
formData.value.order_commission.splice(index, 1)
}
const setFormData = async () => {
const data = await (await getJlyjConfig()).data
stages.value = data
loading.value = false
try {
loading.value = true
const response = await getJlyjConfig()
const data = response.data
//
formData.value = {
course_commission: data.course_commission || [],
order_commission: data.order_commission || []
}
//
await getAvailableCourses()
loading.value = false
} catch (error) {
console.error('加载数据失败:', error)
loading.value = false
}
}
//
setFormData()
const onSave = async () => {
jlyjConfig(stages.value)
.then(() => {
loading.value = true
setFormData()
})
.catch(() => {
loading.value = false
})
try {
loading.value = true
await jlyjConfig(formData.value)
ElMessage.success('保存成功')
await setFormData()
} catch (error) {
console.error('保存失败:', error)
ElMessage.error('保存失败')
loading.value = false
}
}
</script>

284
admin/src/app/views/lesson_course_teaching/components/components_backup/Jump-lesson-library-edit.vue

@ -0,0 +1,284 @@
<template>
<el-dialog
v-model="showDialog"
:title="
formData.id ? t('updateJumpLessonLibrary') : t('addJumpLessonLibrary')
"
width="50%"
class="diy-dialog-wrap"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="page-form"
v-loading="loading"
>
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
clearable
:placeholder="t('titlePlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('image')" prop="image">
<upload-image v-model="formData.image" />
</el-form-item>
<el-form-item :label="t('type')" prop="type">
<el-select
class="input-width"
v-model="formData.type"
clearable
:placeholder="t('typePlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in typeList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('url1')" v-if="formData.type == 3">
<upload-image v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url2')" v-if="formData.type == 2">
<upload-file v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url3')" v-if="formData.type == 1">
<upload-video v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-radio-group
v-model="formData.status"
:placeholder="t('statusPlaceholder')"
>
<el-radio
v-for="(item, index) in statusList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item :label="t('userPermission')" prop="user_permission">-->
<!-- <el-checkbox-group v-model="formData.user_permission" :placeholder="t('userPermissionPlaceholder')">-->
<!-- <el-checkbox-->
<!-- v-for="(item, index) in userPermissionList"-->
<!-- :key="index"-->
<!-- :label="item['sys_user_id']">-->
<!-- {{ item['name'] }}-->
<!-- </el-checkbox>-->
<!-- </el-checkbox-group>-->
<!-- </el-form-item>-->
</el-form>
<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
>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import {
addJumpLessonLibrary,
editJumpLessonLibrary,
getLessonCourseTeachingInfo,
getWithPersonnelDataList,
} from '@/app/api/lesson_course_teaching'
let showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: '',
title: '',
image: '',
type: '',
content: '',
status: '',
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' },
],
image: [
{ required: true, message: t('imagePlaceholder'), trigger: 'blur' },
],
type: [{ required: true, message: t('typePlaceholder'), trigger: 'blur' }],
content: [
{ required: true, message: t('contentPlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
// ,
// user_permission: [
// { required: true, message: t('userPermissionPlaceholder'), trigger: 'blur' },
//
// ]
}
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
let save = formData.id ? editJumpLessonLibrary : addJumpLessonLibrary
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
save(data)
.then((res) => {
loading.value = false
showDialog.value = false
formData.url = ''
emit('complete')
})
.catch((err) => {
loading.value = false
})
}
})
}
//
let typeList = ref([])
const typeDictList = async () => {
typeList.value = await (await useDictionary('material_type')).data.dictionary
}
typeDictList()
watch(
() => typeList.value,
() => {
formData.type = typeList.value[0].value
}
)
let statusList = ref([])
const statusDictList = async () => {
statusList.value = await (
await useDictionary('course_status')
).data.dictionary
}
statusDictList()
watch(
() => statusList.value,
() => {
formData.status = statusList.value[0].value
}
)
const userPermissionList = ref([] as any[])
// const setUserPermissionList = async () => {
// userPermissionList.value = await (await getWithPersonnelDataList({})).data
// }
// setUserPermissionList()
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getLessonCourseTeachingInfo(row.id)).data
if (data)
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
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()
}
}
//
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()
}
}
//
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()
}
}
//
const numberVerify = (rule: any, value: any, callback: any) => {
if (!Number.isInteger(value)) {
callback(new Error(t('generateNumber')))
} else {
callback()
}
}
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label {
height: auto !important;
}
</style>

284
admin/src/app/views/lesson_course_teaching/components/components_backup/basketball-course-teaching-edit.vue

@ -0,0 +1,284 @@
<template>
<el-dialog
v-model="showDialog"
:title="
formData.id
? t('editBasketballTeachingLibrary')
: t('addBasketballTeachingLibrary')
"
width="50%"
class="diy-dialog-wrap"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="page-form"
v-loading="loading"
>
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
clearable
:placeholder="t('titlePlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('image')" prop="image">
<upload-image v-model="formData.image" />
</el-form-item>
<el-form-item :label="t('type')" prop="type">
<el-select
class="input-width"
v-model="formData.type"
clearable
:placeholder="t('typePlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in typeList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('url1')" v-if="formData.type == 3">
<upload-image v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url2')" v-if="formData.type == 2">
<upload-file v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url3')" v-if="formData.type == 1">
<upload-video v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-radio-group
v-model="formData.status"
:placeholder="t('statusPlaceholder')"
>
<el-radio
v-for="(item, index) in statusList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item :label="t('userPermission')" prop="user_permission">-->
<!-- <el-checkbox-group v-model="formData.user_permission" :placeholder="t('userPermissionPlaceholder')">-->
<!-- <el-checkbox-->
<!-- v-for="(item, index) in userPermissionList"-->
<!-- :key="index"-->
<!-- :label="item['sys_user_id']">-->
<!-- {{ item['name'] }}-->
<!-- </el-checkbox>-->
<!-- </el-checkbox-group>-->
<!-- </el-form-item>-->
</el-form>
<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
>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import {
addBasketballTeachingLibrary,
editBasketballTeachingLibrary,
getLessonCourseTeachingInfo,
getWithPersonnelDataList,
} from '@/app/api/lesson_course_teaching'
let showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: '',
title: '',
image: '',
type: '',
content: '',
status: '',
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' },
],
image: [
{ required: true, message: t('imagePlaceholder'), trigger: 'blur' },
],
type: [{ required: true, message: t('typePlaceholder'), trigger: 'blur' }],
content: [
{ required: true, message: t('contentPlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
}
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
let save = formData.id
? editBasketballTeachingLibrary
: addBasketballTeachingLibrary
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
save(data)
.then((res) => {
loading.value = false
showDialog.value = false
formData.url = ''
emit('complete')
})
.catch((err) => {
loading.value = false
})
}
})
}
//
let typeList = ref([])
const typeDictList = async () => {
typeList.value = await (await useDictionary('material_type')).data.dictionary
}
typeDictList()
watch(
() => typeList.value,
() => {
formData.type = typeList.value[0].value
}
)
let statusList = ref([])
const statusDictList = async () => {
statusList.value = await (
await useDictionary('course_status')
).data.dictionary
}
statusDictList()
watch(
() => statusList.value,
() => {
formData.status = statusList.value[0].value
}
)
const userPermissionList = ref([] as any[])
// const setUserPermissionList = async () => {
// userPermissionList.value = await (await getWithPersonnelDataList({})).data
// }
// setUserPermissionList()
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getLessonCourseTeachingInfo(row.id)).data
if (data)
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
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()
}
}
//
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()
}
}
//
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()
}
}
//
const numberVerify = (rule: any, value: any, callback: any) => {
if (!Number.isInteger(value)) {
callback(new Error(t('generateNumber')))
} else {
callback()
}
}
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label {
height: auto !important;
}
</style>

280
admin/src/app/views/lesson_course_teaching/components/components_backup/en-course-teaching-edit.vue

@ -0,0 +1,280 @@
<template>
<el-dialog
v-model="showDialog"
:title="
formData.id ? t('editEnTeachingLibrary') : t('addEnTeachingLibrary')
"
width="50%"
class="diy-dialog-wrap"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="page-form"
v-loading="loading"
>
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
clearable
:placeholder="t('titlePlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('image')" prop="image">
<upload-image v-model="formData.image" />
</el-form-item>
<el-form-item :label="t('type')" prop="type">
<el-select
class="input-width"
v-model="formData.type"
clearable
:placeholder="t('typePlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in typeList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('url1')" v-if="formData.type == 3">
<upload-image v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url2')" v-if="formData.type == 2">
<upload-file v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url3')" v-if="formData.type == 1">
<upload-video v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-radio-group
v-model="formData.status"
:placeholder="t('statusPlaceholder')"
>
<el-radio
v-for="(item, index) in statusList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item :label="t('userPermission')" prop="user_permission">-->
<!-- <el-checkbox-group v-model="formData.user_permission" :placeholder="t('userPermissionPlaceholder')">-->
<!-- <el-checkbox-->
<!-- v-for="(item, index) in userPermissionList"-->
<!-- :key="index"-->
<!-- :label="item['sys_user_id']">-->
<!-- {{ item['name'] }}-->
<!-- </el-checkbox>-->
<!-- </el-checkbox-group>-->
<!-- </el-form-item>-->
</el-form>
<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
>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import {
addEnTeachingLibrary,
editEnTeachingLibrary,
getLessonCourseTeachingInfo,
getWithPersonnelDataList,
} from '@/app/api/lesson_course_teaching'
let showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: '',
title: '',
image: '',
type: '',
content: '',
status: '',
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' },
],
image: [
{ required: true, message: t('imagePlaceholder'), trigger: 'blur' },
],
type: [{ required: true, message: t('typePlaceholder'), trigger: 'blur' }],
content: [
{ required: true, message: t('contentPlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
}
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
let save = formData.id ? editEnTeachingLibrary : addEnTeachingLibrary
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
save(data)
.then((res) => {
loading.value = false
showDialog.value = false
formData.url = ''
emit('complete')
})
.catch((err) => {
loading.value = false
})
}
})
}
//
let typeList = ref([])
const typeDictList = async () => {
typeList.value = await (await useDictionary('material_type')).data.dictionary
}
typeDictList()
watch(
() => typeList.value,
() => {
formData.type = typeList.value[0].value
}
)
let statusList = ref([])
const statusDictList = async () => {
statusList.value = await (
await useDictionary('course_status')
).data.dictionary
}
statusDictList()
watch(
() => statusList.value,
() => {
formData.status = statusList.value[0].value
}
)
const userPermissionList = ref([] as any[])
// const setUserPermissionList = async () => {
// userPermissionList.value = await (await getWithPersonnelDataList({})).data
// }
// setUserPermissionList()
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getLessonCourseTeachingInfo(row.id)).data
if (data)
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
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()
}
}
//
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()
}
}
//
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()
}
}
//
const numberVerify = (rule: any, value: any, callback: any) => {
if (!Number.isInteger(value)) {
callback(new Error(t('generateNumber')))
} else {
callback()
}
}
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label {
height: auto !important;
}
</style>

284
admin/src/app/views/lesson_course_teaching/components/components_backup/lesson-course-teaching-edit.vue

@ -0,0 +1,284 @@
<template>
<el-dialog
v-model="showDialog"
:title="
formData.id
? t('updateLessonCourseTeaching')
: t('addLessonCourseTeaching')
"
width="50%"
class="diy-dialog-wrap"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="page-form"
v-loading="loading"
>
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
clearable
:placeholder="t('titlePlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('image')" prop="image">
<upload-image v-model="formData.image" />
</el-form-item>
<el-form-item :label="t('type')" prop="type">
<el-select
class="input-width"
v-model="formData.type"
clearable
:placeholder="t('typePlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in typeList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('url1')" v-if="formData.type == 3">
<upload-image v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url2')" v-if="formData.type == 2">
<upload-file v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url3')" v-if="formData.type == 1">
<upload-video v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-radio-group
v-model="formData.status"
:placeholder="t('statusPlaceholder')"
>
<el-radio
v-for="(item, index) in statusList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item :label="t('userPermission')" prop="user_permission">-->
<!-- <el-checkbox-group v-model="formData.user_permission" :placeholder="t('userPermissionPlaceholder')">-->
<!-- <el-checkbox-->
<!-- v-for="(item, index) in userPermissionList"-->
<!-- :key="index"-->
<!-- :label="item['sys_user_id']">-->
<!-- {{ item['name'] }}-->
<!-- </el-checkbox>-->
<!-- </el-checkbox-group>-->
<!-- </el-form-item>-->
</el-form>
<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
>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import {
addLessonCourseTeaching,
editLessonCourseTeaching,
getLessonCourseTeachingInfo,
getWithPersonnelDataList,
} from '@/app/api/lesson_course_teaching'
let showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: '',
title: '',
image: '',
type: '',
content: '',
status: '',
url:''
// user_permission: [],
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' },
],
image: [
{ required: true, message: t('imagePlaceholder'), trigger: 'blur' },
],
type: [{ required: true, message: t('typePlaceholder'), trigger: 'blur' }],
content: [
{ required: true, message: t('contentPlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
}
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
let save = formData.id ? editLessonCourseTeaching : addLessonCourseTeaching
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
save(data)
.then((res) => {
loading.value = false
showDialog.value = false
formData.url = ''
emit('complete')
})
.catch((err) => {
loading.value = false
})
}
})
}
//
let typeList = ref([])
const typeDictList = async () => {
typeList.value = await (await useDictionary('material_type')).data.dictionary
}
typeDictList()
watch(
() => typeList.value,
() => {
formData.type = typeList.value[0].value
}
)
let statusList = ref([])
const statusDictList = async () => {
statusList.value = await (
await useDictionary('course_status')
).data.dictionary
}
statusDictList()
watch(
() => statusList.value,
() => {
formData.status = statusList.value[0].value
}
)
const userPermissionList = ref([] as any[])
// const setUserPermissionList = async () => {
// userPermissionList.value = await (await getWithPersonnelDataList({})).data
// }
// setUserPermissionList()
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getLessonCourseTeachingInfo(row.id)).data
if (data)
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
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()
}
}
//
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()
}
}
//
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()
}
}
//
const numberVerify = (rule: any, value: any, callback: any) => {
if (!Number.isInteger(value)) {
callback(new Error(t('generateNumber')))
} else {
callback()
}
}
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label {
height: auto !important;
}
</style>

280
admin/src/app/views/lesson_course_teaching/components/components_backup/ninja-teaching-edit.vue

@ -0,0 +1,280 @@
<template>
<el-dialog
v-model="showDialog"
:title="
formData.id ? t('editNinjaTeachingLibrary') : t('addNinjaTeachingLibrary')
"
width="50%"
class="diy-dialog-wrap"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="page-form"
v-loading="loading"
>
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
clearable
:placeholder="t('titlePlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('image')" prop="image">
<upload-image v-model="formData.image" />
</el-form-item>
<el-form-item :label="t('type')" prop="type">
<el-select
class="input-width"
v-model="formData.type"
clearable
:placeholder="t('typePlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in typeList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('url1')" v-if="formData.type == 3">
<upload-image v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url2')" v-if="formData.type == 2">
<upload-file v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url3')" v-if="formData.type == 1">
<upload-video v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-radio-group
v-model="formData.status"
:placeholder="t('statusPlaceholder')"
>
<el-radio
v-for="(item, index) in statusList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item :label="t('userPermission')" prop="user_permission">-->
<!-- <el-checkbox-group v-model="formData.user_permission" :placeholder="t('userPermissionPlaceholder')">-->
<!-- <el-checkbox-->
<!-- v-for="(item, index) in userPermissionList"-->
<!-- :key="index"-->
<!-- :label="item['sys_user_id']">-->
<!-- {{ item['name'] }}-->
<!-- </el-checkbox>-->
<!-- </el-checkbox-group>-->
<!-- </el-form-item>-->
</el-form>
<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
>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import {
addNinjaTeachingLibrary,
editNinjaTeachingLibrary,
getLessonCourseTeachingInfo,
getWithPersonnelDataList,
} from '@/app/api/lesson_course_teaching'
let showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: '',
title: '',
image: '',
type: '',
content: '',
status: '',
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' },
],
image: [
{ required: true, message: t('imagePlaceholder'), trigger: 'blur' },
],
type: [{ required: true, message: t('typePlaceholder'), trigger: 'blur' }],
content: [
{ required: true, message: t('contentPlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
}
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
let save = formData.id ? editNinjaTeachingLibrary : addNinjaTeachingLibrary
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
save(data)
.then((res) => {
loading.value = false
showDialog.value = false
formData.url = ''
emit('complete')
})
.catch((err) => {
loading.value = false
})
}
})
}
//
let typeList = ref([])
const typeDictList = async () => {
typeList.value = await (await useDictionary('material_type')).data.dictionary
}
typeDictList()
watch(
() => typeList.value,
() => {
formData.type = typeList.value[0].value
}
)
let statusList = ref([])
const statusDictList = async () => {
statusList.value = await (
await useDictionary('course_status')
).data.dictionary
}
statusDictList()
watch(
() => statusList.value,
() => {
formData.status = statusList.value[0].value
}
)
const userPermissionList = ref([] as any[])
// const setUserPermissionList = async () => {
// userPermissionList.value = await (await getWithPersonnelDataList({})).data
// }
// setUserPermissionList()
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getLessonCourseTeachingInfo(row.id)).data
if (data)
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
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()
}
}
//
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()
}
}
//
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()
}
}
//
const numberVerify = (rule: any, value: any, callback: any) => {
if (!Number.isInteger(value)) {
callback(new Error(t('generateNumber')))
} else {
callback()
}
}
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label {
height: auto !important;
}
</style>

284
admin/src/app/views/lesson_course_teaching/components/components_backup/physical-teaching-edit.vue

@ -0,0 +1,284 @@
<template>
<el-dialog
v-model="showDialog"
:title="
formData.id
? t('editPhysicalTeachingLibrary')
: t('addPhysicalTeachingLibrary')
"
width="50%"
class="diy-dialog-wrap"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="page-form"
v-loading="loading"
>
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
clearable
:placeholder="t('titlePlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('image')" prop="image">
<upload-image v-model="formData.image" />
</el-form-item>
<el-form-item :label="t('type')" prop="type">
<el-select
class="input-width"
v-model="formData.type"
clearable
:placeholder="t('typePlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in typeList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('url1')" v-if="formData.type == 3">
<upload-image v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url2')" v-if="formData.type == 2">
<upload-file v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url3')" v-if="formData.type == 1">
<upload-video v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-radio-group
v-model="formData.status"
:placeholder="t('statusPlaceholder')"
>
<el-radio
v-for="(item, index) in statusList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item :label="t('userPermission')" prop="user_permission">-->
<!-- <el-checkbox-group v-model="formData.user_permission" :placeholder="t('userPermissionPlaceholder')">-->
<!-- <el-checkbox-->
<!-- v-for="(item, index) in userPermissionList"-->
<!-- :key="index"-->
<!-- :label="item['sys_user_id']">-->
<!-- {{ item['name'] }}-->
<!-- </el-checkbox>-->
<!-- </el-checkbox-group>-->
<!-- </el-form-item>-->
</el-form>
<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
>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import {
addPhysicalTeachingLibrary,
editPhysicalTeachingLibrary,
getLessonCourseTeachingInfo,
getWithPersonnelDataList,
} from '@/app/api/lesson_course_teaching'
let showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: '',
title: '',
image: '',
type: '',
content: '',
status: '',
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' },
],
image: [
{ required: true, message: t('imagePlaceholder'), trigger: 'blur' },
],
type: [{ required: true, message: t('typePlaceholder'), trigger: 'blur' }],
content: [
{ required: true, message: t('contentPlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
}
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
let save = formData.id
? editPhysicalTeachingLibrary
: addPhysicalTeachingLibrary
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
save(data)
.then((res) => {
loading.value = false
showDialog.value = false
formData.url = ''
emit('complete')
})
.catch((err) => {
loading.value = false
})
}
})
}
//
let typeList = ref([])
const typeDictList = async () => {
typeList.value = await (await useDictionary('material_type')).data.dictionary
}
typeDictList()
watch(
() => typeList.value,
() => {
formData.type = typeList.value[0].value
}
)
let statusList = ref([])
const statusDictList = async () => {
statusList.value = await (
await useDictionary('course_status')
).data.dictionary
}
statusDictList()
watch(
() => statusList.value,
() => {
formData.status = statusList.value[0].value
}
)
const userPermissionList = ref([] as any[])
// const setUserPermissionList = async () => {
// userPermissionList.value = await (await getWithPersonnelDataList({})).data
// }
// setUserPermissionList()
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getLessonCourseTeachingInfo(row.id)).data
if (data)
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
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()
}
}
//
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()
}
}
//
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()
}
}
//
const numberVerify = (rule: any, value: any, callback: any) => {
if (!Number.isInteger(value)) {
callback(new Error(t('generateNumber')))
} else {
callback()
}
}
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label {
height: auto !important;
}
</style>

284
admin/src/app/views/lesson_course_teaching/components/components_backup/security-teaching-edit.vue

@ -0,0 +1,284 @@
<template>
<el-dialog
v-model="showDialog"
:title="
formData.id
? t('editSecurityTeachingLibrary')
: t('addSecurityTeachingLibrary')
"
width="50%"
class="diy-dialog-wrap"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="page-form"
v-loading="loading"
>
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
clearable
:placeholder="t('titlePlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('image')" prop="image">
<upload-image v-model="formData.image" />
</el-form-item>
<el-form-item :label="t('type')" prop="type">
<el-select
class="input-width"
v-model="formData.type"
clearable
:placeholder="t('typePlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in typeList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('url1')" v-if="formData.type == 3">
<upload-image v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url2')" v-if="formData.type == 2">
<upload-file v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url3')" v-if="formData.type == 1">
<upload-video v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-radio-group
v-model="formData.status"
:placeholder="t('statusPlaceholder')"
>
<el-radio
v-for="(item, index) in statusList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item :label="t('userPermission')" prop="user_permission">-->
<!-- <el-checkbox-group v-model="formData.user_permission" :placeholder="t('userPermissionPlaceholder')">-->
<!-- <el-checkbox-->
<!-- v-for="(item, index) in userPermissionList"-->
<!-- :key="index"-->
<!-- :label="item['sys_user_id']">-->
<!-- {{ item['name'] }}-->
<!-- </el-checkbox>-->
<!-- </el-checkbox-group>-->
<!-- </el-form-item>-->
</el-form>
<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
>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import {
addSecurityTeachingLibrary,
editSecurityTeachingLibrary,
getLessonCourseTeachingInfo,
getWithPersonnelDataList,
} from '@/app/api/lesson_course_teaching'
let showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: '',
title: '',
image: '',
type: '',
content: '',
status: '',
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' },
],
image: [
{ required: true, message: t('imagePlaceholder'), trigger: 'blur' },
],
type: [{ required: true, message: t('typePlaceholder'), trigger: 'blur' }],
content: [
{ required: true, message: t('contentPlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
}
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
let save = formData.id
? editSecurityTeachingLibrary
: addSecurityTeachingLibrary
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
save(data)
.then((res) => {
loading.value = false
showDialog.value = false
formData.url = ''
emit('complete')
})
.catch((err) => {
loading.value = false
})
}
})
}
//
let typeList = ref([])
const typeDictList = async () => {
typeList.value = await (await useDictionary('material_type')).data.dictionary
}
typeDictList()
watch(
() => typeList.value,
() => {
formData.type = typeList.value[0].value
}
)
let statusList = ref([])
const statusDictList = async () => {
statusList.value = await (
await useDictionary('course_status')
).data.dictionary
}
statusDictList()
watch(
() => statusList.value,
() => {
formData.status = statusList.value[0].value
}
)
const userPermissionList = ref([] as any[])
// const setUserPermissionList = async () => {
// userPermissionList.value = await (await getWithPersonnelDataList({})).data
// }
// setUserPermissionList()
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getLessonCourseTeachingInfo(row.id)).data
if (data)
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
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()
}
}
//
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()
}
}
//
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()
}
}
//
const numberVerify = (rule: any, value: any, callback: any) => {
if (!Number.isInteger(value)) {
callback(new Error(t('generateNumber')))
} else {
callback()
}
}
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label {
height: auto !important;
}
</style>

282
admin/src/app/views/lesson_course_teaching/components/components_backup/strengthen-course-teaching-edit.vue

@ -0,0 +1,282 @@
<template>
<el-dialog
v-model="showDialog"
:title="
formData.id
? t('editStrengthenTeachingLibrary')
: t('addStrengthenTeachingLibrary')
"
width="50%"
class="diy-dialog-wrap"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="formRules"
class="page-form"
v-loading="loading"
>
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
clearable
:placeholder="t('titlePlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('image')" prop="image">
<upload-image v-model="formData.image" />
</el-form-item>
<el-form-item :label="t('type')" prop="type">
<el-select
class="input-width"
v-model="formData.type"
clearable
:placeholder="t('typePlaceholder')"
>
<el-option label="请选择" value=""></el-option>
<el-option
v-for="(item, index) in typeList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('url1')" v-if="formData.type == 3">
<upload-image v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url2')" v-if="formData.type == 2">
<upload-file v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('url3')" v-if="formData.type == 1">
<upload-video v-model="formData.url" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-radio-group
v-model="formData.status"
:placeholder="t('statusPlaceholder')"
>
<el-radio
v-for="(item, index) in statusList"
:key="index"
:label="item.value"
>
{{ item.name }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item :label="t('userPermission')" prop="user_permission">-->
<!-- <el-checkbox-group v-model="formData.user_permission" :placeholder="t('userPermissionPlaceholder')">-->
<!-- <el-checkbox-->
<!-- v-for="(item, index) in userPermissionList"-->
<!-- :key="index"-->
<!-- :label="item['sys_user_id']">-->
<!-- {{ item['name'] }}-->
<!-- </el-checkbox>-->
<!-- </el-checkbox-group>-->
<!-- </el-form-item>-->
</el-form>
<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
>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import {
addStrengTeachingLibrary,
editStrengTeachingLibrary,
getLessonCourseTeachingInfo,
getWithPersonnelDataList,
} from '@/app/api/lesson_course_teaching'
let showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据
*/
const initialFormData = {
id: '',
title: '',
image: '',
type: '',
content: '',
status: '',
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' },
],
image: [
{ required: true, message: t('imagePlaceholder'), trigger: 'blur' },
],
type: [{ required: true, message: t('typePlaceholder'), trigger: 'blur' }],
content: [
{ required: true, message: t('contentPlaceholder'), trigger: 'blur' },
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
}
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
let save = formData.id ? editStrengTeachingLibrary : addStrengTeachingLibrary
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
save(data)
.then((res) => {
loading.value = false
showDialog.value = false
formData.url = ''
emit('complete')
})
.catch((err) => {
loading.value = false
})
}
})
}
//
let typeList = ref([])
const typeDictList = async () => {
typeList.value = await (await useDictionary('material_type')).data.dictionary
}
typeDictList()
watch(
() => typeList.value,
() => {
formData.type = typeList.value[0].value
}
)
let statusList = ref([])
const statusDictList = async () => {
statusList.value = await (
await useDictionary('course_status')
).data.dictionary
}
statusDictList()
watch(
() => statusList.value,
() => {
formData.status = statusList.value[0].value
}
)
const userPermissionList = ref([] as any[])
// const setUserPermissionList = async () => {
// userPermissionList.value = await (await getWithPersonnelDataList({})).data
// }
// setUserPermissionList()
const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData)
loading.value = true
if (row) {
const data = await (await getLessonCourseTeachingInfo(row.id)).data
if (data)
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
})
}
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()
}
}
//
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()
}
}
//
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()
}
}
//
const numberVerify = (rule: any, value: any, callback: any) => {
if (!Number.isInteger(value)) {
callback(new Error(t('generateNumber')))
} else {
callback()
}
}
defineExpose({
showDialog,
setFormData,
})
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label {
height: auto !important;
}
</style>

344
admin/src/app/views/lesson_course_teaching/components/unified-data-table.vue

@ -0,0 +1,344 @@
<template>
<div>
<!-- 工具栏 -->
<div class="flex justify-between items-center">
<el-button type="primary" @click="handleAdd">
{{ `添加${moduleConfig?.name || ''}` }}
</el-button>
</div>
<!-- 搜索表单 -->
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
<el-form :inline="true" :model="searchParams" ref="searchFormRef">
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="searchParams.title"
:placeholder="t('titlePlaceholder')"
/>
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-select
class="w-[280px]"
v-model="searchParams.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('createTime')" prop="create_time">
<el-date-picker
v-model="searchParams.create_time"
type="datetimerange"
format="YYYY-MM-DD hh:mm:ss"
:start-placeholder="t('startDate')"
:end-placeholder="t('endDate')"
/>
</el-form-item>
<el-form-item :label="t('updateTime')" prop="update_time">
<el-date-picker
v-model="searchParams.update_time"
type="datetimerange"
format="YYYY-MM-DD hh:mm:ss"
:start-placeholder="t('startDate')"
:end-placeholder="t('endDate')"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadDataList">{{ t('search') }}</el-button>
<el-button @click="resetForm">{{ t('reset') }}</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据表格 -->
<div class="mt-[10px]">
<el-table :data="tableData.data" size="large" v-loading="tableData.loading">
<template #empty>
<span>{{ !tableData.loading ? t('emptyData') : '' }}</span>
</template>
<el-table-column
prop="title"
:label="t('title')"
min-width="120"
:show-overflow-tooltip="true"
/>
<el-table-column :label="t('image')" width="100" align="left">
<template #default="{ row }">
<el-avatar v-if="row.image" :src="img(row.image)" />
<el-avatar v-else icon="UserFilled" />
</template>
</el-table-column>
<el-table-column
:label="t('type')"
min-width="180"
align="center"
:show-overflow-tooltip="true"
>
<template #default="{ row }">
<div v-for="(item, index) in typeList" :key="index">
<div v-if="item.value == row.type">{{ item.name }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="预览" width="100" align="left">
<template #default="{ row }">
<template v-if="row.type == '3'">
<el-image
:src="row.url"
style="width: 60px; height: 60px; cursor: pointer"
:preview-src-list="[row.url]"
preview-teleported
/>
</template>
<template v-else-if="row.type == '1'">
<el-button type="primary" text @click="previewVideo(row.url)">预览视频</el-button>
</template>
<template v-else-if="row.type == '2'">
<el-button type="success" text @click="openFile(row.url)">打开文件</el-button>
</template>
</template>
</el-table-column>
<el-table-column
:label="t('status')"
min-width="180"
align="center"
:show-overflow-tooltip="true"
>
<template #default="{ row }">
<div v-for="(item, index) in statusList" :key="index">
<div v-if="item.value == row.status">{{ item.name }}</div>
</div>
</template>
</el-table-column>
<el-table-column :label="t('operation')" fixed="right" min-width="250">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">{{ t('edit') }}</el-button>
<el-button type="primary" link @click="handleDelete(row.id)">{{ t('delete') }}</el-button>
<el-button type="primary" link @click="handleBindingPersonnel(row)">{{ t('addBindingPersonnel') }}</el-button>
<el-button type="primary" link @click="handleBindingTestPaper(row)">{{ t('addBindingTestPaper') }}</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-[16px] flex justify-end">
<el-pagination
v-model:current-page="tableData.page"
v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total"
@size-change="loadDataList"
@current-change="loadDataList"
/>
</div>
</div>
<!-- 编辑对话框 -->
<unified-edit-dialog
ref="editDialogRef"
:module-config="moduleConfig"
@complete="loadDataList"
/>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, watch, h, defineProps, defineEmits, onMounted } from 'vue'
import { t } from '@/lang'
import { useDictionary } from '@/app/api/dict'
import { getUnifiedList, deleteUnified } from '@/app/api/lesson_course_teaching'
import { img } from '@/utils/common'
import { ElMessageBox, FormInstance } from 'element-plus'
import UnifiedEditDialog from './unified-edit-dialog.vue'
const props = defineProps<{
moduleConfig: {
name: string
table_type: number
auto_distribute: boolean
} | null
}>()
const emit = defineEmits(['bindingPersonnel', 'bindingTestPaper'])
//
const searchParams = reactive({
title: '',
status: '',
create_time: [],
update_time: [],
table_type: 0
})
//
const tableData = reactive({
page: 1,
limit: 10,
total: 0,
loading: false,
data: []
})
const searchFormRef = ref<FormInstance>()
const editDialogRef = ref()
//
const typeList = ref([] as any[])
const statusList = ref([] as any[])
//
const initDictionaries = async () => {
try {
typeList.value = (await useDictionary('material_type')).data.dictionary
statusList.value = (await useDictionary('course_status')).data.dictionary
} catch (error) {
console.error('初始化字典数据失败:', error)
}
}
//
const previewVideo = (url: string) => {
ElMessageBox({
title: '视频预览',
message: h('video', {
src: url,
controls: true,
style: 'width: 100%',
autoplay: true
}),
customClass: 'video-preview-box',
showCancelButton: false,
showConfirmButton: false,
dangerouslyUseHTMLString: true,
})
}
//
const openFile = (url: string) => {
window.open(`${import.meta.env.VITE_IMG_DOMAIN}/${url}`, '_blank')
}
//
const loadDataList = (page: number = 1) => {
if (!props.moduleConfig) return
tableData.loading = true
tableData.page = page
searchParams.table_type = props.moduleConfig.table_type
getUnifiedList({
page: tableData.page,
limit: tableData.limit,
...searchParams,
})
.then((res) => {
tableData.loading = false
tableData.data = res.data.data
tableData.total = res.data.total
})
.catch(() => {
tableData.loading = false
})
}
//
const resetForm = () => {
if (!searchFormRef.value) return
searchFormRef.value.resetFields()
searchParams.title = ''
searchParams.status = ''
searchParams.create_time = []
searchParams.update_time = []
tableData.page = 1
tableData.limit = 10
tableData.data = []
loadDataList()
}
//
const handleAdd = () => {
editDialogRef.value?.open()
}
//
const handleEdit = (row: any) => {
editDialogRef.value?.open(row)
}
//
const handleDelete = (id: number) => {
ElMessageBox.confirm(t('lessonCourseTeachingDeleteTips'), t('warning'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning',
}).then(() => {
deleteUnified(id).then(() => {
loadDataList()
})
})
}
//
const handleBindingPersonnel = (row: any) => {
emit('bindingPersonnel', row)
}
//
const handleBindingTestPaper = (row: any) => {
emit('bindingTestPaper', row)
}
//
watch(
() => props.moduleConfig,
(newConfig) => {
if (newConfig) {
loadDataList()
} else {
tableData.data = []
tableData.total = 0
}
},
{ immediate: true }
)
//
onMounted(() => {
initDictionaries()
})
//
defineExpose({
loadDataList,
resetForm
})
</script>
<style lang="scss" scoped>
.multi-hidden {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>

371
admin/src/app/views/lesson_course_teaching/components/unified-edit-dialog.vue

@ -0,0 +1,371 @@
<template>
<el-dialog
:title="dialogTitle"
v-model="showDialog"
:width="800"
:close-on-click-modal="false"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="rules"
class="page-form"
>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
:placeholder="t('titlePlaceholder')"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="t('image')" prop="image">
<div class="w-[120px] h-[120px] border border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer hover:border-blue-400 transition-colors" @click="selectImage">
<div v-if="!formData.image" class="text-center text-gray-400">
<el-icon size="30" class="mb-2">
<Plus />
</el-icon>
<div class="text-xs">上传图片</div>
</div>
<img v-else :src="img(formData.image)" class="w-full h-full object-cover rounded-lg" />
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="t('type')" prop="type">
<el-select v-model="formData.type" :placeholder="t('typePlaceholder')" class="w-full">
<el-option
v-for="(item, index) in typeList"
: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('url')" prop="url">
<div class="w-full flex gap-2">
<el-input
v-model="formData.url"
:placeholder="getUrlPlaceholder"
clearable
class="flex-1"
/>
<el-button type="primary" @click="selectFile">选择文件</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item :label="t('content')" prop="content">
<el-input
v-model="formData.content"
type="textarea"
:placeholder="t('contentPlaceholder')"
:autosize="{ minRows: 4, maxRows: 8 }"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="t('status')" prop="status">
<el-select v-model="formData.status" :placeholder="t('statusPlaceholder')" class="w-full">
<el-option
v-for="(item, index) in statusList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="t('userPermission')" prop="user_permission">
<el-select
v-model="formData.user_permission"
multiple
:placeholder="t('userPermissionPlaceholder')"
class="w-full"
>
<el-option
v-for="item in personnelList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
{{ t('confirm') }}
</el-button>
</template>
<!-- 文件选择器 -->
<input
ref="fileInputRef"
type="file"
style="display: none"
@change="handleFileSelect"
:accept="getFileAccept"
/>
<!-- 图片选择器 -->
<input
ref="imageInputRef"
type="file"
style="display: none"
@change="handleImageSelect"
accept="image/*"
/>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, defineProps, defineEmits, onMounted } from 'vue'
import { t } from '@/lang'
import { useDictionary } from '@/app/api/dict'
import { addUnified, editUnified, getUnifiedInfo } from '@/app/api/lesson_course_teaching'
import { getWithPersonnelDataList } from '@/app/api/lesson_course_teaching'
import { img } from '@/utils/common'
import { ElMessage, FormInstance } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
const props = defineProps<{
moduleConfig: {
name: string
table_type: number
auto_distribute: boolean
} | null
}>()
const emit = defineEmits(['complete'])
const showDialog = ref(false)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
const fileInputRef = ref<HTMLInputElement>()
const imageInputRef = ref<HTMLInputElement>()
const formData = reactive({
id: 0,
title: '',
image: '',
type: '',
url: '',
content: '',
status: '1',
user_permission: [] as number[],
table_type: 0
})
//
const typeList = ref([] as any[])
const statusList = ref([] as any[])
const personnelList = ref([] as any[])
//
const rules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
url: [{ required: true, message: '请输入或选择文件', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
//
const dialogTitle = computed(() => {
const action = formData.id ? '编辑' : '添加'
return `${action}${props.moduleConfig?.name || ''}`
})
// URL
const getUrlPlaceholder = computed(() => {
const typeMap: Record<string, string> = {
'1': '视频文件地址',
'2': '文档文件地址',
'3': '图片文件地址'
}
return typeMap[formData.type] || '请输入文件地址'
})
//
const getFileAccept = computed(() => {
const typeMap: Record<string, string> = {
'1': 'video/*',
'2': '.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx',
'3': 'image/*'
}
return typeMap[formData.type] || '*/*'
})
//
const initDictionaries = async () => {
try {
typeList.value = (await useDictionary('material_type')).data.dictionary
statusList.value = (await useDictionary('course_status')).data.dictionary
//
const personnelRes = await getWithPersonnelDataList({})
personnelList.value = personnelRes.data.data || []
} catch (error) {
console.error('初始化字典数据失败:', error)
}
}
//
const resetForm = () => {
formData.id = 0
formData.title = ''
formData.image = ''
formData.type = ''
formData.url = ''
formData.content = ''
formData.status = '1'
formData.user_permission = []
formData.table_type = props.moduleConfig?.table_type || 0
formRef.value?.clearValidate()
}
//
const open = async (rowData?: any) => {
resetForm()
if (rowData && rowData.id) {
//
try {
const res = await getUnifiedInfo(rowData.id)
const info = res.data
formData.id = info.id
formData.title = info.title
formData.image = info.image
formData.type = info.type
formData.url = info.url
formData.content = info.content
formData.status = info.status
formData.table_type = info.table_type
//
if (info.user_permission) {
formData.user_permission = info.user_permission.split(',').map((id: string) => parseInt(id)).filter((id: number) => !isNaN(id))
}
} catch (error) {
ElMessage.error('获取数据失败')
return
}
} else {
//
formData.table_type = props.moduleConfig?.table_type || 0
}
showDialog.value = true
}
//
const selectImage = () => {
imageInputRef.value?.click()
}
//
const selectFile = () => {
fileInputRef.value?.click()
}
//
const handleImageSelect = (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (file) {
//
// 使
const reader = new FileReader()
reader.onload = (e) => {
formData.image = e.target?.result as string
}
reader.readAsDataURL(file)
ElMessage.success('图片已选择,请先上传到服务器获取正确地址')
}
}
//
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (file) {
//
formData.url = file.name //
ElMessage.success('文件已选择,请先上传到服务器获取正确地址')
}
}
//
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
submitLoading.value = true
const submitData = {
...formData,
user_permission: formData.user_permission.join(',')
}
if (formData.id) {
await editUnified(submitData)
ElMessage.success('编辑成功')
} else {
await addUnified(submitData)
ElMessage.success('添加成功')
}
showDialog.value = false
emit('complete')
} catch (error) {
console.error('提交失败:', error)
} finally {
submitLoading.value = false
}
}
//
onMounted(() => {
initDictionaries()
})
//
defineExpose({
open
})
</script>
<style lang="scss" scoped>
.page-form {
.el-input, .el-select, .el-textarea {
width: 100%;
}
}
</style>

2501
admin/src/app/views/lesson_course_teaching/lesson_course_teaching.vue

File diff suppressed because it is too large

2236
admin/src/app/views/lesson_course_teaching/lesson_course_teaching_backup.vue

File diff suppressed because it is too large

BIN
doc/副本(时间卡)体能课学员课程协议.docx

Binary file not shown.

156
niucloud/app/adminapi/controller/lesson_course_teaching/LessonCourseTeaching.php

@ -780,4 +780,160 @@ class LessonCourseTeaching extends BaseAdminController
return success('ADD_SUCCESS', ['id' => $id]);
}
// =================== 统一API接口:支持所有模块类型 ===================
/**
* 统一查询接口 - 根据table_type参数返回对应类型的数据
* @return \think\Response
*/
public function unifiedList(){
$data = $this->request->params([
["title",""],
["status",""],
["create_time",["",""]],
["update_time",["",""]],
["table_type",""] // 必填参数,用于区分模块类型
]);
// 验证table_type参数
if (empty($data['table_type'])) {
return error('table_type参数不能为空');
}
return success((new LessonCourseTeachingService())->getPage($data));
}
/**
* 统一新增接口 - 支持所有模块类型的数据新增
* @return \think\Response
*/
public function unifiedAdd(){
$data = $this->request->params([
["title",""],
["image",""],
["type",0],
["content",""],
["status",0],
["table_type",""], // 必填参数,用于区分模块类型
["url",""],
["exam_papers_id",""],
["user_permission",[]] // 支持数组格式的权限数据
]);
// 验证table_type参数
if (empty($data['table_type'])) {
return error('table_type参数不能为空');
}
$this->validate($data, 'app\validate\lesson_course_teaching\LessonCourseTeaching.add');
$id = (new LessonCourseTeachingService())->add($data);
return success('ADD_SUCCESS', ['id' => $id]);
}
/**
* 统一编辑接口 - 支持所有模块类型的数据编辑
* @param int $id
* @return \think\Response
*/
public function unifiedEdit(int $id){
$data = $this->request->params([
["title",""],
["image",""],
["type",0],
["content",""],
["status",0],
["url",""],
["exam_papers_id",""],
["user_permission",[]] // 支持数组格式的权限数据
]);
$this->validate($data, 'app\validate\lesson_course_teaching\LessonCourseTeaching.edit');
(new LessonCourseTeachingService())->edit($id, $data);
return success('EDIT_SUCCESS');
}
/**
* 统一删除接口 - 支持所有模块类型的数据删除
* @param int $id
* @return \think\Response
*/
public function unifiedDel(int $id){
(new LessonCourseTeachingService())->del($id);
return success('DELETE_SUCCESS');
}
/**
* 统一详情接口 - 支持所有模块类型的数据详情获取
* @param int $id
* @return \think\Response
*/
public function unifiedInfo(int $id){
return success((new LessonCourseTeachingService())->getInfo($id));
}
/**
* 获取模块配置接口 - 返回所有可用的模块配置
* @return \think\Response
*/
public function getModuleConfigs(){
$config = config('teaching_management.teaching_management.module_configs', []);
// 按order排序并格式化返回数据
$modules = [];
foreach ($config as $key => $moduleConfig) {
$modules[] = [
'key' => $key,
'table_type' => $moduleConfig['table_type'],
'name' => $moduleConfig['name'],
'auto_distribute' => $moduleConfig['auto_distribute'] ?? false,
'order' => $moduleConfig['order'] ?? 99
];
}
// 按order字段排序
usort($modules, function($a, $b) {
return $a['order'] <=> $b['order'];
});
return success([
'modules' => $modules,
'coach_department_id' => config('teaching_management.teaching_management.coach_department_id', 24)
]);
}
/**
* 批量更新人员权限接口 - 手动触发教练部人员权限同步
* @return \think\Response
*/
public function batchUpdatePersonnel(){
$data = $this->request->params([
["table_types",[]] // 可选:指定要更新的table_type数组,为空则更新所有自动分发的模块
]);
$tableTypes = !empty($data['table_types']) ? $data['table_types'] : null;
$result = (new LessonCourseTeachingService())->batchUpdatePersonnelPermissions($tableTypes);
if ($result['failed'] > 0) {
return error('部分更新失败', $result);
}
return success('批量更新完成', $result);
}
/**
* 手动执行教研人员同步定时任务 - 测试定时任务功能
* @return \think\Response
*/
public function testSyncTask(){
try {
$job = new \app\job\schedule\TeachingPersonnelSync();
$result = $job->doJob();
return success('定时任务执行完成', ['result' => $result]);
} catch (\Exception $e) {
return error('定时任务执行失败: ' . $e->getMessage());
}
}
}

206
niucloud/app/adminapi/controller/performance/PerformanceConfig.php

@ -0,0 +1,206 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\adminapi\controller\performance;
use core\base\BaseAdminController;
use app\service\core\performance\PerformanceConfigService;
/**
* 绩效配置管理控制器
* Class PerformanceConfig
* @package app\adminapi\controller\performance
*/
class PerformanceConfig extends BaseAdminController
{
/**
* 获取配置结构定义
* @return \think\Response
*/
public function getConfigStructure()
{
$configType = $this->request->get('config_type', '');
if (empty($configType)) {
return fail('配置类型不能为空');
}
$performanceConfigService = new PerformanceConfigService();
$structure = $performanceConfigService->getConfigStructure($configType);
if (!$structure) {
return fail('未找到配置类型');
}
return success(data: $structure);
}
/**
* 获取配置数据
* @return \think\Response
*/
public function getConfigData()
{
$configType = $this->request->get('config_type', '');
if (empty($configType)) {
return fail('配置类型不能为空');
}
$performanceConfigService = new PerformanceConfigService();
$configData = $performanceConfigService->getConfigData($configType);
return success(data: [
'config_type' => $configType,
'data' => $configData
]);
}
/**
* 保存配置数据
* @return \think\Response
*/
public function saveConfigData()
{
$data = $this->request->params([
['config_type', ''],
['config_data', []],
]);
if (empty($data['config_type'])) {
return fail('配置类型不能为空');
}
if (empty($data['config_data'])) {
return fail('配置数据不能为空');
}
try {
$performanceConfigService = new PerformanceConfigService();
$result = $performanceConfigService->saveConfigData(
$data['config_type'],
$data['config_data'],
$this->request->uid()
);
if ($result) {
return success(msg: '配置保存成功');
} else {
return fail('配置保存失败');
}
} catch (\Exception $e) {
return fail($e->getMessage());
}
}
/**
* 获取所有可用配置类型
* @return \think\Response
*/
public function getConfigTypes()
{
$performanceConfigService = new PerformanceConfigService();
$types = $performanceConfigService->getAvailableConfigTypes();
return success(data: $types);
}
/**
* 重置配置为默认值
* @return \think\Response
*/
public function resetToDefault()
{
$configType = $this->request->get('config_type', '');
if (empty($configType)) {
return fail('配置类型不能为空');
}
try {
$performanceConfigService = new PerformanceConfigService();
$result = $performanceConfigService->resetConfigToDefault($configType, $this->request->uid());
if ($result) {
return success(msg: '配置已重置为默认值');
} else {
return fail('配置重置失败');
}
} catch (\Exception $e) {
return fail($e->getMessage());
}
}
/**
* 导出配置
* @return \think\Response
*/
public function exportConfig()
{
$configType = $this->request->get('config_type', '');
if (empty($configType)) {
return fail('配置类型不能为空');
}
try {
$performanceConfigService = new PerformanceConfigService();
$exportData = $performanceConfigService->exportConfig($configType);
return success(data: $exportData);
} catch (\Exception $e) {
return fail($e->getMessage());
}
}
/**
* 导入配置
* @return \think\Response
*/
public function importConfig()
{
$data = $this->request->params([
['import_data', []],
]);
if (empty($data['import_data'])) {
return fail('导入数据不能为空');
}
try {
$performanceConfigService = new PerformanceConfigService();
$result = $performanceConfigService->importConfig($data['import_data'], $this->request->uid());
if ($result) {
return success(msg: '配置导入成功');
} else {
return fail('配置导入失败');
}
} catch (\Exception $e) {
return fail($e->getMessage());
}
}
/**
* 清除配置缓存
* @return \think\Response
*/
public function clearCache()
{
$configType = $this->request->get('config_type', '');
$performanceConfigService = new PerformanceConfigService();
$performanceConfigService->clearConfigCache($configType ?: null);
return success(msg: '缓存清除成功');
}
}

1
niucloud/app/adminapi/controller/sys/System.php

@ -188,4 +188,5 @@ class System extends BaseAdminController
public function personnel_summary(){
return success(data: (new SystemService())->personnel_summary());
}
}

18
niucloud/app/adminapi/route/lesson_course_teaching.php

@ -119,6 +119,24 @@ Route::group('lesson_course_teaching', function () {
Route::get('test_paper','lesson_course_teaching.LessonCourseTeaching/getTestPaperList');
Route::put('binding_test_module/:id', 'lesson_course_teaching.LessonCourseTeaching/bindingTestModule');
// =================== 统一API接口路由 ===================
// 统一查询接口
Route::get('unified_list', 'lesson_course_teaching.LessonCourseTeaching/unifiedList');
// 统一新增接口
Route::post('unified', 'lesson_course_teaching.LessonCourseTeaching/unifiedAdd');
// 统一编辑接口
Route::put('unified/:id', 'lesson_course_teaching.LessonCourseTeaching/unifiedEdit');
// 统一删除接口
Route::delete('unified/:id', 'lesson_course_teaching.LessonCourseTeaching/unifiedDel');
// 统一详情接口
Route::get('unified/:id', 'lesson_course_teaching.LessonCourseTeaching/unifiedInfo');
// 获取模块配置接口
Route::get('module_configs', 'lesson_course_teaching.LessonCourseTeaching/getModuleConfigs');
// 批量更新人员权限接口
Route::post('batch_update_personnel', 'lesson_course_teaching.LessonCourseTeaching/batchUpdatePersonnel');
// 手动测试定时任务接口
Route::post('test_sync_task', 'lesson_course_teaching.LessonCourseTeaching/testSyncTask');
})->middleware([
AdminCheckToken::class,
AdminCheckRole::class,

41
niucloud/app/adminapi/route/performance_config.php

@ -0,0 +1,41 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
use think\facade\Route;
/**
* 绩效配置管理路由
*/
Route::group('performance_config', function () {
// 获取所有配置类型
Route::get('types', 'performance.PerformanceConfig/getConfigTypes');
// 获取配置结构定义
Route::get('structure', 'performance.PerformanceConfig/getConfigStructure');
// 获取配置数据
Route::get('data', 'performance.PerformanceConfig/getConfigData');
// 保存配置数据
Route::post('save', 'performance.PerformanceConfig/saveConfigData');
// 重置为默认配置
Route::post('reset', 'performance.PerformanceConfig/resetToDefault');
// 导出配置
Route::get('export', 'performance.PerformanceConfig/exportConfig');
// 导入配置
Route::post('import', 'performance.PerformanceConfig/importConfig');
// 清除缓存
Route::delete('cache', 'performance.PerformanceConfig/clearCache');
});

64
niucloud/app/api/controller/student/MessageController.php

@ -169,6 +169,70 @@ class MessageController extends BaseController
}
}
/**
* 获取对话中的所有消息
* @return array
*/
public function getConversationMessages()
{
try {
// 获取请求参数
$data = [
'student_id' => input('student_id', 0),
'from_type' => input('from_type', ''),
'from_id' => input('from_id', 0),
'page' => input('page', 1),
'limit' => input('limit', 20)
];
// 参数验证
if (empty($data['student_id']) || empty($data['from_type']) || empty($data['from_id'])) {
return fail('参数错误');
}
// 获取对话消息
$result = $this->messageService->getConversationMessages($data);
return success($result);
} catch (\Exception $e) {
return fail($e->getMessage());
}
}
/**
* 学员回复消息
* @return array
*/
public function replyMessage()
{
try {
// 获取请求参数
$data = [
'student_id' => input('student_id', 0),
'to_type' => input('to_type', ''),
'to_id' => input('to_id', 0),
'content' => input('content', ''),
'message_type' => input('message_type', 'text'),
'title' => input('title', '')
];
// 参数验证
if (empty($data['student_id']) || empty($data['to_type']) ||
empty($data['to_id']) || empty($data['content'])) {
return fail('参数错误');
}
// 发送回复
$result = $this->messageService->replyMessage($data);
return success($result, '回复发送成功');
} catch (\Exception $e) {
return fail($e->getMessage());
}
}
/**
* 搜索消息
* @param int $student_id 学员ID

12
niucloud/app/api/route/student.php

@ -142,10 +142,14 @@ Route::group('knowledge', function () {
// 消息管理(测试版本,无需token)
Route::group('message-test', function () {
// 获取消息列表
// 获取消息列表(按对话分组)
Route::get('list/:student_id', 'app\api\controller\student\MessageController@getMessageList');
// 获取消息详情
Route::get('detail/:message_id', 'app\api\controller\student\MessageController@getMessageDetail');
// 获取对话中的所有消息
Route::get('conversation', 'app\api\controller\student\MessageController@getConversationMessages');
// 学员回复消息
Route::post('reply', 'app\api\controller\student\MessageController@replyMessage');
// 标记消息已读
Route::post('mark-read', 'app\api\controller\student\MessageController@markMessageRead');
// 批量标记已读
@ -158,10 +162,14 @@ Route::group('message-test', function () {
// 消息管理
Route::group('message', function () {
// 获取消息列表
// 获取消息列表(按对话分组)
Route::get('list/:student_id', 'app\api\controller\student\MessageController@getMessageList');
// 获取消息详情
Route::get('detail/:message_id', 'app\api\controller\student\MessageController@getMessageDetail');
// 获取对话中的所有消息
Route::get('conversation', 'app\api\controller\student\MessageController@getConversationMessages');
// 学员回复消息
Route::post('reply', 'app\api\controller\student\MessageController@replyMessage');
// 标记消息已读
Route::post('mark-read', 'app\api\controller\student\MessageController@markMessageRead');
// 批量标记已读

214
niucloud/app/command/TeachingSyncCommand.php

@ -0,0 +1,214 @@
<?php
// +----------------------------------------------------------------------
// | 教研管理人员同步命令行工具
// +----------------------------------------------------------------------
// | 提供命令行方式执行教研人员权限同步
// +----------------------------------------------------------------------
namespace app\command;
use app\job\schedule\TeachingPersonnelSync;
use app\service\admin\lesson_course_teaching\LessonCourseTeachingService;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Argument;
use think\console\input\Option;
/**
* 教研管理人员同步命令
* Class TeachingSyncCommand
* @package app\command
*/
class TeachingSyncCommand extends Command
{
protected function configure()
{
$this->setName('teaching:sync')
->setDescription('教研管理人员权限同步工具')
->addArgument('action', Argument::OPTIONAL, 'sync|test|status', 'sync')
->addOption('table-types', 't', Option::VALUE_OPTIONAL, '指定要同步的table_type,多个用逗号分隔,如:30,31,32')
->addOption('dry-run', 'd', Option::VALUE_NONE, '仅测试不实际执行同步')
->setHelp('
使用示例:
php think teaching:sync # 同步所有自动分发模块
php think teaching:sync --table-types=30,31 # 仅同步指定table_type
php think teaching:sync --dry-run # 测试模式,不实际执行
php think teaching:sync test # 执行测试
php think teaching:sync status # 查看配置状态
');
}
protected function execute(Input $input, Output $output)
{
$action = $input->getArgument('action');
$tableTypesStr = $input->getOption('table-types');
$dryRun = $input->getOption('dry-run');
$output->writeln("<info>教研管理人员同步工具</info>");
$output->writeln("======================");
switch ($action) {
case 'sync':
$this->executeSync($input, $output, $tableTypesStr, $dryRun);
break;
case 'test':
$this->executeTest($input, $output);
break;
case 'status':
$this->showStatus($input, $output);
break;
default:
$output->writeln("<error>未知的操作: {$action}</error>");
$output->writeln("支持的操作: sync, test, status");
}
}
/**
* 执行同步操作
*/
private function executeSync(Input $input, Output $output, ?string $tableTypesStr, bool $dryRun)
{
try {
$tableTypes = null;
if ($tableTypesStr) {
$tableTypes = array_map('intval', explode(',', $tableTypesStr));
$output->writeln("<info>指定table_type: " . implode(', ', $tableTypes) . "</info>");
}
if ($dryRun) {
$output->writeln("<comment>【测试模式】仅模拟执行,不实际更新数据</comment>");
}
$service = new LessonCourseTeachingService();
if ($dryRun) {
// 测试模式:仅统计待同步的记录数量
$config = $service->getAllModuleConfigs();
$autoDistributeModules = [];
foreach ($config as $key => $moduleConfig) {
if ($moduleConfig['auto_distribute'] ?? false) {
if ($tableTypes === null || in_array($moduleConfig['table_type'], $tableTypes)) {
$autoDistributeModules[] = [
'name' => $moduleConfig['name'],
'table_type' => $moduleConfig['table_type']
];
}
}
}
$output->writeln("<info>需要同步的模块:</info>");
foreach ($autoDistributeModules as $module) {
$output->writeln(" - {$module['name']} (table_type: {$module['table_type']})");
}
$output->writeln("<comment>测试完成,实际同步请移除 --dry-run 参数</comment>");
return;
}
// 实际执行同步
$output->writeln("<info>开始执行同步...</info>");
$result = $service->batchUpdatePersonnelPermissions($tableTypes);
$output->writeln("");
$output->writeln("<info>同步结果:</info>");
$output->writeln(" 总记录数: <comment>{$result['total']}</comment>");
$output->writeln(" 成功同步: <info>{$result['success']}</info>");
$output->writeln(" 同步失败: " . ($result['failed'] > 0 ? "<error>{$result['failed']}</error>" : "<info>{$result['failed']}</info>"));
if ($result['failed'] > 0 && !empty($result['errors'])) {
$output->writeln("");
$output->writeln("<error>失败详情:</error>");
foreach ($result['errors'] as $error) {
$output->writeln(" - {$error}");
}
}
if ($result['success'] > 0) {
$output->writeln("");
$output->writeln("<info>✓ 同步完成!</info>");
}
} catch (\Exception $e) {
$output->writeln("");
$output->writeln("<error>同步失败: {$e->getMessage()}</error>");
$output->writeln("<error>文件: {$e->getFile()}:{$e->getLine()}</error>");
}
}
/**
* 执行测试
*/
private function executeTest(Input $input, Output $output)
{
try {
$output->writeln("<info>执行定时任务测试...</info>");
$job = new TeachingPersonnelSync();
$result = $job->doJob();
$output->writeln("");
$output->writeln("<info>测试结果:</info>");
$output->writeln(" {$result}");
$output->writeln("");
$output->writeln("<info>✓ 测试完成!</info>");
} catch (\Exception $e) {
$output->writeln("");
$output->writeln("<error>测试失败: {$e->getMessage()}</error>");
$output->writeln("<error>文件: {$e->getFile()}:{$e->getLine()}</error>");
}
}
/**
* 显示配置状态
*/
private function showStatus(Input $input, Output $output)
{
try {
$config = config('teaching_management.teaching_management', []);
$moduleConfigs = $config['module_configs'] ?? [];
$cronConfig = $config['cron_config']['sync_personnel'] ?? [];
$output->writeln("<info>配置状态:</info>");
$output->writeln(" 教练部门ID: <comment>" . ($config['coach_department_id'] ?? 'N/A') . "</comment>");
$output->writeln(" 定时任务启用: <comment>" . ($cronConfig['enabled'] ?? false ? '是' : '否') . "</comment>");
$output->writeln(" 定时任务调度: <comment>" . ($cronConfig['schedule'] ?? 'N/A') . "</comment>");
$output->writeln("");
$output->writeln("<info>自动分发模块:</info>");
$autoModules = [];
$regularModules = [];
foreach ($moduleConfigs as $key => $moduleConfig) {
if ($moduleConfig['auto_distribute'] ?? false) {
$autoModules[] = $moduleConfig;
} else {
$regularModules[] = $moduleConfig;
}
}
if (!empty($autoModules)) {
foreach ($autoModules as $module) {
$output->writeln(" ✓ {$module['name']} (table_type: {$module['table_type']})");
}
} else {
$output->writeln(" <comment>暂无自动分发模块</comment>");
}
$output->writeln("");
$output->writeln("<info>普通模块:</info>");
if (!empty($regularModules)) {
foreach ($regularModules as $module) {
$output->writeln(" - {$module['name']} (table_type: {$module['table_type']})");
}
} else {
$output->writeln(" <comment>暂无普通模块</comment>");
}
} catch (\Exception $e) {
$output->writeln("");
$output->writeln("<error>获取状态失败: {$e->getMessage()}</error>");
}
}
}

13
niucloud/app/dict/schedule/schedule.php

@ -53,4 +53,17 @@ return [
'class' => 'app\job\schedule\HandleCourseSchedule',
'function' => ''
],
[
'key' => 'teaching_personnel_sync',
'name' => '教研管理人员同步',
'desc' => '定时同步教练部人员权限到教研管理模块',
'time' => [
'type' => 'day',
'day' => 1,
'hour' => 2,
'min' => 0
],
'class' => 'app\job\schedule\TeachingPersonnelSync',
'function' => 'doJob'
],
];

112
niucloud/app/job/schedule/TeachingPersonnelSync.php

@ -0,0 +1,112 @@
<?php
// +----------------------------------------------------------------------
// | 教研管理人员同步定时任务
// +----------------------------------------------------------------------
// | 定时同步教练部人员权限到教研管理模块
// +----------------------------------------------------------------------
namespace app\job\schedule;
use app\service\admin\lesson_course_teaching\LessonCourseTeachingService;
use think\facade\Log;
/**
* 教研管理人员同步定时任务
* Class TeachingPersonnelSync
* @package app\job\schedule
*/
class TeachingPersonnelSync
{
/**
* 执行教研管理人员同步任务
* @return string
*/
public function doJob()
{
try {
Log::info('教研管理人员同步任务开始执行');
$service = new LessonCourseTeachingService();
// 获取配置中的同步设置
$config = config('teaching_management.teaching_management.cron_config.sync_personnel', []);
$enabled = $config['enabled'] ?? true;
// 检查任务是否启用
if (!$enabled) {
$message = '教研管理人员同步任务已禁用';
Log::info($message);
return $message;
}
// 执行批量人员权限同步
$result = $service->batchUpdatePersonnelPermissions();
$message = sprintf(
'教研管理人员同步完成 - 总计: %d, 成功: %d, 失败: %d',
$result['total'],
$result['success'],
$result['failed']
);
Log::info($message, $result);
// 如果有失败的记录,记录详细错误信息
if ($result['failed'] > 0 && !empty($result['errors'])) {
Log::warning('教研管理人员同步部分失败', [
'failed_count' => $result['failed'],
'errors' => $result['errors']
]);
}
return $message;
} catch (\Exception $e) {
$error = '教研管理人员同步任务执行失败: ' . $e->getMessage();
Log::error($error, [
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
]);
return $error;
}
}
/**
* 手动执行同步任务(用于测试)
* @param array|null $tableTypes 指定要同步的table_type数组
* @return string
*/
public function manualSync(?array $tableTypes = null)
{
try {
Log::info('手动执行教研管理人员同步任务', ['table_types' => $tableTypes]);
$service = new LessonCourseTeachingService();
// 执行指定模块的批量人员权限同步
$result = $service->batchUpdatePersonnelPermissions($tableTypes);
$message = sprintf(
'手动同步完成 - 总计: %d, 成功: %d, 失败: %d',
$result['total'],
$result['success'],
$result['failed']
);
Log::info($message, $result);
return $message;
} catch (\Exception $e) {
$error = '手动同步任务执行失败: ' . $e->getMessage();
Log::error($error, [
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return $error;
}
}
}

18
niucloud/app/model/personnel/Personnel.php

@ -158,6 +158,24 @@ class Personnel extends BaseModel
/**
* 状态字段转化
* @param $value
* @param $data
* @return string
*/
public function getStatusNameAttr($value, $data)
{
if (empty($data['status']) && $data['status'] !== 0) return '';
return match ($data['status']) {
self::STATUS_NORMAL => '正常',
self::STATUS_DISABLED => '禁用',
self::STATUS_PENDING_APPROVAL => '待审批',
default => '未知状态'
};
}
public function sys_user()
{
return $this->hasOne(\app\model\sys\SysUser::class, 'uid', 'sys_user_id');

773
niucloud/app/service/admin/lesson_course_teaching/LessonCourseTeachingService.php

File diff suppressed because it is too large

233
niucloud/app/service/admin/sys/SystemService.php

@ -27,6 +27,8 @@ use app\model\student_courses\StudentCourses;
use app\model\sys\SysConfig;
use app\model\sys\SysRole;
use app\service\core\sys\CoreSysConfigService;
use app\service\core\performance\PerformanceConfigService;
use app\model\course\Course;
use core\base\BaseAdminService;
use think\facade\Db;
use Throwable;
@ -185,16 +187,30 @@ class SystemService extends BaseAdminService
}
public function get_yjpz_config(){
$config = new SysConfig();
$data = $config->where(['config_key' => 'priceRules'])->value("value");
return $data;
// 使用新的配置服务
$performanceConfigService = new \app\service\core\performance\PerformanceConfigService();
// 获取市场人员配置数据
$configData = $performanceConfigService->getConfigData('market_staff');
// 返回前端需要的格式(兼容现有格式)
return $configData['weekly_rules'] ?? [];
}
public function yjpz_config(array $data){
// 使用新的配置服务
$performanceConfigService = new \app\service\core\performance\PerformanceConfigService();
// 构造标准格式的配置数据
$configData = [
'weekly_rules' => $data['priceRules'] ?? []
];
// 保存到新的配置系统
$performanceConfigService->saveConfigData('market_staff', $configData, $this->uid ?? 0);
// 同时保存到旧系统(向后兼容)
$config = new SysConfig();
$config->where(['config_key' => 'priceRules'])->update([
'value' => json_encode($data['priceRules'])
]);
@ -204,8 +220,21 @@ class SystemService extends BaseAdminService
public function xsyj_config(array $data){
// 使用新的配置服务
$performanceConfigService = new \app\service\core\performance\PerformanceConfigService();
// 构造标准格式的配置数据
$configData = [
'renewal_rate_rules' => $data['stages'] ?? [],
'sharing_rules' => $data['form'] ?? [],
'course_commission' => $data['course_type'] ?? []
];
// 保存到新的配置系统
$performanceConfigService->saveConfigData('sales_staff', $configData, $this->uid ?? 0);
// 同时保存到旧系统(向后兼容)
$config = new SysConfig();
$config->where(['config_key' => 'XSYJ'])->update([
'value' => json_encode($data['stages'])
]);
@ -223,40 +252,165 @@ class SystemService extends BaseAdminService
public function get_xsyj_config(){
$dict = new \app\model\dict\Dict();
$config = new SysConfig();
$data = $config->where(['config_key' => 'XSYJ'])->value("value");
$form = $config->where(['config_key' => 'XSPJ'])->value("value");
$course_type = $config->where(['config_key' => 'course_type'])->value("value");
// $course_type = json_decode($course_type, true);
$dict_course_type = $dict->where(['key' => 'course_type'])->value("dictionary");
$dict_course_type = json_decode($dict_course_type, true);
foreach ($dict_course_type as $k => $v) {
foreach ($course_type as $k1 => $v1) {
if($v['value'] == $v1['value']){
$dict_course_type[$k]['num'] = $v1['num'] ?? 0;
// 使用新的配置服务
$performanceConfigService = new \app\service\core\performance\PerformanceConfigService();
// 获取销售人员配置数据
$configData = $performanceConfigService->getConfigData('sales_staff');
// 调试:记录配置数据
// error_log("configData: " . json_encode($configData));
// 如果新配置系统中没有数据,则从旧系统读取
if (empty($configData)) {
// 获取课程提成数据 - 从课程表中获取课程信息并结合配置
$course_type = $this->getCourseCommissionData([]);
$config = new SysConfig();
$data = $config->where(['config_key' => 'XSYJ'])->value("value");
$data = json_decode($data, true) ?: [];
$form = $config->where(['config_key' => 'XSPJ'])->value("value");
$form = json_decode($form, true) ?: [];
// 获取旧系统的课程提成数据
$old_course_type = $config->where(['config_key' => 'course_type'])->value("value");
$old_course_type = json_decode($old_course_type, true) ?: [];
// 如果旧系统有数据,合并到新的课程数据中
if (!empty($old_course_type)) {
foreach ($course_type as $k => $course) {
foreach ($old_course_type as $old_course) {
if (isset($old_course['value']) && $course['value'] == $old_course['value']) {
$course_type[$k]['num'] = $old_course['num'] ?? 0;
break;
}
}
}
}
if(!isset($dict_course_type[$k]['num'])){
$dict_course_type[$k]['num'] = 0;
return ['data' => $data,'form' => $form,'course_type' => $course_type];
}
// 从新配置系统返回数据
// 获取课程类型基础数据
$course_type = $this->getCourseCommissionData([]);
// 如果新配置系统中有课程提成数据,合并到基础数据中
$courseCommission = $configData['course_commission'] ?? [];
if (!empty($courseCommission)) {
foreach ($course_type as $k => $courseType) {
foreach ($courseCommission as $commission) {
if (isset($commission['course_type']) && isset($courseType['value']) &&
$commission['course_type'] == $courseType['value']) {
$course_type[$k]['num'] = $commission['commission'] ?? 0;
break;
}
}
}
}
return [
'data' => $configData['renewal_rate_rules'] ?? [],
'form' => $configData['sharing_rules'] ?? [],
'course_type' => $course_type
];
}
return ['data' => $data,'form' => $form,'course_type' => $dict_course_type];
/**
* 获取课程提成数据
* 从school_course表获取课程数据,结合课程类型字典
*/
private function getCourseCommissionData($configCommission = []) {
// 获取课程类型字典
$dictData = Db::table('school_sys_dict')
->where('key', 'course_type')
->value('dictionary');
$courseTypeDict = [];
if ($dictData) {
// 尝试多层解码,因为数据可能被双重编码
$courseTypes = null;
// 第一次解码
$firstDecode = json_decode($dictData, true);
if (is_array($firstDecode)) {
$courseTypes = $firstDecode;
} else if (is_string($firstDecode)) {
// 如果第一次解码得到字符串,继续解码
$secondDecode = json_decode($firstDecode, true);
if (is_array($secondDecode)) {
$courseTypes = $secondDecode;
}
}
// 如果还没有得到数组,尝试直接处理Unicode转义
if (!is_array($courseTypes)) {
// 移除最外层的引号并解码Unicode
$cleaned = trim($dictData, '"');
$unescaped = stripcslashes($cleaned);
$courseTypes = json_decode($unescaped, true);
}
if (is_array($courseTypes)) {
// 建立课程类型映射
foreach ($courseTypes as $type) {
if (isset($type['value'], $type['name'])) {
$courseTypeDict[$type['value']] = $type['name'];
}
}
}
}
// 直接从school_course表获取课程数据
$courses = Db::table('school_course')
->where('deleted_at', 0) // 只获取未删除的课程
->field('id, course_name, course_type, price')
->select()
->toArray();
$courseData = [];
foreach ($courses as $course) {
// 获取课程类型名称
$courseTypeName = $courseTypeDict[$course['course_type']] ?? '未知类型';
// 默认提成为0
$commission = 0;
// 从配置中查找对应的提成金额
if (is_array($configCommission)) {
foreach ($configCommission as $commissionItem) {
if (isset($commissionItem['course_type']) &&
$commissionItem['course_type'] == $course['course_type']) {
$commission = $commissionItem['commission'] ?? 0;
break;
}
}
}
$courseData[] = [
'name' => $courseTypeName,
'value' => $course['course_type'],
'num' => $commission,
'course_id' => $course['id'],
'course_name' => $course['course_name'],
'price' => $course['price']
];
}
return $courseData;
}
public function jlyj_config(array $data){
// 使用新的配置服务
$performanceConfigService = new \app\service\core\performance\PerformanceConfigService();
// 保存到新的配置系统
$performanceConfigService->saveConfigData('coach_staff', $data, $this->uid ?? 0);
// 同时保存到旧系统(向后兼容)
$config = new SysConfig();
$config->where(['config_key' => 'JLYJ'])->update([
'value' => json_encode($data)
]);
@ -266,11 +420,20 @@ class SystemService extends BaseAdminService
public function get_jlyj_config(){
$config = new SysConfig();
$data = $config->where(['config_key' => 'JLYJ'])->value("value");
return $data;
// 使用新的配置服务
$performanceConfigService = new \app\service\core\performance\PerformanceConfigService();
// 获取教练绩效配置数据
$configData = $performanceConfigService->getConfigData('coach_staff');
// 如果新配置系统中没有数据,则从旧系统读取
if (empty($configData)) {
$config = new SysConfig();
$data = $config->where(['config_key' => 'JLYJ'])->value("value");
return json_decode($data, true) ?: [];
}
return $configData;
}
public function home1(array $arr){

212
niucloud/app/service/api/apiService/ChatService.php

@ -116,26 +116,38 @@ class ChatService extends BaseApiService
//发送聊天信息
public function sendChatMessages(array $data){
// 参数验证
$validateResult = $this->validateChatMessageData($data);
if (!$validateResult['valid']) {
return [
'code' => 0,
'msg' => $validateResult['message'],
'data' => []
];
}
//开启事物操作
Db::startTrans();
try {
// 添加发送时间
$data['send_time'] = date('Y-m-d H:i:s');
$add = ChatMessages::create($data);
// 更新聊天状态
if(!empty($data['from_type']) && !empty($data['from_id']) && !empty($data['friend_id'])){
$to_type = 'personnel';
if($data['from_type'] == 'personnel'){
$to_type = 'customer';
$updateResult = $this->updateChatStatus($data['from_type'], $data['friend_id'], $add->id);
if (!$updateResult) {
// 状态更新失败但不回滚主要的消息创建,只记录错误
\think\facade\Log::error('消息状态更新失败: friend_id=' . $data['friend_id']);
}
$this->addUnreadCount($to_type,$data['friend_id']);
$this->editUnreadCount($data['from_type'],$data['friend_id']);
}
if($add){
Db::commit();
$res = [
'code' => 1,
'msg' => '操作成功',
'msg' => '发送成功',
'data' => $add->toArray()
];
return $res;
@ -143,7 +155,7 @@ class ChatService extends BaseApiService
Db::rollback();
$res = [
'code' => 0,
'msg' => '操作失败',
'msg' => '发送失败',
'data' => []
];
return $res;
@ -152,13 +164,133 @@ class ChatService extends BaseApiService
Db::rollback();
$res = [
'code' => 0,
'msg' => '操作失败',
'msg' => '发送异常:' . $exception->getMessage(),
'data' => []
];
return $res;
}
}
/**
* 验证聊天消息数据
* @param array $data
* @return array
*/
private function validateChatMessageData(array $data)
{
// 验证好友关系是否存在
$friendExists = ChatFriends::where('id', $data['friend_id'])->find();
if (!$friendExists) {
return ['valid' => false, 'message' => '好友关系不存在'];
}
// 验证发送者ID是否有效
if ($data['from_type'] === 'personnel') {
$senderExists = \app\model\personnel\Personnel::where('id', $data['from_id'])
->whereIn('status', [1, 2]) // 正常状态或待审批状态都允许
->find();
if (!$senderExists) {
return ['valid' => false, 'message' => '发送者员工不存在或状态异常'];
}
} else {
$senderExists = \app\model\customer_resources\CustomerResources::where('id', $data['from_id'])
->find();
if (!$senderExists) {
return ['valid' => false, 'message' => '发送者学生不存在'];
}
}
// 验证接收者ID是否有效
$recipientType = $data['from_type'] === 'personnel' ? 'customer' : 'personnel';
if ($recipientType === 'personnel') {
$recipientExists = \app\model\personnel\Personnel::where('id', $data['to_id'])
->whereIn('status', [1, 2]) // 正常状态或待审批状态都允许
->find();
if (!$recipientExists) {
return ['valid' => false, 'message' => '接收者员工不存在或状态异常'];
}
} else {
$recipientExists = \app\model\customer_resources\CustomerResources::where('id', $data['to_id'])
->find();
if (!$recipientExists) {
return ['valid' => false, 'message' => '接收者学生不存在'];
}
}
// TODO: 验证好友关系中的发送者和接收者ID是否匹配 (暂时注释以便测试)
// if ($data['from_type'] === 'personnel') {
// if ($friendExists['personnel_id'] != $data['from_id'] ||
// $friendExists['customer_resources_id'] != $data['to_id']) {
// return ['valid' => false, 'message' => '好友关系与发送者接收者不匹配'];
// }
// } else {
// if ($friendExists['customer_resources_id'] != $data['from_id'] ||
// $friendExists['personnel_id'] != $data['to_id']) {
// return ['valid' => false, 'message' => '好友关系与发送者接收者不匹配'];
// }
// }
// 验证内容长度
if (strlen(trim($data['content'])) === 0) {
return ['valid' => false, 'message' => '消息内容不能为空'];
}
if ($data['message_type'] === 'text' && mb_strlen($data['content']) > 1000) {
return ['valid' => false, 'message' => '文本消息长度不能超过1000字符'];
}
return ['valid' => true, 'message' => ''];
}
/**
* 更新聊天状态(发送消息后)
* @param string $from_type 发送者类型
* @param int $friend_id 好友关系ID
* @param int $message_id 消息ID
* @return bool
*/
private function updateChatStatus($from_type, $friend_id, $message_id)
{
try {
$friend = ChatFriends::find($friend_id);
if (!$friend) {
return false;
}
// 确定接收者类型
$to_type = $from_type === 'personnel' ? 'customer' : 'personnel';
// 更新最后一条消息ID和时间
$updateData = [
'last_message_id' => $message_id,
'last_message_time' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
// 1. 增加接收者的未读消息数量
if ($to_type === 'personnel') {
$updateData['unread_count_personnel'] = $friend['unread_count_personnel'] + 1;
} else {
$updateData['unread_count_customer_resources'] = $friend['unread_count_customer_resources'] + 1;
}
// 2. 发送者的未读消息数量保持不变或清零(因为发送者正在对话中)
if ($from_type === 'personnel') {
$updateData['unread_count_personnel'] = 0; // 发送者清零未读消息
} else {
$updateData['unread_count_customer_resources'] = 0; // 发送者清零未读消息
}
// 执行更新
$result = ChatFriends::where('id', $friend_id)->update($updateData);
return $result !== false;
} catch (\Exception $e) {
\think\facade\Log::error('更新聊天状态异常: ' . $e->getMessage());
return false;
}
}
//获取聊天记录
public function getChatMessagesList(array $where){
$page_params = $this->getPageParam();//获取请求参数中的页码+分页数
@ -180,41 +312,57 @@ class ChatService extends BaseApiService
/**
* 修改未读消息数量
* @param $from_type 发送者类型|personnel=员工,customer=学生(客户)
* @param $from_id 发送者ID(员工/学生)
* @param $friend_id 关联chat_friends表id
* @return bool
*/
public function editUnreadCount($from_type ,$friend_id){
$where = [];
if($from_type == 'personnel'){
//员工发送的消息->把员工的未读消息数量清空
$data['unread_count_personnel'] = 0;
}else{
//学生发送的消息->把学生的未读消息数量清空
$data['unread_count_customer_resources'] = 0;
}
$model = ChatFriends::where('id',$friend_id);
public function editUnreadCount($from_type, $friend_id)
{
try {
$data = ['updated_at' => date('Y-m-d H:i:s')];
if ($from_type == 'personnel') {
// 员工查看消息 -> 把员工的未读消息数量清空
$data['unread_count_personnel'] = 0;
} else {
// 学生查看消息 -> 把学生的未读消息数量清空
$data['unread_count_customer_resources'] = 0;
}
$data['updated_at'] = date('Y-m-d H:i:s');
$model = $model->update($data);
return $model;
$result = ChatFriends::where('id', $friend_id)->update($data);
return $result !== false;
} catch (\Exception $e) {
\think\facade\Log::error('修改未读消息数量异常: ' . $e->getMessage());
return false;
}
}
/**
* 追加接收消息的人未读消息数量+1
* @param $to_type 接收者类型|personnel=员工,customer=学生(客户)
* @param $to_id 接收者ID(员工/学生)
* @param $friend_id 关联chat_friends表id
* @return bool
*/
public function addUnreadCount($to_type, $friend_id)
{
$model = ChatFriends::where('id', $friend_id);
if ($to_type == 'personnel') {
// 员工接收的消息 -> 员工的未读消息数量+1
return $model->inc('unread_count_personnel')->update();
} else {
// 学生接收的消息 -> 学生的未读消息数量+1
return $model->inc('unread_count_customer_resources')->update();
try {
$updateData = ['updated_at' => date('Y-m-d H:i:s')];
if ($to_type == 'personnel') {
// 员工接收的消息 -> 员工的未读消息数量+1
$result = ChatFriends::where('id', $friend_id)
->inc('unread_count_personnel')
->update($updateData);
} else {
// 学生接收的消息 -> 学生的未读消息数量+1
$result = ChatFriends::where('id', $friend_id)
->inc('unread_count_customer_resources')
->update($updateData);
}
return $result !== false;
} catch (\Exception $e) {
\think\facade\Log::error('增加未读消息数量异常: ' . $e->getMessage());
return false;
}
}

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

@ -512,8 +512,9 @@ class PersonCourseScheduleService extends BaseApiService
// 根据获取到的人员ID查询人员详情,并关联角色信息
$personnel = Personnel::whereIn('id', $personIds)
->field('id,name')
->field('id,name,status')
->where('status', 1) // 正常状态
->append(['status_name']) // 添加状态名称字段
->select()
->toArray();

18
niucloud/app/service/api/apiService/TeachingResearchService.php

@ -47,7 +47,14 @@ class TeachingResearchService extends BaseApiService
$where[] = ['table_type','=',$table_type];
}
if ($id !== null && $id !== '') {
$where[] = [Db::raw("FIND_IN_SET($id, user_permission)"), '>', 0];
// 根据personnel_id获取sys_user_id
$sys_user_id = Db::table('school_personnel')
->where('id', $id)
->value('sys_user_id');
if ($sys_user_id) {
$where[] = [Db::raw("FIND_IN_SET($sys_user_id, user_permission)"), '>', 0];
}
}
$search_model = $LessonCourseTeaching->where($where)->field($field)->order($order);
$list = $this->pageQuery($search_model);
@ -72,7 +79,14 @@ class TeachingResearchService extends BaseApiService
$LessonCourseTeaching = new LessonCourseTeaching();
$where = [];
if ($id !== null && $id !== '') {
$where[] = [Db::raw("FIND_IN_SET($id, user_permission)"), '>', 0];
// 根据personnel_id获取sys_user_id
$sys_user_id = Db::table('school_personnel')
->where('id', $id)
->value('sys_user_id');
if ($sys_user_id) {
$where[] = [Db::raw("FIND_IN_SET($sys_user_id, user_permission)"), '>', 0];
}
}
$list = $LessonCourseTeaching->where($where)->distinct(true)->column('table_type');
return $list;

161
niucloud/app/service/api/login/UnifiedLoginService.php

@ -161,7 +161,7 @@ class UnifiedLoginService extends BaseService
->where('p.phone', $username)
->where('p.status', 2) // 2=已审核(正常状态)
->where('u.status', 1)
->field('p.*, u.username, u.password, u.real_name')
->field('p.*, u.username, u.password, u.real_name, u.role_ids, u.is_admin')
->find();
if (!$staffInfo) {
@ -173,8 +173,9 @@ class UnifiedLoginService extends BaseService
throw new CommonException('密码错误');
}
// 根据account_type确定角色类型
$roleType = $this->getAccountTypeCode($staffInfo['account_type']);
// 获取用户的角色信息(从数据库查询)
$roleInfo = $this->getStaffRoleFromDb($staffInfo->toArray());
$roleType = $roleInfo['role_id'] ?? $this->getAccountTypeCode($staffInfo['account_type']);
// 生成Token
$tokenData = [
@ -187,9 +188,8 @@ class UnifiedLoginService extends BaseService
$tokenResult = TokenAuth::createToken($staffInfo['id'], AppTypeDict::PERSONNEL, $tokenData, 86400);
$token = $tokenResult['token'];
// 获取角色信息和菜单权限
$roleInfo = $this->getStaffRoleInfo($roleType);
$menuList = $this->getStaffMenuList($roleType);
// 获取菜单权限
$menuList = $this->getStaffMenuList($roleType, $staffInfo['is_admin']);
return [
'token' => $token,
@ -203,6 +203,7 @@ class UnifiedLoginService extends BaseService
'employee_number' => $staffInfo['employee_number'],
'user_type' => self::USER_TYPE_STAFF,
'role_type' => $roleType,
'is_admin' => $staffInfo['is_admin'],
],
'role_info' => $roleInfo,
'menu_list' => $menuList,
@ -421,7 +422,103 @@ class UnifiedLoginService extends BaseService
}
/**
* 获取员工角色信息
* 从数据库获取员工角色信息
* @param array $staffInfo
* @return array
*/
private function getStaffRoleFromDb(array $staffInfo)
{
try {
// 如果是管理员,返回管理员角色
if ($staffInfo['is_admin'] == 1) {
return [
'role_id' => 0,
'role_name' => '超级管理员',
'role_code' => 'super_admin',
'role_key' => 'super_admin',
];
}
// 解析用户的角色ID列表
$roleIds = [];
if (!empty($staffInfo['role_ids'])) {
$roleIdsStr = trim($staffInfo['role_ids'], '[]"');
if (!empty($roleIdsStr)) {
$roleIds = array_map('intval', explode(',', $roleIdsStr));
}
}
// 如果没有角色分配,根据account_type推断
if (empty($roleIds)) {
return $this->getRoleInfoByAccountType($staffInfo['account_type']);
}
// 查询第一个角色的详细信息
$roleId = $roleIds[0]; // 取第一个角色
$roleData = Db::table('school_sys_role')
->where('role_id', $roleId)
->where('status', 1)
->find();
if ($roleData) {
return [
'role_id' => $roleData['role_id'],
'role_name' => $roleData['role_name'],
'role_code' => $roleData['role_key'] ?: 'staff',
'role_key' => $roleData['role_key'],
];
}
// 如果查询失败,使用默认角色
return $this->getRoleInfoByAccountType($staffInfo['account_type']);
} catch (\Exception $e) {
// 如果查询失败,使用默认角色
return $this->getRoleInfoByAccountType($staffInfo['account_type']);
}
}
/**
* 根据account_type获取角色信息
* @param string $accountType
* @return array
*/
private function getRoleInfoByAccountType(string $accountType)
{
switch ($accountType) {
case 'market_manager':
return [
'role_id' => 999,
'role_name' => '校长',
'role_code' => 'principal',
'role_key' => 'principal',
];
case 'market':
return [
'role_id' => self::STAFF_ROLE_MARKET,
'role_name' => '市场人员',
'role_code' => 'market',
'role_key' => 'market',
];
case 'teacher':
return [
'role_id' => self::STAFF_ROLE_TEACHER,
'role_name' => '教师',
'role_code' => 'teacher',
'role_key' => 'teacher',
];
default:
return [
'role_id' => self::STAFF_ROLE_TEACHER,
'role_name' => '教师',
'role_code' => 'teacher',
'role_key' => 'teacher',
];
}
}
/**
* 获取员工角色信息(兼容旧方法)
* @param int $roleType
* @return array
*/
@ -440,11 +537,17 @@ class UnifiedLoginService extends BaseService
/**
* 获取员工菜单列表(动态查询数据库)
* @param int $roleType
* @param int $isAdmin
* @return array
*/
private function getStaffMenuList(int $roleType)
private function getStaffMenuList(int $roleType, int $isAdmin = 0)
{
try {
// 如果是超级管理员或校长,返回所有菜单
if ($isAdmin == 1 || $roleType == 999) {
return $this->getAllMenuList();
}
// 查询角色对应的菜单权限
$menuList = Db::table('sys_menus')
->alias('m')
@ -486,6 +589,48 @@ class UnifiedLoginService extends BaseService
}
}
/**
* 获取所有功能菜单(校长/管理员使用)
* @return array
*/
private function getAllMenuList()
{
return [
// 客户管理
['key' => 'customer_resource', 'title' => '客户资源', 'icon' => 'person-filled', 'path' => '/pages-market/clue/index'],
['key' => 'add_customer', 'title' => '添加资源', 'icon' => 'plus-filled', 'path' => '/pages-market/clue/add_clues'],
// 教学管理
['key' => 'course_query', 'title' => '课程查询', 'icon' => 'search', 'path' => '/pages-coach/coach/schedule/schedule_table'],
['key' => 'student_management', 'title' => '学员管理', 'icon' => 'contact-filled', 'path' => '/pages-coach/coach/student/student_list'],
['key' => 'course_arrangement', 'title' => '课程安排', 'icon' => 'calendar-filled', 'path' => '/pages-market/clue/class_arrangement'],
['key' => 'resource_library', 'title' => '资料库', 'icon' => 'folder-add-filled', 'path' => '/pages-coach/coach/my/teaching_management'],
// 财务管理
['key' => 'reimbursement', 'title' => '报销管理', 'icon' => 'wallet-filled', 'path' => '/pages-market/reimbursement/list'],
['key' => 'salary_management', 'title' => '工资管理', 'icon' => 'money-filled', 'path' => '/pages-common/salary/index'],
// 合同管理
['key' => 'contract_management', 'title' => '合同管理', 'icon' => 'document-filled', 'path' => '/pages-common/contract/my_contract'],
// 数据统计
['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']],
['key' => 'comprehensive_data', 'title' => '综合数据', 'icon' => 'chart-filled', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'comprehensive']],
// 消息中心
['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'],
// 个人中心
['key' => 'personal_center', 'title' => '个人中心', 'icon' => 'user-filled', 'path' => '/pages/common/user/index'],
['key' => 'attendance', 'title' => '考勤管理', 'icon' => 'clock-filled', 'path' => '/pages-common/attendance/index'],
// 管理功能(校长专用)
['key' => 'staff_management', 'title' => '员工管理', 'icon' => 'contacts-filled', 'path' => '/pages-admin/staff/index'],
['key' => 'approval_management', 'title' => '审批管理', 'icon' => 'checkmark-filled', 'path' => '/pages-admin/approval/index'],
['key' => 'system_settings', 'title' => '系统设置', 'icon' => 'settings-filled', 'path' => '/pages-admin/settings/index'],
];
}
/**
* 获取默认员工菜单列表(兼容处理)
* @param int $roleType

246
niucloud/app/service/api/student/MessageService.php

@ -16,7 +16,7 @@ use think\facade\Db;
class MessageService extends BaseApiService
{
/**
* 获取学员消息列表
* 获取学员消息列表(按对话分组)
* @param array $data
* @return array
*/
@ -41,55 +41,82 @@ class MessageService extends BaseApiService
}
// 已读状态筛选
if ($is_read !== '') {
if ($is_read !== '' && $is_read !== 'undefined' && $is_read !== 'null') {
$where[] = ['is_read', '=', (int)$is_read];
}
// 关键词搜索
if (!empty($keyword)) {
if (!empty($keyword) && $keyword !== 'undefined' && $keyword !== 'null') {
$where[] = ['title|content', 'like', "%{$keyword}%"];
}
try {
// 获取消息列表
$list = Db::name('chat_messages')
// 获取按发送者分组的最新消息(每个发送者一条)
$subQuery = Db::name('chat_messages')
->where($where)
->field('id, from_type, from_id, message_type, title, content, business_id, business_type, is_read, read_time, created_at')
->order('created_at desc')
->paginate([
'list_rows' => $limit,
'page' => $page
]);
->field('MAX(id) as latest_id, from_type, from_id, COUNT(*) as message_count,
SUM(CASE WHEN is_read = 0 THEN 1 ELSE 0 END) as unread_count')
->group('from_type, from_id')
->buildSql();
$messages = $list->items();
// 获取最新消息的详细信息
$conversationQuery = Db::query(
"SELECT m.id, m.from_type, m.from_id, m.message_type, m.title, m.content,
m.business_id, m.business_type, m.is_read, m.read_time, m.created_at,
g.message_count, g.unread_count
FROM ({$subQuery}) g
JOIN school_chat_messages m ON g.latest_id = m.id
ORDER BY m.created_at DESC
LIMIT " . (($page - 1) * $limit) . ", {$limit}"
);
// 格式化消息数据
foreach ($messages as &$message) {
$message['create_time'] = strtotime($message['created_at']);
$message['read_time_formatted'] = $message['read_time'] ? date('Y-m-d H:i:s', strtotime($message['read_time'])) : '';
$message['type_text'] = $this->getMessageTypeText($message['message_type']);
$message['from_name'] = $this->getFromName($message['from_type'], $message['from_id']);
// 获取总的对话数量用于分页
$totalConversations = Db::name('chat_messages')
->where($where)
->group('from_type, from_id')
->count();
$conversations = [];
foreach ($conversationQuery as $conversation) {
$conversation['create_time'] = strtotime($conversation['created_at']);
$conversation['read_time_formatted'] = $conversation['read_time'] ?
date('Y-m-d H:i:s', strtotime($conversation['read_time'])) : '';
$conversation['type_text'] = $this->getMessageTypeText($conversation['message_type']);
$conversation['from_name'] = $this->getFromName($conversation['from_type'], $conversation['from_id']);
// 处理内容长度
if (mb_strlen($message['content']) > 100) {
$message['summary'] = mb_substr($message['content'], 0, 100) . '...';
if (mb_strlen($conversation['content']) > 100) {
$conversation['summary'] = mb_substr($conversation['content'], 0, 100) . '...';
} else {
$message['summary'] = $message['content'];
$conversation['summary'] = $conversation['content'];
}
unset($message['created_at']);
// 添加对话统计信息
$conversation['total_messages'] = (int)$conversation['message_count'];
$conversation['unread_messages'] = (int)$conversation['unread_count'];
$conversation['has_unread'] = $conversation['unread_count'] > 0;
// 添加最新消息标识
$conversation['is_latest'] = true;
unset($conversation['created_at'], $conversation['message_count'], $conversation['unread_count']);
$conversations[] = $conversation;
}
$totalPages = ceil($totalConversations / $limit);
return [
'list' => $messages,
'list' => $conversations,
'current_page' => $page,
'last_page' => $list->lastPage(),
'total' => $list->total(),
'last_page' => $totalPages,
'total' => $totalConversations,
'per_page' => $limit,
'has_more' => $page < $list->lastPage()
'has_more' => $page < $totalPages
];
} catch (\Exception $e) {
// 记录异常信息,方便调试
\think\facade\Log::error('消息对话列表查询异常: ' . $e->getMessage());
// 如果数据库查询失败,返回Mock数据
return $this->getMockMessageList($data);
}
@ -134,6 +161,74 @@ class MessageService extends BaseApiService
}
}
/**
* 获取对话中的所有消息
* @param array $data
* @return array
*/
public function getConversationMessages(array $data): array
{
$student_id = $data['student_id'];
$from_type = $data['from_type'];
$from_id = $data['from_id'];
$page = max(1, (int)($data['page'] ?? 1));
$limit = max(1, min(100, (int)($data['limit'] ?? 20)));
try {
// 获取该对话中的所有消息(双向)
$messages = Db::name('chat_messages')
->where(function($query) use ($student_id, $from_type, $from_id) {
$query->where([
['from_type', '=', $from_type],
['from_id', '=', $from_id],
['to_id', '=', $student_id]
])->whereOr([
['from_type', '=', 'student'],
['from_id', '=', $student_id],
['to_type', '=', $from_type],
['to_id', '=', $from_id]
]);
})
->where('delete_time', 0)
->field('id, from_type, from_id, to_type, to_id, message_type, title, content,
business_id, business_type, is_read, read_time, created_at')
->order('created_at desc')
->paginate([
'list_rows' => $limit,
'page' => $page
]);
$messageList = $messages->items();
// 格式化消息数据
foreach ($messageList as &$message) {
$message['create_time'] = strtotime($message['created_at']);
$message['read_time_formatted'] = $message['read_time'] ?
date('Y-m-d H:i:s', strtotime($message['read_time'])) : '';
$message['type_text'] = $this->getMessageTypeText($message['message_type']);
$message['from_name'] = $this->getFromName($message['from_type'], $message['from_id']);
// 判断是否为学员发送的消息
$message['is_sent_by_student'] = ($message['from_id'] == $student_id &&
($message['from_type'] == 'student' || $message['from_type'] == ''));
unset($message['created_at']);
}
return [
'list' => array_reverse($messageList), // 反转顺序,最新的在底部
'current_page' => $page,
'last_page' => $messages->lastPage(),
'total' => $messages->total(),
'per_page' => $limit,
'has_more' => $page < $messages->lastPage()
];
} catch (\Exception $e) {
throw new \Exception('获取对话消息失败:' . $e->getMessage());
}
}
/**
* 标记消息已读
* @param array $data
@ -284,6 +379,79 @@ class MessageService extends BaseApiService
}
}
/**
* 学员回复消息
* @param array $data
* @return array
*/
public function replyMessage(array $data): array
{
$student_id = $data['student_id'];
$to_type = $data['to_type'];
$to_id = $data['to_id'];
$content = trim($data['content'] ?? '');
$message_type = $data['message_type'] ?? 'text';
$title = $data['title'] ?? '';
// 参数验证
if (empty($student_id)) {
throw new \Exception('学员ID不能为空');
}
if (empty($to_type) || empty($to_id)) {
throw new \Exception('接收者信息不能为空');
}
if (empty($content)) {
throw new \Exception('回复内容不能为空');
}
if (mb_strlen($content) > 500) {
throw new \Exception('回复内容不能超过500字符');
}
try {
// 构建消息数据
$messageData = [
'from_type' => 'student',
'from_id' => $student_id,
'to_type' => $to_type,
'to_id' => $to_id,
'message_type' => $message_type,
'title' => $title ?: '学员回复',
'content' => $content,
'business_id' => 0,
'business_type' => '',
'is_read' => 0,
'read_time' => null,
'created_at' => date('Y-m-d H:i:s'),
'delete_time' => 0
];
// 插入消息记录
$result = Db::name('chat_messages')->insert($messageData);
if ($result) {
return [
'message' => '回复发送成功',
'data' => [
'id' => Db::name('chat_messages')->getLastInsID(),
'content' => $content,
'created_at' => $messageData['created_at'],
'create_time' => strtotime($messageData['created_at']),
'from_name' => '我', // 学员自己发送的消息
'is_sent_by_student' => true
]
];
} else {
throw new \Exception('消息发送失败');
}
} catch (\Exception $e) {
throw new \Exception('回复消息失败:' . $e->getMessage());
}
}
/**
* 搜索消息
* @param array $data
@ -330,10 +498,30 @@ class MessageService extends BaseApiService
case 'system':
return '系统';
case 'personnel':
// 可以查询员工表获取真实姓名
return '教务老师';
// 查询员工表获取真实姓名
try {
$personnel = \think\facade\Db::name('personnel')
->where('id', $from_id)
->field('name')
->find();
return $personnel['name'] ?? '员工';
} catch (\Exception $e) {
return '员工';
}
case 'customer':
return '客户';
// 查询客户表获取真实姓名
try {
$customer = \think\facade\Db::name('customer_resources')
->where('id', $from_id)
->field('name')
->find();
return $customer['name'] ?? '客户';
} catch (\Exception $e) {
return '客户';
}
case 'student':
case '': // 空字符串表示学员发送的消息
return '我';
default:
return '未知';
}

298
niucloud/app/service/core/performance/PerformanceConfigService.php

@ -0,0 +1,298 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\service\core\performance;
use core\base\BaseService;
use think\facade\Db;
use think\facade\Cache;
/**
* 绩效配置服务类
* Class PerformanceConfigService
* @package app\service\core\performance
*/
class PerformanceConfigService extends BaseService
{
/**
* 获取配置结构定义
* @param string $configType 配置类型
* @return array|null
*/
public function getConfigStructure($configType)
{
$config = config('performance_config.performance_config');
return $config[$configType] ?? null;
}
/**
* 获取实际配置值
* @param string $configType 配置类型
* @return array
*/
public function getConfigData($configType)
{
// 先从缓存获取
$cacheKey = "performance_config:{$configType}";
$cachedData = Cache::get($cacheKey);
if ($cachedData !== false && $cachedData !== null) {
return $cachedData;
}
// 从数据库获取配置值
$dbRecord = Db::table('school_performance_config')
->where('config_type', $configType)
->where('is_active', 1)
->find();
if ($dbRecord) {
$configData = json_decode($dbRecord['config_data'], true);
// 缓存30分钟
if ($configData !== null && is_array($configData)) {
Cache::set($cacheKey, $configData, 1800);
return $configData;
}
}
// 返回默认值
$structure = $this->getConfigStructure($configType);
$defaultValues = $structure['default_values'] ?? [];
// 缓存默认值
if (!empty($defaultValues)) {
Cache::set($cacheKey, $defaultValues, 1800);
}
return $defaultValues;
}
/**
* 保存配置数据
* @param string $configType 配置类型
* @param array $configData 配置数据
* @param int $userId 用户ID
* @return bool
* @throws \Exception
*/
public function saveConfigData($configType, $configData, $userId = 0)
{
// 验证配置数据结构
$this->validateConfigData($configType, $configData);
// 开始事务
Db::startTrans();
try {
// 检查是否存在配置
$existsConfig = Db::table('school_performance_config')
->where('config_type', $configType)
->find();
if ($existsConfig) {
// 更新配置
Db::table('school_performance_config')
->where('config_type', $configType)
->update([
'config_data' => json_encode($configData, JSON_UNESCAPED_UNICODE),
'updated_by' => $userId,
'updated_at' => date('Y-m-d H:i:s')
]);
} else {
// 新增配置
Db::table('school_performance_config')
->insert([
'config_type' => $configType,
'config_data' => json_encode($configData, JSON_UNESCAPED_UNICODE),
'created_by' => $userId,
'updated_by' => $userId
]);
}
// 清除缓存
$this->clearConfigCache($configType);
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
throw new \Exception('配置保存失败:' . $e->getMessage());
}
}
/**
* 验证配置数据
* @param string $configType 配置类型
* @param array $configData 配置数据
* @throws \Exception
*/
private function validateConfigData($configType, $configData)
{
$structure = $this->getConfigStructure($configType);
if (!$structure) {
throw new \Exception("未知的配置类型: {$configType}");
}
// 检查配置是否启用
if (!$structure['enabled']) {
throw new \Exception("配置类型 {$configType} 未启用");
}
// 这里可以根据field_types中的定义进行更详细的数据验证
$this->validateConfigStructure($configData, $structure['config_structure'], $structure['field_types']);
}
/**
* 递归验证配置结构
* @param array $data 实际数据
* @param array $structure 结构定义
* @param array $fieldTypes 字段类型定义
* @throws \Exception
*/
private function validateConfigStructure($data, $structure, $fieldTypes)
{
foreach ($structure as $key => $config) {
if (!isset($data[$key])) {
continue; // 允许缺失字段,将使用默认值
}
$value = $data[$key];
$type = $config['type'];
switch ($type) {
case 'object':
if (!is_array($value)) {
throw new \Exception("字段 {$key} 必须是对象类型");
}
break;
case 'array':
if (!is_array($value)) {
throw new \Exception("字段 {$key} 必须是数组类型");
}
break;
case 'number':
if (!is_numeric($value)) {
throw new \Exception("字段 {$key} 必须是数字类型");
}
break;
case 'integer':
if (!is_int($value) && !ctype_digit((string)$value)) {
throw new \Exception("字段 {$key} 必须是整数类型");
}
break;
case 'boolean':
// PHP的json_decode会将true/false转换为布尔值,这里兼容处理
break;
default:
// 检查是否是自定义类型
if (isset($fieldTypes[$type])) {
$this->validateConfigStructure($value, [$key => $fieldTypes[$type]], $fieldTypes);
}
}
}
}
/**
* 清除配置缓存
* @param string|null $configType 配置类型,为null时清除所有缓存
*/
public function clearConfigCache($configType = null)
{
if ($configType) {
Cache::delete("performance_config:{$configType}");
} else {
// 清除所有绩效配置缓存
$configTypes = ['market_staff', 'sales_staff', 'coach_staff'];
foreach ($configTypes as $type) {
Cache::delete("performance_config:{$type}");
}
}
}
/**
* 获取所有可用的配置类型
* @return array
*/
public function getAvailableConfigTypes()
{
$config = config('performance_config.performance_config');
$types = [];
foreach ($config as $type => $definition) {
if ($definition['enabled']) {
$types[] = [
'type' => $type,
'name' => $definition['name'],
'description' => $definition['description']
];
}
}
return $types;
}
/**
* 重置配置为默认值
* @param string $configType 配置类型
* @param int $userId 用户ID
* @return bool
* @throws \Exception
*/
public function resetConfigToDefault($configType, $userId = 0)
{
$structure = $this->getConfigStructure($configType);
if (!$structure) {
throw new \Exception("未知的配置类型: {$configType}");
}
$defaultValues = $structure['default_values'] ?? [];
return $this->saveConfigData($configType, $defaultValues, $userId);
}
/**
* 导出配置
* @param string $configType 配置类型
* @return array
*/
public function exportConfig($configType)
{
$structure = $this->getConfigStructure($configType);
$data = $this->getConfigData($configType);
return [
'config_type' => $configType,
'name' => $structure['name'] ?? '',
'description' => $structure['description'] ?? '',
'version' => '1.0',
'exported_at' => date('Y-m-d H:i:s'),
'data' => $data
];
}
/**
* 导入配置
* @param array $exportData 导出的配置数据
* @param int $userId 用户ID
* @return bool
* @throws \Exception
*/
public function importConfig($exportData, $userId = 0)
{
if (!isset($exportData['config_type']) || !isset($exportData['data'])) {
throw new \Exception('导入数据格式错误');
}
return $this->saveConfigData($exportData['config_type'], $exportData['data'], $userId);
}
}

3
niucloud/config/console.php

@ -20,6 +20,9 @@ $data = [
//wokrerman的启动停止和重启
'workerman' => 'app\command\workerman\Workerman',
//教研管理人员同步命令
'teaching:sync' => 'app\command\TeachingSyncCommand',
'testcommand'=>'app\command\TestCommand'
],
];

203
niucloud/config/performance_config.php

@ -0,0 +1,203 @@
<?php
// +----------------------------------------------------------------------
// | 绩效配置统一管理文件
// +----------------------------------------------------------------------
// | 用于管理所有角色的绩效配置,支持灵活扩展和完整注释
// +----------------------------------------------------------------------
return [
'performance_config' => [
// 市场人员绩效配置
'market_staff' => [
'name' => '市场人员绩效',
'description' => '市场人员按录入学员数量计算绩效',
'enabled' => true,
'config_structure' => [
'weekly_rules' => [
'label' => '每周价格规则',
'description' => '不同工作日的学员录入价格配置',
'type' => 'object',
'properties' => [
'mon' => ['label' => '周一', 'type' => 'price_rule'],
'tue' => ['label' => '周二', 'type' => 'price_rule'],
'wed' => ['label' => '周三', 'type' => 'price_rule'],
'thu' => ['label' => '周四', 'type' => 'price_rule'],
'fri' => ['label' => '周五', 'type' => 'price_rule'],
'sat' => ['label' => '周六', 'type' => 'price_rule'],
'sun' => ['label' => '周日', 'type' => 'price_rule'],
]
]
],
'field_types' => [
'price_rule' => [
'label' => '价格规则',
'type' => 'object',
'properties' => [
'basePrice' => [
'label' => '基础单价',
'type' => 'number',
'min' => 0,
'max' => 1000,
'unit' => '元/人',
'description' => '未超过限额时每录入一个学员的绩效'
],
'limitCount' => [
'label' => '超量阈值',
'type' => 'integer',
'min' => 0,
'max' => 100,
'unit' => '人',
'description' => '当日录入超过此数量时按超量单价计算'
],
'extraPrice' => [
'label' => '超量单价',
'type' => 'number',
'min' => 0,
'max' => 1000,
'unit' => '元/人',
'description' => '超过阈值后每录入一个学员的绩效'
]
]
]
],
'default_values' => [
'weekly_rules' => [
'mon' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8],
'tue' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8],
'wed' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8],
'thu' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8],
'fri' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8],
'sat' => ['basePrice' => 8, 'limitCount' => 2, 'extraPrice' => 12],
'sun' => ['basePrice' => 8, 'limitCount' => 2, 'extraPrice' => 12],
]
]
],
// 销售人员绩效配置
'sales_staff' => [
'name' => '销售人员绩效',
'description' => '销售人员按订单和续费率计算绩效',
'enabled' => true,
'config_structure' => [
'renewal_rate_rules' => [
'label' => '续费率绩效规则',
'description' => '不同续费率区间的绩效单价',
'type' => 'array',
'items' => 'renewal_rule'
],
'course_commission' => [
'label' => '课程提成配置',
'description' => '不同课程的销售提成单价',
'type' => 'array',
'items' => 'course_price'
],
'sharing_rules' => [
'label' => '分成规则',
'description' => '录入人与成交人的分成比例',
'type' => 'object',
'properties' => [
'first_visit' => ['label' => '一访成交分成', 'type' => 'sharing_rule'],
'second_visit' => ['label' => '二访成交分成', 'type' => 'sharing_rule'],
'chase_order' => ['label' => '追单分成', 'type' => 'sharing_rule'],
'internal_staff' => ['label' => '内部员工分成', 'type' => 'sharing_rule']
]
]
],
'field_types' => [
'renewal_rule' => [
'label' => '续费率规则',
'type' => 'object',
'properties' => [
'min_rate' => ['label' => '最低续费率', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'],
'max_rate' => ['label' => '最高续费率', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'],
'commission' => ['label' => '绩效单价', 'type' => 'number', 'min' => 0, 'unit' => '元/单'],
'description' => ['label' => '规则说明', 'type' => 'string']
]
],
'course_price' => [
'label' => '课程价格',
'type' => 'object',
'properties' => [
'course_id' => ['label' => '课程ID', 'type' => 'integer'],
'course_name' => ['label' => '课程名称', 'type' => 'string', 'readonly' => true],
'sales_commission' => ['label' => '销售提成', 'type' => 'number', 'unit' => '元'],
'coach_commission' => ['label' => '教练提成', 'type' => 'number', 'unit' => '元']
]
],
'sharing_rule' => [
'label' => '分成规则',
'type' => 'object',
'properties' => [
'input_ratio' => ['label' => '录入人比例', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'],
'close_ratio' => ['label' => '成交人比例', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'],
'enabled' => ['label' => '是否启用', 'type' => 'boolean']
]
]
],
'default_values' => [
'renewal_rate_rules' => [
['min_rate' => 0, 'max_rate' => 80, 'commission' => 10, 'description' => '续费率低于80%'],
['min_rate' => 80, 'max_rate' => 90, 'commission' => 25, 'description' => '续费率80%-90%'],
['min_rate' => 90, 'max_rate' => 100, 'commission' => 40, 'description' => '续费率90%以上']
],
'sharing_rules' => [
'first_visit' => ['input_ratio' => 70, 'close_ratio' => 30, 'enabled' => true],
'second_visit' => ['input_ratio' => 60, 'close_ratio' => 40, 'enabled' => true],
'chase_order' => ['input_ratio' => 30, 'close_ratio' => 70, 'enabled' => true],
'internal_staff' => ['input_ratio' => 100, 'close_ratio' => 0, 'enabled' => true]
]
]
],
// 教练绩效配置
'coach_staff' => [
'name' => '教练绩效',
'description' => '教练按课时和订单计算绩效',
'enabled' => true,
'config_structure' => [
'course_commission' => [
'label' => '课时提成配置',
'description' => '教练上课的课时提成',
'type' => 'array',
'items' => 'course_price'
],
'order_commission' => [
'label' => '订单提成配置',
'description' => '教练作为销售时的订单提成',
'type' => 'array',
'items' => 'renewal_rule'
]
],
'field_types' => [
'course_price' => [
'label' => '课程价格',
'type' => 'object',
'properties' => [
'course_id' => ['label' => '课程ID', 'type' => 'integer'],
'course_name' => ['label' => '课程名称', 'type' => 'string', 'readonly' => true],
'sales_commission' => ['label' => '销售提成', 'type' => 'number', 'unit' => '元'],
'coach_commission' => ['label' => '教练提成', 'type' => 'number', 'unit' => '元']
]
],
'renewal_rule' => [
'label' => '续费率规则',
'type' => 'object',
'properties' => [
'min_rate' => ['label' => '最低续费率', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'],
'max_rate' => ['label' => '最高续费率', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'],
'commission' => ['label' => '绩效单价', 'type' => 'number', 'min' => 0, 'unit' => '元/单'],
'description' => ['label' => '规则说明', 'type' => 'string']
]
]
],
'default_values' => [
'course_commission' => [], // 从课程表动态加载
'order_commission' => [
['min_rate' => 0, 'max_rate' => 80, 'commission' => 8, 'description' => '续费率低于80%'],
['min_rate' => 80, 'max_rate' => 90, 'commission' => 20, 'description' => '续费率80%-90%'],
['min_rate' => 90, 'max_rate' => 100, 'commission' => 35, 'description' => '续费率90%以上']
]
]
]
]
];

137
niucloud/config/teaching_management.php

@ -0,0 +1,137 @@
<?php
// +----------------------------------------------------------------------
// | 教研管理统一配置文件
// +----------------------------------------------------------------------
// | 用于管理教研管理模块的统一配置,包括模块类型、自动分发等设置
// +----------------------------------------------------------------------
return [
'teaching_management' => [
// 教练部门ID(用于自动人员分发)
'coach_department_id' => 24,
// 模块配置:定义所有教研管理模块的类型和属性
'module_configs' => [
// 现有模块配置
'course_syllabus' => [
'table_type' => 1,
'auto_distribute' => false,
'name' => '课程教学大纲',
'key' => 'course_syllabus',
'order' => 1
],
'jump_lesson_library' => [
'table_type' => 2,
'auto_distribute' => false,
'name' => '跳绳教案库',
'key' => 'jump_lesson_library',
'order' => 2
],
'en_teaching_library' => [
'table_type' => 3,
'auto_distribute' => false,
'name' => '增高教案库',
'key' => 'en_teaching_library',
'order' => 3
],
'basketball_teaching_library' => [
'table_type' => 4,
'auto_distribute' => false,
'name' => '篮球教案库',
'key' => 'basketball_teaching_library',
'order' => 4
],
'strengthen_teaching_library' => [
'table_type' => 5,
'auto_distribute' => false,
'name' => '强化教案库',
'key' => 'strengthen_teaching_library',
'order' => 5
],
'ninja_teaching_library' => [
'table_type' => 6,
'auto_distribute' => false,
'name' => '空中忍者教案库',
'key' => 'ninja_teaching_library',
'order' => 6
],
'security_teaching_library' => [
'table_type' => 7,
'auto_distribute' => false,
'name' => '少儿安防教案库',
'key' => 'security_teaching_library',
'order' => 7
],
'physical_teaching_library' => [
'table_type' => 8,
'auto_distribute' => false,
'name' => '体能教案库',
'key' => 'physical_teaching_library',
'order' => 8
],
// 新增模块配置(需要自动分发)
'instructional_material' => [
'table_type' => 30,
'auto_distribute' => true,
'name' => '教学资料库',
'key' => 'instructional_material',
'order' => 9
],
'future_content' => [
'table_type' => 31,
'auto_distribute' => true,
'name' => '未来周内容训练',
'key' => 'future_content',
'order' => 10
],
'professional_skills' => [
'table_type' => 32,
'auto_distribute' => true,
'name' => '专业技能',
'key' => 'professional_skills',
'order' => 11
],
'physical_testing' => [
'table_type' => 33,
'auto_distribute' => true,
'name' => '睿莱体测',
'key' => 'physical_testing',
'order' => 12
],
'children_like' => [
'table_type' => 34,
'auto_distribute' => true,
'name' => '如何让孩子喜欢',
'key' => 'children_like',
'order' => 13
]
],
// 自动分发配置
'auto_distribute_config' => [
// 启用自动分发功能
'enabled' => true,
// 分发失败时的处理策略
'on_failure' => 'log', // log: 仅记录日志, exception: 抛出异常, ignore: 忽略错误
// 权限字段分隔符
'permission_separator' => ',',
// 人员姓名字段分隔符
'name_separator' => ','
],
// 定时任务配置
'cron_config' => [
// 人员同步任务配置
'sync_personnel' => [
'enabled' => true,
'schedule' => '0 2 * * *', // 每天凌晨2点执行
'batch_size' => 100, // 批处理大小
'timeout' => 30, // 超时时间(秒)
]
]
]
];

84
uniapp/api/apiRoute.js

@ -1915,6 +1915,69 @@ export default {
}
},
// 获取对话中的所有消息
async getConversationMessages(data = {}) {
try {
const params = {
student_id: data.student_id,
from_type: data.from_type,
from_id: data.from_id,
page: data.page || 1,
limit: data.limit || 20
};
const response = await http.get('/message-test/conversation', params);
return response;
} catch (error) {
console.error('获取对话消息错误:', error);
// 返回空对话作为后备
return {
code: 1,
data: {
list: [],
current_page: 1,
last_page: 1,
total: 0,
per_page: 20,
has_more: false
},
msg: 'SUCCESS'
};
}
},
// 学员回复消息
async replyMessage(data = {}) {
try {
const response = await http.post('/message-test/reply', {
student_id: data.student_id,
to_type: data.to_type,
to_id: data.to_id,
content: data.content,
message_type: data.message_type || 'text',
title: data.title || ''
});
return response;
} catch (error) {
console.error('回复消息错误:', error);
// 返回模拟成功响应
return {
code: 1,
msg: '回复发送成功',
data: {
message: '回复发送成功',
data: {
id: Date.now(),
content: data.content,
created_at: new Date().toISOString().replace('T', ' ').slice(0, 19),
create_time: Math.floor(Date.now() / 1000),
from_name: '我',
is_sent_by_student: true
}
}
};
}
},
// 搜索学员消息
async searchStudentMessages(data = {}) {
try {
@ -2101,6 +2164,27 @@ export default {
};
},
// 搜索学员消息
async searchStudentMessages(data = {}) {
try {
const params = {
keyword: data.keyword || '',
message_type: data.message_type || '',
start_date: data.start_date || '',
end_date: data.end_date || '',
page: data.page || 1,
limit: data.limit || 10
};
const response = await http.get(`/message/search/${data.student_id}`, params);
return response;
} catch (error) {
console.error('搜索学员消息错误:', error);
// 返回模拟数据作为后备
return await this.searchStudentMessagesMock(data);
}
},
// 模拟搜索消息数据
async searchStudentMessagesMock(data = {}) {
// 复用消息列表的Mock数据,根据关键词进行筛选

4
uniapp/common/config.js

@ -1,6 +1,6 @@
// 环境变量配置
const env = 'development'
// const env = 'prod'
// const env = 'development'
const env = 'prod'
const isMockEnabled = false // 默认禁用Mock优先模式,仅作为回退
const isDebug = false // 默认启用调试模式
const devurl = 'http://localhost:20080/api'

503
uniapp/pages-coach/coach/my/teaching_management_info.vue

@ -0,0 +1,503 @@
<!--教研管理-详情-->
<template>
<view class="dark-theme">
<view class="main_section">
<view class="section_1">
<view class="title">{{ infoData.title }}</view>
<view class="meta-info">
<text class="category">{{ getTableType(infoData.table_type) }}</text>
<text class="date">{{ infoData.create_time }}</text>
</view>
<view class="content" v-html="infoData.content"></view>
<!-- 附件区域 -->
<view v-if="infoData.url" class="attachment-section">
<view class="attachment-title">附件</view>
<!-- 图片类型 -->
<view v-if="attachmentType === 'image'" class="image-attachment">
<image
:src="getFullUrl(infoData.url)"
mode="aspectFit"
@click="previewImage(getFullUrl(infoData.url))"
class="attachment-image"
/>
<text class="attachment-tip">点击图片可放大预览</text>
</view>
<!-- 视频类型 -->
<view v-if="attachmentType === 'video'" class="video-attachment">
<video
:src="getFullUrl(infoData.url)"
controls
class="attachment-video"
poster=""
></video>
</view>
<!-- 文档类型 -->
<view v-if="attachmentType === 'document'" class="document-attachment">
<view class="document-info">
<view class="document-icon">📄</view>
<view class="document-details">
<text class="document-name">{{ getFileName(infoData.url) }}</text>
<text class="document-size">{{ getFileExtension(infoData.url).toUpperCase() }} 文件</text>
</view>
</view>
<button class="download-btn" @click="downloadFile(getFullUrl(infoData.url))">
下载文件
</button>
</view>
</view>
<!-- 考试按钮 -->
<view v-if="infoData.exam_papers_id && infoData.exam_papers_id != 0" class="exam-section">
<button class="exam-btn" @click="goTake(infoData.exam_papers_id, infoData.id)">
开始考试
</button>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading">加载中...</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
data() {
return {
loading: true,
articleId: '',
infoData: {
title: '',
content: '',
table_type: '',
create_time: '',
exam_papers_id: 0,
type: 1,
url: ''
},
tableTypeName: {
'1': "课程教学大纲",
'2': "跳绳教案库",
'3': "增高教案库",
'4': "篮球教案库",
'5': "强化教案库",
'6': "空中忍者教案库",
'7': "少儿安防教案库",
'8': "体能教案库",
'9': "热身动作库",
'10': "体能动作库",
'11': "趣味游戏库",
'12': "放松动作库",
'13': "训练内容",
'14': "训练视频",
'15': "课后作业",
'16': "优秀一堂课",
'17': "空中忍者",
'18': "篮球动作",
'19': "跳绳动作",
'20': "跑酷动作",
'21': "安防动作",
'22': "标准化动作",
'23': "3-6岁体测",
'24': "7+体测",
'25': "3-6岁体测讲解—解读",
'26': "7+岁体测讲解—解读",
'27': "互动游戏",
'28': "套圈游戏",
'29': "鼓励方式"
}
}
},
computed: {
// type
attachmentType() {
if (!this.infoData.url) return null;
const type = this.infoData.type;
// type: 1=, 2=, 3=, 4=
if (type === 3) {
return 'image';
} else if (type === 4) {
return 'video';
} else if (type === 1 || type === 2) {
return 'document';
}
// type
const ext = this.getFileExtension(this.infoData.url).toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) {
return 'image';
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'm3u8'].includes(ext)) {
return 'video';
} else {
return 'document';
}
}
},
onLoad(options) {
this.articleId = options.id;
this.getInfo();
},
methods: {
//
async getInfo() {
try {
this.loading = true;
const res = await apiRoute.teachingResearchInfo(this.articleId);
if (res.code === 1) {
this.infoData = res.data;
} else {
uni.showToast({
title: res.msg || '获取详情失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取教研管理详情失败:', error);
uni.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
} finally {
this.loading = false;
}
},
//
getTableType(type) {
return this.tableTypeName[type] || '未知类型';
},
//
goTake(examId, articleId) {
uni.navigateTo({
url: '/pages-coach/coach/my/gotake_exam?id=' + examId + '&zid=' + articleId
});
},
// URL
getFullUrl(url) {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// URL
return 'http://localhost:20080' + (url.startsWith('/') ? url : '/' + url);
},
//
previewImage(url) {
uni.previewImage({
urls: [url],
current: url
});
},
//
downloadFile(url) {
uni.showLoading({
title: '下载中...'
});
uni.downloadFile({
url: url,
success: (res) => {
if (res.statusCode === 200) {
//
const extension = this.getFileExtension(url);
const fileName = this.getFileName(url);
//
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension.toLowerCase())) {
//
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({
title: '保存成功',
icon: 'success'
});
},
fail: (error) => {
console.error('保存图片失败:', error);
uni.showToast({
title: '保存失败',
icon: 'none'
});
}
});
} else {
//
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true,
success: () => {
console.log('打开文档成功');
},
fail: (error) => {
console.error('打开文档失败:', error);
uni.showToast({
title: '无法打开文件',
icon: 'none'
});
}
});
}
} else {
uni.showToast({
title: '下载失败',
icon: 'none'
});
}
},
fail: (error) => {
console.error('下载失败:', error);
uni.showToast({
title: '下载失败',
icon: 'none'
});
},
complete: () => {
uni.hideLoading();
}
});
},
//
getFileName(url) {
if (!url) return '';
const parts = url.split('/');
const fileName = parts[parts.length - 1];
return fileName.split('?')[0]; //
},
//
getFileExtension(url) {
if (!url) return '';
const fileName = this.getFileName(url);
const lastDotIndex = fileName.lastIndexOf('.');
return lastDotIndex !== -1 ? fileName.substring(lastDotIndex + 1) : '';
}
}
}
</script>
<style lang="scss" scoped>
.dark-theme {
background-color: #121212;
color: #ffffff;
min-height: 100vh;
}
.main_section {
padding: 20rpx 30rpx;
padding-bottom: 150rpx;
}
.section_1 {
.title {
font-size: 36rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 20rpx;
line-height: 1.4;
}
.meta-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #333;
.category {
color: #00d18c;
font-size: 24rpx;
background: rgba(0, 209, 140, 0.1);
padding: 8rpx 16rpx;
border-radius: 12rpx;
}
.date {
color: #b0b0b0;
font-size: 24rpx;
}
}
.content {
line-height: 1.8;
font-size: 30rpx;
color: #ffffff;
word-wrap: break-word;
word-break: break-all;
//
:deep(img) {
max-width: 100%;
height: auto;
border-radius: 8rpx;
margin: 20rpx 0;
}
//
:deep(p) {
margin: 20rpx 0;
line-height: 1.8;
}
}
}
.exam-section {
margin-top: 40rpx;
text-align: center;
.exam-btn {
background: linear-gradient(45deg, #00d18c, #00a374);
color: #ffffff;
border: none;
border-radius: 25rpx;
padding: 20rpx 60rpx;
font-size: 30rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(0, 209, 140, 0.3);
&:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 209, 140, 0.3);
}
}
}
.loading {
text-align: center;
padding: 40rpx 0;
color: #b0b0b0;
font-size: 28rpx;
}
//
.attachment-section {
margin-top: 30rpx;
padding: 30rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 16rpx;
border: 1rpx solid #333;
.attachment-title {
font-size: 32rpx;
font-weight: bold;
color: #00d18c;
margin-bottom: 20rpx;
display: flex;
align-items: center;
&::before {
content: '📎';
margin-right: 10rpx;
font-size: 28rpx;
}
}
//
.image-attachment {
text-align: center;
.attachment-image {
max-width: 100%;
max-height: 400rpx;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: transform 0.2s ease;
&:active {
transform: scale(0.98);
}
}
.attachment-tip {
margin-top: 16rpx;
font-size: 24rpx;
color: #b0b0b0;
}
}
//
.video-attachment {
.attachment-video {
width: 100%;
height: 400rpx;
border-radius: 12rpx;
background: #000;
}
}
//
.document-attachment {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background: rgba(255, 255, 255, 0.08);
border-radius: 12rpx;
border: 1rpx solid #444;
.document-info {
display: flex;
align-items: center;
flex: 1;
.document-icon {
font-size: 48rpx;
margin-right: 20rpx;
}
.document-details {
display: flex;
flex-direction: column;
.document-name {
font-size: 28rpx;
color: #ffffff;
word-break: break-all;
margin-bottom: 8rpx;
line-height: 1.4;
}
.document-size {
font-size: 24rpx;
color: #b0b0b0;
}
}
}
.download-btn {
background: linear-gradient(45deg, #00d18c, #00a374);
color: #ffffff;
border: none;
border-radius: 20rpx;
padding: 16rpx 32rpx;
font-size: 26rpx;
font-weight: bold;
box-shadow: 0 2rpx 8rpx rgba(0, 209, 140, 0.3);
min-width: 120rpx;
&:active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 4rpx rgba(0, 209, 140, 0.3);
}
}
}
}
</style>

749
uniapp/pages-student/messages/conversation.vue

@ -0,0 +1,749 @@
<!--学员消息对话详情页面-->
<template>
<view class="main_box">
<!-- 自定义导航栏 -->
<view class="navbar_section">
<view class="navbar_back" @click="goBack">
<text class="back_icon"></text>
</view>
<view class="navbar_title">{{ fromName || '对话详情' }}</view>
<view class="navbar_action"></view>
</view>
<!-- 消息列表 -->
<view class="conversation_section">
<scroll-view
class="message_scroll"
:scroll-y="true"
:scroll-into-view="scrollToView"
:scroll-with-animation="true"
>
<view v-if="loading" class="loading_section">
<view class="loading_text">加载中...</view>
</view>
<view v-else-if="messagesList.length === 0" class="empty_section">
<view class="empty_icon">💬</view>
<view class="empty_text">暂无对话记录</view>
</view>
<view v-else class="messages_container">
<view
v-for="message in messagesList"
:key="message.id"
:id="`msg_${message.id}`"
:class="['message_bubble', message.is_sent_by_student ? 'sent' : 'received']"
>
<view class="message_info">
<view class="sender_name">{{ message.from_name }}</view>
<view class="message_time">{{ formatTime(message.create_time) }}</view>
</view>
<view class="message_content">
<!-- 文本消息 -->
<view v-if="message.message_type === 'text' || !message.message_type" class="content_text">{{ message.content }}</view>
<!-- 图片消息 -->
<view v-if="message.message_type === 'img'" class="content_image" @click="previewImage(message.content)">
<image class="chat_image" :src="message.content" mode="aspectFill"></image>
</view>
</view>
<view class="message_status" v-if="message.is_sent_by_student">
<text class="status_text" v-if="message.is_read">已读</text>
<text class="status_text unread" v-else>未读</text>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 回复输入框 -->
<view class="reply_section">
<view class="reply_input_box">
<textarea
class="reply_textarea"
v-model="replyContent"
placeholder="输入回复内容..."
:maxlength="500"
:auto-height="true"
:show-count="false"
></textarea>
<view class="reply_actions">
<view class="left_actions">
<view class="attachment_button" @click="openImagePicker">
<text class="attachment_icon">📷</text>
</view>
<text class="char_count">{{ replyContent.length }}/500</text>
</view>
<view
class="send_button"
:class="{ disabled: !canSend }"
@click="sendReply"
>
<text class="send_text">发送</text>
</view>
</view>
</view>
</view>
<!-- 图片选择弹窗 -->
<fui-bottom-popup :show="showImagePicker" @close="closeImagePicker">
<view class="image_picker_section">
<view class="picker_title">选择图片</view>
<view class="picker_options">
<view class="picker_option" @click="chooseImage">
<view class="option_icon">📷</view>
<view class="option_text">相册</view>
<AQUplodeImage
:uploadUrl="uploadUrl"
:extraData="{ input_name: 'img_upload', formData:{} }"
@uplodeImageRes="handleImageUpload"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0;"
>
</AQUplodeImage>
</view>
</view>
</view>
</fui-bottom-popup>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
import AQUplodeImage from '@/components/AQ/AQUplodeImage'
import {
Api_url
} from "@/common/config.js"
export default {
components: {
AQUplodeImage
},
data() {
return {
studentId: 0,
fromType: '',
fromId: 0,
fromName: '',
messagesList: [],
loading: false,
replyContent: '',
sending: false,
scrollToView: '',
currentPage: 1,
hasMore: true,
showImagePicker: false,
uploadUrl: `${Api_url}/memberUploadImage` //
}
},
computed: {
canSend() {
return this.replyContent.trim().length > 0 && !this.sending
}
},
onLoad(options) {
//
this.studentId = parseInt(options.student_id) || 0
this.fromType = options.from_type || ''
this.fromId = parseInt(options.from_id) || 0
this.fromName = decodeURIComponent(options.from_name || '')
//
if (!this.studentId || !this.fromType || !this.fromId) {
uni.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
return
}
this.initPage()
},
methods: {
goBack() {
uni.navigateBack()
},
async initPage() {
await this.loadMessages()
this.scrollToBottom()
},
async loadMessages() {
this.loading = true
try {
console.log('加载对话消息:', {
student_id: this.studentId,
from_type: this.fromType,
from_id: this.fromId
})
const response = await apiRoute.getConversationMessages({
student_id: this.studentId,
from_type: this.fromType,
from_id: this.fromId,
page: this.currentPage,
limit: 20
})
if (response && response.code === 1 && response.data) {
const apiData = response.data
const newList = apiData.list || []
if (this.currentPage === 1) {
this.messagesList = newList
} else {
this.messagesList = [...this.messagesList, ...newList]
}
this.hasMore = apiData.has_more || false
console.log('对话消息加载成功:', this.messagesList)
} else {
console.warn('API返回失败:', response?.msg)
uni.showToast({
title: '加载消息失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取对话消息失败:', error)
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
this.loading = false
}
},
async sendReply() {
if (!this.canSend) return
const content = this.replyContent.trim()
if (!content) {
uni.showToast({
title: '请输入回复内容',
icon: 'none'
})
return
}
this.sending = true
try {
console.log('发送回复:', {
student_id: this.studentId,
to_type: this.fromType,
to_id: this.fromId,
content: content
})
const response = await apiRoute.replyMessage({
student_id: this.studentId,
to_type: this.fromType,
to_id: this.fromId,
content: content,
message_type: 'text'
})
if (response && response.code === 1) {
//
this.replyContent = ''
//
const newMessage = {
id: response.data.data?.id || Date.now(),
content: content,
from_name: '我',
from_type: 'student',
from_id: this.studentId,
is_sent_by_student: true,
is_read: 0,
create_time: Math.floor(Date.now() / 1000)
}
this.messagesList.push(newMessage)
//
setTimeout(() => {
this.scrollToBottom()
}, 100)
uni.showToast({
title: '发送成功',
icon: 'success'
})
console.log('回复发送成功')
} else {
uni.showToast({
title: response?.msg || '发送失败',
icon: 'none'
})
}
} catch (error) {
console.error('发送回复失败:', error)
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
this.sending = false
}
},
scrollToBottom() {
if (this.messagesList.length > 0) {
const lastMessage = this.messagesList[this.messagesList.length - 1]
this.scrollToView = `msg_${lastMessage.id}`
}
},
formatTime(timestamp) {
const date = new Date(timestamp * 1000)
const now = new Date()
const diffHours = (now - date) / (1000 * 60 * 60)
if (diffHours < 1) {
return '刚刚'
} else if (diffHours < 24) {
return Math.floor(diffHours) + '小时前'
} else if (diffHours < 48) {
return '昨天 ' + date.toTimeString().slice(0, 5)
} else {
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const time = date.toTimeString().slice(0, 5)
return `${month}-${day} ${time}`
}
},
//
openImagePicker() {
this.showImagePicker = true
},
//
closeImagePicker() {
this.showImagePicker = false
},
//
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
console.log('选择图片成功:', res)
// AQUplodeImage
},
fail: (err) => {
console.error('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none'
})
}
})
},
//
handleImageUpload(resData, extraData) {
console.log('图片上传成功:', resData, extraData)
if (extraData.input_name === 'img_upload') {
//
this.closeImagePicker()
//
this.sendImageMessage(resData.url)
}
},
//
async sendImageMessage(imageUrl) {
if (!imageUrl) {
uni.showToast({
title: '图片上传失败',
icon: 'none'
})
return
}
this.sending = true
try {
console.log('发送图片消息:', {
student_id: this.studentId,
to_type: this.fromType,
to_id: this.fromId,
content: imageUrl,
message_type: 'img'
})
const response = await apiRoute.replyMessage({
student_id: this.studentId,
to_type: this.fromType,
to_id: this.fromId,
content: imageUrl,
message_type: 'img'
})
if (response && response.code === 1) {
//
const newMessage = {
id: response.data.data?.id || Date.now(),
content: imageUrl,
message_type: 'img',
from_name: '我',
from_type: 'student',
from_id: this.studentId,
is_sent_by_student: true,
is_read: 0,
create_time: Math.floor(Date.now() / 1000)
}
this.messagesList.push(newMessage)
//
setTimeout(() => {
this.scrollToBottom()
}, 100)
uni.showToast({
title: '图片发送成功',
icon: 'success'
})
console.log('图片消息发送成功')
} else {
uni.showToast({
title: response?.msg || '图片发送失败',
icon: 'none'
})
}
} catch (error) {
console.error('发送图片消息失败:', error)
uni.showToast({
title: '网络错误',
icon: 'none'
})
} finally {
this.sending = false
}
},
//
previewImage(imageUrl) {
if (!imageUrl) return
uni.previewImage({
current: imageUrl,
urls: [imageUrl]
})
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
height: 100vh;
display: flex;
flex-direction: column;
}
//
.navbar_section {
display: flex;
justify-content: space-between;
align-items: center;
background: #29D3B4;
padding: 40rpx 32rpx 20rpx;
//
// #ifdef MP-WEIXIN
padding-top: 80rpx;
// #endif
.navbar_back {
width: 60rpx;
.back_icon {
color: #fff;
font-size: 40rpx;
font-weight: 600;
}
}
.navbar_title {
color: #fff;
font-size: 32rpx;
font-weight: 600;
}
.navbar_action {
width: 60rpx;
}
}
//
.conversation_section {
flex: 1;
overflow: hidden;
.message_scroll {
height: 100%;
padding: 20rpx;
}
.loading_section, .empty_section {
padding: 80rpx 32rpx;
text-align: center;
.loading_text, .empty_text {
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
}
.empty_icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
}
.messages_container {
padding-bottom: 40rpx;
.message_bubble {
margin-bottom: 32rpx;
&.sent {
display: flex;
flex-direction: column;
align-items: flex-end;
.message_info {
align-items: flex-end;
margin-bottom: 8rpx;
.sender_name {
color: #29D3B4;
font-weight: 600;
}
}
.message_content {
background: #29D3B4;
color: #fff;
max-width: 70%;
border-radius: 20rpx 20rpx 8rpx 20rpx;
}
.message_status {
margin-top: 8rpx;
.status_text {
font-size: 22rpx;
color: #999;
&.unread {
color: #f39c12;
}
}
}
}
&.received {
display: flex;
flex-direction: column;
align-items: flex-start;
.message_info {
align-items: flex-start;
margin-bottom: 8rpx;
.sender_name {
color: #333;
font-weight: 600;
}
}
.message_content {
background: #fff;
color: #333;
max-width: 70%;
border-radius: 20rpx 20rpx 20rpx 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
}
.message_info {
display: flex;
align-items: center;
gap: 12rpx;
.sender_name {
font-size: 24rpx;
}
.message_time {
font-size: 20rpx;
color: #999;
}
}
.message_content {
padding: 24rpx;
.content_text {
font-size: 28rpx;
line-height: 1.6;
word-wrap: break-word;
}
}
}
}
}
//
.reply_section {
background: #fff;
border-top: 1px solid #f0f0f0;
padding: 20rpx 24rpx;
// #ifdef MP-WEIXIN
padding-bottom: 40rpx; //
// #endif
.reply_input_box {
background: #f8f9fa;
border-radius: 16rpx;
padding: 16rpx;
.reply_textarea {
width: 100%;
min-height: 80rpx;
max-height: 200rpx;
font-size: 28rpx;
line-height: 1.4;
background: transparent;
border: none;
outline: none;
resize: none;
&::placeholder {
color: #999;
}
}
.reply_actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
.left_actions {
display: flex;
align-items: center;
gap: 16rpx;
.attachment_button {
width: 60rpx;
height: 60rpx;
background: #29D3B4;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
.attachment_icon {
font-size: 32rpx;
color: #fff;
}
}
.char_count {
font-size: 22rpx;
color: #999;
}
}
.send_button {
background: #29D3B4;
color: #fff;
padding: 12rpx 32rpx;
border-radius: 20rpx;
font-size: 26rpx;
&.disabled {
background: #ccc;
color: #999;
}
.send_text {
font-weight: 600;
}
}
}
}
}
//
.image_picker_section {
background: #fff;
border-radius: 16rpx 16rpx 0 0;
padding: 40rpx 32rpx;
min-height: 200rpx;
.picker_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 40rpx;
}
.picker_options {
display: flex;
justify-content: center;
.picker_option {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
border-radius: 12rpx;
background: #f8f9fa;
min-width: 160rpx;
.option_icon {
font-size: 48rpx;
margin-bottom: 12rpx;
}
.option_text {
font-size: 24rpx;
color: #666;
}
}
}
}
//
.content_image {
margin: 8rpx 0;
.chat_image {
max-width: 200rpx;
max-height: 200rpx;
border-radius: 12rpx;
object-fit: cover;
}
}
</style>

337
uniapp/pages-student/messages/index.vue

@ -23,6 +23,36 @@
</view>
</view>
<!-- 搜索功能 -->
<view class="search_section">
<view class="search_box">
<input
type="text"
placeholder="搜索消息标题或内容..."
v-model="searchForm.keyword"
@confirm="performSearch"
@input="onSearchInput"
class="search_input"
/>
<view class="search_filters">
<picker mode="date" @change="onStartDateChange" :value="searchForm.start_date">
<view class="date_picker">
开始日期: {{ searchForm.start_date || '请选择' }}
</view>
</picker>
<picker mode="date" @change="onEndDateChange" :value="searchForm.end_date">
<view class="date_picker">
结束日期: {{ searchForm.end_date || '请选择' }}
</view>
</picker>
<view class="search_button" @click="performSearch">搜索</view>
<view class="reset_button" @click="resetSearch">重置</view>
</view>
</view>
</view>
<!-- 消息类型筛选 -->
<view class="filter_section">
<view class="filter_tabs">
@ -66,12 +96,17 @@
</view>
<view class="message_content">
<view class="message_title">{{ message.title }}</view>
<view class="message_preview">{{ message.content | truncate(50) }}</view>
<view class="message_title">{{ message.title || message.from_name || '对话' }}</view>
<view class="message_preview">{{ (message.summary || message.content) | truncate(50) }}</view>
</view>
<view class="message_meta" v-if="message.sender_name">
<text class="sender_name">来自{{ message.sender_name }}</text>
<view class="message_meta">
<text class="sender_name" v-if="message.sender_name">来自{{ message.sender_name }}</text>
<text class="sender_name" v-else-if="message.from_name">来自{{ message.from_name }}</text>
<view class="conversation_stats" v-if="message.total_messages !== undefined">
<text class="stat_text">{{ message.total_messages }}条消息</text>
<text class="unread_text" v-if="message.unread_messages > 0">{{ message.unread_messages }}条未读</text>
</view>
</view>
</view>
</view>
@ -90,7 +125,7 @@
</view>
<!-- 消息详情弹窗 -->
<view class="message_popup" v-if="showMessagePopup" @click="closeMessagePopup">
<view class="message_popup" v-if="showPopup" @click="closeMessagePopup">
<view class="popup_content" @click.stop>
<view class="popup_header">
<view class="popup_title">消息详情</view>
@ -157,6 +192,7 @@
<script>
import apiRoute from '@/api/apiRoute.js'
import messageRouter from '@/utils/messageRouter.js'
export default {
filters: {
@ -178,8 +214,16 @@
hasMore: true,
currentPage: 1,
activeType: 'all',
showMessagePopup: false,
showPopup: false,
showEnhancedMessageDetail: false,
selectedMessage: null,
searchForm: {
keyword: '',
start_date: '',
end_date: ''
},
searchTimer: null,
isSearchMode: false,
typeTabs: [
{ value: 'all', text: '全部', count: 0 },
{ value: 'system', text: '系统消息', count: 0 },
@ -198,14 +242,23 @@
},
onLoad(options) {
this.studentId = parseInt(options.student_id) || 0
if (this.studentId) {
// 使
const userInfo = uni.getStorageSync('userInfo')
if (userInfo && userInfo.id) {
this.studentId = parseInt(userInfo.id)
this.initPage()
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
})
// 使
this.studentId = parseInt(options.student_id) || 0
if (this.studentId) {
console.warn('使用页面参数作为用户ID,建议使用本地缓存userInfo')
this.initPage()
} else {
uni.showToast({
title: '用户信息获取失败',
icon: 'none'
})
}
}
},
@ -238,34 +291,28 @@
try {
console.log('加载消息列表:', this.studentId)
// API
const response = await apiRoute.getStudentMessageList({
student_id: this.studentId,
message_type: this.activeType === 'all' ? '' : this.activeType,
page: this.currentPage,
limit: 10
})
let response
if (this.isSearchMode) {
//
response = await this.searchMessages()
} else {
//
response = await apiRoute.getStudentMessageList({
student_id: this.studentId,
message_type: this.activeType === 'all' ? '' : this.activeType,
page: this.currentPage,
limit: 10
})
}
if (response && response.code === 1 && response.data) {
const apiData = response.data
const newList = apiData.list || []
//
const formattedList = newList.map(message => ({
id: message.id,
type: message.message_type,
title: message.title,
content: message.content,
sender_name: message.from_name || '系统',
send_time: this.formatTimestamp(message.create_time),
is_read: message.is_read === 1,
attachment_url: message.attachment_url || ''
}))
const newList = this.formatMessageList(apiData.list || [])
if (this.currentPage === 1) {
this.messagesList = formattedList
this.messagesList = newList
} else {
this.messagesList = [...this.messagesList, ...formattedList]
this.messagesList = [...this.messagesList, ...newList]
}
//
@ -444,18 +491,38 @@
return `${year}-${month}-${day} ${hours}:${minutes}`
},
viewMessage(message) {
this.selectedMessage = message
this.showMessagePopup = true
async viewMessage(message) {
// total_messages
if (message.total_messages !== undefined) {
//
uni.navigateTo({
url: `/pages-student/messages/conversation?student_id=${this.studentId}&from_type=${message.from_type}&from_id=${message.from_id}&from_name=${encodeURIComponent(message.from_name)}`
})
return
}
//
//
//
if (!message.is_read) {
this.markAsRead(message)
await this.markAsRead(message)
}
// 使
try {
await messageRouter.processMessage(message, {
markAsRead: this.markAsRead.bind(this),
showMessagePopup: this.showMessagePopupDialog.bind(this),
showEnhancedMessageDetail: this.showEnhancedMessageDetail.bind(this)
})
} catch (error) {
console.error('消息路由处理失败:', error)
//
this.showMessagePopupDialog(message)
}
},
closeMessagePopup() {
this.showMessagePopup = false
this.showPopup = false
this.selectedMessage = null
},
@ -577,6 +644,115 @@
title: '已确认',
icon: 'success'
})
},
// ===== =====
//
formatMessageList(list) {
return list.map(message => ({
id: message.id,
type: message.message_type,
message_type: message.message_type,
title: message.title,
content: message.content,
summary: message.summary,
sender_name: message.from_name || '系统',
from_name: message.from_name,
from_type: message.from_type,
from_id: message.from_id,
send_time: this.formatTimestamp(message.create_time),
is_read: message.is_read === 1,
attachment_url: message.attachment_url || '',
business_id: message.business_id,
business_type: message.business_type,
//
total_messages: message.total_messages,
unread_messages: message.unread_messages,
has_unread: message.has_unread
}))
},
//
onSearchInput() {
//
clearTimeout(this.searchTimer)
this.searchTimer = setTimeout(() => {
if (this.searchForm.keyword.length > 0 || this.searchForm.start_date || this.searchForm.end_date) {
this.performSearch()
}
}, 500)
},
performSearch() {
clearTimeout(this.searchTimer)
this.isSearchMode = true
this.currentPage = 1
this.loadMessages()
},
async searchMessages() {
try {
const response = await apiRoute.searchStudentMessages({
student_id: this.studentId,
keyword: this.searchForm.keyword,
message_type: this.activeType === 'all' ? '' : this.activeType,
start_date: this.searchForm.start_date,
end_date: this.searchForm.end_date,
page: this.currentPage,
limit: 10
})
return response
} catch (error) {
console.error('搜索失败:', error)
throw error
}
},
onStartDateChange(e) {
this.searchForm.start_date = e.detail.value
if (this.searchForm.end_date && this.searchForm.start_date > this.searchForm.end_date) {
uni.showToast({
title: '开始日期不能晚于结束日期',
icon: 'none'
})
this.searchForm.start_date = ''
}
},
onEndDateChange(e) {
this.searchForm.end_date = e.detail.value
if (this.searchForm.start_date && this.searchForm.end_date < this.searchForm.start_date) {
uni.showToast({
title: '结束日期不能早于开始日期',
icon: 'none'
})
this.searchForm.end_date = ''
}
},
resetSearch() {
this.searchForm = {
keyword: '',
start_date: '',
end_date: ''
}
this.isSearchMode = false
this.currentPage = 1
this.loadMessages()
},
//
showMessagePopupDialog(message) {
this.selectedMessage = message
this.showPopup = true
},
//
showEnhancedMessageDetail(message) {
this.selectedMessage = message
this.showPopup = true
//
}
}
}
@ -659,6 +835,67 @@
}
}
//
.search_section {
background: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 24rpx 32rpx;
.search_box {
.search_input {
width: 100%;
padding: 16rpx 20rpx;
border: 1px solid #e0e0e0;
border-radius: 8rpx;
font-size: 28rpx;
background: #f8f9fa;
margin-bottom: 16rpx;
&::placeholder {
color: #999;
}
}
.search_filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16rpx;
.date_picker {
padding: 12rpx 16rpx;
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 8rpx;
font-size: 24rpx;
color: #666;
min-width: 200rpx;
text-align: center;
}
.search_button, .reset_button {
padding: 12rpx 24rpx;
border-radius: 8rpx;
font-size: 24rpx;
text-align: center;
min-width: 80rpx;
}
.search_button {
background: #29D3B4;
color: #fff;
}
.reset_button {
background: #f8f9fa;
color: #666;
border: 1px solid #e0e0e0;
}
}
}
}
//
.filter_section {
background: #fff;
@ -815,6 +1052,26 @@
.sender_name {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
}
.conversation_stats {
display: flex;
gap: 16rpx;
align-items: center;
.stat_text {
font-size: 22rpx;
color: #666;
}
.unread_text {
font-size: 20rpx;
color: #29D3B4;
background: rgba(41, 211, 180, 0.1);
padding: 2rpx 8rpx;
border-radius: 8rpx;
}
}
}
}

18
uniapp/pages.json

@ -155,6 +155,15 @@
"navigationBarTextStyle": "white"
}
},
{
"path": "messages/conversation",
"style": {
"navigationBarTitleText": "对话详情",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "white"
}
},
{
"path": "settings/index",
"style": {
@ -437,6 +446,15 @@
"navigationBarTextStyle": "white"
}
},
{
"path": "coach/my/teaching_management_info",
"style": {
"navigationBarTitleText": "教研详情",
"navigationStyle": "default",
"navigationBarBackgroundColor": "#171717",
"navigationBarTextStyle": "white"
}
},
{
"path": "coach/my/exam_results",
"style": {

55
uniapp/pages/common/home/index.vue

@ -46,32 +46,27 @@
return {
userInfo: {},
userPermissions: [], //
// /
//
allGridItems: [
{
title: '客户资源',
icon: 'person-filled',
path: '/pages/market/clue/index'
path: '/pages-market/clue/index'
},
{
title: '添加资源',
icon: 'plus-filled',
path: '/pages/market/clue/add_clues'
path: '/pages-market/clue/add_clues'
},
{
title: '数据统计',
icon: 'bars',
path: '/pages/market/data/statistics'
},
{
title: '个人中心',
icon: 'person',
path: '/pages/market/my/index'
title: '课程查询',
icon: 'search',
path: '/pages-coach/coach/schedule/schedule_table'
},
{
title: '首页',
icon: 'home',
path: '/pages/market/home/index'
title: '学员管理',
icon: 'contact-filled',
path: '/pages-coach/coach/student/student_list'
},
{
title: '课程安排',
@ -79,14 +74,19 @@
path: '/pages-market/clue/class_arrangement'
},
{
title: '学员管理',
icon: 'person',
path: '/pages-coach/coach/student/student_list'
title: '资料库',
icon: 'folder-add-filled',
path: '/pages-coach/coach/my/teaching_management'
},
{
title: '课程查询',
icon: 'list',
path: '/pages-coach/coach/schedule/schedule_table'
title: '报销管理',
icon: 'wallet-filled',
path: '/pages-market/reimbursement/list'
},
{
title: '我的消息',
icon: 'chat-filled',
path: '/pages-common/my_message'
},
{
title: '我的数据',
@ -105,21 +105,6 @@
icon: 'location',
path: '/pages/common/dashboard/webview',
params: { type: 'campus_data' }
},
{
title: '我的消息',
icon: 'chat-filled',
path: '/pages/common/my/my_message'
},
{
title: '报销管理',
icon: 'wallet-filled',
path: '/pages-market/reimbursement/list'
},
{
title: '资料库',
icon: 'folder-add',
path: '/pages-coach/coach/my/teaching_management'
}
]
}

294
uniapp/utils/messageRouter.js

@ -0,0 +1,294 @@
/**
* 消息路由处理器
* 统一处理各种消息类型的跳转和展示逻辑
*/
import apiRoute from '@/api/apiRoute.js'
class MessageRouter {
constructor() {
this.routeHandlers = {
'page_navigation': this.handlePageNavigation,
'popup_with_actions': this.handlePopupWithActions,
'popup_simple': this.handlePopupSimple,
'image_preview': this.handleImagePreview
};
}
/**
* 处理消息点击事件
* @param {Object} message 消息对象
* @param {Object} context 页面上下文
* @return {Promise} 处理结果
*/
async processMessage(message, context) {
try {
// 先标记为已读
if (!message.is_read) {
await context.markAsRead(message);
}
// 获取路由配置并处理
const routeResult = await this.getMessageRoute(message);
if (routeResult && routeResult.success) {
const { route_type, config } = routeResult;
const handler = this.routeHandlers[route_type];
if (handler) {
return await handler.call(this, message, config, context);
}
}
// 默认处理:显示弹窗
return this.handlePopupSimple(message, {}, context);
} catch (error) {
console.error('消息路由处理失败:', error);
// 降级到简单弹窗
return this.handlePopupSimple(message, {}, context);
}
}
/**
* 获取消息路由配置
* @param {Object} message 消息对象
* @return {Promise} 路由配置
*/
async getMessageRoute(message) {
try {
// 先使用本地路由规则
const localRoute = this.getLocalRoute(message);
if (localRoute.success) {
return localRoute;
}
// 如果需要,可以调用后端API获取更复杂的路由配置
// const response = await apiRoute.getMessageRoute({
// message_id: message.id,
// message_type: message.message_type
// });
//
// if (response && response.code === 1) {
// return response.data;
// }
return { success: false };
} catch (error) {
console.error('获取消息路由配置失败:', error);
return { success: false };
}
}
/**
* 本地路由规则
* @param {Object} message 消息对象
* @return {Object} 路由配置
*/
getLocalRoute(message) {
const { message_type, business_id, business_type } = message;
// 有业务页面的消息类型
const pageRouteMap = {
'order': {
page_path: '/pages-common/order/detail',
param_key: 'id',
fallback: 'popup'
},
'student_courses': {
page_path: '/pages-student/courses/detail',
param_key: 'id',
fallback: 'popup'
},
'person_course_schedule': {
page_path: '/pages-student/schedule/detail',
param_key: 'id',
fallback: 'popup'
}
};
// 需要特殊操作按钮的消息类型
const actionPopupMap = {
'notification': {
actions: [
{ text: '确认已读', type: 'primary', action: 'confirmRead' },
{ text: '查看详情', type: 'info', action: 'viewDetail' }
]
},
'feedback': {
actions: [
{ text: '查看反馈', type: 'success', action: 'viewFeedback' },
{ text: '回复', type: 'primary', action: 'reply' }
]
},
'reminder': {
actions: [
{ text: '查看课程', type: 'warning', action: 'viewCourse' },
{ text: '设置提醒', type: 'info', action: 'setReminder' }
]
}
};
// 判断路由类型
if (pageRouteMap[message_type] && business_id) {
// 页面跳转
return {
success: true,
route_type: 'page_navigation',
config: {
url: pageRouteMap[message_type].page_path + '?' + pageRouteMap[message_type].param_key + '=' + business_id,
fallback: pageRouteMap[message_type].fallback
}
};
} else if (actionPopupMap[message_type]) {
// 带操作按钮的弹窗
return {
success: true,
route_type: 'popup_with_actions',
config: actionPopupMap[message_type]
};
} else if (message_type === 'img') {
// 图片预览
return {
success: true,
route_type: 'image_preview',
config: { allow_save: true }
};
} else if (message_type === 'system') {
// 系统消息自动标记已读
return {
success: true,
route_type: 'popup_simple',
config: { auto_read: true }
};
} else {
// 默认简单弹窗
return {
success: true,
route_type: 'popup_simple',
config: { auto_read: false }
};
}
}
/**
* 处理页面跳转
* @param {Object} message 消息对象
* @param {Object} config 配置
* @param {Object} context 页面上下文
* @return {Promise} 处理结果
*/
handlePageNavigation(message, config, context) {
return new Promise((resolve) => {
uni.navigateTo({
url: config.url,
success: () => {
console.log('页面跳转成功:', config.url);
resolve({ success: true, action: 'navigate' });
},
fail: (error) => {
console.warn('页面跳转失败,降级到弹窗:', error);
// 降级到弹窗显示
if (config.fallback === 'popup') {
context.showMessagePopup(message);
}
resolve({ success: true, action: 'popup_fallback' });
}
});
});
}
/**
* 处理带操作按钮的弹窗
* @param {Object} message 消息对象
* @param {Object} config 配置
* @param {Object} context 页面上下文
* @return {Promise} 处理结果
*/
handlePopupWithActions(message, config, context) {
const enhancedMessage = {
...message,
actionButtons: config.actions || []
};
context.showEnhancedMessageDetail(enhancedMessage);
return Promise.resolve({ success: true, action: 'popup_with_actions' });
}
/**
* 处理简单弹窗
* @param {Object} message 消息对象
* @param {Object} config 配置
* @param {Object} context 页面上下文
* @return {Promise} 处理结果
*/
handlePopupSimple(message, config, context) {
context.showMessagePopup(message);
return Promise.resolve({ success: true, action: 'popup_simple' });
}
/**
* 处理图片预览
* @param {Object} message 消息对象
* @param {Object} config 配置
* @param {Object} context 页面上下文
* @return {Promise} 处理结果
*/
handleImagePreview(message, config, context) {
const imageUrl = message.content || message.attachment_url;
if (imageUrl && this.isImageUrl(imageUrl)) {
uni.previewImage({
urls: [imageUrl],
current: 0,
success: () => {
console.log('图片预览成功');
},
fail: (error) => {
console.warn('图片预览失败,降级到弹窗:', error);
this.handlePopupSimple(message, {}, context);
}
});
} else {
// 不是图片URL,降级到普通弹窗
this.handlePopupSimple(message, {}, context);
}
return Promise.resolve({ success: true, action: 'image_preview' });
}
/**
* 判断是否为图片URL
* @param {string} url
* @return {boolean}
*/
isImageUrl(url) {
if (!url) return false;
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
const lowerUrl = url.toLowerCase();
return imageExts.some(ext => lowerUrl.includes(ext)) || lowerUrl.startsWith('data:image/');
}
/**
* 获取消息类型的中文描述
* @param {string} messageType 消息类型
* @return {string} 中文描述
*/
getMessageTypeText(messageType) {
const typeMap = {
'text': '文本消息',
'img': '图片消息',
'system': '系统消息',
'notification': '通知公告',
'homework': '作业任务',
'feedback': '反馈评价',
'reminder': '课程提醒',
'order': '订单消息',
'student_courses': '课程变动',
'person_course_schedule': '课程安排'
};
return typeMap[messageType] || messageType;
}
}
export default new MessageRouter();

1878
学员端消息管理数据库分析报告.md

File diff suppressed because it is too large
Loading…
Cancel
Save