Browse Source

修改 bug

master
王泽彦 8 months ago
parent
commit
474d8a28e4
  1. 423
      niucloud/app/api/controller/Dashboard.php
  2. 54
      niucloud/app/api/controller/apiController/Personnel.php
  3. 3
      niucloud/app/api/route/route.php
  4. 94
      niucloud/app/service/api/apiService/PersonnelService.php
  5. 66
      niucloud/app/service/school_approval/SchoolApprovalProcessService.php
  6. 7
      uniapp/api/apiRoute.js
  7. 88
      uniapp/pages-market/clue/add_clues.vue
  8. 95
      uniapp/pages-market/clue/edit_clues.vue
  9. 197
      uniapp/pages-market/reimbursement/add.vue
  10. 1818
      学员端消息管理数据库分析报告.md
  11. 574
      学员端知识库模块详细开发任务.md

423
niucloud/app/api/controller/Dashboard.php

@ -4,6 +4,7 @@ namespace app\api\controller;
use app\service\api\member\MemberService;
use core\base\BaseApiController;
use think\facade\View;
/**
* Dashboard WebView 控制器
@ -61,8 +62,13 @@ class Dashboard extends BaseApiController
$pageTitle = $titleMap[$type] ?? '数据统计';
// 生成HTML内容
return $this->generateHTML($pageTitle, $pageData, $platform);
// 使用视图模板渲染页面
return View::fetch('dashboard/main', [
'pageTitle' => $pageTitle,
'pageData' => $pageData,
'platform' => $platform,
'userInfo' => $userInfo
]);
}
/**
@ -179,420 +185,17 @@ class Dashboard extends BaseApiController
];
}
/**
* 生成HTML内容
*/
private function generateHTML($title, $data, $platform)
{
$statsHtml = '';
if (!empty($data['stats'])) {
foreach ($data['stats'] as $stat) {
$trendColor = strpos($stat['trend'], '+') === 0 ? '#4CAF50' : '#FF5722';
$statsHtml .= "
<div class='stat-card'>
<div class='stat-label'>{$stat['label']}</div>
<div class='stat-value'>{$stat['value']}<span class='stat-unit'>{$stat['unit']}</span></div>
<div class='stat-trend' style='color: {$trendColor}'>{$stat['trend']}</div>
</div>
";
}
}
// 生成图表数据的JavaScript
$chartsScript = '';
if (!empty($data['charts'])) {
foreach ($data['charts'] as $chartId => $chart) {
$chartData = json_encode($chart['data']);
$chartsScript .= "
renderChart('{$chartId}', '{$chart['title']}', {$chartData});
";
}
}
return "
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>{$title}</title>
<script src='https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js'></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #181A20;
color: #fff;
padding: 0;
line-height: 1.6;
}
.container {
padding: 20px;
max-width: 100%;
}
.page-title {
text-align: center;
font-size: 24px;
font-weight: bold;
margin-bottom: 30px;
color: #29d3b4;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #29d3b4 0%, #1a9b7c 100%);
border-radius: 12px;
padding: 20px;
text-align: center;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
pointer-events: none;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 8px;
}
.stat-value {
font-size: 20px;
font-weight: bold;
color: #fff;
margin-bottom: 5px;
}
.stat-unit {
font-size: 12px;
font-weight: normal;
margin-left: 2px;
}
.stat-trend {
font-size: 12px;
font-weight: bold;
}
.chart-container {
background-color: #292929;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #fff;
}
.chart {
height: 300px;
width: 100%;
}
.refresh-btn {
position: fixed;
bottom: 30px;
right: 30px;
width: 56px;
height: 56px;
background: linear-gradient(135deg, #29d3b4 0%, #1a9b7c 100%);
border-radius: 50%;
border: none;
color: #fff;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(41, 211, 180, 0.3);
z-index: 1000;
}
.refresh-btn:active {
transform: scale(0.95);
}
.loading {
text-align: center;
padding: 40px;
color: #29d3b4;
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.stat-value {
font-size: 18px;
}
.chart {
height: 250px;
}
}
</style>
</head>
<body>
<div class='container'>
<h1 class='page-title'>{$title}</h1>
<div class='stats-grid'>
{$statsHtml}
</div>
<div id='charts-container'></div>
</div>
<button class='refresh-btn' onclick='refreshData()'></button>
<script>
// 图表渲染函数
function renderChart(chartId, title, data) {
const container = document.getElementById('charts-container');
const chartDiv = document.createElement('div');
chartDiv.className = 'chart-container';
chartDiv.innerHTML = `
<div class='chart-title'>\${title}</div>
<div class='chart' id='\${chartId}'></div>
`;
container.appendChild(chartDiv);
const chart = echarts.init(document.getElementById(chartId));
let option;
if (Array.isArray(data) && data[0] && typeof data[0] === 'object' && 'name' in data[0]) {
// 饼图或柱状图
if (chartId.includes('source') || chartId.includes('distribution')) {
// 饼图
option = {
backgroundColor: 'transparent',
textStyle: { color: '#fff' },
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,0,0,0.8)',
textStyle: { color: '#fff' }
},
legend: {
orient: 'horizontal',
bottom: '0%',
textStyle: { color: '#fff' }
},
series: [{
type: 'pie',
radius: '60%',
center: ['50%', '45%'],
data: data,
itemStyle: {
borderRadius: 5,
borderColor: '#181A20',
borderWidth: 2
},
label: {
color: '#fff'
}
}]
};
} else {
// 柱状图
option = {
backgroundColor: 'transparent',
textStyle: { color: '#fff' },
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,0,0,0.8)',
textStyle: { color: '#fff' }
},
xAxis: {
type: 'category',
data: data.map(item => item.name),
axisLabel: { color: '#fff' },
axisLine: { lineStyle: { color: '#666' } }
},
yAxis: {
type: 'value',
axisLabel: { color: '#fff' },
axisLine: { lineStyle: { color: '#666' } },
splitLine: { lineStyle: { color: '#333' } }
},
series: [{
type: 'bar',
data: data.map(item => item.value),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: '#29d3b4'},
{offset: 1, color: '#1a9b7c'}
]),
borderRadius: [4, 4, 0, 0]
}
}]
};
}
} else {
// 折线图
option = {
backgroundColor: 'transparent',
textStyle: { color: '#fff' },
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,0,0,0.8)',
textStyle: { color: '#fff' }
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
axisLabel: { color: '#fff' },
axisLine: { lineStyle: { color: '#666' } }
},
yAxis: {
type: 'value',
axisLabel: { color: '#fff' },
axisLine: { lineStyle: { color: '#666' } },
splitLine: { lineStyle: { color: '#333' } }
},
series: [{
type: 'line',
data: data,
smooth: true,
itemStyle: { color: '#29d3b4' },
lineStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{offset: 0, color: '#29d3b4'},
{offset: 1, color: '#1a9b7c'}
]),
width: 3
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: 'rgba(41, 211, 180, 0.3)'},
{offset: 1, color: 'rgba(41, 211, 180, 0.05)'}
])
}
}]
};
}
chart.setOption(option);
// 响应式
window.addEventListener('resize', () => {
chart.resize();
});
}
// 渲染所有图表
{$chartsScript}
// 刷新数据
function refreshData() {
// 发送消息给UniApp
if (typeof uni !== 'undefined' && uni.postMessage) {
uni.postMessage({
data: {
type: 'refresh'
}
});
} else {
// 页面刷新
location.reload();
}
}
// 页面加载完成后的处理
document.addEventListener('DOMContentLoaded', function() {
console.log('Dashboard页面加载完成');
// 通知UniApp页面加载完成
if (typeof uni !== 'undefined' && uni.postMessage) {
uni.postMessage({
data: {
type: 'loaded'
}
});
}
});
</script>
</body>
</html>";
}
/**
* 渲染错误页面
*/
private function renderErrorPage($message)
{
return response("
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>页面错误</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #181A20;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.error-container {
text-align: center;
max-width: 400px;
}
.error-icon {
font-size: 48px;
color: #ff5722;
margin-bottom: 20px;
}
.error-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
}
.error-message {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
line-height: 1.5;
}
</style>
</head>
<body>
<div class='error-container'>
<div class='error-icon'>⚠️</div>
<div class='error-title'>页面加载失败</div>
<div class='error-message'>{$message}</div>
</div>
</body>
</html>")->header([
$errorHtml = View::fetch('dashboard/error', [
'message' => $message
]);
return response($errorHtml)->header([
'Content-Type' => 'text/html; charset=utf-8'
]);
}

