49 changed files with 12789 additions and 3088 deletions
@ -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 { |
|||
// 这里模拟API调用,实际需要实现相应的API接口 |
|||
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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
Binary file not shown.
@ -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: '缓存清除成功'); |
|||
} |
|||
|
|||
} |
|||
@ -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'); |
|||
}); |
|||
@ -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>"); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -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); |
|||
} |
|||
} |
|||
@ -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%以上'] |
|||
] |
|||
] |
|||
] |
|||
] |
|||
]; |
|||
@ -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, // 超时时间(秒) |
|||
] |
|||
] |
|||
] |
|||
]; |
|||
@ -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> |
|||
@ -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> |
|||
@ -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(); |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue