From b52410234577286b50a1ee8b7a7fa686a2b3573a Mon Sep 17 00:00:00 2001
From: zeyan <258785420@qq.com>
Date: Tue, 19 Aug 2025 12:58:53 +0800
Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20bug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../api/controller/apiController/Contract.php | 14 +-
niucloud/app/common.php | 27 +
.../document/DocumentTemplateService.php | 719 +++++++++++++++++-
.../apiService/ContractSignFormService.php | 36 +-
uniapp/common/config.js | 4 +-
5 files changed, 784 insertions(+), 16 deletions(-)
diff --git a/niucloud/app/api/controller/apiController/Contract.php b/niucloud/app/api/controller/apiController/Contract.php
index 846c0112..b8eede19 100644
--- a/niucloud/app/api/controller/apiController/Contract.php
+++ b/niucloud/app/api/controller/apiController/Contract.php
@@ -488,15 +488,23 @@ class Contract extends BaseApiService
$fillData[$placeholder] = $defaultValue;
}
- // 调用DocumentTemplateService生成Word文档
+ // 调试信息:记录填充数据
+ Log::info('准备的填充数据', [
+ 'contract_sign_id' => $contract_sign_id,
+ 'fill_data_count' => count($fillData),
+ 'fill_data' => $fillData
+ ]);
+
+ // 调用DocumentTemplateService的新XML字符串方法生成Word文档
$documentService = new \app\service\admin\document\DocumentTemplateService();
$generateData = [
'template_id' => $contractSign['contract_id'],
'fill_data' => $fillData,
- 'output_filename' => $contract['contract_name'] . '_' . $contractSign['student_id'] . '_' . date('YmdHis') . '.docx'
+ 'output_filename' => $contract['contract_name'] . '_' . $contractSign['student_id'] . '_' . date('YmdHis') . '.docx',
+ 'use_direct_values' => true // 直接使用已处理的值,不进行二次处理
];
- $result = $documentService->generateDocument($generateData);
+ $result = $documentService->generateDocumentByXmlString($generateData);
// 更新school_contract_sign表的sign_file字段和status字段
$updateResult = Db::table('school_contract_sign')
diff --git a/niucloud/app/common.php b/niucloud/app/common.php
index 40e351be..d7b89f99 100644
--- a/niucloud/app/common.php
+++ b/niucloud/app/common.php
@@ -1672,6 +1672,33 @@ function get_current_week()
return date('W');
}
+/**
+ * 获取当前年份(别名函数,兼容配置中的函数名)
+ * @return string
+ */
+function current_year()
+{
+ return get_current_year();
+}
+
+/**
+ * 获取当前月份(别名函数,兼容配置中的函数名)
+ * @return string
+ */
+function current_month()
+{
+ return get_current_month();
+}
+
+/**
+ * 获取当前日(别名函数,兼容配置中的函数名)
+ * @return string
+ */
+function current_day()
+{
+ return get_current_day();
+}
+
/**
* 获取当前季度
* @return string
diff --git a/niucloud/app/service/admin/document/DocumentTemplateService.php b/niucloud/app/service/admin/document/DocumentTemplateService.php
index 1b73c87e..ee2adc90 100644
--- a/niucloud/app/service/admin/document/DocumentTemplateService.php
+++ b/niucloud/app/service/admin/document/DocumentTemplateService.php
@@ -652,7 +652,19 @@ class DocumentTemplateService extends BaseAdminService
// 准备填充数据
$placeholderConfig = json_decode($template['placeholder_config'], true);
- $fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']);
+ // 检查是否传递了 use_direct_values 参数,如果是则直接使用传递的值
+ if (!empty($data['use_direct_values']) && $data['use_direct_values'] === true) {
+ // 直接使用传递的 fill_data 作为填充值,不进行二次处理
+ $fillValues = $data['fill_data'];
+ Log::info('使用直接填充模式', [
+ 'template_id' => $data['template_id'],
+ 'fill_data_count' => count($fillValues),
+ 'fill_data_keys' => array_keys($fillValues)
+ ]);
+ } else {
+ // 使用原有的配置处理模式
+ $fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']);
+ }
// 生成文档
$templatePath = public_path() . '/upload/' . $template['contract_template'];
@@ -695,12 +707,14 @@ class DocumentTemplateService extends BaseAdminService
throw new \Exception('无法创建临时文档存储目录,请检查系统权限');
}
+ // 预处理:修复被格式化分割的占位符
+ $fixedTemplatePath = $this->fixBrokenPlaceholders($templatePath);
+
// 使用 PhpWord 模板处理器
- $templateProcessor = new TemplateProcessor($templatePath);
+ $templateProcessor = new TemplateProcessor($fixedTemplatePath);
- foreach ($fillValues as $placeholder => $value) {
- $templateProcessor->setValue($placeholder, $value);
- }
+ // 智能处理占位符,根据类型使用不同的处理方法
+ $this->processPlaceholders($templateProcessor, $fillValues, $placeholderConfig);
$templateProcessor->saveAs($fullOutputPath);
@@ -849,13 +863,37 @@ class DocumentTemplateService extends BaseAdminService
$tableName = $config['table_name'];
$fieldName = $config['field_name'];
- // 简单的数据库查询(实际应用中需要更完善的查询逻辑)
+ // 改进的数据库查询逻辑,支持条件查询
$model = \think\facade\Db::connect();
- $result = $model->table($tableName)->field($fieldName)->find();
+ $query = $model->table($tableName);
+
+ // 如果有传入的查询条件(比如学员ID),使用条件查询
+ if (!empty($userFillData) && is_array($userFillData)) {
+ foreach ($userFillData as $key => $value) {
+ // 支持常见的查询字段
+ if (in_array($key, ['id', 'student_id', 'user_id', 'person_id', 'contract_id'])) {
+ $query->where($key, $value);
+ break; // 只使用第一个匹配的条件
+ }
+ }
+ }
+
+ $result = $query->field($fieldName)->find();
+
+ Log::info('数据库查询', [
+ 'table' => $tableName,
+ 'field' => $fieldName,
+ 'result' => $result,
+ 'user_data' => $userFillData
+ ]);
return $result[$fieldName] ?? $config['default_value'] ?? '';
} catch (\Exception $e) {
- Log::error('数据库查询失败:' . $e->getMessage());
+ Log::error('数据库查询失败:' . $e->getMessage(), [
+ 'table' => $config['table_name'] ?? '',
+ 'field' => $config['field_name'] ?? '',
+ 'config' => $config
+ ]);
return $config['default_value'] ?? '';
}
}
@@ -1042,6 +1080,130 @@ class DocumentTemplateService extends BaseAdminService
return $this->pageQuery($searchModel);
}
+ /**
+ * 修复被Word格式化分割的占位符
+ * Word在编辑时会在占位符中插入格式化标签,导致{{placeholder}}被分割
+ * 这个方法会创建一个修复后的临时文件
+ *
+ * @param string $templatePath 原始模板文件路径
+ * @return string 修复后的模板文件路径
+ */
+ private function fixBrokenPlaceholders(string $templatePath)
+ {
+ try {
+ // 创建临时文件
+ $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'niucloud_templates';
+ if (!is_dir($tempDir)) {
+ mkdir($tempDir, 0755, true);
+ }
+
+ $fixedTemplatePath = $tempDir . DIRECTORY_SEPARATOR . 'fixed_' . basename($templatePath);
+
+ // 复制原文件到临时位置
+ if (!copy($templatePath, $fixedTemplatePath)) {
+ Log::warning('无法创建临时模板文件,使用原始模板', ['template_path' => $templatePath]);
+ return $templatePath;
+ }
+
+ // 打开ZIP文件进行修复
+ $zip = new \ZipArchive();
+
+ if ($zip->open($fixedTemplatePath) === TRUE) {
+ $content = $zip->getFromName('word/document.xml');
+
+ if ($content !== false) {
+ $originalLength = strlen($content);
+ $fixedContent = $content;
+
+ // 第一步:修复跨XML标签的占位符(最常见的模式)
+ // 模式1:修复 {{...内容}} 这种分割
+ $pattern1 = '/]*>\{\{<\/w:t>([^<]*)]*>([^<]*)\}\}<\/w:t>/';
+ $fixedContent = preg_replace_callback($pattern1, function($matches) {
+ $placeholder = '{{' . $matches[1] . $matches[2] . '}}';
+ return '' . $placeholder . '';
+ }, $fixedContent);
+
+ // 模式2:修复 {...{内容}} 这种分割
+ $pattern2 = '/]*>\{<\/w:t>([^<]*)]*>\{([^<]*)\}\}<\/w:t>/';
+ $fixedContent = preg_replace_callback($pattern2, function($matches) {
+ $placeholder = '{{' . $matches[1] . $matches[2] . '}}';
+ return '' . $placeholder . '';
+ }, $fixedContent);
+
+ // 模式3:修复三段式分割 {{...中间...}}
+ $pattern3 = '/]*>\{\{<\/w:t>([^<]*)]*>([^<]*)<\/w:t>([^<]*)]*>([^<]*)\}\}<\/w:t>/';
+ $fixedContent = preg_replace_callback($pattern3, function($matches) {
+ $placeholder = '{{' . $matches[1] . $matches[2] . $matches[4] . '}}';
+ return '' . $placeholder . '';
+ }, $fixedContent);
+
+ // 第二步:处理更复杂的格式化分割(包含rPr标签的)
+ // 匹配包含格式化信息的分割模式
+ $complexPattern = '/\{\{[^}]*?(?:<\/w:t><\/w:r>]*?>(?:]*?>.*?<\/w:rPr>)?]*?>)[^}]*?\}\}/s';
+ $fixedContent = preg_replace_callback($complexPattern, function($matches) {
+ $placeholder = $matches[0];
+ // 移除所有XML标签,保留纯文本
+ $cleaned = preg_replace('/<[^>]*?>/', '', $placeholder);
+ // 验证是否为有效占位符
+ if (!preg_match('/^\{\{[^}]+\}\}$/', $cleaned)) {
+ return $placeholder;
+ }
+ // 保持原有的第一个w:t标签结构
+ if (preg_match('/^([^<]*]*>)/', $placeholder, $tagMatch)) {
+ return $tagMatch[1] . $cleaned . '';
+ }
+ return $cleaned;
+ }, $fixedContent);
+
+ // 第三步:修复任何剩余的基本分割模式
+ $basicPattern = '/\{\{[^}]*?<[^>]*?>[^}]*?\}\}/';
+ $fixedContent = preg_replace_callback($basicPattern, function($matches) {
+ $placeholder = $matches[0];
+ $cleaned = preg_replace('/<[^>]*?>/', '', $placeholder);
+ if (!preg_match('/^\{\{[^}]+\}\}$/', $cleaned)) {
+ return $placeholder;
+ }
+ return $cleaned;
+ }, $fixedContent);
+
+ // 将修复后的内容写回ZIP文件
+ if ($zip->addFromString('word/document.xml', $fixedContent)) {
+ $zip->close();
+
+ Log::info('占位符修复完成', [
+ 'template_file' => $templatePath,
+ 'fixed_file' => $fixedTemplatePath,
+ 'original_length' => $originalLength,
+ 'fixed_length' => strlen($fixedContent),
+ 'size_change' => strlen($fixedContent) - $originalLength
+ ]);
+
+ return $fixedTemplatePath;
+ } else {
+ $zip->close();
+ Log::warning('无法写入修复后的内容,使用原始模板', ['template_path' => $templatePath]);
+ return $templatePath;
+ }
+ } else {
+ $zip->close();
+ Log::warning('无法读取document.xml,使用原始模板', ['template_path' => $templatePath]);
+ return $templatePath;
+ }
+ } else {
+ Log::warning('无法打开Word文档,使用原始模板', ['template_path' => $templatePath]);
+ return $templatePath;
+ }
+ } catch (\Exception $e) {
+ Log::error('修复占位符失败', [
+ 'template_path' => $templatePath,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+ // 修复失败不影响主流程,返回原始模板路径
+ return $templatePath;
+ }
+ }
+
/**
* 格式化文件大小
* @param int $size
@@ -1431,4 +1593,545 @@ class DocumentTemplateService extends BaseAdminService
throw new \Exception('模板文档更新失败:' . $e->getMessage());
}
}
+
+ /**
+ * 使用XML字符串操作生成Word文档
+ * 此方法直接读取Word模板的XML内容,进行字符串替换,然后保存为新的Word文档
+ * @param array $data
+ * @return array
+ * @throws \Exception
+ */
+ public function generateDocumentByXmlString(array $data)
+ {
+ $template = $this->contractModel->find($data['template_id']);
+ if (!$template) {
+ throw new \Exception('模板不存在');
+ }
+
+ if (empty($template['placeholder_config'])) {
+ throw new \Exception('模板尚未配置占位符');
+ }
+
+ // 创建生成记录
+ $logData = [
+ 'site_id' => $this->site_id,
+ 'template_id' => $data['template_id'],
+ 'user_id' => $this->uid,
+ 'user_type' => 1,
+ 'fill_data' => json_encode($data['fill_data']),
+ 'status' => 'pending',
+ 'completed_at' => date('Y-m-d H:i:s')
+ ];
+
+ $log = $this->logModel->create($logData);
+
+ try {
+ // 更新状态为处理中
+ $log->status = 'processing';
+ $log->process_start_time = date('Y-m-d H:i:s');
+ $log->save();
+
+ // 准备填充数据
+ $placeholderConfig = json_decode($template['placeholder_config'], true);
+ if (!empty($data['use_direct_values']) && $data['use_direct_values'] === true) {
+ $fillValues = $data['fill_data'];
+ Log::info('使用直接填充模式(XML字符串方法)', [
+ 'template_id' => $data['template_id'],
+ 'fill_data_count' => count($fillValues),
+ 'fill_data_keys' => array_keys($fillValues)
+ ]);
+ } else {
+ $fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']);
+ }
+
+ // 原始模板路径
+ $templatePath = public_path() . '/upload/' . $template['contract_template'];
+ $outputFileName = $data['output_filename'] ?: ($template['contract_name'] . '_' . date('YmdHis') . '.docx');
+
+ // 生成输出路径
+ $outputPath = 'generated_documents/' . date('Y/m/') . $outputFileName;
+ $publicOutputPath = public_path() . '/upload/' . $outputPath;
+ $publicOutputDir = dirname($publicOutputPath);
+
+ // 确保输出目录存在
+ if (!is_dir($publicOutputDir)) {
+ if (!mkdir($publicOutputDir, 0755, true) && !is_dir($publicOutputDir)) {
+ throw new \Exception('无法创建输出目录:' . $publicOutputDir);
+ }
+ }
+
+ // 使用XML字符串方法生成文档
+ $success = $this->processWordDocumentXml($templatePath, $publicOutputPath, $fillValues);
+
+ if (!$success) {
+ throw new \Exception('XML字符串处理失败');
+ }
+
+ // 更新生成记录
+ $log->status = 'completed';
+ $log->generated_file_path = $outputPath;
+ $log->generated_file_name = $outputFileName;
+ $log->process_end_time = date('Y-m-d H:i:s');
+ $log->save();
+
+ Log::info('XML字符串方法生成Word文档成功', [
+ 'template_id' => $data['template_id'],
+ 'output_path' => $outputPath,
+ 'fill_values_count' => count($fillValues)
+ ]);
+
+ return [
+ 'log_id' => $log->id,
+ 'file_path' => $outputPath,
+ 'file_name' => $outputFileName,
+ 'download_url' => url('/upload/' . $outputPath)
+ ];
+
+ } catch (\Exception $e) {
+ // 更新记录为失败状态
+ $log->status = 'failed';
+ $log->error_msg = $e->getMessage();
+ $log->process_end_time = date('Y-m-d H:i:s');
+ $log->save();
+
+ Log::error('XML字符串方法生成Word文档失败', [
+ 'template_id' => $data['template_id'],
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+
+ throw new \Exception('文档生成失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 处理Word文档的XML内容进行占位符替换
+ * @param string $templatePath 模板文件路径
+ * @param string $outputPath 输出文件路径
+ * @param array $fillValues 填充值数组
+ * @return bool
+ * @throws \Exception
+ */
+ private function processWordDocumentXml(string $templatePath, string $outputPath, array $fillValues): bool
+ {
+ try {
+ // 检查模板文件是否存在
+ if (!file_exists($templatePath)) {
+ throw new \Exception('模板文件不存在:' . $templatePath);
+ }
+
+ // 创建临时工作目录
+ $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'word_processing_' . uniqid();
+ if (!mkdir($tempDir, 0755, true)) {
+ throw new \Exception('无法创建临时工作目录');
+ }
+
+ try {
+ // 1. 解压Word文档到临时目录
+ $zip = new \ZipArchive();
+ $result = $zip->open($templatePath);
+
+ if ($result !== TRUE) {
+ throw new \Exception('无法打开Word模板文件,错误代码:' . $result);
+ }
+
+ if (!$zip->extractTo($tempDir)) {
+ $zip->close();
+ throw new \Exception('无法解压Word模板文件');
+ }
+ $zip->close();
+
+ // 2. 读取document.xml文件
+ $documentXmlPath = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . 'document.xml';
+ if (!file_exists($documentXmlPath)) {
+ throw new \Exception('Word文档缺少document.xml文件');
+ }
+
+ $xmlContent = file_get_contents($documentXmlPath);
+ if ($xmlContent === false) {
+ throw new \Exception('无法读取document.xml文件内容');
+ }
+
+ Log::info('读取XML内容成功', [
+ 'original_length' => strlen($xmlContent),
+ 'placeholders_to_replace' => array_keys($fillValues)
+ ]);
+
+ // 3. 进行占位符替换
+ $modifiedXmlContent = $this->replaceXmlPlaceholders($xmlContent, $fillValues);
+
+ Log::info('XML占位符替换完成', [
+ 'original_length' => strlen($xmlContent),
+ 'modified_length' => strlen($modifiedXmlContent),
+ 'size_change' => strlen($modifiedXmlContent) - strlen($xmlContent)
+ ]);
+
+ // 4. 写回修改后的document.xml
+ if (file_put_contents($documentXmlPath, $modifiedXmlContent) === false) {
+ throw new \Exception('无法写入修改后的document.xml');
+ }
+
+ // 5. 重新打包为Word文档
+ $newZip = new \ZipArchive();
+ $result = $newZip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
+
+ if ($result !== TRUE) {
+ throw new \Exception('无法创建输出Word文件,错误代码:' . $result);
+ }
+
+ // 递归添加所有文件到ZIP
+ $this->addDirectoryToZip($newZip, $tempDir, '');
+
+ if (!$newZip->close()) {
+ throw new \Exception('无法保存输出Word文件');
+ }
+
+ Log::info('Word文档重新打包成功', [
+ 'output_path' => $outputPath,
+ 'file_size' => filesize($outputPath)
+ ]);
+
+ return true;
+
+ } finally {
+ // 清理临时目录
+ $this->removeDirectory($tempDir);
+ }
+
+ } catch (\Exception $e) {
+ Log::error('处理Word文档XML失败', [
+ 'template_path' => $templatePath,
+ 'output_path' => $outputPath,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+ throw $e;
+ }
+ }
+
+ /**
+ * 在XML内容中替换占位符
+ * @param string $xmlContent 原始XML内容
+ * @param array $fillValues 替换值数组
+ * @return string 替换后的XML内容
+ */
+ private function replaceXmlPlaceholders(string $xmlContent, array $fillValues): string
+ {
+ $modifiedContent = $xmlContent;
+ $replacementCount = 0;
+
+ foreach ($fillValues as $placeholder => $value) {
+ // 确保占位符格式正确
+ $searchPattern = '{{' . $placeholder . '}}';
+
+ // 转义特殊字符,确保安全的XML替换
+ $safeValue = htmlspecialchars((string)$value, ENT_XML1, 'UTF-8');
+
+ // 进行替换
+ $beforeLength = strlen($modifiedContent);
+ $modifiedContent = str_replace($searchPattern, $safeValue, $modifiedContent, $count);
+ $afterLength = strlen($modifiedContent);
+
+ if ($count > 0) {
+ $replacementCount += $count;
+ Log::info('占位符替换成功', [
+ 'placeholder' => $placeholder,
+ 'value' => $value,
+ 'safe_value' => $safeValue,
+ 'replacement_count' => $count,
+ 'content_length_change' => $afterLength - $beforeLength
+ ]);
+ } else {
+ Log::warning('占位符未找到', [
+ 'placeholder' => $placeholder,
+ 'search_pattern' => $searchPattern
+ ]);
+ }
+ }
+
+ Log::info('所有占位符处理完成', [
+ 'total_replacements' => $replacementCount,
+ 'processed_placeholders' => count($fillValues)
+ ]);
+
+ return $modifiedContent;
+ }
+
+ /**
+ * 递归添加目录到ZIP文件
+ * @param \ZipArchive $zip ZIP文件对象
+ * @param string $sourcePath 源目录路径
+ * @param string $relativePath ZIP内的相对路径
+ * @return void
+ */
+ private function addDirectoryToZip(\ZipArchive $zip, string $sourcePath, string $relativePath): void
+ {
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($sourcePath, \RecursiveDirectoryIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ foreach ($iterator as $file) {
+ $filePath = $file->getRealPath();
+ $relativeName = $relativePath . substr($filePath, strlen($sourcePath) + 1);
+
+ // 在Windows系统中统一使用正斜杠
+ $relativeName = str_replace('\\', '/', $relativeName);
+
+ if ($file->isDir()) {
+ // 添加目录
+ $zip->addEmptyDir($relativeName);
+ } else {
+ // 添加文件
+ $zip->addFile($filePath, $relativeName);
+ }
+ }
+ }
+
+ /**
+ * 递归删除目录
+ * @param string $dir 要删除的目录路径
+ * @return void
+ */
+ private function removeDirectory(string $dir): void
+ {
+ if (!is_dir($dir)) {
+ return;
+ }
+
+ $files = array_diff(scandir($dir), ['.', '..']);
+ foreach ($files as $file) {
+ $filePath = $dir . DIRECTORY_SEPARATOR . $file;
+ if (is_dir($filePath)) {
+ $this->removeDirectory($filePath);
+ } else {
+ unlink($filePath);
+ }
+ }
+ rmdir($dir);
+ }
+
+ /**
+ * 智能处理占位符,根据类型使用不同的处理方法
+ * @param TemplateProcessor $templateProcessor
+ * @param array $fillValues
+ * @param array $placeholderConfig
+ * @return void
+ */
+ private function processPlaceholders(TemplateProcessor $templateProcessor, array $fillValues, array $placeholderConfig)
+ {
+ // 如果没有配置信息,使用简单的文本替换
+ if (empty($placeholderConfig)) {
+ foreach ($fillValues as $placeholder => $value) {
+ $templateProcessor->setValue($placeholder, $value);
+ }
+ return;
+ }
+
+ $placeholderConfig = is_string($placeholderConfig) ? json_decode($placeholderConfig, true) : $placeholderConfig;
+
+ foreach ($fillValues as $placeholder => $value) {
+ $config = $placeholderConfig[$placeholder] ?? [];
+ $fieldType = $config['field_type'] ?? 'text';
+ $dataType = $config['data_type'] ?? 'user_input';
+
+ Log::info('处理占位符', [
+ 'placeholder' => $placeholder,
+ 'value' => $value,
+ 'field_type' => $fieldType,
+ 'data_type' => $dataType
+ ]);
+
+ try {
+ // 根据字段类型选择处理方式
+ if ($fieldType === 'image' || $dataType === 'sign_img' || $dataType === 'signature') {
+ // 处理图片类型
+ $this->setImageValue($templateProcessor, $placeholder, $value);
+ } else {
+ // 处理文本类型
+ $templateProcessor->setValue($placeholder, $value);
+ }
+ } catch (\Exception $e) {
+ Log::error('占位符处理失败', [
+ 'placeholder' => $placeholder,
+ 'value' => $value,
+ 'error' => $e->getMessage()
+ ]);
+ // 如果图片处理失败,尝试作为文本处理
+ $templateProcessor->setValue($placeholder, $value);
+ }
+ }
+ }
+
+ /**
+ * 设置图片占位符的值
+ * @param TemplateProcessor $templateProcessor
+ * @param string $placeholder
+ * @param mixed $value
+ * @return void
+ */
+ private function setImageValue(TemplateProcessor $templateProcessor, string $placeholder, $value)
+ {
+ if (empty($value)) {
+ // 如果值为空,设置为空文本
+ $templateProcessor->setValue($placeholder, '');
+ return;
+ }
+
+ $imagePath = null;
+
+ try {
+ // 判断图片数据类型并处理
+ if (is_string($value)) {
+ if (str_starts_with($value, 'data:image/')) {
+ // 处理base64图片数据
+ $imagePath = $this->saveBase64Image($value);
+ } elseif (str_starts_with($value, 'http://') || str_starts_with($value, 'https://')) {
+ // 处理网络图片URL
+ $imagePath = $this->downloadImage($value);
+ } elseif (file_exists($value)) {
+ // 处理本地文件路径
+ $imagePath = $value;
+ } elseif (file_exists(public_path() . '/' . ltrim($value, '/'))) {
+ // 处理相对路径
+ $imagePath = public_path() . '/' . ltrim($value, '/');
+ }
+ }
+
+ if ($imagePath && file_exists($imagePath)) {
+ // 验证图片文件
+ $imageInfo = getimagesize($imagePath);
+ if ($imageInfo === false) {
+ throw new \Exception('无效的图片文件');
+ }
+
+ // 设置图片,限制尺寸
+ $templateProcessor->setImageValue($placeholder, [
+ 'path' => $imagePath,
+ 'width' => 100, // 可以根据需要调整
+ 'height' => 100,
+ 'ratio' => true
+ ]);
+
+ Log::info('图片占位符设置成功', [
+ 'placeholder' => $placeholder,
+ 'image_path' => $imagePath,
+ 'image_size' => $imageInfo
+ ]);
+
+ // 如果是临时文件,标记稍后删除
+ if (str_contains($imagePath, sys_get_temp_dir())) {
+ register_shutdown_function(function() use ($imagePath) {
+ if (file_exists($imagePath)) {
+ @unlink($imagePath);
+ }
+ });
+ }
+ } else {
+ // 如果无法处理为图片,使用文本替换
+ $templateProcessor->setValue($placeholder, $value);
+ Log::warning('图片处理失败,使用文本替换', [
+ 'placeholder' => $placeholder,
+ 'value' => $value
+ ]);
+ }
+ } catch (\Exception $e) {
+ Log::error('图片设置失败', [
+ 'placeholder' => $placeholder,
+ 'value' => $value,
+ 'error' => $e->getMessage()
+ ]);
+ // 如果图片处理失败,使用文本替换
+ $templateProcessor->setValue($placeholder, $value);
+ }
+ }
+
+ /**
+ * 保存base64图片数据到临时文件
+ * @param string $base64Data
+ * @return string|null
+ */
+ private function saveBase64Image(string $base64Data): ?string
+ {
+ try {
+ // 解析base64数据
+ if (preg_match('/^data:image\/(\w+);base64,(.+)$/', $base64Data, $matches)) {
+ $imageType = $matches[1];
+ $imageData = base64_decode($matches[2]);
+
+ if ($imageData === false) {
+ throw new \Exception('base64解码失败');
+ }
+
+ // 创建临时文件
+ $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'niucloud_images';
+ if (!is_dir($tempDir)) {
+ mkdir($tempDir, 0755, true);
+ }
+
+ $fileName = uniqid('img_') . '.' . $imageType;
+ $filePath = $tempDir . DIRECTORY_SEPARATOR . $fileName;
+
+ if (file_put_contents($filePath, $imageData) === false) {
+ throw new \Exception('保存图片文件失败');
+ }
+
+ return $filePath;
+ }
+ } catch (\Exception $e) {
+ Log::error('保存base64图片失败:' . $e->getMessage());
+ }
+
+ return null;
+ }
+
+ /**
+ * 下载网络图片到临时文件
+ * @param string $url
+ * @return string|null
+ */
+ private function downloadImage(string $url): ?string
+ {
+ try {
+ // 创建临时文件
+ $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'niucloud_images';
+ if (!is_dir($tempDir)) {
+ mkdir($tempDir, 0755, true);
+ }
+
+ $pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
+ $extension = $pathInfo['extension'] ?? 'jpg';
+ $fileName = uniqid('img_') . '.' . $extension;
+ $filePath = $tempDir . DIRECTORY_SEPARATOR . $fileName;
+
+ // 下载图片
+ $context = stream_context_create([
+ 'http' => [
+ 'timeout' => 30,
+ 'user_agent' => 'Mozilla/5.0 (compatible; Document Generator)',
+ ]
+ ]);
+
+ $imageData = file_get_contents($url, false, $context);
+ if ($imageData === false) {
+ throw new \Exception('下载图片失败');
+ }
+
+ if (file_put_contents($filePath, $imageData) === false) {
+ throw new \Exception('保存图片文件失败');
+ }
+
+ // 验证是否为有效图片
+ if (getimagesize($filePath) === false) {
+ unlink($filePath);
+ throw new \Exception('下载的文件不是有效图片');
+ }
+
+ return $filePath;
+ } catch (\Exception $e) {
+ Log::error('下载图片失败:' . $e->getMessage(), ['url' => $url]);
+ }
+
+ return null;
+ }
}
\ No newline at end of file
diff --git a/niucloud/app/service/api/apiService/ContractSignFormService.php b/niucloud/app/service/api/apiService/ContractSignFormService.php
index 6797ab41..05ad9bc6 100644
--- a/niucloud/app/service/api/apiService/ContractSignFormService.php
+++ b/niucloud/app/service/api/apiService/ContractSignFormService.php
@@ -636,14 +636,43 @@ class ContractSignFormService extends BaseApiService
private function getSignSpecificFormFields($contract_sign_id, $student)
{
try {
- // 从document_data_source_config表获取该签署关系专属的字段配置
+ // 先获取签署记录以得到合同ID
+ $sign_record = Db::table('school_contract_sign')
+ ->where('id', $contract_sign_id)
+ ->find();
+
+ if (!$sign_record) {
+ Log::error('签署记录不存在', ['contract_sign_id' => $contract_sign_id]);
+ return [];
+ }
+
+ $contract_id = $sign_record['contract_id'];
+
+ // 优先获取该签署关系专属的字段配置
$configs = Db::table('school_document_data_source_config')
->where('contract_sign_id', $contract_sign_id)
->select()
->toArray();
+ // 如果该签署记录没有专属配置,则获取合同模板的基础配置
if (empty($configs)) {
- Log::warning('该签署记录无字段配置', ['contract_sign_id' => $contract_sign_id]);
+ Log::info('该签署记录无专属配置,使用合同模板基础配置', [
+ 'contract_sign_id' => $contract_sign_id,
+ 'contract_id' => $contract_id
+ ]);
+
+ $configs = Db::table('school_document_data_source_config')
+ ->where('contract_id', $contract_id)
+ ->whereNull('contract_sign_id')
+ ->select()
+ ->toArray();
+ }
+
+ if (empty($configs)) {
+ Log::warning('该合同无字段配置', [
+ 'contract_sign_id' => $contract_sign_id,
+ 'contract_id' => $contract_id
+ ]);
return [];
}
@@ -736,8 +765,9 @@ class ContractSignFormService extends BaseApiService
switch ($table_name) {
case 'school_student':
+ case 'students': // 兼容配置中的简化表名
// 学员表:使用CoreFieldMappingService进行字段映射和转义
- $service = new CoreFieldMappingService($config['table_name'], ['id' => $student['id']]);
+ $service = new CoreFieldMappingService('school_student', ['id' => $student['id']]);
// 配置枚举映射
$service->setFieldEnums([
diff --git a/uniapp/common/config.js b/uniapp/common/config.js
index 2459f37b..c98a2253 100644
--- a/uniapp/common/config.js
+++ b/uniapp/common/config.js
@@ -1,6 +1,6 @@
// 环境变量配置
-// const env = 'development'
-const env = 'prod'
+const env = 'development'
+// const env = 'prod'
const isMockEnabled = false // 默认禁用Mock优先模式,仅作为回退
const isDebug = false // 默认启用调试模式
const devurl = 'http://localhost:20080/api'
From 05e2c05622467612c76bd97940d257aaa5fdf280 Mon Sep 17 00:00:00 2001
From: zeyan <258785420@qq.com>
Date: Tue, 19 Aug 2025 15:41:01 +0800
Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20bug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../document/DocumentTemplateService.php | 677 +++++++++++++++++-
1 file changed, 673 insertions(+), 4 deletions(-)
diff --git a/niucloud/app/service/admin/document/DocumentTemplateService.php b/niucloud/app/service/admin/document/DocumentTemplateService.php
index ee2adc90..7ff7b9b1 100644
--- a/niucloud/app/service/admin/document/DocumentTemplateService.php
+++ b/niucloud/app/service/admin/document/DocumentTemplateService.php
@@ -1791,6 +1791,9 @@ class DocumentTemplateService extends BaseAdminService
'file_size' => filesize($outputPath)
]);
+ // 处理文档中的URL图片(第二次处理)
+ $this->processUrlImagesInDocument($outputPath, $fillValues);
+
return true;
} finally {
@@ -1818,18 +1821,24 @@ class DocumentTemplateService extends BaseAdminService
private function replaceXmlPlaceholders(string $xmlContent, array $fillValues): string
{
$modifiedContent = $xmlContent;
+
+ // 首先清理被格式化标签分割的占位符
+ $modifiedContent = $this->cleanBrokenPlaceholdersInXml($modifiedContent);
+
$replacementCount = 0;
foreach ($fillValues as $placeholder => $value) {
- // 确保占位符格式正确
- $searchPattern = '{{' . $placeholder . '}}';
+ // 使用正则表达式匹配占位符,支持空格、换行符、制表符
+ // 模式:{{ + 任意空白字符 + placeholder + 任意空白字符 + }}
+ $escapedPlaceholder = preg_quote($placeholder, '/');
+ $searchPattern = '/\{\{\s*' . $escapedPlaceholder . '\s*\}\}/';
// 转义特殊字符,确保安全的XML替换
$safeValue = htmlspecialchars((string)$value, ENT_XML1, 'UTF-8');
- // 进行替换
+ // 使用正则替换
$beforeLength = strlen($modifiedContent);
- $modifiedContent = str_replace($searchPattern, $safeValue, $modifiedContent, $count);
+ $modifiedContent = preg_replace($searchPattern, $safeValue, $modifiedContent, -1, $count);
$afterLength = strlen($modifiedContent);
if ($count > 0) {
@@ -1857,6 +1866,234 @@ class DocumentTemplateService extends BaseAdminService
return $modifiedContent;
}
+ /**
+ * 清理XML中被格式化标签分割的占位符
+ * @param string $xmlContent
+ * @return string
+ */
+ private function cleanBrokenPlaceholdersInXml(string $xmlContent): string
+ {
+ $cleanedContent = $xmlContent;
+
+ Log::info('开始清理分割的占位符');
+
+ // 多阶段清理策略:从简单到复杂
+
+ // 第一阶段:处理完整的双大括号分割情况
+ $cleanedContent = $this->fixCompleteBracketSplits($cleanedContent);
+
+ // 第二阶段:处理单个大括号分割情况
+ $cleanedContent = $this->fixSingleBracketSplits($cleanedContent);
+
+ // 第三阶段:通用清理,移除占位符内部的XML标签
+ $cleanedContent = $this->generalPlaceholderCleanup($cleanedContent);
+
+ // 第四阶段:最终验证和修复
+ $cleanedContent = $this->finalPlaceholderValidation($cleanedContent);
+
+ Log::info('占位符清理完成');
+
+ return $cleanedContent;
+ }
+
+ /**
+ * 修复完整双大括号被分割的情况
+ * @param string $content
+ * @return string
+ */
+ private function fixCompleteBracketSplits(string $content): string
+ {
+ Log::info('阶段1:修复完整双大括号分割');
+
+ $patterns = [
+ // 模式1: {{...内容}}
+ '/\{\{<\/w:t>.*?]*>([^<]*)\}\}/',
+
+ // 模式2: {{...内容}}
+ '/\{\{<\/w:t><\/w:r>]*>.*?]*>([^<]*)\}\}/',
+
+ // 模式3: 包含rPr格式标签的复杂嵌套
+ '/\{\{<\/w:t><\/w:r>]*>.*?<\/w:rPr>]*>([^<]*)\}\}/'
+ ];
+
+ foreach ($patterns as $index => $pattern) {
+ $content = preg_replace_callback($pattern, function($matches) use ($index) {
+ $placeholderContent = $matches[1];
+ $result = '{{' . $placeholderContent . '}}';
+
+ Log::info('修复完整双大括号分割', [
+ 'pattern_index' => $index + 1,
+ 'original' => substr($matches[0], 0, 100) . '...',
+ 'fixed' => $result,
+ 'placeholder_content' => $placeholderContent
+ ]);
+
+ return $result;
+ }, $content);
+ }
+
+ return $content;
+ }
+
+ /**
+ * 修复单个大括号被分割的情况
+ * @param string $content
+ * @return string
+ */
+ private function fixSingleBracketSplits(string $content): string
+ {
+ Log::info('阶段2:修复单个大括号分割');
+
+ // 处理左大括号被分割:{...{内容}}
+ $leftBracketPatterns = [
+ // 标准模式:{...{内容}}
+ '/\{<\/w:t><\/w:r>]*>.*?]*>\{([^}]*)\}\}/',
+
+ // 包含rPr的复杂模式
+ '/\{<\/w:t><\/w:r>]*>.*?<\/w:rPr>]*>\{([^}]*)\}\}/',
+
+ // 简单模式:{...{内容}}
+ '/\{<\/w:t>.*?]*>\{([^}]*)\}\}/'
+ ];
+
+ foreach ($leftBracketPatterns as $index => $pattern) {
+ $content = preg_replace_callback($pattern, function($matches) use ($index) {
+ $placeholderContent = $matches[1];
+ $result = '{{' . $placeholderContent . '}}';
+
+ Log::info('修复左大括号分割', [
+ 'pattern_index' => $index + 1,
+ 'original' => substr($matches[0], 0, 100) . '...',
+ 'fixed' => $result,
+ 'placeholder_content' => $placeholderContent
+ ]);
+
+ return $result;
+ }, $content);
+ }
+
+ // 处理右大括号被分割:{{内容}...}
+ $rightBracketPatterns = [
+ // 标准模式:{{内容}...}
+ '/\{\{([^}]*)\}<\/w:t><\/w:r>]*>.*?]*>\}/',
+
+ // 包含rPr的复杂模式
+ '/\{\{([^}]*)\}<\/w:t><\/w:r>]*>.*?<\/w:rPr>]*>\}/',
+
+ // 简单模式:{{内容}...}
+ '/\{\{([^}]*)\}<\/w:t>.*?]*>\}/'
+ ];
+
+ foreach ($rightBracketPatterns as $index => $pattern) {
+ $content = preg_replace_callback($pattern, function($matches) use ($index) {
+ $placeholderContent = $matches[1];
+ $result = '{{' . $placeholderContent . '}}';
+
+ Log::info('修复右大括号分割', [
+ 'pattern_index' => $index + 1,
+ 'original' => substr($matches[0], 0, 100) . '...',
+ 'fixed' => $result,
+ 'placeholder_content' => $placeholderContent
+ ]);
+
+ return $result;
+ }, $content);
+ }
+
+ return $content;
+ }
+
+ /**
+ * 通用占位符清理:移除占位符内部的XML标签
+ * @param string $content
+ * @return string
+ */
+ private function generalPlaceholderCleanup(string $content): string
+ {
+ Log::info('阶段3:通用占位符清理');
+
+ // 匹配并清理占位符内部的XML标签
+ $generalPattern = '/\{\{([^}]*?)<[^>]*?>([^}]*?)\}\}/';
+
+ $iterations = 0;
+ $maxIterations = 10; // 防止无限循环
+
+ do {
+ $beforeLength = strlen($content);
+ $iterations++;
+
+ $content = preg_replace_callback($generalPattern, function($matches) {
+ // 移除XML标签,只保留纯文本
+ $content = $matches[1] . $matches[2];
+ $cleanContent = preg_replace('/<[^>]*?>/', '', $content);
+ $result = '{{' . $cleanContent . '}}';
+
+ Log::info('通用占位符清理', [
+ 'original' => substr($matches[0], 0, 50) . '...',
+ 'cleaned' => $result,
+ 'content' => $cleanContent
+ ]);
+
+ return $result;
+ }, $content);
+
+ $afterLength = strlen($content);
+
+ Log::info('通用清理迭代', [
+ 'iteration' => $iterations,
+ 'length_change' => $afterLength - $beforeLength,
+ 'has_more_matches' => preg_match($generalPattern, $content)
+ ]);
+
+ } while ($beforeLength !== $afterLength &&
+ preg_match($generalPattern, $content) &&
+ $iterations < $maxIterations);
+
+ return $content;
+ }
+
+ /**
+ * 最终占位符验证和修复
+ * @param string $content
+ * @return string
+ */
+ private function finalPlaceholderValidation(string $content): string
+ {
+ Log::info('阶段4:最终验证和修复');
+
+ // 查找所有可能的占位符模式
+ if (preg_match_all('/\{[^{}]*\}/', $content, $matches)) {
+ foreach ($matches[0] as $match) {
+ // 检查是否为不完整的占位符(只有一个大括号)
+ if (preg_match('/^\{[^{}]+\}$/', $match)) {
+ // 查看前后文是否有对应的大括号
+ $singleBracketPattern = '/' . preg_quote($match, '/') . '/';
+
+ // 尝试修复为双大括号
+ $possibleFix = '{' . $match . '}';
+
+ Log::info('发现可能的不完整占位符', [
+ 'found' => $match,
+ 'possible_fix' => $possibleFix
+ ]);
+ }
+ }
+ }
+
+ // 查找并统计最终的占位符数量
+ $finalPlaceholders = [];
+ if (preg_match_all('/\{\{([^}]+)\}\}/', $content, $matches)) {
+ $finalPlaceholders = $matches[1];
+ }
+
+ Log::info('最终占位符统计', [
+ 'count' => count($finalPlaceholders),
+ 'placeholders' => $finalPlaceholders
+ ]);
+
+ return $content;
+ }
+
/**
* 递归添加目录到ZIP文件
* @param \ZipArchive $zip ZIP文件对象
@@ -2134,4 +2371,436 @@ class DocumentTemplateService extends BaseAdminService
return null;
}
+
+ /**
+ * 处理Word文档中的URL图片,将URL转换为嵌入的图片附件
+ * @param string $documentPath Word文档路径
+ * @param array $fillValues 填充值数组(用于识别哪些是图片URL)
+ * @return bool
+ */
+ private function processUrlImagesInDocument(string $documentPath, array $fillValues): bool
+ {
+ try {
+ Log::info('开始处理Word文档中的URL图片', [
+ 'document_path' => $documentPath,
+ 'fill_values_count' => count($fillValues)
+ ]);
+
+ // 创建临时工作目录
+ $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'word_url_processing_' . uniqid();
+ if (!mkdir($tempDir, 0755, true)) {
+ throw new \Exception('无法创建临时工作目录');
+ }
+
+ try {
+ // 1. 解压Word文档到临时目录
+ $zip = new \ZipArchive();
+ $result = $zip->open($documentPath);
+
+ if ($result !== TRUE) {
+ throw new \Exception('无法打开Word文档,错误代码:' . $result);
+ }
+
+ if (!$zip->extractTo($tempDir)) {
+ $zip->close();
+ throw new \Exception('无法解压Word文档');
+ }
+ $zip->close();
+
+ // 2. 读取document.xml文件
+ $documentXmlPath = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . 'document.xml';
+ if (!file_exists($documentXmlPath)) {
+ throw new \Exception('Word文档缺少document.xml文件');
+ }
+
+ $xmlContent = file_get_contents($documentXmlPath);
+ if ($xmlContent === false) {
+ throw new \Exception('无法读取document.xml文件内容');
+ }
+
+ // 3. 查找并处理URL图片
+ $urlImageMap = $this->findUrlImagesInFillValues($fillValues);
+
+ if (empty($urlImageMap)) {
+ Log::info('未找到需要处理的URL图片');
+ return true;
+ }
+
+ // 4. 下载图片并创建关系文件
+ $imageRelations = $this->downloadAndCreateImageRelations($urlImageMap, $tempDir);
+
+ if (empty($imageRelations)) {
+ Log::warning('没有成功下载任何图片');
+ return true;
+ }
+
+ // 5. 更新document.xml中的URL为图片引用
+ $modifiedXmlContent = $this->replaceUrlsWithImageReferences($xmlContent, $imageRelations);
+
+ // 6. 更新关系文件
+ $this->updateDocumentRelations($tempDir, $imageRelations);
+
+ // 7. 写回修改后的document.xml
+ if (file_put_contents($documentXmlPath, $modifiedXmlContent) === false) {
+ throw new \Exception('无法写入修改后的document.xml');
+ }
+
+ // 8. 重新打包Word文档
+ $newZip = new \ZipArchive();
+ $result = $newZip->open($documentPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
+
+ if ($result !== TRUE) {
+ throw new \Exception('无法重新打开Word文件,错误代码:' . $result);
+ }
+
+ // 递归添加所有文件到ZIP
+ $this->addDirectoryToZip($newZip, $tempDir, '');
+
+ if (!$newZip->close()) {
+ throw new \Exception('无法保存修改后的Word文件');
+ }
+
+ Log::info('URL图片处理完成', [
+ 'processed_images' => count($imageRelations),
+ 'document_size' => filesize($documentPath)
+ ]);
+
+ return true;
+
+ } finally {
+ // 清理临时目录
+ $this->removeDirectory($tempDir);
+ }
+
+ } catch (\Exception $e) {
+ Log::error('处理Word文档URL图片失败', [
+ 'document_path' => $documentPath,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+ // 即使失败也不影响主流程
+ return false;
+ }
+ }
+
+ /**
+ * 从填充值中查找URL图片
+ * @param array $fillValues
+ * @return array
+ */
+ private function findUrlImagesInFillValues(array $fillValues): array
+ {
+ $urlImages = [];
+
+ foreach ($fillValues as $placeholder => $value) {
+ if (is_string($value)) {
+ // 检查是否为HTTP/HTTPS URL
+ if (preg_match('/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i', $value)) {
+ $urlImages[$placeholder] = $value;
+ Log::info('发现URL图片', [
+ 'placeholder' => $placeholder,
+ 'url' => $value
+ ]);
+ }
+ }
+ }
+
+ return $urlImages;
+ }
+
+ /**
+ * 下载图片并创建图片关系
+ * @param array $urlImageMap
+ * @param string $tempDir
+ * @return array
+ */
+ private function downloadAndCreateImageRelations(array $urlImageMap, string $tempDir): array
+ {
+ $imageRelations = [];
+ $relationId = 1000; // 起始关系ID,避免与现有关系冲突
+
+ // 确保media目录存在
+ $mediaDir = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . 'media';
+ if (!is_dir($mediaDir)) {
+ mkdir($mediaDir, 0755, true);
+ }
+
+ foreach ($urlImageMap as $placeholder => $url) {
+ try {
+ // 下载图片
+ $imageData = $this->downloadImageData($url);
+ if (!$imageData) {
+ continue;
+ }
+
+ // 确定文件扩展名
+ $pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
+ $extension = strtolower($pathInfo['extension'] ?? 'jpg');
+
+ // 验证图片数据
+ $tempImagePath = tempnam(sys_get_temp_dir(), 'img_validate_');
+ file_put_contents($tempImagePath, $imageData);
+ $imageInfo = getimagesize($tempImagePath);
+ unlink($tempImagePath);
+
+ if ($imageInfo === false) {
+ Log::warning('下载的文件不是有效图片', ['url' => $url]);
+ continue;
+ }
+
+ // 生成文件名和关系ID
+ $fileName = 'image' . $relationId . '.' . $extension;
+ $filePath = $mediaDir . DIRECTORY_SEPARATOR . $fileName;
+ $relationIdStr = 'rId' . $relationId;
+
+ // 保存图片文件
+ if (file_put_contents($filePath, $imageData) === false) {
+ Log::error('保存图片文件失败', ['file_path' => $filePath]);
+ continue;
+ }
+
+ // 记录图片关系信息
+ $imageRelations[$placeholder] = [
+ 'url' => $url,
+ 'relation_id' => $relationIdStr,
+ 'file_name' => $fileName,
+ 'file_path' => $filePath,
+ 'width' => $imageInfo[0],
+ 'height' => $imageInfo[1],
+ 'mime_type' => $imageInfo['mime']
+ ];
+
+ $relationId++;
+
+ Log::info('图片下载成功', [
+ 'placeholder' => $placeholder,
+ 'url' => $url,
+ 'file_name' => $fileName,
+ 'size' => $imageInfo[0] . 'x' . $imageInfo[1]
+ ]);
+
+ } catch (\Exception $e) {
+ Log::error('处理图片失败', [
+ 'placeholder' => $placeholder,
+ 'url' => $url,
+ 'error' => $e->getMessage()
+ ]);
+ }
+ }
+
+ return $imageRelations;
+ }
+
+ /**
+ * 下载图片数据
+ * @param string $url
+ * @return string|false
+ */
+ private function downloadImageData(string $url)
+ {
+ try {
+ $context = stream_context_create([
+ 'http' => [
+ 'timeout' => 30,
+ 'user_agent' => 'Mozilla/5.0 (compatible; Document Generator)',
+ 'follow_location' => true,
+ 'max_redirects' => 3
+ ]
+ ]);
+
+ return file_get_contents($url, false, $context);
+ } catch (\Exception $e) {
+ Log::error('下载图片数据失败:' . $e->getMessage(), ['url' => $url]);
+ return false;
+ }
+ }
+
+ /**
+ * 将XML中的URL替换为图片引用
+ * @param string $xmlContent
+ * @param array $imageRelations
+ * @return string
+ */
+ private function replaceUrlsWithImageReferences(string $xmlContent, array $imageRelations): string
+ {
+ $modifiedContent = $xmlContent;
+
+ foreach ($imageRelations as $placeholder => $relation) {
+ $url = $relation['url'];
+
+ // 构建图片引用XML
+ $imageXml = $this->generateImageXml($relation);
+
+ // 替换URL为图片引用
+ $modifiedContent = str_replace($url, $imageXml, $modifiedContent);
+
+ Log::info('URL替换为图片引用', [
+ 'placeholder' => $placeholder,
+ 'url' => $url,
+ 'relation_id' => $relation['relation_id']
+ ]);
+ }
+
+ return $modifiedContent;
+ }
+
+ /**
+ * 生成图片引用XML
+ * @param array $relation
+ * @return string
+ */
+ private function generateImageXml(array $relation): string
+ {
+ // 强制限制最大尺寸
+ $maxWidth = 40; // 最大宽度(像素)
+ $maxHeight = 30; // 最大高度(像素)
+
+ $originalWidth = $relation['width'];
+ $originalHeight = $relation['height'];
+
+ Log::info('图片原始尺寸', [
+ 'original_width' => $originalWidth,
+ 'original_height' => $originalHeight,
+ 'max_width' => $maxWidth,
+ 'max_height' => $maxHeight
+ ]);
+
+ // 计算缩放比例 - 如果图片超过最大尺寸就缩放,否则使用较小的固定尺寸
+ $displayWidth = $maxWidth;
+ $displayHeight = $maxHeight;
+
+ if ($originalWidth > 0 && $originalHeight > 0) {
+ // 计算保持宽高比的缩放
+ $widthRatio = $maxWidth / $originalWidth;
+ $heightRatio = $maxHeight / $originalHeight;
+ $ratio = min($widthRatio, $heightRatio); // 移除1的限制,允许缩放
+
+ $displayWidth = (int)($originalWidth * $ratio);
+ $displayHeight = (int)($originalHeight * $ratio);
+
+ // 确保不超过最大尺寸
+ $displayWidth = min($displayWidth, $maxWidth);
+ $displayHeight = min($displayHeight, $maxHeight);
+ }
+
+ Log::info('图片显示尺寸', [
+ 'display_width' => $displayWidth,
+ 'display_height' => $displayHeight,
+ 'ratio' => isset($ratio) ? $ratio : 'fixed'
+ ]);
+
+ // Word中的尺寸单位转换(像素转EMU)
+ $emuWidth = $displayWidth * 9525; // 1像素 = 9525 EMU
+ $emuHeight = $displayHeight * 9525;
+
+ $relationId = $relation['relation_id'];
+
+ return '
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+';
+ }
+
+ /**
+ * 更新文档关系文件
+ * @param string $tempDir
+ * @param array $imageRelations
+ * @return void
+ */
+ private function updateDocumentRelations(string $tempDir, array $imageRelations): void
+ {
+ try {
+ $relsPath = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . '_rels' . DIRECTORY_SEPARATOR . 'document.xml.rels';
+
+ // 如果关系文件不存在,创建基础关系文件
+ if (!file_exists($relsPath)) {
+ $relsDir = dirname($relsPath);
+ if (!is_dir($relsDir)) {
+ mkdir($relsDir, 0755, true);
+ }
+
+ $baseRelsContent = '
+
+';
+ file_put_contents($relsPath, $baseRelsContent);
+ }
+
+ // 读取现有关系文件
+ $relsContent = file_get_contents($relsPath);
+ if ($relsContent === false) {
+ throw new \Exception('无法读取关系文件');
+ }
+
+ // 解析XML
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $dom->preserveWhiteSpace = false;
+ $dom->formatOutput = true;
+
+ if (!$dom->loadXML($relsContent)) {
+ throw new \Exception('无法解析关系文件XML');
+ }
+
+ $relationshipsElement = $dom->documentElement;
+
+ // 添加图片关系
+ foreach ($imageRelations as $relation) {
+ $relationshipElement = $dom->createElement('Relationship');
+ $relationshipElement->setAttribute('Id', $relation['relation_id']);
+ $relationshipElement->setAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image');
+ $relationshipElement->setAttribute('Target', 'media/' . $relation['file_name']);
+
+ $relationshipsElement->appendChild($relationshipElement);
+ }
+
+ // 写回关系文件
+ if (file_put_contents($relsPath, $dom->saveXML()) === false) {
+ throw new \Exception('无法保存关系文件');
+ }
+
+ Log::info('文档关系文件更新成功', [
+ 'relations_added' => count($imageRelations),
+ 'rels_path' => $relsPath
+ ]);
+
+ } catch (\Exception $e) {
+ Log::error('更新文档关系文件失败', [
+ 'error' => $e->getMessage(),
+ 'temp_dir' => $tempDir
+ ]);
+ throw $e;
+ }
+ }
}
\ No newline at end of file
From e3b3237721802f4bdf7e0957919d891e4ea55b3f Mon Sep 17 00:00:00 2001
From: zeyan <258785420@qq.com>
Date: Wed, 20 Aug 2025 20:44:49 +0800
Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20bug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...本课程协议—月卡篮球(1).docx | Bin 95152 -> 94305 bytes
uniapp/common/config.js | 4 +-
.../coach/schedule/schedule_table.vue | 541 +++++++++---------
uniapp/pages-market/clue/add_clues.vue | 10 +-
uniapp/pages/common/home/index.vue | 12 +-
5 files changed, 287 insertions(+), 280 deletions(-)
diff --git a/doc/副本课程协议—月卡篮球(1).docx b/doc/副本课程协议—月卡篮球(1).docx
index 6d1ee5bbf0cf039b99c343431f210bb855102583..525d3291ce369fcad45dd21f2da600c771ea3cfa 100644
GIT binary patch
delta 19467
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
GDE?gNAK;y$>$yEC{u`~6#cvAY+kRCUrP
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
diff --git a/uniapp/common/config.js b/uniapp/common/config.js
index c98a2253..2459f37b 100644
--- a/uniapp/common/config.js
+++ b/uniapp/common/config.js
@@ -1,6 +1,6 @@
// 环境变量配置
-const env = 'development'
-// const env = 'prod'
+// const env = 'development'
+const env = 'prod'
const isMockEnabled = false // 默认禁用Mock优先模式,仅作为回退
const isDebug = false // 默认启用调试模式
const devurl = 'http://localhost:20080/api'
diff --git a/uniapp/pages-coach/coach/schedule/schedule_table.vue b/uniapp/pages-coach/coach/schedule/schedule_table.vue
index f4662dbd..5aaac5a6 100644
--- a/uniapp/pages-coach/coach/schedule/schedule_table.vue
+++ b/uniapp/pages-coach/coach/schedule/schedule_table.vue
@@ -56,111 +56,45 @@
-
-
-
-
-
-
-
-
-
-
-
- {{ timeSlot.time }}
-
-
-
-
-
-
- {{ teacher.name }}
-
-
-
-
-
-
- {{ venue.name }}
-
-
-
-
-
-
- {{ cls.name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -554,11 +511,10 @@ export default {
selectedClasses: [],
// 滚动相关
- scrollTop: 0,
scrollTimer: null, // 滚动防抖定时器
- // 表格配置
- tableWidth: 1500, // 表格总宽度,确保7天都能显示 (7*180+120=1380rpx)
+ // 表格配置 - 统一滚动后不包含左侧列宽度
+ tableWidth: 1260, // 7天内容宽度 (7*180=1260rpx),左侧120rpx在容器内部
// 时间段配置(动态生成,支持场地时间限制)
timeSlots: [],
@@ -591,6 +547,12 @@ export default {
selectedScheduleId: null,
showScheduleDetail: false,
+ // 平台识别
+ isH5: false,
+
+ // 滚动视图高度
+ scrollViewHeight: 0,
+
// 筛选参数
filterParams: {
start_date: '',
@@ -644,12 +606,20 @@ export default {
},
mounted() {
+ // 检测平台
+ // #ifdef H5
+ this.isH5 = true
+ // #endif
+
this.initCurrentWeek()
this.initTimeSlots()
// 初始化响应式布局
this.handleResize()
+ // 计算scroll-view高度
+ this.calculateScrollViewHeight()
+
// 先加载筛选选项,然后加载课程安排列表
this.loadFilterOptions().then(() => {
this.loadScheduleList()
@@ -679,6 +649,72 @@ export default {
},
methods: {
+ // 时间标准化函数 - 统一时间格式为 HH:mm
+ normalizeTime(timeStr) {
+ if (!timeStr) return '';
+
+ // 移除空格并转换为字符串
+ const cleanTime = String(timeStr).trim();
+
+ // 如果已经是 HH:mm 格式,直接返回
+ if (/^\d{2}:\d{2}$/.test(cleanTime)) {
+ return cleanTime;
+ }
+
+ // 如果是 H:mm 格式,补零
+ if (/^\d{1}:\d{2}$/.test(cleanTime)) {
+ return '0' + cleanTime;
+ }
+
+ // 如果是 HH:m 格式,补零
+ if (/^\d{2}:\d{1}$/.test(cleanTime)) {
+ return cleanTime.slice(0, 3) + '0' + cleanTime.slice(3);
+ }
+
+ // 如果是 H:m 格式,都补零
+ if (/^\d{1}:\d{1}$/.test(cleanTime)) {
+ const [hour, minute] = cleanTime.split(':');
+ return `0${hour}:0${minute}`;
+ }
+
+ // 其他格式尝试解析
+ try {
+ const [hour, minute] = cleanTime.split(':');
+ const h = parseInt(hour).toString().padStart(2, '0');
+ const m = parseInt(minute || 0).toString().padStart(2, '0');
+ return `${h}:${m}`;
+ } catch (e) {
+ console.warn('时间格式解析失败:', timeStr);
+ return cleanTime;
+ }
+ },
+
+ // 时间匹配函数 - 支持时间段内匹配
+ isTimeInSlot(courseTime, slotTime) {
+ const normalizedCourseTime = this.normalizeTime(courseTime);
+ const normalizedSlotTime = this.normalizeTime(slotTime);
+
+ if (!normalizedCourseTime || !normalizedSlotTime) return false;
+
+ // 精确匹配
+ if (normalizedCourseTime === normalizedSlotTime) return true;
+
+ // 查找时间段匹配 - 课程开始时间在这个时间段内
+ const slotIndex = this.timeSlots.findIndex(slot =>
+ this.normalizeTime(slot.time) === normalizedSlotTime
+ );
+
+ if (slotIndex >= 0 && slotIndex < this.timeSlots.length - 1) {
+ const currentSlotTime = this.normalizeTime(this.timeSlots[slotIndex].time);
+ const nextSlotTime = this.normalizeTime(this.timeSlots[slotIndex + 1].time);
+
+ // 检查课程时间是否在当前时间段内(包含开始时间,不包含结束时间)
+ return normalizedCourseTime >= currentSlotTime && normalizedCourseTime < nextSlotTime;
+ }
+
+ return false;
+ },
+
// 初始化当前周
initCurrentWeek() {
const today = new Date()
@@ -857,8 +893,8 @@ export default {
getCoursesByTimeAndDate(time, date) {
const matchedCourses = this.courses.filter(course => {
if (course.date !== date) return false
- // 只在课程开始时间显示课程,不在后续时间段重复显示
- return course.time === time
+ // 使用改进的时间匹配算法
+ return this.isTimeInSlot(course.time, time)
})
return matchedCourses
},
@@ -933,8 +969,7 @@ export default {
this.filterParams.venue_id = '';
this.filterParams.class_id = '';
- // 如果切换了模式,重置滚动位置
- this.scrollTop = 0;
+ // 模式切换完成
// 切换模式后重新加载课程安排
this.loadScheduleList();
@@ -956,9 +991,8 @@ export default {
this.applyFilters()
this.closeFilterModal()
- // 重新加载数据后,重置滚动位置
+ // 重新加载数据
await this.loadScheduleList()
- this.scrollTop = 0
// 如果筛选改变了时间段,需要重新生成时间列
if (this.selectedTimeRange !== '' || this.selectedVenueId !== null) {
@@ -1146,34 +1180,35 @@ export default {
if (res.code === 1) {
// 转换数据格式
- this.courses = res.data.list.map(item => ({
- id: item.id,
- date: item.course_date,
- time: item.time_info?.start_time || item.time_slot?.split('-')[0],
- courseName: item.course_name || '未命名课程',
- students: `已报名${item.enrolled_count || 0}人`,
- teacher: item.coach_name || '待分配',
- teacher_id: item.coach_id, // 保存教练ID
- status: item.status_text || '待定',
- type: this.getCourseType(item),
- venue: item.venue_name || '待分配',
- venue_id: item.venue_id, // 保存场地ID
- campus_name: item.campus_name || '', // 添加校区名称
- class_id: item.class_id, // 保存班级ID
- class_name: item.class_name || '', // 添加班级名称
- duration: item.time_info?.duration || 60,
- time_slot: item.time_slot,
- raw: item, // 保存原始数据
- }))
+ this.courses = res.data.list.map(item => {
+ // 提取并标准化时间
+ const rawTime = item.time_info?.start_time || item.time_slot?.split('-')[0] || '';
+ const normalizedTime = this.normalizeTime(rawTime);
+
+ return {
+ id: item.id,
+ date: item.course_date,
+ time: normalizedTime, // 使用标准化后的时间
+ courseName: item.course_name || '未命名课程',
+ students: `已报名${item.enrolled_count || 0}人`,
+ teacher: item.coach_name || '待分配',
+ teacher_id: item.coach_id, // 保存教练ID
+ status: item.status_text || '待定',
+ type: this.getCourseType(item),
+ venue: item.venue_name || '待分配',
+ venue_id: item.venue_id, // 保存场地ID
+ campus_name: item.campus_name || '', // 添加校区名称
+ class_id: item.class_id, // 保存班级ID
+ class_name: item.class_name || '', // 添加班级名称
+ duration: item.time_info?.duration || 60,
+ time_slot: item.time_slot,
+ raw: item, // 保存原始数据
+ }
+ })
// 根据当前视图模式动态更新左侧列数据
this.updateLeftColumnData();
- // 同步左右高度
- this.$nextTick(() => {
- this.syncRowHeights();
- });
-
} else {
uni.showToast({
title: res.msg || '加载课程安排列表失败',
@@ -1266,61 +1301,18 @@ export default {
},
- // 同步左右行高度
- syncRowHeights() {
- this.$nextTick(() => {
- try {
- let itemCount = 0;
-
- // 根据当前筛选模式确定行数
- if (this.activeFilter === 'time' || this.activeFilter === '') {
- itemCount = this.timeSlots.length;
- } else if (this.activeFilter === 'teacher') {
- itemCount = this.teacherOptions.length;
- } else if (this.activeFilter === 'classroom') {
- itemCount = this.venues.length;
- } else if (this.activeFilter === 'class') {
- itemCount = this.classOptions.length;
- }
-
- // 同步每一行的高度
- for (let i = 0; i < itemCount; i++) {
- const scheduleRow = this.$refs[`scheduleRow_${i}`];
- const frozenCell = this.$refs[`frozenCell_${i}`];
-
- if (scheduleRow && scheduleRow[0] && frozenCell && frozenCell[0]) {
- // 获取右侧行的实际高度
- const rightRowHeight = scheduleRow[0].$el ?
- scheduleRow[0].$el.offsetHeight :
- scheduleRow[0].offsetHeight;
-
- // 设置左侧冻结单元格的高度
- if (frozenCell[0].$el) {
- frozenCell[0].$el.style.minHeight = rightRowHeight + 'px';
- } else if (frozenCell[0].style) {
- frozenCell[0].style.minHeight = rightRowHeight + 'px';
- }
- }
- }
- } catch (error) {
- console.warn('高度同步失败:', error);
- }
- });
- },
-
- // 滚动事件处理函数 - 优化垂直滚动同步
+ // 滚动事件处理函数 - 简化版本
onScroll(e) {
- // 使用防抖优化滚动同步性能
+ // 统一滚动区域后不需要同步滚动位置
+ // 只保留防抖逻辑以优化性能
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
}
this.scrollTimer = setTimeout(() => {
- // 只需要同步垂直滚动位置给左侧时间列
- if (e.detail.scrollTop !== undefined && e.detail.scrollTop !== this.scrollTop) {
- this.scrollTop = e.detail.scrollTop
- }
- }, 16) // 约60fps的更新频率
+ // 可以在这里添加其他滚动相关的处理逻辑
+ // 例如懒加载、滚动到顶部/底部的处理等
+ }, 16)
},
// 单元格点击
@@ -1523,6 +1515,36 @@ export default {
// 暂时只显示提示信息,具体API调用可以后续实现
},
+ // 计算scroll-view高度
+ calculateScrollViewHeight() {
+ uni.getSystemInfo({
+ success: (res) => {
+ // 获取屏幕高度
+ let screenHeight = res.screenHeight || res.windowHeight
+
+ // 计算其他元素占用的高度
+ // 筛选区域 + 日期导航 + 统计信息 + 状态栏等
+ let otherHeight = 0
+
+ // #ifdef H5
+ otherHeight = 200 // H5端大约200px
+ // #endif
+
+ // #ifdef MP-WEIXIN
+ otherHeight = 160 // 小程序端大约160px (rpx转换)
+ // #endif
+
+ // 设置scroll-view高度
+ this.scrollViewHeight = screenHeight - otherHeight
+
+ // 最小高度限制
+ if (this.scrollViewHeight < 300) {
+ this.scrollViewHeight = 300
+ }
+ }
+ })
+ },
+
// 响应式调整
handleResize() {
// 根据窗口宽度调整表格尺寸
@@ -1545,12 +1567,15 @@ export default {
// #endif
if (width <= 375) {
- this.tableWidth = 1220 // 7*160+100=1220rpx
+ this.tableWidth = 1120 // 7*160=1120rpx (小屏幕每列160rpx)
} else if (width <= 768) {
- this.tableWidth = 1400
+ this.tableWidth = 1260 // 7*180=1260rpx
} else {
- this.tableWidth = 1500
+ this.tableWidth = 1260 // 7*180=1260rpx
}
+
+ // 重新计算scroll-view高度
+ this.calculateScrollViewHeight()
},
// 初始化模拟数据
@@ -1666,24 +1691,32 @@ export default {
.schedule-main {
flex: 1;
position: relative;
- display: flex;
overflow: hidden;
+ height: 0; /* 配合flex: 1 确保高度计算正确 */
}
-// 左侧冻结列
-.frozen-column {
- width: 120rpx;
- position: relative;
- z-index: 3;
- background-color: #292929;
+// 统一滚动区域内部容器
+.schedule-container-inner {
display: flex;
flex-direction: column;
- box-shadow: 4rpx 0 8rpx rgba(0, 0, 0, 0.1);
- flex-shrink: 0;
+ min-width: 1380rpx; /* 左列120rpx + 7天 * 180rpx = 1380rpx */
+}
+
+// 表头行
+.schedule-header-row {
+ display: flex;
+ background: #434544;
+ border-bottom: 2px solid #29d3b4;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
+// 左上角标题单元格
.time-header-cell {
width: 120rpx;
+ min-width: 120rpx;
min-height: 120rpx;
padding: 20rpx 10rpx;
color: #29d3b4;
@@ -1691,75 +1724,13 @@ export default {
font-weight: 500;
text-align: center;
border-right: 1px solid #555;
- border-bottom: 2px solid #29d3b4;
background-color: #434544;
display: flex;
align-items: center;
justify-content: center;
word-wrap: break-word;
overflow-wrap: break-word;
-}
-
-.frozen-content-scroll {
- flex: 1;
- overflow: hidden;
- /* 优化滚动性能 */
- -webkit-overflow-scrolling: touch;
- scroll-behavior: auto;
- overscroll-behavior: none;
-}
-
-.frozen-content {
- width: 100%;
-}
-
-.frozen-cell {
- width: 120rpx;
- min-height: 120rpx;
- padding: 20rpx 10rpx;
- color: #999;
- font-size: 24rpx;
- text-align: center;
- border-right: 1px solid #434544;
- border-bottom: 1px solid #434544;
- background: #3a3a3a;
- display: flex;
- align-items: center;
- justify-content: center;
- word-wrap: break-word;
- overflow-wrap: break-word;
-
- &.time-unavailable {
- background: #2a2a2a;
- color: #555;
- opacity: 0.5;
- }
-}
-
-// 右侧内容区域
-.schedule-content-area {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-// 合并滚动区域内部容器
-.schedule-container-inner {
- display: flex;
- flex-direction: column;
- min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */
-}
-
-.date-header-container {
- display: flex;
- background: #434544;
- border-bottom: 2px solid #29d3b4;
- position: sticky;
- top: 0;
- z-index: 10;
- /* 确保完全覆盖下方内容 */
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
+ flex-shrink: 0;
}
.date-header-cell {
@@ -1796,16 +1767,23 @@ export default {
// 内容滚动区域
.schedule-scroll {
flex: 1;
- overflow: scroll;
+ height: 100%;
+ width: 100%;
+ min-height: 0; /* 关键:允许flex子项收缩 */
/* 优化滚动性能 */
-webkit-overflow-scrolling: touch;
scroll-behavior: auto;
overscroll-behavior: none;
+
+ /* 小程序端专用优化 */
+ // #ifdef MP-WEIXIN
+ scroll-behavior: smooth;
+ // #endif
}
.schedule-grid {
width: 100%;
- min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */
+ min-width: 1380rpx; /* 左列120rpx + 7天 * 180rpx = 1380rpx */
}
// 行布局
@@ -1815,6 +1793,32 @@ export default {
border-bottom: 1px solid #434544;
}
+// 左侧列单元格 (时间/教练/教室/班级)
+.left-column-cell {
+ width: 120rpx;
+ min-width: 120rpx;
+ min-height: 120rpx;
+ padding: 20rpx 10rpx;
+ color: #999;
+ font-size: 24rpx;
+ text-align: center;
+ border-right: 1px solid #434544;
+ border-bottom: 1px solid #434544;
+ background: #3a3a3a;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ flex-shrink: 0;
+
+ &.time-unavailable {
+ background: #2a2a2a;
+ color: #555;
+ opacity: 0.5;
+ }
+}
+
.course-cell {
width: 180rpx;
min-width: 180rpx;
@@ -1849,12 +1853,15 @@ export default {
// 响应式适配
@media screen and (max-width: 375px) {
- .time-column-fixed {
+ .left-column-cell {
width: 100rpx;
+ min-width: 100rpx;
+ font-size: 22rpx;
}
- .time-header-cell, .time-cell {
+ .time-header-cell {
width: 100rpx;
+ min-width: 100rpx;
font-size: 22rpx;
}
diff --git a/uniapp/pages-market/clue/add_clues.vue b/uniapp/pages-market/clue/add_clues.vue
index 7f99c75b..dce862aa 100644
--- a/uniapp/pages-market/clue/add_clues.vue
+++ b/uniapp/pages-market/clue/add_clues.vue
@@ -353,7 +353,7 @@
+ @click="openDate(`promised_visit_time`)">
{{ (formData.promised_visit_time) ? formData.promised_visit_time : '点击选择' }}
{
- this.datetime_picker_show = true
+ this.date_picker_show = true
})
},
//选择跟进时间
diff --git a/uniapp/pages/common/home/index.vue b/uniapp/pages/common/home/index.vue
index a65f0292..e66df791 100644
--- a/uniapp/pages/common/home/index.vue
+++ b/uniapp/pages/common/home/index.vue
@@ -106,12 +106,12 @@
path: '/pages/common/dashboard/webview',
params: { type: 'campus_data' }
},
- {
- title: '报销管理',
- icon: 'wallet-filled',
- path: '/pages-market/reimbursement/list',
- params: { type: 'reimbursement' }
- }
+ // {
+ // title: '报销管理',
+ // icon: 'wallet-filled',
+ // path: '/pages-market/reimbursement/list',
+ // params: { type: 'reimbursement' }
+ // }
]
}
},