54
niucloud/app/api/controller/apiController/Personnel.php

@ -159,7 +159,61 @@ class Personnel extends BaseApiService
'receipt_url' => $request->param('receipt_url', ''),
];
return success((new PersonnelService())->reimbursement_add($data));
}
/**
* 添加报销申请(带审批流程)
* @param Request $request
* @return mixed
*/
public function reimbursementAddWithApproval(Request $request)
{
$params = $request->all();
// 验证必填字段
if (empty($params['amount'])) {
return fail('报销金额不能为空');
}
if (empty($params['description'])) {
return fail('报销描述不能为空');
}
// 验证金额格式
if (!is_numeric($params['amount']) || floatval($params['amount']) <= 0) {
return fail('请输入正确的报销金额');
}
try {
$res = (new PersonnelService())->reimbursementAddWithApproval($params);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data'], $res['msg']);
} catch (\Exception $e) {
return fail('添加报销申请失败:' . $e->getMessage());
}
}
/**
* 获取报销审批配置列表
* @param Request $request
* @return mixed
*/
public function getReimbursementApprovalConfigs(Request $request)
{
try {
$params = $request->all();
$businessType = $params['business_type'] ?? 'expense_approval';
$approvalService = new \app\service\school_approval\SchoolApprovalConfigService();
$configs = $approvalService->getActiveConfigs($businessType);
return success($configs);
} catch (\Exception $e) {
return fail('获取审批配置失败:' . $e->getMessage());
}
}
/**

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

@ -434,6 +434,9 @@ Route::group(function () {
Route::get('personnel/reimbursement_list', 'apiController.Personnel/reimbursement_list');
Route::post('personnel/reimbursement_add', 'apiController.Personnel/reimbursement_add');
Route::get('personnel/reimbursement_info', 'apiController.Personnel/reimbursement_info');
//报销审批流程
Route::post('personnel/reimbursement_add_with_approval', 'apiController.Personnel/reimbursementAddWithApproval');
Route::get('personnel/reimbursement/approval-configs', 'apiController.Personnel/getReimbursementApprovalConfigs');
//合同管理
Route::get('contract/myContracts', 'apiController.Contract/myContracts');

94
niucloud/app/service/api/apiService/PersonnelService.php

@ -399,14 +399,104 @@ class PersonnelService extends BaseApiService
'applicant_id' => $this->member_id,
'amount' => $data['amount'],
'description' => $data['description'],
'receipt_url' => $data['receipt_url']
'receipt_url' => $data['receipt_url'],
'status' => 'pending'
]);
}
return true;
}
/**
* 添加报销申请(带审批流程)
* @param array $data
* @return array
*/
public function reimbursementAddWithApproval(array $data)
{
$res = [
'code' => 0,
'msg' => '添加失败',
'data' => []
];
return true;
try {
// 开启事务
\think\facade\Db::startTrans();
// 创建报销记录
$reimbursementData = [
'applicant_id' => $this->member_id,
'amount' => $data['amount'],
'description' => $data['description'],
'receipt_url' => $data['receipt_url'] ?? '',
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
$reimbursement = new Reimbursement();
$reimbursementId = $reimbursement->insertGetId($reimbursementData);
if (!$reimbursementId) {
\think\facade\Db::rollback();
$res['msg'] = '创建报销记录失败';
return $res;
}
// 检查是否使用审批流程
if (isset($data['use_approval']) && $data['use_approval'] &&
isset($data['approval_config_id']) && $data['approval_config_id'] > 0) {
// 启动审批流程
$approvalService = new \app\service\school_approval\SchoolApprovalProcessService();
$processData = [
'process_name' => '报销申请 - ' . $data['description'],
'applicant_id' => $this->member_id,
'remarks' => $data['remarks'] ?? '报销申请',
'business_type' => 'expense_approval',
'business_id' => $reimbursementId,
'business_data' => $data
];
$processId = $approvalService->create($processData, $data['approval_config_id']);
// 更新报销记录关联审批流程
$reimbursement->where(['id' => $reimbursementId])->update([
'process_id' => $processId
]);
\think\facade\Db::commit();
$res = [
'code' => 1,
'msg' => '报销申请已提交,等待审批',
'data' => [
'type' => 'approval',
'reimbursement_id' => $reimbursementId,
'process_id' => $processId
]
];
} else {
// 直接创建(无审批)
\think\facade\Db::commit();
$res = [
'code' => 1,
'msg' => '报销记录创建成功',
'data' => [
'type' => 'direct',
'reimbursement_id' => $reimbursementId
]
];
}
} catch (\Exception $e) {
\think\facade\Db::rollback();
$res['msg'] = '创建报销申请失败:' . $e->getMessage();
}
return $res;
}
/**

66
niucloud/app/service/school_approval/SchoolApprovalProcessService.php

@ -367,6 +367,9 @@ class SchoolApprovalProcessService
case 'personnel_add':
$this->handlePersonnelAddApproval($process);
break;
case 'expense_approval':
$this->handleExpenseApproval($process);
break;
default:
// 其他业务类型的处理逻辑
break;
@ -455,6 +458,9 @@ class SchoolApprovalProcessService
case 'personnel_add':
// 人员添加被拒绝,不需要特殊处理
break;
case 'expense_approval':
$this->handleExpenseRejected($process);
break;
default:
// 其他业务类型的处理逻辑
break;
@ -726,4 +732,64 @@ class SchoolApprovalProcessService
throw new \Exception('创建人员角色绑定关系失败:' . $e->getMessage());
}
}
/**
* 处理报销审批完成
* @param $process
* @throws \Exception
*/
private function handleExpenseApproval($process): void
{
if (empty($process['business_id'])) {
throw new Exception('报销ID不存在');
}
try {
// 更新报销记录状态为已审核
$reimbursementModel = new \app\model\reimbursement\Reimbursement();
$updateResult = $reimbursementModel->where(['id' => $process['business_id']])
->update([
'status' => 'approved',
'updated_at' => date('Y-m-d H:i:s')
]);
if (!$updateResult) {
throw new Exception('更新报销记录状态失败');
}
\think\facade\Log::info("报销审批完成,报销ID: {$process['business_id']},流程ID: {$process['id']}");
} catch (\Exception $e) {
throw new Exception('处理报销审批完成失败:' . $e->getMessage());
}
}
/**
* 处理报销审批拒绝
* @param $process
* @throws \Exception
*/
private function handleExpenseRejected($process): void
{
if (empty($process['business_id'])) {
return;
}
try {
// 更新报销记录状态为已拒绝
$reimbursementModel = new \app\model\reimbursement\Reimbursement();
$updateResult = $reimbursementModel->where(['id' => $process['business_id']])
->update([
'status' => 'rejected',
'updated_at' => date('Y-m-d H:i:s')
]);
if ($updateResult) {
\think\facade\Log::info("报销审批拒绝,报销ID: {$process['business_id']},流程ID: {$process['id']}");
}
} catch (\Exception $e) {
\think\facade\Log::error('处理报销审批拒绝失败:' . $e->getMessage());
}
}
}

