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