Showing
6 changed files
with
191 additions
and
90 deletions
| ... | @@ -125,6 +125,7 @@ class VideoTempController extends AdminController | ... | @@ -125,6 +125,7 @@ class VideoTempController extends AdminController |
| 125 | $form->radio('draw', '文字效果') | 125 | $form->radio('draw', '文字效果') |
| 126 | ->options(['fade'=>'淡入淡出', 'fix'=>'固定显示'])->default('fade') | 126 | ->options(['fade'=>'淡入淡出', 'fix'=>'固定显示'])->default('fade') |
| 127 | ->when('fade',function (Form\NestedForm $form){ | 127 | ->when('fade',function (Form\NestedForm $form){ |
| 128 | + $form->number('fade_time', 'fade时间')->default(1500); | ||
| 128 | $form->selectTable('font_file','字体') | 129 | $form->selectTable('font_file','字体') |
| 129 | ->title('字体选择') | 130 | ->title('字体选择') |
| 130 | ->from(FontTable::make()) | 131 | ->from(FontTable::make()) | ... | ... |
| ... | @@ -66,6 +66,12 @@ class DevFFmpeg extends Command | ... | @@ -66,6 +66,12 @@ class DevFFmpeg extends Command |
| 66 | */ | 66 | */ |
| 67 | public function handle() | 67 | public function handle() |
| 68 | { | 68 | { |
| 69 | +// $json = shell_exec(env('FFPROBE_CMD') . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg(storage_path('app/public/uploads/131/309/K4LTgHuDhcmDr3MZBDkd0vyMUmRbfBxrFbU0CoNs.png')). ' 2>&1'); | ||
| 70 | +// $arr = json_decode($json,true); | ||
| 71 | +// dd($arr); | ||
| 72 | +// dd(AdminMakeVideo::query()->find(1)->poem2()); | ||
| 73 | + dd(AdminMakeVideo::query()->find(1)->poem2->verses->toArray()); | ||
| 74 | + return 0; | ||
| 69 | dd(AdminMakeVideo::query()->find(33)->temp->components->toArray()); | 75 | dd(AdminMakeVideo::query()->find(33)->temp->components->toArray()); |
| 70 | AdminMakeImmerse::dispatch(AdminMakeVideo::query()->find(33)->temp->components); | 76 | AdminMakeImmerse::dispatch(AdminMakeVideo::query()->find(33)->temp->components); |
| 71 | 77 | ... | ... |
| ... | @@ -51,52 +51,7 @@ class MakeVideo implements ShouldQueue | ... | @@ -51,52 +51,7 @@ class MakeVideo implements ShouldQueue |
| 51 | // 素材准备 | 51 | // 素材准备 |
| 52 | $drawtext = $this->getTextContentString(); | 52 | $drawtext = $this->getTextContentString(); |
| 53 | 53 | ||
| 54 | - if ($media_info['format']['nb_streams'] >= 2) { | ||
| 55 | - /** 音频视频轨都有 */ | ||
| 56 | - if ($is_bgm) { | ||
| 57 | - // 有背景音 融合 | ||
| 58 | - $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); | ||
| 59 | - $cmd = $this->ffmpeg . | ||
| 60 | - ' -y -i ' . escapeshellarg($file) . | ||
| 61 | - ' -y -i ' . escapeshellarg($bgm) . | ||
| 62 | - ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . | ||
| 63 | - '-ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
| 64 | - if (!$this->execmd($cmd)) return; | ||
| 65 | 54 | ||
| 66 | - $audio_input = ' -i ' . escapeshellarg($audio); | ||
| 67 | - $audio_filter = '2:a'; | ||
| 68 | - } else { | ||
| 69 | - // 没有背景音 | ||
| 70 | - $audio_input = ''; | ||
| 71 | - $audio_filter = '0:a'; | ||
| 72 | - } | ||
| 73 | - } elseif ($media_info['format']['nb_streams'] == 1) { | ||
| 74 | - /** 只有视频轨 */ | ||
| 75 | - // 生成一段无声音频 | ||
| 76 | - $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); | ||
| 77 | - $cmd = $this->ffmpeg . | ||
| 78 | - ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) . | ||
| 79 | - ' -ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
| 80 | - if (!$this->execmd($cmd)) return; | ||
| 81 | - | ||
| 82 | - if ($is_bgm) { | ||
| 83 | - // 有背景音 融合 | ||
| 84 | - $audio_empty = $audio; | ||
| 85 | - $audio = $this->getAbsolutePath($this->getTempPath('.mp3')); | ||
| 86 | - $cmd = $this->ffmpeg . | ||
| 87 | - ' -y -i ' . escapeshellarg($audio_empty) . | ||
| 88 | - ' -y -i ' . escapeshellarg($bgm) . | ||
| 89 | - ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . | ||
| 90 | - '-ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
| 91 | - if (!$this->execmd($cmd)) return; | ||
| 92 | - } | ||
| 93 | - $audio_input = ' -i ' . escapeshellarg($audio); | ||
| 94 | - $audio_filter = '2:a'; | ||
| 95 | - } else { | ||
| 96 | - /** 音频视频轨都没有 */ | ||
| 97 | - Log::channel('daily')->error('视频没有video track, url:' . $file); | ||
| 98 | - return; | ||
| 99 | - } | ||
| 100 | 55 | ||
| 101 | $thumbnail = $this->getTempPath('.jpg','thumbnail'); | 56 | $thumbnail = $this->getTempPath('.jpg','thumbnail'); |
| 102 | if ($adminMakeVideo->thumbnail == 2){ | 57 | if ($adminMakeVideo->thumbnail == 2){ |
| ... | @@ -174,9 +129,57 @@ class MakeVideo implements ShouldQueue | ... | @@ -174,9 +129,57 @@ class MakeVideo implements ShouldQueue |
| 174 | // 准备素材 | 129 | // 准备素材 |
| 175 | 130 | ||
| 176 | // 组装文字参数 | 131 | // 组装文字参数 |
| 132 | + $drawtext = $this->getTextContentString(); | ||
| 177 | 133 | ||
| 178 | // 合成视频 | 134 | // 合成视频 |
| 179 | 135 | ||
| 136 | + if ($this->media_info['format']['nb_streams'] >= 2) { | ||
| 137 | + /** 音频视频轨都有 */ | ||
| 138 | + if ($is_bgm) { | ||
| 139 | + // 有背景音 融合 | ||
| 140 | + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); | ||
| 141 | + $cmd = $this->ffmpeg . | ||
| 142 | + ' -y -i ' . escapeshellarg($file) . | ||
| 143 | + ' -y -i ' . escapeshellarg($bgm) . | ||
| 144 | + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . | ||
| 145 | + '-ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
| 146 | + if (!$this->execmd($cmd)) return; | ||
| 147 | + | ||
| 148 | + $audio_input = ' -i ' . escapeshellarg($audio); | ||
| 149 | + $audio_filter = '2:a'; | ||
| 150 | + } else { | ||
| 151 | + // 没有背景音 | ||
| 152 | + $audio_input = ''; | ||
| 153 | + $audio_filter = '0:a'; | ||
| 154 | + } | ||
| 155 | + } elseif ($media_info['format']['nb_streams'] == 1) { | ||
| 156 | + /** 只有视频轨 */ | ||
| 157 | + // 生成一段无声音频 | ||
| 158 | + $audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio')); | ||
| 159 | + $cmd = $this->ffmpeg . | ||
| 160 | + ' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) . | ||
| 161 | + ' -ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
| 162 | + if (!$this->execmd($cmd)) return; | ||
| 163 | + | ||
| 164 | + if ($is_bgm) { | ||
| 165 | + // 有背景音 融合 | ||
| 166 | + $audio_empty = $audio; | ||
| 167 | + $audio = $this->getAbsolutePath($this->getTempPath('.mp3')); | ||
| 168 | + $cmd = $this->ffmpeg . | ||
| 169 | + ' -y -i ' . escapeshellarg($audio_empty) . | ||
| 170 | + ' -y -i ' . escapeshellarg($bgm) . | ||
| 171 | + ' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' . | ||
| 172 | + '-ar 48000 -ab 64k ' . escapeshellarg($audio); | ||
| 173 | + if (!$this->execmd($cmd)) return; | ||
| 174 | + } | ||
| 175 | + $audio_input = ' -i ' . escapeshellarg($audio); | ||
| 176 | + $audio_filter = '2:a'; | ||
| 177 | + } else { | ||
| 178 | + /** 音频视频轨都没有 */ | ||
| 179 | + Log::channel('daily')->error('视频没有video track, url:' . $file); | ||
| 180 | + return; | ||
| 181 | + } | ||
| 182 | + | ||
| 180 | // 制作封面图 | 183 | // 制作封面图 |
| 181 | 184 | ||
| 182 | // 分析视频 入库 | 185 | // 分析视频 入库 |
| ... | @@ -194,7 +197,7 @@ class MakeVideo implements ShouldQueue | ... | @@ -194,7 +197,7 @@ class MakeVideo implements ShouldQueue |
| 194 | if ($this->media_info) return $this->media_info; | 197 | if ($this->media_info) return $this->media_info; |
| 195 | 198 | ||
| 196 | $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file); | 199 | $cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file); |
| 197 | - $output = $this->execmd($cmd); | 200 | + $output = $this->execCmd($cmd); |
| 198 | $data = json_decode($output, true); | 201 | $data = json_decode($output, true); |
| 199 | if (json_last_error() === JSON_ERROR_UTF8) { | 202 | if (json_last_error() === JSON_ERROR_UTF8) { |
| 200 | $output = mb_convert_encoding($output, "UTF-8"); | 203 | $output = mb_convert_encoding($output, "UTF-8"); |
| ... | @@ -204,57 +207,140 @@ class MakeVideo implements ShouldQueue | ... | @@ -204,57 +207,140 @@ class MakeVideo implements ShouldQueue |
| 204 | return $data; | 207 | return $data; |
| 205 | } | 208 | } |
| 206 | 209 | ||
| 207 | - public function execmd($cmd, $update_progress = false) { | 210 | + public function execCmd($cmd) |
| 208 | - echo $cmd . "\n". "\n". "\n"; | 211 | + { |
| 209 | - $descriptorspec = array( | 212 | + return shell_exec("{$cmd} 2>&1"); |
| 210 | - 1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据 | 213 | + } |
| 211 | - ); | 214 | + |
| 212 | - $process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes); | 215 | + public function getTextContentString() |
| 213 | - if (is_resource($process)) { | ||
| 214 | - $error0 = ''; | ||
| 215 | - $error1 = ''; | ||
| 216 | - $stdout = ''; | ||
| 217 | - while (!feof($pipes[1])) { | ||
| 218 | - $line = fgets($pipes[1], 150); | ||
| 219 | - $stdout .= $line; | ||
| 220 | - if ($line) { | ||
| 221 | - //记录错误 | ||
| 222 | - $error0 = $error1; | ||
| 223 | - $error1 = $line; | ||
| 224 | - if ($update_progress && | ||
| 225 | - false !== strpos($line, 'size=') && | ||
| 226 | - false !== strpos($line, 'time=') && | ||
| 227 | - false !== strpos($line, 'bitrate=')) | ||
| 228 | { | 216 | { |
| 229 | - //记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s | 217 | + $components = $this->adminMakeVideo->temp->components; |
| 230 | - $line = explode(' ', $line); | 218 | + |
| 231 | - $time = null; | 219 | + $drawtext = ''; |
| 232 | - foreach ($line as $item) { | 220 | + foreach ($components as $component) { |
| 233 | - $item = explode('=', $item); | 221 | + $text_color = $component->text_color ?? 'white'; |
| 234 | - if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') { | 222 | + $text_bg_color = $component->text_bg_color ?? '0xd0cdcc'; |
| 235 | - $time = $item[1]; | 223 | + $opacity = $component->opacity ? $component->opacity / 100 : 0.5; |
| 224 | + $font_file = $this->getAbsolutePath($component->font_file); | ||
| 225 | + $text_bg_box = $component->text_bg_box ?? 0; | ||
| 226 | + | ||
| 227 | + // 文字淡入淡出模式 | ||
| 228 | + if ($component->draw == 'fade'){ | ||
| 229 | + $contents = []; // | ||
| 230 | + switch ($component->name){ | ||
| 231 | + case 'one_poem': | ||
| 232 | + foreach ($this->adminMakeVideo->poem2->verses as $item) { | ||
| 233 | + if ($item->stanza != '') $contents[] = $item->stanza; | ||
| 234 | + } | ||
| 236 | break; | 235 | break; |
| 236 | + case 'one_poem_with_annotate': | ||
| 237 | + foreach ($this->adminMakeVideo->poem2->verses as $item) { | ||
| 238 | + if ($item->stanza != '') $contents[] = $item->stanza; | ||
| 239 | + if ($item->annotate != '') $contents[] = $item->annotate; | ||
| 237 | } | 240 | } |
| 241 | + break; | ||
| 242 | + case 'weather': | ||
| 243 | + $contents[] = $this->adminMakeVideo->weather; | ||
| 244 | + break; | ||
| 245 | + case 'date': | ||
| 246 | + $contents[] = Carbon::now()->format('Y年m月d日H时'); | ||
| 247 | + break; | ||
| 248 | + case 'feel': | ||
| 249 | + $contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。'; | ||
| 250 | + break; | ||
| 238 | } | 251 | } |
| 252 | + | ||
| 253 | + $FID = $FOD = floatval($component->fade_time / 1000); | ||
| 254 | + $round = round($this->media_info['format']['duration'] / count($contents),1); | ||
| 255 | + if ($round < 1) $round = 1; | ||
| 256 | + $sub_text = ''; | ||
| 257 | + foreach ($contents as $key => $content){ | ||
| 258 | + $DS = $key * $round; | ||
| 259 | + $DE = $DS + $round; | ||
| 260 | + $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text')); | ||
| 261 | + file_put_contents($text_file, $content); | ||
| 262 | + $sub_text .= 'drawtext="'. | ||
| 263 | + 'fontfile=' . escapeshellarg($font_file) . ':' . | ||
| 264 | + 'textfile=' . escapeshellarg($text_file) . ':' . | ||
| 265 | + 'fontsize=' . $this->calcFontSize($component->font_size) . ':' . | ||
| 266 | + 'fontcolor_expr=' . escapeshellarg($text_color . '%{eif\\\\: clip(255*(1*between(t\\, ' . $DS . ' + ' . $FID . '\\, ' . $DE . ' - ' . $FOD . ') + ((t - ' . $DS . ')/' . $FID . ')*between(t\\, ' . $DS . '\\, ' . $DS . ' + ' . $FID . ') + (-(t - ' . $DE . ')/' . $FOD . ')*between(t\\, ' . $DE . ' - ' . $FOD . '\\, ' . $DE . '))\\, 0\\, 255) \\\\: x\\\\: 2 }') . ':' . | ||
| 267 | + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . | ||
| 268 | + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . | ||
| 269 | + '", '; | ||
| 239 | } | 270 | } |
| 271 | + | ||
| 272 | + $drawtext .= $sub_text; | ||
| 240 | } | 273 | } |
| 274 | + | ||
| 275 | + // 文字固定模式 | ||
| 276 | + if ($component->draw == 'fix'){ | ||
| 277 | + $contents = []; // | ||
| 278 | + switch ($component->name){ | ||
| 279 | + case 'one_poem_with_annotate': | ||
| 280 | + case 'one_poem': | ||
| 281 | + $stanzas = ''; | ||
| 282 | + foreach ($this->adminMakeVideo->poem2->verses as $item) { | ||
| 283 | + if ($item->stanza != '') $stanzas = $item->stanza . "\n"; | ||
| 241 | } | 284 | } |
| 242 | - // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。 | 285 | + $contents[] = $stanzas; |
| 243 | - fclose($pipes[1]); | 286 | + break; |
| 244 | - $exitedcode = proc_close($process); | 287 | + case 'weather': |
| 245 | - if ($exitedcode === 0) { | 288 | + $contents[] = $this->adminMakeVideo->weather; |
| 246 | - return $stdout; | 289 | + break; |
| 247 | - } else { | 290 | + case 'date': |
| 248 | - $error = trim($error0,"\n") . ' '. trim($error1,"\n"); | 291 | + $contents[] = Carbon::now()->format('Y年m月d日H时'); |
| 249 | - // LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__); | 292 | + break; |
| 250 | - // ErrorUtil::triggerErrorMsg($error, $exitedcode); | 293 | + case 'feel': |
| 251 | - Log::error("cmd:{$cmd}"); | 294 | + $contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。'; |
| 252 | - Log::error($error); | 295 | + break; |
| 253 | - Log::error("stdout:{$stdout}"); | ||
| 254 | } | 296 | } |
| 255 | - } else { | 297 | + $sub_text = ''; |
| 256 | - // return ErrorUtil::triggerErrorMsg('proc_open error'); | 298 | + foreach ($contents as $key => $content){ |
| 257 | - Log::error('proc_open error'); | 299 | + $text_file = $this->getAbsolutePath($this->getTempPath('.txt','text')); |
| 300 | + file_put_contents($text_file, $content); | ||
| 301 | + $sub_text .= 'drawtext="'. | ||
| 302 | + 'fontfile=' . escapeshellarg($font_file) . ':' . | ||
| 303 | + 'textfile=' . escapeshellarg($text_file) . ':' . | ||
| 304 | + 'fontsize=' . $this->calcFontSize($component->font_size) . ':' . | ||
| 305 | + 'fontcolor=' . $text_color . '@' . $opacity . ':' . | ||
| 306 | + 'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' . | ||
| 307 | + 'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' . | ||
| 308 | + 'box=1:boxborderw='. $text_bg_box . ':' . | ||
| 309 | + 'boxcolor=' . $text_bg_color . '@' . $opacity . '", '; | ||
| 310 | + } | ||
| 311 | + $drawtext .= $sub_text; | ||
| 312 | + } | ||
| 313 | + } | ||
| 314 | + | ||
| 315 | + return rtrim($drawtext,', '); | ||
| 316 | + } | ||
| 317 | + | ||
| 318 | + public function calcFontSize($width) | ||
| 319 | + { | ||
| 320 | + return ceil($this->output_width / 360 * $width); | ||
| 258 | } | 321 | } |
| 322 | + | ||
| 323 | + /** | ||
| 324 | + * 获取输出临时文件名 | ||
| 325 | + * @param string $ext | ||
| 326 | + * @param string $dir | ||
| 327 | + * @return string | ||
| 328 | + */ | ||
| 329 | + public function getTempPath($ext = '.mp4',$dir = 'video') | ||
| 330 | + { | ||
| 331 | + $filename = "/output_" . time() . rand(0, 10000); | ||
| 332 | + | ||
| 333 | + $hash_hex = md5($filename); | ||
| 334 | + // 16进制表示的字符串一共32字节,表示16个二进制字节。 | ||
| 335 | + // 前16个字符用来第一级求摸,后16个用做第二级 | ||
| 336 | + $hash_hex_l1 = substr($hash_hex, 0, 8); | ||
| 337 | + $hash_hex_l2 = substr($hash_hex, 8, 8); | ||
| 338 | + $dir_l1 = hexdec($hash_hex_l1) % 256; | ||
| 339 | + $dir_l2 = hexdec($hash_hex_l2) % 512; | ||
| 340 | + $dir = $dir . '/' . $dir_l1 . '/' . $dir_l2; | ||
| 341 | + | ||
| 342 | + if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir); | ||
| 343 | + | ||
| 344 | + return $dir . $filename . $ext; | ||
| 259 | } | 345 | } |
| 260 | } | 346 | } | ... | ... |
| ... | @@ -38,6 +38,11 @@ class AdminMakeVideo extends Model | ... | @@ -38,6 +38,11 @@ class AdminMakeVideo extends Model |
| 38 | return $this->hasOne(OnePoem::class,'id','poem_id'); | 38 | return $this->hasOne(OnePoem::class,'id','poem_id'); |
| 39 | } | 39 | } |
| 40 | 40 | ||
| 41 | + public function poem2() | ||
| 42 | + { | ||
| 43 | + return $this->hasOne(OnePoem2::class,'id','poem_id'); | ||
| 44 | + } | ||
| 45 | + | ||
| 41 | public function temp() | 46 | public function temp() |
| 42 | { | 47 | { |
| 43 | return $this->hasOne(VideoTemp::class,'id','temp_id'); | 48 | return $this->hasOne(VideoTemp::class,'id','temp_id'); | ... | ... |
| ... | @@ -34,7 +34,7 @@ class VideoTemp extends Model | ... | @@ -34,7 +34,7 @@ class VideoTemp extends Model |
| 34 | public function componentsTable() | 34 | public function componentsTable() |
| 35 | { | 35 | { |
| 36 | return $this->hasMany('App\Models\Component', 'temp_id') | 36 | return $this->hasMany('App\Models\Component', 'temp_id') |
| 37 | - ->select(['id', 'temp_id', 'name', 'position', 'font_size', 'text_color', 'text_bg_color', 'text_bg_box','opacity','fix_bounds']); | 37 | + ->select(['id', 'temp_id', 'name', 'position', 'font_size', 'text_color', 'text_bg_color', 'text_bg_box','opacity']); |
| 38 | } | 38 | } |
| 39 | 39 | ||
| 40 | public function admin_make_video() | 40 | public function admin_make_video() | ... | ... |
| ... | @@ -17,6 +17,7 @@ class UpdateComponentsTable extends Migration | ... | @@ -17,6 +17,7 @@ class UpdateComponentsTable extends Migration |
| 17 | 17 | ||
| 18 | Schema::table('components', function (Blueprint $table) { | 18 | Schema::table('components', function (Blueprint $table) { |
| 19 | $table->string('draw')->after('position')->comment('文字效果'); | 19 | $table->string('draw')->after('position')->comment('文字效果'); |
| 20 | + $table->integer('fade_time')->after('draw')->default(1500)->comment('fade切换时间(毫秒)'); | ||
| 20 | }); | 21 | }); |
| 21 | } | 22 | } |
| 22 | 23 | ||
| ... | @@ -27,6 +28,8 @@ class UpdateComponentsTable extends Migration | ... | @@ -27,6 +28,8 @@ class UpdateComponentsTable extends Migration |
| 27 | */ | 28 | */ |
| 28 | public function down() | 29 | public function down() |
| 29 | { | 30 | { |
| 31 | + Schema::dropColumns('components', ['draw', 'fade_time']); | ||
| 32 | + | ||
| 30 | Schema::table('components', function (Blueprint $table) { | 33 | Schema::table('components', function (Blueprint $table) { |
| 31 | $table->string('fix_bounds')->after('opacity')->comment('超出避免剪切'); | 34 | $table->string('fix_bounds')->after('opacity')->comment('超出避免剪切'); |
| 32 | }); | 35 | }); | ... | ... |
-
Please register or login to post a comment