7
uniapp/api/apiRoute.js

@ -664,6 +664,13 @@ export default {
async reimbursement_info(data = {}) {
return await http.get('/personnel/reimbursement_info', data)
},
// 报销审批相关接口
async reimbursement_add_with_approval(data = {}) {
return await http.post('/personnel/reimbursement_add_with_approval', data)
},
async reimbursementApprovalConfigs(data = {}) {
return await http.get('/personnel/reimbursement/approval-configs', data)
},
async schedule_del(data = {}) {
return await http.post('/course/schedule_del', data)
},

88
uniapp/pages-market/clue/add_clues.vue

@ -393,7 +393,7 @@
<view
class="input-title"
style="margin-right:14rpx;"
@click="openDate(`promised_visit_time`)">
@click="openDateTime(`promised_visit_time`)">
{{ (formData.promised_visit_time) ? formData.promised_visit_time : '点击选择' }}
</view>
</view>
@ -478,6 +478,17 @@
@cancel="cancel_date">
</fui-date-picker>
<!-- 日期时间选择器 -->
<fui-date-picker
:show="datetime_picker_show"
type="1"
:startYear="2020"
:endYear="2030"
:value="getCurrentDateTime()"
@change="change_datetime"
@cancel="cancel_datetime">
</fui-date-picker>
<!-- 选择器 -->
<fui-picker
:linkage='picker_linkage'
@ -727,6 +738,10 @@ export default {
//
data_picker_input_name: '',//input_name
date_picker_show: false,//
//
datetime_picker_input_name: '',//input_name
datetime_picker_show: false,//
//
@ -997,6 +1012,24 @@ export default {
return this.formatDate(today)
},
//
getCurrentDateTime() {
const now = new Date()
return this.formatDateTime(now)
},
//
formatDateTime(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
// FirstUI YYYY-MM-DD HH:mm
return `${year}-${month}-${day} ${hours}:${minutes}`
},
//
async getBatchDictData() {
try {
@ -1630,6 +1663,17 @@ export default {
this.date_picker_show = true
})
},
//
openDateTime(input_name) {
console.log('打开日期时间选择器:', input_name)
this.datetime_picker_input_name = input_name
//
this.$nextTick(() => {
this.datetime_picker_show = true
})
},
//
change_date(e) {
console.log('日期选择器返回数据:', e)
@ -1677,6 +1721,48 @@ export default {
this.date_picker_show = false
},
//
change_datetime(e) {
console.log('日期时间选择器返回数据:', e)
//
let val = ''
if (e.result) {
val = e.result
} else if (e.value) {
val = e.value
} else if (e.detail && e.detail.result) {
val = e.detail.result
} else if (e.detail && e.detail.value) {
val = e.detail.value
}
// YYYY-MM-DD HH:mm
if (val && typeof val === 'string') {
//
if (/^\d+$/.test(val)) {
const date = new Date(parseInt(val))
val = this.formatDateTime(date)
}
// YYYY-MM-DD HH:mm
if (val.includes('/')) {
// YYYY/MM/DD HH:mm
val = val.replace(/\//g, '-')
}
}
let input_name = this.datetime_picker_input_name
this.formData[input_name] = val
console.log(`设置${input_name}为:`, val)
this.cancel_datetime()
},
//
cancel_datetime() {
this.datetime_picker_show = false
},
// index|0=,1
async nextStep(index) {

95
uniapp/pages-market/clue/edit_clues.vue

@ -95,7 +95,7 @@
</view>
</fui-form-item>
<!-- 资源是否有效 -->
<fui-form-item label="资源是否有效" labelSize='26' prop="is_valid" background='#434544' labelColor='#fff' :bottomBorder='false'>
<fui-form-item label="资源是否有效" labelSize='26' label-width="200" prop="is_valid" background='#434544' labelColor='#fff' :bottomBorder='false'>
<view class="input-title" style="margin-right:14rpx;">
<fui-radio-group name="radio" v-model="formData.efficacious">
<view class="fui-list__item" style="display: flex;justify-content: flex-end;">
@ -167,7 +167,7 @@
<!-- 承诺到访时间 -->
<fui-form-item labelWidth="240" label="2、承诺到访时间" labelSize='26' prop="promised_visit_time" background='#434544' labelColor='#fff' :bottomBorder='false'>
<view class="input-title" style="margin-right:14rpx;">
<view class="input-title" style="margin-right:14rpx;" @click="openDate('promised_visit_time')">
<view class="input-title" style="margin-right:14rpx;" @click="openDateTime('promised_visit_time')">
{{ formData.promised_visit_time ? formData.promised_visit_time : '点击选择' }}
</view>
</view>
@ -219,9 +219,9 @@
</fui-form-item>
<!-- 沟通备注 -->
<fui-form-item label="沟通备注" labelSize='26' prop="remark" background='#434544' labelColor='#fff' :bottomBorder='false'>
<fui-form-item label="沟通备注" labelSize='26' prop="communication" background='#434544' labelColor='#fff' :bottomBorder='false'>
<view class="input-title" style="margin-right:14rpx;">
<fui-textarea v-model="formData.remark" placeholder="点击填写" backgroundColor="#434544" size="26" color="#fff" :borderTop="false" :isCounter="true" :maxlength="500" :minHeight="250" :isAutoHeight="true"></fui-textarea>
<fui-textarea v-model="formData.communication" placeholder="点击填写" backgroundColor="#434544" size="26" color="#fff" :borderTop="false" :isCounter="true" :maxlength="500" :minHeight="250" :isAutoHeight="true"></fui-textarea>
</view>
</fui-form-item>
</view>
@ -307,6 +307,10 @@
<!-- 选择器日期选择等控件保留原有 -->
<fui-date-picker :show="date_picker_show" type="5" @change="change_date" @cancel="cancel_date" :value="default_date_value"></fui-date-picker>
<!-- 日期时间选择器 -->
<fui-date-picker :show="datetime_picker_show" type="1" @change="change_datetime" @cancel="cancel_datetime" :value="default_datetime_value"></fui-date-picker>
<fui-picker :linkage='picker_linkage' :options="picker_options" :layer="1" :show="picker_show" @change="changeCicker" @cancel="cancelCicker"></fui-picker>
<!-- 快速填写弹窗 -->
@ -389,7 +393,8 @@
first_visit_status: '', //访
second_visit_status: '', //访
efficacious:'1',
call_intent:'2'
call_intent: '', //
emotional_stickiness_score: '' //
},
//
@ -472,12 +477,22 @@
text: '',
options: [],
},
//
emotional_stickiness_score: {
text: '',
options: [],
},
}, //
//
data_picker_input_name: '', //input_name
date_picker_show: false, //
default_date_value: '', //
//
datetime_picker_input_name: '', //input_name
datetime_picker_show: false, //
default_datetime_value: '', //
@ -736,17 +751,24 @@
is_bm: sixSpeed.is_bm || 2, //
efficacious: sixSpeed.efficacious || '',
call_intent: sixSpeed.call_intent || '', //
emotional_stickiness_score: sixSpeed.emotional_stickiness_score || '', //
};
console.log('getInfo - 表单数据设置完成:', this.formData);
//
if (sixSpeed.promised_visit_time) {
this.formData.promised_visit_time = this.$util.formatToDateTime(sixSpeed.promised_visit_time, 'Y-m-d');
//
if (sixSpeed.promised_visit_time.includes(' ')) {
this.formData.promised_visit_time = sixSpeed.promised_visit_time;
} else {
this.formData.promised_visit_time = this.$util.formatToDateTime(sixSpeed.promised_visit_time, 'Y-m-d');
}
}
//
if (sixSpeed.preferred_class_time) {
this.formData.optional_class_time = this.$util.formatToDateTime(sixSpeed.preferred_class_time, 'Y-m-d');
this.formData.optional_class_time = sixSpeed.preferred_class_time;
}
if (sixSpeed.first_visit_time) {
@ -792,6 +814,7 @@
this.setPickerTextByValue('purchasing_power', this.formData.purchasing_power, sixSpeed.purchase_power_name);
this.setPickerTextByValue('cognitive_idea', this.formData.cognitive_idea, sixSpeed.concept_awareness_name);
this.setPickerTextByValue('distance', this.formData.distance, sixSpeed.distance_name);
this.setPickerTextByValue('emotional_stickiness_score', this.formData.emotional_stickiness_score, sixSpeed.emotional_stickiness_score_name);
// call_intent
console.log('选择器文本回显完成');
@ -1156,6 +1179,13 @@
this.date_picker_show = true;
},
//
openDateTime(inputName) {
this.datetime_picker_input_name = inputName;
this.setDefaultDateTimeValue(inputName);
this.datetime_picker_show = true;
},
//
setDefaultDateValue(inputName) {
if (this.formData[inputName]) {
@ -1166,6 +1196,16 @@
}
},
//
setDefaultDateTimeValue(inputName) {
if (this.formData[inputName]) {
this.default_datetime_value = this.formData[inputName];
} else {
const now = new Date();
this.default_datetime_value = this.formatDateTimeToString(now);
}
},
//
formatDateToString(date) {
const year = date.getFullYear();
@ -1174,6 +1214,16 @@
return `${year}-${month}-${day}`;
},
//
formatDateTimeToString(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
},
//
change_date(e) {
const val = e.result || '';
@ -1188,6 +1238,37 @@
this.date_picker_show = false;
},
//
change_datetime(e) {
console.log('日期时间选择器返回数据:', e);
let val = e.result || e.value || '';
//
if (val && typeof val === 'string') {
//
if (val.includes(' ') || val.includes('T')) {
try {
const date = new Date(val);
if (!isNaN(date.getTime())) {
val = this.formatDateTimeToString(date);
}
} catch (error) {
console.warn('日期时间格式化失败:', error);
}
}
}
const inputName = this.datetime_picker_input_name;
this.formData[inputName] = val;
this.cancel_datetime();
},
//
cancel_datetime() {
this.datetime_picker_show = false;
},
// index|0=,1
async nextStep(index) {
this.optionTableId = String(index)

197
uniapp/pages-market/reimbursement/add.vue

@ -30,8 +30,30 @@
</view>
</view>
</view>
<!-- 审批流程提示 -->
<view class="form-box approval-info-section" v-if="!disabled && !form.id && approvalData.selectedConfig">
<view class="section-title">审批流程</view>
<view class="approval-info">
<view class="info-item">
<text class="info-label">当前配置</text>
<text class="info-value">{{ approvalData.selectedConfig.config_name || '报销审批' }}</text>
</view>
<view class="info-item">
<text class="info-label">流程说明</text>
<text class="info-value">{{ approvalData.selectedConfig.description || '报销审批流程' }}</text>
</view>
<view class="info-note">
<text>提交后将自动进入审批流程请等待审批完成</text>
</view>
</view>
</view>
<view class="save-btn-box" v-if="!disabled">
<fui-button background="#434544" color="#24BA9F" borderColor="#24BA9F" @click="submit">提交</fui-button>
<fui-button background="#434544" color="#24BA9F" borderColor="#24BA9F" @click="submit"
:loading="submitting">
{{ submitting ? '提交中...' : '提交' }}
</fui-button>
</view>
</view>
</template>
@ -47,18 +69,30 @@
uploadUrl: `${Api_url}/uploadImage`,
pageTitle: '新增报销',
disabled: false,
submitting: false,
loadingApprovalConfigs: false,
form: {
id: null,
amount: '',
description: '',
receipt_url: '',
}
},
//
approvalData: {
useApproval: true, //
selectedConfig: null,
selectedIndex: 0
},
approvalConfigs: []
}
},
onLoad(options) {
if (options.id) {
this.form.id = options.id;
this.fetchDetail(options.id);
} else {
//
this.loadApprovalConfigs();
}
},
methods: {
@ -131,7 +165,48 @@
});
}
},
submit() {
//
async loadApprovalConfigs() {
this.loadingApprovalConfigs = true;
try {
const response = await apiRoute.reimbursementApprovalConfigs({
business_type: 'expense_approval'
})
if (response.code === 1) {
this.approvalConfigs = response.data || []
//
if (this.approvalConfigs.length > 0) {
this.approvalData.selectedConfig = this.approvalConfigs[0]
this.approvalData.selectedIndex = 0
}
console.log('审批配置加载成功:', this.approvalConfigs)
} else {
console.error('审批配置加载失败:', response.msg)
//
this.approvalData.selectedConfig = {
id: 0,
config_name: '报销审批',
description: '报销审批流程',
business_type: 'expense_approval'
}
}
} catch (error) {
console.error('加载审批配置失败:', error)
//
this.approvalData.selectedConfig = {
id: 0,
config_name: '报销审批',
description: '报销审批流程',
business_type: 'expense_approval'
}
} finally {
this.loadingApprovalConfigs = false;
}
},
async submit() {
//
if (!this.form.amount || !this.form.description) {
uni.showToast({
title: '请填写完整',
@ -139,18 +214,71 @@
});
return;
}
let res = apiRoute.reimbursement_add(this.form)
if (res['code'] == 1) {
//
if (parseFloat(this.form.amount) <= 0) {
uni.showToast({
title: '提交成功',
icon: 'success'
title: '请输入正确的报销金额',
icon: 'none'
});
return;
}
setTimeout(() => {
uni.navigateBack();
}, 1000);
//
if (!this.approvalData.selectedConfig) {
uni.showToast({
title: '审批配置加载失败,请稍后重试',
icon: 'none'
});
return;
}
this.submitting = true;
try {
//
const submitData = {
...this.form,
use_approval: true, // true
approval_config_id: this.approvalData.selectedConfig.id
};
console.log('提交报销数据:', submitData);
let response;
if (this.form.id) {
// 使
response = await apiRoute.reimbursement_add(submitData);
} else {
// 使
response = await apiRoute.reimbursement_add_with_approval(submitData);
}
if (response.code === 1) {
uni.showToast({
title: '报销申请已提交,等待审批',
icon: 'success',
duration: 2000
});
setTimeout(() => {
uni.navigateBack();
}, 2000);
} else {
uni.showToast({
title: response.msg || '提交失败',
icon: 'none'
});
}
} catch (error) {
console.error('提交报销失败:', error);
uni.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
} finally {
this.submitting = false;
}
}
}
}
@ -269,4 +397,53 @@
padding: 20rpx 40rpx 40rpx 40rpx;
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.12);
}
/* 审批流程样式 */
.approval-info-section {
margin-top: 20rpx;
}
.section-title {
font-size: 32rpx;
color: #fff;
font-weight: bold;
margin-bottom: 32rpx;
padding-left: 16rpx;
border-left: 6rpx solid #24BA9F;
}
.approval-info {
margin-top: 24rpx;
padding: 24rpx;
background: #3a3a3a;
border-radius: 12rpx;
}
.approval-info .info-item {
margin-bottom: 16rpx;
}
.approval-info .info-label {
color: #aaa;
font-size: 26rpx;
}
.approval-info .info-value {
color: #24BA9F;
font-size: 26rpx;
margin-left: 8rpx;
}
.approval-info .info-note {
padding: 16rpx;
background: #2a2a2a;
border-radius: 8rpx;
border-left: 4rpx solid #24BA9F;
}
.approval-info .info-note text {
color: #999;
font-size: 24rpx;
line-height: 1.5;
}
</style>

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

File diff suppressed because it is too large

574
学员端知识库模块详细开发任务.md

@ -1,574 +0,0 @@
# 学员端知识库模块详细开发任务
## 📋 **功能需求分析**
基于 `pages-student/knowledge/index.vue` 页面分析,知识库模块需要实现以下功能:
### **前端页面功能**
1. **知识文章列表展示**:支持分页加载
2. **分类筛选**:按知识库类型筛选内容
3. **推荐文章**:首页推荐优质内容
4. **搜索功能**:关键词搜索文章
5. **收藏功能**:收藏/取消收藏文章
6. **阅读状态**:标记文章已读
7. **统计信息**:总文章数、收藏数统计
8. **文章详情**:跳转到详情页面查看
### **数据表结构**
- **主表**:`school_lesson_course_teaching` - 知识库内容表
- **权限控制**:通过 `student_ids` 字段(逗号分割)控制访问权限
- **分类字段**:`table_type` 字段区分不同类型的知识内容
### **实际数据库字段**
基于 `school_lesson_course_teaching` 表的实际结构:
- `id` - 主键ID
- `title` - 标题
- `image` - 图片/封面
- `type` - 类型
- `url` - 链接地址
- `content` - 内容(富文本)
- `status` - 状态
- `create_time` - 创建时间
- `update_time` - 更新时间
- `delete_time` - 删除时间(软删除)
- `table_type` - 分类类型(1-29枚举值)
- `user_permission` - 用户权限
- `exam_papers_id` - 试卷ID
- `student_ids` - 学员权限控制(逗号分割)
### **table_type 枚举值详细说明**
根据数据库字段注释,`table_type` 包含以下29种类型:
**教案库类型(1-8)**:
- `1`:课程教学大纲
- `2`:跳绳教案库
- `3`:增高教案库
- `4`:篮球教案库
- `5`:强化教案库
- `6`:空中忍者教案库
- `7`:少儿安防教案库
- `8`:体能教案库
**动作库类型(9-12, 17-22)**:
- `9`:热身动作库
- `10`:体能动作库
- `11`:趣味游戏库
- `12`:放松动作库
- `17`:空中忍者动作
- `18`:篮球动作
- `19`:跳绳动作
- `20`:跑酷动作
- `21`:安防动作
- `22`:标准化动作
**训练内容类型(13-16)**:
- `13`:训练内容
- `14`:训练视频
- `15`:课后作业
- `16`:优秀一堂课
**体测相关类型(23-26)**:
- `23`:3-6岁体测
- `24`:7+体测
- `25`:3-6岁体测讲解—解读
- `26`:7+岁体测讲解—解读
**游戏互动类型(27-29)**:
- `27`:互动游戏
- `28`:套圈游戏
- `29`:鼓励方式
## 🔧 **后端开发任务细化**
### **任务1:知识库基础接口开发**
**负责人**:后端开发者
**工期**:1天
**优先级**:高
#### **1.1 获取知识文章列表接口**
```php
GET /api/student/knowledge/list/{student_id}
```
**请求参数**:
- `student_id` (必填): 学员ID
- `category` (可选): 分类筛选,对应 table_type 字段
- `page` (可选): 页码,默认1
- `limit` (可选): 每页数量,默认10
- `keyword` (可选): 搜索关键词
**响应格式**:
```json
{
"code": 1,
"msg": "获取成功",
"data": {
"list": [
{
"id": 1,
"title": "少儿体适能训练的核心要素",
"image": "https://example.com/cover.jpg",
"content": "富文本内容",
"table_type": "1",
"category_name": "课程教学大纲",
"type": 1,
"url": "",
"status": 1,
"create_time": 1705294800,
"update_time": 1705294800,
"user_permission": "1,2,3",
"is_read": false,
"is_favorite": false
}
],
"current_page": 1,
"last_page": 5,
"total": 45,
"per_page": 10
}
}
```
**核心业务逻辑**:
```php
// 1. 权限验证:检查 student_ids 字段是否包含当前学员ID
$where[] = ['student_ids', 'like', "%,{$student_id},%"];
// 或者使用 FIND_IN_SET 函数
$where[] = ['', 'exp', "FIND_IN_SET({$student_id}, student_ids)"];
// 2. 状态筛选:只查询启用状态的内容
$where[] = ['status', '=', 1];
// 3. 软删除筛选:排除已删除的内容
$where[] = ['delete_time', '=', 0];
// 4. 分类筛选
if (!empty($category)) {
$where[] = ['table_type', '=', $category];
}
// 5. 关键词搜索(只搜索标题和内容)
if (!empty($keyword)) {
$where[] = ['title|content', 'like', "%{$keyword}%"];
}
// 6. 查询收藏状态和阅读状态(需要关联查询)
// 7. 分页查询
// 8. 数据格式化(时间戳转换等)
```
#### **1.2 获取知识分类列表接口**
```php
GET /api/student/knowledge/categories
```
**响应格式**:
```json
{
"code": 1,
"msg": "获取成功",
"data": [
{
"value": "1",
"text": "课程教学大纲",
"icon": "📖",
"count": 12
},
{
"value": "2",
"text": "跳绳教案库",
"icon": "🏃",
"count": 8
}
]
}
```
#### **1.3 获取推荐文章接口**
```php
GET /api/student/knowledge/recommend/{student_id}
```
**业务逻辑**:
- 返回热门文章(按阅读量排序)
- 或者返回最新发布的文章
- 限制返回数量(如5篇)
### **任务2:知识库交互功能开发**
**负责人**:后端开发者
**工期**:1天
**优先级**:中
#### **2.1 文章详情接口**
```php
GET /api/student/knowledge/detail/{id}
```
**请求参数**:
- `id` (必填): 文章ID
- `student_id` (必填): 学员ID(权限验证)
**响应格式**:
```json
{
"code": 1,
"msg": "获取成功",
"data": {
"id": 1,
"title": "少儿体适能训练的核心要素",
"image": "https://example.com/cover.jpg",
"content": "<p>富文本内容...</p>",
"table_type": "1",
"category_name": "课程教学大纲",
"type": 1,
"url": "",
"status": 1,
"create_time": 1705294800,
"update_time": 1705294800,
"user_permission": "1,2,3",
"exam_papers_id": "",
"is_read": true,
"is_favorite": false
}
}
```
#### **2.2 标记文章已读接口**
```php
POST /api/student/knowledge/mark-read
```
**请求参数**:
```json
{
"article_id": 1,
"student_id": 31
}
```
**业务逻辑**:
- 需要创建阅读记录表或在现有表中添加字段
- 更新文章的阅读次数
- 防止重复标记
#### **2.3 收藏/取消收藏接口**
```php
POST /api/student/knowledge/toggle-favorite
```
**请求参数**:
```json
{
"article_id": 1,
"student_id": 31,
"action": "add" // add-收藏, remove-取消收藏
}
```
**业务逻辑**:
- 需要创建收藏表 `school_student_favorites`
- 支持收藏和取消收藏操作
- 返回当前收藏状态
### **任务3:统计和搜索功能开发**
**负责人**:后端开发者
**工期**:0.5天
**优先级**:低
#### **3.1 获取知识库统计接口**
```php
GET /api/student/knowledge/stats/{student_id}
```
**响应格式**:
```json
{
"code": 1,
"msg": "获取成功",
"data": {
"total_articles": 45,
"favorites": 12,
"read_articles": 28,
"categories_count": {
"1": 12,
"2": 8,
"3": 6
}
}
}
```
#### **3.2 搜索文章接口**
```php
GET /api/student/knowledge/search/{student_id}
```
**请求参数**:
- `keyword` (必填): 搜索关键词
- `category` (可选): 分类筛选
- `page` (可选): 页码
**搜索范围**:
- 文章标题(title字段)
- 文章内容(content字段)
## 🗄️ **数据库设计补充**
### **需要新增的表**
#### **1. 学员文章阅读记录表**
```sql
CREATE TABLE `school_student_article_reads` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`student_id` int(11) NOT NULL COMMENT '学员ID',
`article_id` int(11) NOT NULL COMMENT '文章ID',
`read_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间',
`read_duration` int(11) DEFAULT 0 COMMENT '阅读时长(秒)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_article` (`student_id`, `article_id`),
KEY `idx_student_id` (`student_id`),
KEY `idx_article_id` (`article_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员文章阅读记录表';
```
#### **2. 学员收藏表**
```sql
CREATE TABLE `school_student_favorites` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`student_id` int(11) NOT NULL COMMENT '学员ID',
`target_type` varchar(50) NOT NULL COMMENT '收藏类型:article-文章',
`target_id` int(11) NOT NULL COMMENT '目标ID',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_target` (`student_id`, `target_type`, `target_id`),
KEY `idx_student_id` (`student_id`),
KEY `idx_target` (`target_type`, `target_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员收藏表';
```
### **现有表字段补充**
#### **school_lesson_course_teaching 表优化**
```sql
-- 注意:school_lesson_course_teaching 表已有完整字段结构
-- 现有字段已足够支持知识库功能,无需添加额外字段
-- 如果需要统计功能,通过关联查询其他表实现
```
## 🔗 **前后端联调任务**
### **阶段1:基础功能联调**
**负责人**:前端+后端
**工期**:0.5天
#### **联调内容**
1. **文章列表接口联调**
- 验证分页功能
- 验证分类筛选
- 验证权限控制
- 验证数据格式
2. **分类列表接口联调**
- 验证分类数据正确性
- 验证统计数量准确性
3. **推荐文章接口联调**
- 验证推荐算法
- 验证数据格式
#### **测试用例**
```javascript
// 1. 测试文章列表
const listResponse = await apiRoute.getKnowledgeArticles({
student_id: 31,
category: '',
page: 1,
limit: 10
});
// 2. 测试分类筛选
const categoryResponse = await apiRoute.getKnowledgeArticles({
student_id: 31,
category: '1', // 课程教学大纲
page: 1,
limit: 10
});
// 3. 测试权限控制
const unauthorizedResponse = await apiRoute.getKnowledgeArticles({
student_id: 999, // 无权限的学员ID
page: 1,
limit: 10
});
```
### **阶段2:交互功能联调**
**负责人**:前端+后端
**工期**:0.5天
#### **联调内容**
1. **文章详情接口联调**
- 验证富文本内容渲染
- 验证权限控制
- 验证阅读状态
2. **阅读标记功能联调**
- 验证阅读记录创建
- 验证阅读次数更新
- 验证重复标记处理
3. **收藏功能联调**
- 验证收藏/取消收藏
- 验证收藏状态同步
- 验证收藏统计更新
#### **测试用例**
```javascript
// 1. 测试文章详情
const detailResponse = await apiRoute.getKnowledgeDetail({
id: 1,
student_id: 31
});
// 2. 测试标记已读
const readResponse = await apiRoute.markArticleRead({
article_id: 1,
student_id: 31
});
// 3. 测试收藏功能
const favoriteResponse = await apiRoute.toggleArticleFavorite({
article_id: 1,
student_id: 31,
action: 'add'
});
```
### **阶段3:搜索和统计功能联调**
**负责人**:前端+后端
**工期**:0.5天
#### **联调内容**
1. **搜索功能联调**
- 验证关键词搜索
- 验证搜索结果准确性
- 验证搜索性能
2. **统计功能联调**
- 验证文章总数统计
- 验证收藏数统计
- 验证分类统计
#### **测试用例**
```javascript
// 1. 测试搜索功能
const searchResponse = await apiRoute.searchKnowledgeArticles({
student_id: 31,
keyword: '体适能',
category: '',
page: 1,
limit: 10
});
// 2. 测试统计功能
const statsResponse = await apiRoute.getKnowledgeStats({
student_id: 31
});
```
## 📊 **开发进度安排**
### **第1天:基础接口开发**
- 上午:文章列表接口开发
- 下午:分类列表和推荐文章接口开发
### **第2天:交互功能开发**
- 上午:文章详情和阅读标记接口开发
- 下午:收藏功能接口开发
### **第3天:搜索统计和联调**
- 上午:搜索和统计接口开发
- 下午:前后端联调测试
**总工期:2.5天**
## ✅ **验收标准**
### **功能验收**
- [ ] 文章列表正确显示,支持分页
- [ ] 分类筛选功能正常
- [ ] 权限控制严格(只能看到有权限的文章)
- [ ] 搜索功能准确
- [ ] 收藏功能正常
- [ ] 阅读状态正确标记
- [ ] 统计数据准确
### **性能验收**
- [ ] 文章列表加载时间 < 1秒
- [ ] 搜索响应时间 < 2秒
- [ ] 富文本内容渲染正常
- [ ] 图片加载优化
### **安全验收**
- [ ] 权限控制严格
- [ ] 防止SQL注入
- [ ] 防止XSS攻击
- [ ] 敏感数据过滤
### **兼容性验收**
- [ ] 富文本内容在小程序中正常显示
- [ ] 图片在不同设备上正常显示
- [ ] 分页功能在不同网络环境下正常
## 🔧 **技术实现要点**
### **权限控制实现**
```php
// 使用 FIND_IN_SET 函数检查权限(推荐)
$where[] = ['', 'exp', "FIND_IN_SET({$student_id}, student_ids)"];
// 或者使用 LIKE 查询(需要确保 student_ids 格式为 ",1,2,3,")
$where[] = ['student_ids', 'like', "%,{$student_id},%"];
// 同时确保只查询启用状态的内容
$where[] = ['status', '=', 1];
$where[] = ['delete_time', '=', 0];
```
### **富文本内容处理**
```php
// 过滤危险标签
$content = strip_tags($content, '<p><br><strong><em><u><img><a>');
// 处理图片路径(如果需要)
$content = str_replace('src="/', 'src="' . $domain . '/', $content);
// 处理时间戳格式化
$create_time_formatted = date('Y-m-d H:i:s', $create_time);
$update_time_formatted = date('Y-m-d H:i:s', $update_time);
```
### **搜索优化**
```php
// 使用全文索引提高搜索性能
ALTER TABLE `school_lesson_course_teaching`
ADD FULLTEXT KEY `ft_title_content` (`title`, `content`);
// 全文搜索查询
$where[] = ['', 'exp', "MATCH(title, content) AGAINST('{$keyword}' IN NATURAL LANGUAGE MODE)"];
```
### **缓存策略**
```php
// 分类列表缓存(1小时)
$categories = Cache::remember('knowledge_categories', 3600, function() {
return $this->getCategoriesFromDB();
});
// 推荐文章缓存(30分钟)
$recommend = Cache::remember("knowledge_recommend_{$student_id}", 1800, function() use ($student_id) {
return $this->getRecommendArticles($student_id);
});
```
这个详细的开发任务文档涵盖了知识库模块的完整开发流程,包括后端接口开发、数据库设计、前后端联调和验收标准,可以作为开发团队的具体执行指南。
Loading…
Cancel
Save