feat: 添加噜噜支付SDK和前端懒加载指令
This commit is contained in:
59
SDK_2.0/SDK/epayapi.php
Normal file
59
SDK_2.0/SDK/epayapi.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>正在为您跳转到支付页面,请稍候...</title>
|
||||
<style type="text/css">
|
||||
body{margin:0;padding:0}
|
||||
p{position:absolute;left:50%;top:50%;height:35px;margin:-35px 0 0 -160px;padding:20px;font:bold 16px/30px "宋体",Arial;text-indent:40px;border:1px solid #c5d0dc}
|
||||
#waiting{font-family:Arial}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
|
||||
require_once("lib/epay.config.php");
|
||||
require_once("lib/EpayCore.class.php");
|
||||
|
||||
/**************************请求参数**************************/
|
||||
$notify_url = "http://127.0.0.1/SDK/notify_url.php";
|
||||
//需http://格式的完整路径,不能加?id=123这类自定义参数
|
||||
|
||||
//页面跳转同步通知页面路径
|
||||
$return_url = "http://127.0.0.1/SDK/return_url.php";
|
||||
//需http://格式的完整路径,不能加?id=123这类自定义参数
|
||||
|
||||
//商户订单号
|
||||
$out_trade_no = $_POST['out_trade_no'];
|
||||
//商户网站订单系统中唯一订单号,必填
|
||||
|
||||
//支付方式(可传入alipay,wxpay,qqpay,bank,jdpay)
|
||||
$type = $_POST['type'];
|
||||
//商品名称
|
||||
$name = $_POST['name'];
|
||||
//付款金额
|
||||
$money = $_POST['money'];
|
||||
|
||||
|
||||
/************************************************************/
|
||||
|
||||
//构造要请求的参数数组,无需改动
|
||||
$parameter = array(
|
||||
"type" => $type,
|
||||
"notify_url" => $notify_url,
|
||||
"return_url" => $return_url,
|
||||
"out_trade_no" => $out_trade_no,
|
||||
"name" => $name,
|
||||
"money" => $money,
|
||||
);
|
||||
|
||||
//建立请求
|
||||
$epay = new EpayCore($epay_config);
|
||||
$html_text = $epay->pagePay($parameter);
|
||||
echo $html_text;
|
||||
|
||||
?>
|
||||
<p>正在为您跳转到支付页面,请稍候...</p>
|
||||
</body>
|
||||
</html>
|
||||
61
SDK_2.0/SDK/index.php
Normal file
61
SDK_2.0/SDK/index.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<body>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>彩虹易支付接口测试</title>
|
||||
<link href="//lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"/>
|
||||
<link rel="stylesheet" href="./assets/css/captcha.css" type="text/css" />
|
||||
<style>.form-group{margin-bottom:18px} #captcha{margin: auto;margin-bottom:16px}</style>
|
||||
</head>
|
||||
<div class="container">
|
||||
<div class="col-xs-12 col-sm-10 col-lg-8 center-block" style="float: none;">
|
||||
<div class="page-header">
|
||||
<h4>彩虹易支付接口测试</h4>
|
||||
</div>
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-body">
|
||||
|
||||
<form name="alipayment" method="POST" action="epayapi.php" class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">商户订单号</label>
|
||||
<div class="col-sm-8">
|
||||
<input class="form-control" type="text" name="out_trade_no" value="<?php echo date("YmdHis").mt_rand(100,999); ?>" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">商品名称</label>
|
||||
<div class="col-sm-8">
|
||||
<input class="form-control" type="text" name="name" value="支付测试" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">支付金额</label>
|
||||
<div class="col-sm-8">
|
||||
<input class="form-control" type="text" name="money" value="1" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">支付方式</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="radio">
|
||||
<label class="i-checks"><input type="radio" name="type" value="alipay" checked="checked"><i></i>支付宝</label>
|
||||
<label class="i-checks"><input type="radio" name="type" value="wxpay"><i></i>微信支付</label>
|
||||
<label class="i-checks"><input type="radio" name="type" value="qqpay"><i></i>QQ钱包</label>
|
||||
<label class="i-checks"><input type="radio" name="type" value="bank"><i></i>云闪付</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-8"><input type="submit" value="确 认" class="btn btn-primary form-control"><br>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="panel-footer text-center">
|
||||
<span class="text-muted">此页面只是为了方便商户测试而提供的样例页面,商户可以根据自己网站的需要,按照技术文档编写</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
184
SDK_2.0/SDK/lib/EpayCore.class.php
Normal file
184
SDK_2.0/SDK/lib/EpayCore.class.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/* *
|
||||
* 彩虹易支付SDK服务类
|
||||
* 说明:
|
||||
* 包含发起支付、查询订单、回调验证等功能
|
||||
*/
|
||||
|
||||
class EpayCore
|
||||
{
|
||||
private $apiurl;
|
||||
private $pid;
|
||||
private $platform_public_key;
|
||||
private $merchant_private_key;
|
||||
|
||||
private $sign_type = 'RSA';
|
||||
|
||||
function __construct($config){
|
||||
$this->apiurl = $config['apiurl'];
|
||||
$this->pid = $config['pid'];
|
||||
$this->platform_public_key = $config['platform_public_key'];
|
||||
$this->merchant_private_key = $config['merchant_private_key'];
|
||||
}
|
||||
|
||||
// 发起支付(页面跳转)
|
||||
public function pagePay($param_tmp, $button='正在跳转'){
|
||||
$requrl = $this->apiurl.'api/pay/submit';
|
||||
$param = $this->buildRequestParam($param_tmp);
|
||||
|
||||
$html = '<form id="dopay" action="'.$requrl.'" method="post">';
|
||||
foreach ($param as $k=>$v) {
|
||||
$html.= '<input type="hidden" name="'.$k.'" value="'.$v.'"/>';
|
||||
}
|
||||
$html .= '<input type="submit" value="'.$button.'"></form><script>document.getElementById("dopay").submit();</script>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// 发起支付(获取链接)
|
||||
public function getPayLink($param_tmp){
|
||||
$requrl = $this->apiurl.'api/pay/submit';
|
||||
$param = $this->buildRequestParam($param_tmp);
|
||||
$url = $requrl.'?'.http_build_query($param);
|
||||
return $url;
|
||||
}
|
||||
|
||||
// 发起支付(API接口)
|
||||
public function apiPay($params){
|
||||
return $this->execute('api/pay/create', $params);
|
||||
}
|
||||
|
||||
// 发起API请求
|
||||
public function execute($path, $params){
|
||||
$path = ltrim($path, '/');
|
||||
$requrl = $this->apiurl.$path;
|
||||
$param = $this->buildRequestParam($params);
|
||||
$response = $this->getHttpResponse($requrl, http_build_query($param));
|
||||
$arr = json_decode($response, true);
|
||||
if($arr && $arr['code'] == 0){
|
||||
if(!$this->verify($arr)){
|
||||
throw new \Exception('返回数据验签失败');
|
||||
}
|
||||
return $arr;
|
||||
}else{
|
||||
throw new \Exception($arr ? $arr['msg'] : '请求失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 回调验证
|
||||
public function verify($arr){
|
||||
if(empty($arr) || empty($arr['sign'])) return false;
|
||||
|
||||
if(empty($arr['timestamp']) || abs(time() - $arr['timestamp']) > 300) return false;
|
||||
|
||||
$sign = $arr['sign'];
|
||||
|
||||
return $this->rsaPublicVerify($this->getSignContent($arr), $sign);
|
||||
}
|
||||
|
||||
// 查询订单支付状态
|
||||
public function orderStatus($trade_no){
|
||||
$result = $this->queryOrder($trade_no);
|
||||
if($result && $result['status']==1){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询订单
|
||||
public function queryOrder($trade_no){
|
||||
$params = [
|
||||
'trade_no' => $trade_no,
|
||||
];
|
||||
return $this->execute('api/pay/query', $params);
|
||||
}
|
||||
|
||||
// 订单退款
|
||||
public function refund($out_refund_no, $trade_no, $money){
|
||||
$params = [
|
||||
'trade_no' => $trade_no,
|
||||
'money' => $money,
|
||||
'out_refund_no' => $out_refund_no,
|
||||
];
|
||||
return $this->execute('api/pay/refund', $params);
|
||||
}
|
||||
|
||||
private function buildRequestParam($params){
|
||||
$params['pid'] = $this->pid;
|
||||
$params['timestamp'] = time().'';
|
||||
$mysign = $this->getSign($params);
|
||||
$params['sign'] = $mysign;
|
||||
$params['sign_type'] = $this->sign_type;
|
||||
return $params;
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
private function getSign($params){
|
||||
return $this->rsaPrivateSign($this->getSignContent($params));
|
||||
}
|
||||
|
||||
// 获取待签名字符串
|
||||
private function getSignContent($params){
|
||||
ksort($params);
|
||||
$signstr = '';
|
||||
foreach ($params as $k => $v) {
|
||||
if(is_array($v) || $this->isEmpty($v) || $k == 'sign' || $k == 'sign_type') continue;
|
||||
$signstr .= '&' . $k . '=' . $v;
|
||||
}
|
||||
$signstr = substr($signstr, 1);
|
||||
return $signstr;
|
||||
}
|
||||
|
||||
private function isEmpty($value)
|
||||
{
|
||||
return $value === null || trim($value) === '';
|
||||
}
|
||||
|
||||
// 商户私钥签名
|
||||
private function rsaPrivateSign($data){
|
||||
$key = "-----BEGIN PRIVATE KEY-----\n" .
|
||||
wordwrap($this->merchant_private_key, 64, "\n", true) .
|
||||
"\n-----END PRIVATE KEY-----";
|
||||
$privatekey = openssl_get_privatekey($key);
|
||||
if(!$privatekey){
|
||||
throw new \Exception('签名失败,商户私钥错误');
|
||||
}
|
||||
openssl_sign($data, $sign, $privatekey, OPENSSL_ALGO_SHA256);
|
||||
return base64_encode($sign);
|
||||
}
|
||||
|
||||
// 平台公钥验签
|
||||
private function rsaPublicVerify($data, $sign){
|
||||
$key = "-----BEGIN PUBLIC KEY-----\n" .
|
||||
wordwrap($this->platform_public_key, 64, "\n", true) .
|
||||
"\n-----END PUBLIC KEY-----";
|
||||
$publickey = openssl_get_publickey($key);
|
||||
if (!$publickey) {
|
||||
throw new \Exception("验签失败,平台公钥错误");
|
||||
}
|
||||
$result = openssl_verify($data, base64_decode($sign), $publickey, OPENSSL_ALGO_SHA256);
|
||||
return $result === 1;
|
||||
}
|
||||
|
||||
// 请求外部资源
|
||||
private function getHttpResponse($url, $post = false, $timeout = 10){
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
$httpheader[] = "Accept: */*";
|
||||
$httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
|
||||
$httpheader[] = "Connection: close";
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
if($post){
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
|
||||
}
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
19
SDK_2.0/SDK/lib/epay.config.php
Normal file
19
SDK_2.0/SDK/lib/epay.config.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/* *
|
||||
* 配置文件
|
||||
*/
|
||||
|
||||
$epay_config = [
|
||||
//支付接口地址
|
||||
'apiurl' => 'http://pay.www.com/',
|
||||
|
||||
//商户ID
|
||||
'pid' => '1000',
|
||||
|
||||
//平台公钥
|
||||
'platform_public_key' => 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApHG7SIN16fd9uZfjZunZuAReemVQe5YNxBhbkogsRkZ86xuDVDCmhRXEzw7Ta3tXPnMIFRJFdjOCfFVarqcOLICtBiiZZ7Y4D6aIMhmOSliIJ3qWUnU75Wr2WMTIJ1o2pnPmczQ2YjAAy1DtQCc/qs35j24zuNYZw2WluSdiMckPFgge93RK6cq/Feqfuzq7y+m87x02gxbbTGVf24YH2f7H9qZSKCxRXHQoVIWTlyHULcY3OY+1CVdU2SKlIWHJ31eoPznXBLUo0UB0rNZnYrHG2mIlD2S119UTwZwx9WTG/v7Cb2lHVybjfL5/KLitddfqcLjJsYXh6KhEtsO6CwIDAQAB',
|
||||
|
||||
//商户私钥
|
||||
'merchant_private_key' => 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCBIB1e5lAYtFyXq5I8UIQ6KidYZcWkn0SwVS8Rk0SNZVrvL/UJk6Q1zkJs4pUCykTBS/tTrP2rNPOsK1VO/AQHIzhvAujsv7UK2LptcsuNRPCF5GYxndQnOawAGKNQKsMuNcDzyuyTMbZIBEYSRWIoU3dMz4wWEFso/VdVS4uKTZWZnBOeCDdzDAJ7TwbmaOkT919DfZbXAoMH9n3sG4BMpqQExTDoFY6dq6EPXCWVZgoUfecAgNKSfX5TagSUaAxq4eF5vsUfvj+LFpYIrIssmSVErtZuRXLHWVSEbsNxdDPNuS3BtxWEY7GRPF9RJevtoC5L5LN7Gn+RYCqZNZv7AgMBAAECggEAEA6ZTb11hQzwsrUAM1s5MNkgbsABIDk6BnTMAfMpRC1awyxYhqoDHTnFTYWuTVwvyUW/PtGKnelbdTPSS5x6jRSr0N+GGDgNYF2Wbpkm3Ni6Jubsb7ZrtRED5Y3Vc9j4JTKZXaJaDEJ9+LNSBLWiFi0C7zH5U/O8ElB8CrxL4ZUaZv0JgV9NcDpS5jAtpPSyBLrdhbEheertJiHQU0V+FaaXq8taNcYIA/Xim6+vqcFFtUA3PBBTXHn/NE5uasXi+N+De4IT+dBmirzVSZjviDPr9RSBUi6KPUSXx6eDa26SKeEqJZvBtlASDM+ZC0yhDz0eyV49tMjk7eF5fnCIwQKBgQC2nEiR2t5Q02tHaKesZMRGOwxEyMFQj6viDW+Yffg59Tu6QYuqdR558/zmzWcJFMH3DVQzTXpzPNU9TA3/yT/Q42iKBP70K8O9tJO+gd/jLHLqgw90Wyh2b4FJXXQqVQMkxGBQKRfNi6krWigJNBs8Z8IhczorQHYNbBIUI05poQKBgQC1BRI8zKf+85GuJXTxJ93RXbkOQMUIhT/6eyFTZvCLC9Qqba1/1ouNbtmxNsFFIC+n+rHRN9btKt90m9YFvXD90m3y34M88QjvaQcA1Kng9Q6Xia8DizpVIYGAR/Pfn36BZQeHHVz9te6QJ9hVOgZO3GG62Echd9M/rwOzuU14GwKBgFGtS2Q5khByz9wLuluIYqXLCWzGoninGkksm0qIpXs+7e0cHh0q72u6rtaI7toH983Jn2ym7esXPYWCPAy5dhq3bG23WFXcMVvrpd2i94IDwo6T+lif4VRAAYLQEwJQLezHDREtoCDmo87pL1kWfkwhWJpfkJgB6AuO1/M763mhAoGBAIPEGj9plcwOzndeSp6UL3IMb/1BBmuqWyTgZiTIpMYCKUFtLsMEj/a2vv2xZsQDpsz2vmMV63weHiRKn2L0QABzIZeOPYCpz6A96lwfcT0QBLwn+95vhVmclyCiv5GDDtnviag/poYD3ZDPgDihkR/sabNRZY2mJH6RzfcQJqULAoGALkSkqr0bplhfyAA6bO42l64th4YUqwouTEgp7rE36wQ28THj0a88HLU4CeiCR6LQAEGpKk04Vst97C1Q5ZeD5rc4xKINl8K5HUH8SsdMDq3r22xur2qr4kanW4hf2P/ehOeEKGuhSL+ZWeApvt1c0rqH4MQT1/7qR/dO2MikkMg=',
|
||||
];
|
||||
|
||||
45
SDK_2.0/SDK/notify_url.php
Normal file
45
SDK_2.0/SDK/notify_url.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/* *
|
||||
* 功能:彩虹易支付异步通知页面
|
||||
* 说明:
|
||||
* 以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要,按照技术文档编写,并非一定要使用该代码。
|
||||
*/
|
||||
|
||||
require_once("lib/epay.config.php");
|
||||
require_once("lib/EpayCore.class.php");
|
||||
|
||||
//计算得出通知验证结果
|
||||
$epay = new EpayCore($epay_config);
|
||||
$verify_result = $epay->verify($_GET);
|
||||
|
||||
if($verify_result) {//验证成功
|
||||
|
||||
//商户订单号
|
||||
$out_trade_no = $_GET['out_trade_no'];
|
||||
|
||||
//彩虹易支付交易号
|
||||
$trade_no = $_GET['trade_no'];
|
||||
|
||||
//交易状态
|
||||
$trade_status = $_GET['trade_status'];
|
||||
|
||||
//支付方式
|
||||
$type = $_GET['type'];
|
||||
|
||||
//支付金额
|
||||
$money = $_GET['money'];
|
||||
|
||||
if ($_GET['trade_status'] == 'TRADE_SUCCESS') {
|
||||
//判断该笔订单是否在商户网站中已经做过处理
|
||||
//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
|
||||
//如果有做过处理,不执行商户的业务程序
|
||||
}
|
||||
|
||||
//验证成功返回
|
||||
echo "success";
|
||||
}
|
||||
else {
|
||||
//验证失败
|
||||
echo "fail";
|
||||
}
|
||||
?>
|
||||
17
SDK_2.0/SDK/query.php
Normal file
17
SDK_2.0/SDK/query.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* 查询订单
|
||||
*/
|
||||
require_once("lib/epay.config.php");
|
||||
require_once("lib/EpayCore.class.php");
|
||||
|
||||
$trade_no = '2024071519404366151';
|
||||
$epay = new EpayCore($epay_config);
|
||||
try{
|
||||
$result = $epay->queryOrder($trade_no);
|
||||
}catch(Exception $e){
|
||||
echo $e->getMessage();
|
||||
exit;
|
||||
}
|
||||
|
||||
print_r($result);
|
||||
19
SDK_2.0/SDK/refund.php
Normal file
19
SDK_2.0/SDK/refund.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* 退款
|
||||
*/
|
||||
require_once("lib/epay.config.php");
|
||||
require_once("lib/EpayCore.class.php");
|
||||
|
||||
$out_refund_no = date("YmdHis").rand(111,999);
|
||||
$trade_no = '2024071519404366151';
|
||||
$money = '1.00';
|
||||
$epay = new EpayCore($epay_config);
|
||||
try{
|
||||
$result = $epay->refund($out_refund_no, $trade_no, $money);
|
||||
}catch(Exception $e){
|
||||
echo $e->getMessage();
|
||||
exit;
|
||||
}
|
||||
|
||||
print_r($result);
|
||||
55
SDK_2.0/SDK/return_url.php
Normal file
55
SDK_2.0/SDK/return_url.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/* *
|
||||
* 功能:彩虹易支付页面跳转同步通知页面
|
||||
* 说明:
|
||||
* 以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要,按照技术文档编写,并非一定要使用该代码。
|
||||
*/
|
||||
|
||||
require_once("lib/epay.config.php");
|
||||
require_once("lib/EpayCore.class.php");
|
||||
?>
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>支付返回页面</title>
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
//计算得出通知验证结果
|
||||
$epay = new EpayCore($epay_config);
|
||||
$verify_result = $epay->verify($_GET);
|
||||
|
||||
if($verify_result) {//验证成功
|
||||
|
||||
//商户订单号
|
||||
$out_trade_no = $_GET['out_trade_no'];
|
||||
|
||||
//支付宝交易号
|
||||
$trade_no = $_GET['trade_no'];
|
||||
|
||||
//交易状态
|
||||
$trade_status = $_GET['trade_status'];
|
||||
|
||||
//支付方式
|
||||
$type = $_GET['type'];
|
||||
|
||||
|
||||
if($_GET['trade_status'] == 'TRADE_SUCCESS') {
|
||||
//判断该笔订单是否在商户网站中已经做过处理
|
||||
//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
|
||||
//如果有做过处理,不执行商户的业务程序
|
||||
}
|
||||
else {
|
||||
echo "trade_status=".$_GET['trade_status'];
|
||||
}
|
||||
|
||||
echo "<h3>验证成功</h3><br />";
|
||||
}
|
||||
else {
|
||||
//验证失败
|
||||
echo "<h3>验证失败</h3>";
|
||||
}
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
112
demo/frontend/package-lock.json
generated
112
demo/frontend/package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.44.1",
|
||||
"vite": "^4.4.9"
|
||||
}
|
||||
},
|
||||
@@ -531,12 +532,55 @@
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
@@ -1036,6 +1080,19 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@@ -1097,6 +1154,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -1176,6 +1240,13 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -1965,6 +2036,16 @@
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1974,6 +2055,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/string-width/-/string-width-4.2.3.tgz",
|
||||
@@ -2000,6 +2092,26 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.44.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/terser/-/terser-5.44.1.tgz",
|
||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
||||
@@ -10,24 +10,26 @@
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"pinia": "^2.1.6",
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"axios": "^1.5.0",
|
||||
"element-plus": "^2.3.8",
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"qrcode": "^1.5.3"
|
||||
"pinia": "^2.1.6",
|
||||
"qrcode": "^1.5.3",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"vite": "^4.4.9",
|
||||
"sass": "^1.66.1"
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.44.1",
|
||||
"vite": "^4.4.9"
|
||||
},
|
||||
"keywords": ["vue", "frontend", "aigc"],
|
||||
"keywords": [
|
||||
"vue",
|
||||
"frontend",
|
||||
"aigc"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
120
demo/frontend/src/directives/lazyLoad.js
Normal file
120
demo/frontend/src/directives/lazyLoad.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 图片懒加载指令
|
||||
* 使用 Intersection Observer API 实现
|
||||
*/
|
||||
|
||||
// 默认占位图(1x1透明像素)
|
||||
const defaultPlaceholder = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
||||
|
||||
// 加载中占位图(可选,灰色背景)
|
||||
const loadingPlaceholder = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23333" width="100" height="100"/%3E%3C/svg%3E'
|
||||
|
||||
// 创建 Intersection Observer
|
||||
let observer = null
|
||||
|
||||
const getObserver = () => {
|
||||
if (observer) return observer
|
||||
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const el = entry.target
|
||||
const src = el.dataset.src
|
||||
|
||||
if (src) {
|
||||
// 创建新图片预加载
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
el.src = src
|
||||
el.classList.add('lazy-loaded')
|
||||
el.classList.remove('lazy-loading')
|
||||
}
|
||||
img.onerror = () => {
|
||||
el.classList.add('lazy-error')
|
||||
el.classList.remove('lazy-loading')
|
||||
}
|
||||
img.src = src
|
||||
}
|
||||
|
||||
// 停止观察
|
||||
observer.unobserve(el)
|
||||
}
|
||||
})
|
||||
}, {
|
||||
rootMargin: '100px', // 提前100px开始加载
|
||||
threshold: 0.1
|
||||
})
|
||||
|
||||
return observer
|
||||
}
|
||||
|
||||
export const lazyLoad = {
|
||||
mounted(el, binding) {
|
||||
const src = binding.value
|
||||
|
||||
if (!src) return
|
||||
|
||||
// 保存真实src
|
||||
el.dataset.src = src
|
||||
|
||||
// 设置占位图
|
||||
el.src = binding.arg === 'loading' ? loadingPlaceholder : defaultPlaceholder
|
||||
el.classList.add('lazy-loading')
|
||||
|
||||
// 开始观察
|
||||
getObserver().observe(el)
|
||||
},
|
||||
|
||||
updated(el, binding) {
|
||||
// 如果src变化,重新加载
|
||||
if (binding.value !== binding.oldValue && binding.value) {
|
||||
el.dataset.src = binding.value
|
||||
el.classList.remove('lazy-loaded', 'lazy-error')
|
||||
el.classList.add('lazy-loading')
|
||||
getObserver().observe(el)
|
||||
}
|
||||
},
|
||||
|
||||
unmounted(el) {
|
||||
if (observer) {
|
||||
observer.unobserve(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 视频懒加载指令
|
||||
export const lazyVideo = {
|
||||
mounted(el, binding) {
|
||||
const src = binding.value
|
||||
|
||||
if (!src) return
|
||||
|
||||
el.dataset.src = src
|
||||
el.preload = 'none' // 不预加载
|
||||
el.classList.add('lazy-loading')
|
||||
|
||||
const videoObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
el.src = el.dataset.src
|
||||
el.preload = 'metadata' // 只加载元数据
|
||||
el.classList.add('lazy-loaded')
|
||||
el.classList.remove('lazy-loading')
|
||||
videoObserver.unobserve(el)
|
||||
}
|
||||
})
|
||||
}, {
|
||||
rootMargin: '50px',
|
||||
threshold: 0.1
|
||||
})
|
||||
|
||||
videoObserver.observe(el)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
app.directive('lazy', lazyLoad)
|
||||
app.directive('lazy-video', lazyVideo)
|
||||
}
|
||||
}
|
||||
@@ -481,7 +481,9 @@ export default {
|
||||
videoFileNotExist: 'Video file may not exist or has been deleted',
|
||||
retry: 'Retry',
|
||||
deleteFailedWork: 'Delete This Work',
|
||||
deleteFailedWorkConfirm: 'This work\'s video failed to load. Are you sure you want to delete it? This action cannot be undone.'
|
||||
deleteFailedWorkConfirm: 'This work\'s video failed to load. Are you sure you want to delete it? This action cannot be undone.',
|
||||
readyToGenerateVideo: 'Storyboard image loaded, ready to generate video',
|
||||
noDownloadUrl: 'No downloadable file available'
|
||||
},
|
||||
|
||||
subscription: {
|
||||
|
||||
@@ -495,7 +495,9 @@ export default {
|
||||
videoFileNotExist: '视频文件可能不存在或已被删除',
|
||||
retry: '重试',
|
||||
deleteFailedWork: '删除此作品',
|
||||
deleteFailedWorkConfirm: '此作品视频加载失败,确定要删除吗?删除后无法恢复。'
|
||||
deleteFailedWorkConfirm: '此作品视频加载失败,确定要删除吗?删除后无法恢复。',
|
||||
readyToGenerateVideo: '已填充分镜图,可以开始生成视频',
|
||||
noDownloadUrl: '没有可下载的文件'
|
||||
},
|
||||
|
||||
subscription: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './locales'
|
||||
import { useUserStore } from './stores/user'
|
||||
import lazyLoadDirective from './directives/lazyLoad'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -15,6 +16,7 @@ app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
app.use(ElementPlus)
|
||||
app.use(lazyLoadDirective)
|
||||
|
||||
console.log('[main.js] i18n 当前语言:', i18n.global.locale.value)
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<!-- 优先使用首帧作为封面,如果没有则使用视频 -->
|
||||
<img
|
||||
v-if="work.firstFrameUrl"
|
||||
:src="work.firstFrameUrl"
|
||||
v-lazy:loading="work.firstFrameUrl"
|
||||
:alt="work.title || work.prompt"
|
||||
class="work-image-thumbnail"
|
||||
/>
|
||||
@@ -287,6 +287,31 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.image-to-video-page {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
></video>
|
||||
</div>
|
||||
<div v-else-if="task.firstFrameUrl" class="history-image-thumbnail">
|
||||
<img :src="task.firstFrameUrl" :alt="t('video.imageToVideo.firstFrameImage')" />
|
||||
<img v-lazy:loading="task.firstFrameUrl" :alt="t('video.imageToVideo.firstFrameImage')" />
|
||||
</div>
|
||||
<div v-else class="history-placeholder">
|
||||
<div class="no-result-text">{{ t('video.imageToVideo.noResult') }}</div>
|
||||
@@ -1546,6 +1546,31 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.image-to-video-create-page {
|
||||
height: 100vh;
|
||||
background: #0a0a0a;
|
||||
|
||||
@@ -170,10 +170,10 @@
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@error="onVideoError"
|
||||
></video>
|
||||
<!-- 如果有封面图,使用图片 -->
|
||||
<!-- 如果有封面图,使用图片(懒加载) -->
|
||||
<img
|
||||
v-else-if="item.cover"
|
||||
:src="item.cover"
|
||||
v-lazy:loading="item.cover"
|
||||
:alt="item.title"
|
||||
@error="onImageError"
|
||||
/>
|
||||
@@ -208,21 +208,28 @@
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<!-- 鼠标悬停时显示的做同款按钮 -->
|
||||
<div class="hover-create-btn" @click.stop="createSimilar(item)">
|
||||
<el-button type="primary" size="small" round>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
{{ t('profile.createSimilar') }}
|
||||
</el-button>
|
||||
<!-- 鼠标悬停时显示的按钮区域 -->
|
||||
<div class="hover-buttons-container">
|
||||
<!-- 做同款按钮 - 左下角 -->
|
||||
<div class="hover-create-btn left" @click.stop="createSimilar(item)">
|
||||
<el-button type="primary" size="small" round>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
{{ t('profile.createSimilar') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 生视频按钮 - 右下角(仅分镜图显示) -->
|
||||
<div v-if="item.category === '分镜图' || item.workType === 'STORYBOARD_IMAGE'" class="hover-create-btn right" @click.stop="goToGenerateVideo(item)">
|
||||
<el-button type="success" size="small" round>
|
||||
<el-icon><Film /></el-icon>
|
||||
{{ t('video.storyboard.generateVideo') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="title" :title="item.title">{{ item.title }}</div>
|
||||
<div class="sub">
|
||||
{{ item.date || t('profile.unknown') }} · {{ item.id }}
|
||||
<span v-if="item.quality && item.type !== 'image'" class="quality-badge" :class="`quality-${(item.quality || '').toLowerCase()}`">
|
||||
{{ formatQuality(item.quality) }}
|
||||
</span>
|
||||
<span v-if="item.sizeText && item.sizeText !== '未知大小'"> · {{ item.sizeText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,13 +365,6 @@
|
||||
<span class="value">{{ selectedItem.aspectRatio || '16:9' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="action-section">
|
||||
<button class="create-similar-btn full-width" @click="createSimilar(selectedItem)">
|
||||
{{ t('works.createSimilar') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -566,8 +566,10 @@ const transformWorkData = (work) => {
|
||||
quality: work.quality || work.resolution || '',
|
||||
username: work.username || work.user?.username || work.creator || work.author || work.owner || '未知用户',
|
||||
status: work.status || 'COMPLETED',
|
||||
uploadedImages: work.uploadedImages || null, // 用户上传的参考图,用于做同款
|
||||
uploadedImages: work.uploadedImages || null, // 分镜图阶段用户上传的参考图
|
||||
videoReferenceImages: work.videoReferenceImages || null, // 视频阶段用户上传的参考图(分镜视频做同款需要)
|
||||
imagePrompt: work.imagePrompt || null, // 分镜图优化后的提示词
|
||||
videoPrompt: work.videoPrompt || null, // 视频优化后的提示词
|
||||
workType: work.workType || '', // 原始作品类型
|
||||
// overlayText 已移除,前端详情不再显示浮动文本
|
||||
}
|
||||
@@ -1113,12 +1115,6 @@ const createSimilar = (item) => {
|
||||
query.referenceImage = item.cover
|
||||
}
|
||||
|
||||
// 用户上传的参考图(分镜图/分镜视频做同款需要)
|
||||
// uploadedImages 现在存储的是 COS URL,可以直接通过 URL 传递
|
||||
if (item.uploadedImages) {
|
||||
query.uploadedImages = item.uploadedImages
|
||||
}
|
||||
|
||||
console.log('[做同款] 跳转参数:', query, 'category:', item.category, 'workType:', item.workType)
|
||||
|
||||
if (item.category === '文生视频') {
|
||||
@@ -1131,14 +1127,22 @@ const createSimilar = (item) => {
|
||||
if (item.imagePrompt) {
|
||||
query.imagePrompt = item.imagePrompt
|
||||
}
|
||||
// 传递分镜图阶段的参考图
|
||||
if (item.uploadedImages) {
|
||||
query.uploadedImages = item.uploadedImages
|
||||
}
|
||||
router.push({ path: '/storyboard-video/create', query })
|
||||
} else if (item.category === '分镜视频') {
|
||||
// 分镜视频做同款:进入 Step 2(生成视频),携带已生成的分镜图
|
||||
// 分镜视频做同款:进入 Step 2(生成视频),携带已生成的分镜图和视频参考图
|
||||
query.step = 'video'
|
||||
// cover 是分镜图(thumbnailUrl),传递给 Step 2 作为分镜图参考
|
||||
// cover 是分镜图(thumbnailUrl),传递给 Step 2 作为分镜图
|
||||
if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') {
|
||||
query.storyboardImage = item.cover
|
||||
}
|
||||
// 传递视频阶段的参考图(videoReferenceImages),不是分镜图阶段的参考图
|
||||
if (item.videoReferenceImages) {
|
||||
query.videoReferenceImages = item.videoReferenceImages
|
||||
}
|
||||
router.push({ path: '/storyboard-video/create', query })
|
||||
} else {
|
||||
// 默认跳转到文生视频
|
||||
@@ -1148,6 +1152,38 @@ const createSimilar = (item) => {
|
||||
ElMessage.success(t('works.createSimilarInfo', { title: item.title }))
|
||||
}
|
||||
|
||||
// 生视频 - 使用分镜图直接生成视频
|
||||
const goToGenerateVideo = (item) => {
|
||||
if (!item) return
|
||||
|
||||
// 跳转到图生视频创作页面,传递分镜图作为参考图
|
||||
const query = {
|
||||
aspectRatio: item.aspectRatio || '',
|
||||
duration: item.duration || '',
|
||||
hdMode: item.quality === 'HD' ? 'true' : 'false'
|
||||
}
|
||||
|
||||
// 使用分镜图作为参考图
|
||||
if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') {
|
||||
query.referenceImage = item.cover
|
||||
} else if (item.resultUrl) {
|
||||
query.referenceImage = item.resultUrl
|
||||
}
|
||||
|
||||
// 传递视频提示词:优先使用 videoPrompt,其次 imagePrompt,最后 prompt
|
||||
if (item.videoPrompt) {
|
||||
query.prompt = item.videoPrompt
|
||||
} else if (item.imagePrompt) {
|
||||
query.prompt = item.imagePrompt
|
||||
} else if (item.prompt) {
|
||||
query.prompt = item.prompt
|
||||
}
|
||||
|
||||
console.log('[生视频] 跳转到图生视频页面,参数:', query)
|
||||
router.push({ path: '/image-to-video/create', query })
|
||||
ElMessage.success(t('works.readyToGenerateVideo'))
|
||||
}
|
||||
|
||||
const download = async (item) => {
|
||||
try {
|
||||
// 检查是否有结果URL
|
||||
@@ -1648,6 +1684,31 @@ onActivated(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.works-page {
|
||||
height: 100vh;
|
||||
background: #0a0a0a;
|
||||
@@ -2174,17 +2235,39 @@ onActivated(() => {
|
||||
.actions { position: absolute; right: 6px; top: 6px; display: flex; gap: 4px; opacity: 0; transition: opacity .2s ease; }
|
||||
.thumb:hover .actions { opacity: 1; }
|
||||
|
||||
/* 鼠标悬停时显示的做同款按钮 */
|
||||
.hover-create-btn {
|
||||
/* 鼠标悬停时显示的按钮容器 */
|
||||
.hover-buttons-container {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 6px;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.thumb:hover .hover-buttons-container {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 鼠标悬停时显示的做同款按钮 */
|
||||
.hover-create-btn {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hover-create-btn.left {
|
||||
/* 左下角 */
|
||||
}
|
||||
|
||||
.hover-create-btn.right {
|
||||
/* 右下角 */
|
||||
}
|
||||
|
||||
.thumb:hover .hover-create-btn {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -2201,6 +2284,16 @@ onActivated(() => {
|
||||
background: rgba(64, 158, 255, 1);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 生视频按钮样式 */
|
||||
.hover-create-btn.right .el-button {
|
||||
background: rgba(103, 194, 58, 0.9);
|
||||
box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3);
|
||||
}
|
||||
|
||||
.hover-create-btn.right .el-button:hover {
|
||||
background: rgba(103, 194, 58, 1);
|
||||
}
|
||||
.work-card.selected .thumb::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
@@ -110,10 +110,10 @@
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@error="onVideoError($event, video)"
|
||||
></video>
|
||||
<!-- 如果有封面图,使用图片 -->
|
||||
<!-- 如果有封面图,使用图片(懒加载) -->
|
||||
<img
|
||||
v-else-if="video.cover"
|
||||
:src="video.cover"
|
||||
v-lazy:loading="video.cover"
|
||||
:alt="video.title"
|
||||
class="video-cover-img"
|
||||
@error="onImageError"
|
||||
@@ -125,10 +125,17 @@
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-action">
|
||||
<el-button v-if="video.status === 'COMPLETED'" type="primary" size="small" @click.stop="createSimilar(video)">{{ t('profile.createSimilar') }}</el-button>
|
||||
<span v-else-if="video.status === 'FAILED'" class="status-text failed">生成失败</span>
|
||||
<span v-else class="status-text processing">{{ video.status === 'PENDING' ? '排队中...' : '生成中...' }}</span>
|
||||
<div class="video-action" v-if="video.status === 'COMPLETED'">
|
||||
<!-- 做同款按钮 - 左侧 -->
|
||||
<el-button type="primary" size="small" @click.stop="createSimilar(video)">{{ t('profile.createSimilar') }}</el-button>
|
||||
<!-- 生视频按钮 - 右侧(仅分镜图显示) -->
|
||||
<el-button v-if="video.category === '分镜图' || video.workType === 'STORYBOARD_IMAGE'" type="success" size="small" @click.stop="goToGenerateVideo(video)">{{ t('video.storyboard.generateVideo') }}</el-button>
|
||||
</div>
|
||||
<div class="video-action" v-else-if="video.status === 'FAILED'">
|
||||
<span class="status-text failed">生成失败</span>
|
||||
</div>
|
||||
<div class="video-action" v-else>
|
||||
<span class="status-text processing">{{ video.status === 'PENDING' ? '排队中...' : '生成中...' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,12 +229,6 @@
|
||||
<span class="value">{{ selectedItem.aspectRatio || t('profile.unknown') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-section">
|
||||
<button class="create-similar-btn" @click="createSimilar(selectedItem)">
|
||||
{{ t('profile.createSimilar') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -541,12 +542,6 @@ const createSimilar = (item) => {
|
||||
query.referenceImage = item.cover
|
||||
}
|
||||
|
||||
// 用户上传的参考图(分镜图/分镜视频做同款需要)
|
||||
// uploadedImages 现在存储的是 COS URL,可以直接通过 URL 传递
|
||||
if (item.uploadedImages) {
|
||||
query.uploadedImages = item.uploadedImages
|
||||
}
|
||||
|
||||
console.log('[做同款] 跳转参数:', query, 'category:', item.category, 'workType:', item.workType)
|
||||
|
||||
// 根据作品类型跳转
|
||||
@@ -560,14 +555,22 @@ const createSimilar = (item) => {
|
||||
if (item.imagePrompt) {
|
||||
query.imagePrompt = item.imagePrompt
|
||||
}
|
||||
// 传递分镜图阶段的参考图
|
||||
if (item.uploadedImages) {
|
||||
query.uploadedImages = item.uploadedImages
|
||||
}
|
||||
router.push({ path: '/storyboard-video/create', query })
|
||||
} else if (item.category === '分镜视频') {
|
||||
// 分镜视频做同款:进入 Step 2(生成视频),携带已生成的分镜图
|
||||
// 分镜视频做同款:进入 Step 2(生成视频),携带已生成的分镜图和视频参考图
|
||||
query.step = 'video'
|
||||
// cover 是分镜图(thumbnailUrl),传递给 Step 2 作为分镜图参考
|
||||
// cover 是分镜图(thumbnailUrl),传递给 Step 2 作为分镜图
|
||||
if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') {
|
||||
query.storyboardImage = item.cover
|
||||
}
|
||||
// 传递视频阶段的参考图(videoReferenceImages),不是分镜图阶段的参考图
|
||||
if (item.videoReferenceImages) {
|
||||
query.videoReferenceImages = item.videoReferenceImages
|
||||
}
|
||||
router.push({ path: '/storyboard-video/create', query })
|
||||
} else {
|
||||
// 默认根据类型跳转
|
||||
@@ -579,6 +582,38 @@ const createSimilar = (item) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 生视频 - 使用分镜图直接生成视频(跳转到图生视频页面)
|
||||
const goToGenerateVideo = (item) => {
|
||||
if (!item) return
|
||||
|
||||
// 跳转到图生视频创作页面,传递分镜图作为参考图
|
||||
const query = {
|
||||
aspectRatio: item.aspectRatio || '',
|
||||
duration: item.duration || '',
|
||||
hdMode: item.quality === 'HD' ? 'true' : 'false'
|
||||
}
|
||||
|
||||
// 使用分镜图作为参考图
|
||||
if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') {
|
||||
query.referenceImage = item.cover
|
||||
} else if (item.resultUrl) {
|
||||
query.referenceImage = item.resultUrl
|
||||
}
|
||||
|
||||
// 传递视频提示词:优先使用 videoPrompt,其次 imagePrompt,最后 prompt
|
||||
if (item.videoPrompt) {
|
||||
query.prompt = item.videoPrompt
|
||||
} else if (item.imagePrompt) {
|
||||
query.prompt = item.imagePrompt
|
||||
} else if (item.prompt) {
|
||||
query.prompt = item.prompt
|
||||
}
|
||||
|
||||
console.log('[生视频] 跳转到图生视频页面,参数:', query)
|
||||
router.push({ path: '/image-to-video/create', query })
|
||||
ElMessage.success(t('works.readyToGenerateVideo'))
|
||||
}
|
||||
|
||||
// 处理URL,确保相对路径正确
|
||||
const processUrl = (url) => {
|
||||
if (!url) return null
|
||||
@@ -620,7 +655,8 @@ const transformWorkData = (work) => {
|
||||
quality: work.quality || work.resolution || '',
|
||||
username: work.username || work.user?.username || work.creator || work.author || work.owner || '未知用户',
|
||||
status: work.status || 'COMPLETED',
|
||||
uploadedImages: work.uploadedImages || null, // 用户上传的参考图,用于做同款
|
||||
uploadedImages: work.uploadedImages || null, // 分镜图阶段用户上传的参考图
|
||||
videoReferenceImages: work.videoReferenceImages || null, // 视频阶段用户上传的参考图(分镜视频做同款需要)
|
||||
imagePrompt: work.imagePrompt || null, // 分镜图优化后的提示词
|
||||
workType: work.workType || '', // 原始作品类型
|
||||
}
|
||||
@@ -1374,12 +1410,26 @@ onUnmounted(() => {
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.video-item:hover .video-action {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 生视频按钮样式 */
|
||||
.video-action .el-button--success {
|
||||
background: rgba(103, 194, 58, 0.9);
|
||||
border-color: rgba(103, 194, 58, 0.9);
|
||||
}
|
||||
|
||||
.video-action .el-button--success:hover {
|
||||
background: rgba(103, 194, 58, 1);
|
||||
border-color: rgba(103, 194, 58, 1);
|
||||
}
|
||||
|
||||
.director-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="works-grid">
|
||||
<div class="work-item" v-for="(work, index) in publishedWorks" :key="work.id" @click="openDetail(work)">
|
||||
<div class="work-thumbnail">
|
||||
<img :src="work.cover" :alt="work.title" />
|
||||
<img v-lazy:loading="work.cover" :alt="work.title" />
|
||||
<!-- 鼠标悬停时显示的做同款按钮 -->
|
||||
<div class="hover-create-btn" @click.stop="goToCreate(work)">
|
||||
<el-button type="primary" size="small" round>
|
||||
@@ -249,6 +249,31 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.storyboard-video-page {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
|
||||
@@ -573,7 +573,7 @@
|
||||
></video>
|
||||
</div>
|
||||
<div v-else-if="task.resultUrl && isImageUrl(task.resultUrl)" class="history-image-thumbnail">
|
||||
<img :src="processHistoryUrl(task.resultUrl)" :alt="t('video.storyboard.storyboardImage')" @error="handleImageError" />
|
||||
<img v-lazy:loading="processHistoryUrl(task.resultUrl)" :alt="t('video.storyboard.storyboardImage')" @error="handleImageError" />
|
||||
</div>
|
||||
<div v-else class="history-placeholder">
|
||||
<div class="no-result-text">{{ t('video.storyboard.noResult') }}</div>
|
||||
@@ -711,6 +711,7 @@ const videoPollIntervalId = ref(null) // 视频任务轮询定时器ID
|
||||
const isCreatingTask = ref(false) // 标记是否正在创建任务,避免重复恢复
|
||||
const hasRestoredTask = ref(false) // 标记是否已经恢复过任务
|
||||
let isFromCreateSimilar = false // 标记是否从"做同款"进入(非响应式,仅用于onMounted判断)
|
||||
const isFromGenerateVideo = ref(false) // 标记是否从"生视频"按钮进入(有分镜图但无taskId)
|
||||
const videoTaskStatus = ref('') // 视频任务状态:PROCESSING, COMPLETED, FAILED
|
||||
const videoResultUrl = ref('') // 视频结果URL
|
||||
const videoProgress = ref(0) // 视频生成进度
|
||||
@@ -1993,7 +1994,8 @@ const startVideoGenerate = async () => {
|
||||
duration: parseInt(duration.value),
|
||||
aspectRatio: aspectRatio.value,
|
||||
hdMode: hdMode.value,
|
||||
referenceImages: referenceImages // 只传递视频阶段的参考图
|
||||
referenceImages: referenceImages, // 只传递视频阶段的参考图
|
||||
storyboardImage: generatedImageUrl.value // 传递分镜图URL,用于恢复被覆盖的分镜图
|
||||
})
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
@@ -2019,8 +2021,13 @@ const startVideoGenerate = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果从"生视频"按钮进入(有分镜图但无taskId),使用分镜图直接生成视频
|
||||
if (isFromGenerateVideo.value && generatedImageUrl.value) {
|
||||
console.log('[startVideoGenerate] 从"生视频"按钮进入,使用分镜图直接生成视频')
|
||||
}
|
||||
|
||||
// 如果没有分镜图任务ID,使用上传的分镜图直接生成视频
|
||||
let imageUrl = mainReferenceImage.value || uploadedImage.value
|
||||
let imageUrl = mainReferenceImage.value || generatedImageUrl.value || uploadedImage.value
|
||||
|
||||
if (!imageUrl) {
|
||||
ElMessage.warning(t('video.storyboard.uploadOrGenerateFirst'))
|
||||
@@ -2114,7 +2121,8 @@ const startVideoGenerate = async () => {
|
||||
const videoResponse = await startVideoGeneration(newTaskId, {
|
||||
duration: parseInt(duration.value),
|
||||
aspectRatio: aspectRatio.value,
|
||||
hdMode: hdMode.value
|
||||
hdMode: hdMode.value,
|
||||
storyboardImage: imageUrl
|
||||
})
|
||||
|
||||
if (videoResponse.data && videoResponse.data.success) {
|
||||
@@ -3133,11 +3141,18 @@ onMounted(async () => {
|
||||
console.log('[做同款-视频] 设置 videoPrompt:', route.query.prompt.substring(0, 100))
|
||||
}
|
||||
|
||||
// 复用分镜图的taskId
|
||||
if (route.query.taskId) {
|
||||
taskId.value = route.query.taskId
|
||||
console.log('[生视频] 复用分镜图taskId:', route.query.taskId)
|
||||
}
|
||||
|
||||
// 设置已生成的分镜图
|
||||
if (route.query.storyboardImage) {
|
||||
generatedImageUrl.value = route.query.storyboardImage
|
||||
mainReferenceImage.value = route.query.storyboardImage
|
||||
isAIGeneratedImage.value = true
|
||||
isFromGenerateVideo.value = true // 标记从"生视频"按钮进入
|
||||
console.log('[做同款-视频] 设置分镜图:', route.query.storyboardImage)
|
||||
}
|
||||
} else {
|
||||
@@ -3163,7 +3178,7 @@ onMounted(async () => {
|
||||
hdMode.value = route.query.hdMode === 'true'
|
||||
}
|
||||
|
||||
// 处理用户上传的参考图(uploadedImages 现在是 COS URL 的 JSON 数组)
|
||||
// 处理分镜图阶段用户上传的参考图(uploadedImages)
|
||||
if (route.query.uploadedImages) {
|
||||
try {
|
||||
const parsedImages = typeof route.query.uploadedImages === 'string'
|
||||
@@ -3178,20 +3193,10 @@ onMounted(async () => {
|
||||
name: `参考图片${idx + 1}`
|
||||
}))
|
||||
|
||||
// 根据当前步骤决定填充到哪个参考图数组
|
||||
if (currentStep.value === 'video') {
|
||||
// Step 2:填充到视频参考图
|
||||
videoReferenceImages.value = [null, null, null]
|
||||
imageObjects.forEach((img, idx) => {
|
||||
if (idx < 3) {
|
||||
videoReferenceImages.value[idx] = img
|
||||
}
|
||||
})
|
||||
console.log('[做同款-视频] 恢复用户上传图片:', imageObjects.length, '张')
|
||||
} else {
|
||||
// Step 1:填充到分镜图参考图
|
||||
// 分镜图阶段的参考图只填充到 Step 1
|
||||
if (currentStep.value !== 'video') {
|
||||
uploadedImages.value = imageObjects
|
||||
console.log('[做同款-图] 恢复用户上传图片:', imageObjects.length, '张')
|
||||
console.log('[做同款-图] 恢复分镜图阶段参考图:', imageObjects.length, '张')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -3207,6 +3212,35 @@ onMounted(async () => {
|
||||
console.log('[做同款] 设置参考图:', route.query.referenceImage)
|
||||
}
|
||||
|
||||
// 处理视频阶段用户上传的参考图(videoReferenceImages)- 分镜视频做同款专用
|
||||
if (route.query.videoReferenceImages) {
|
||||
try {
|
||||
const parsedImages = typeof route.query.videoReferenceImages === 'string'
|
||||
? JSON.parse(route.query.videoReferenceImages)
|
||||
: route.query.videoReferenceImages
|
||||
if (Array.isArray(parsedImages) && parsedImages.length > 0) {
|
||||
const imageObjects = parsedImages
|
||||
.filter(img => img && img !== 'null')
|
||||
.map((url, idx) => ({
|
||||
url: url,
|
||||
file: null,
|
||||
name: `视频参考图${idx + 1}`
|
||||
}))
|
||||
|
||||
// 视频阶段的参考图填充到 videoReferenceImages
|
||||
videoReferenceImages.value = [null, null, null]
|
||||
imageObjects.forEach((img, idx) => {
|
||||
if (idx < 3) {
|
||||
videoReferenceImages.value[idx] = img
|
||||
}
|
||||
})
|
||||
console.log('[做同款-视频] 恢复视频阶段参考图:', imageObjects.length, '张')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[做同款] 解析 videoReferenceImages 失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 静默填充,不显示弹窗提示(减少干扰)
|
||||
// 清除URL中的query参数,避免刷新页面重复填充
|
||||
router.replace({ path: route.path })
|
||||
@@ -3245,6 +3279,31 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.storyboard-video-create-page {
|
||||
height: 100vh;
|
||||
background: #0a0a0a;
|
||||
|
||||
@@ -157,7 +157,7 @@ CREATE TABLE IF NOT EXISTS system_settings (
|
||||
contact_email VARCHAR(120) DEFAULT 'support@example.com' COMMENT '联系邮箱',
|
||||
prompt_optimization_model VARCHAR(50) DEFAULT 'gpt-5.1-thinking' COMMENT '优化提示词使用的模型',
|
||||
storyboard_system_prompt VARCHAR(2000) DEFAULT '' COMMENT '分镜图生成系统引导词',
|
||||
token_expire_hours INT NOT NULL DEFAULT 24 COMMENT 'Token过期时间(小时)'
|
||||
token_expire_hours INT NOT NULL DEFAULT 720 COMMENT 'Token过期时间(小时)'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统设置表';
|
||||
|
||||
-- 用户活跃度统计表
|
||||
|
||||
@@ -213,6 +213,7 @@ public class StoryboardVideoApiController {
|
||||
String aspectRatio = null;
|
||||
Boolean hdMode = null;
|
||||
java.util.List<String> referenceImages = null;
|
||||
String storyboardImage = null; // 前端传递的分镜图URL
|
||||
|
||||
if (requestBody != null) {
|
||||
if (requestBody.containsKey("duration")) {
|
||||
@@ -239,10 +240,21 @@ public class StoryboardVideoApiController {
|
||||
logger.info("参考图数量: {}", referenceImages.size());
|
||||
}
|
||||
}
|
||||
if (requestBody.containsKey("storyboardImage")) {
|
||||
storyboardImage = (String) requestBody.get("storyboardImage");
|
||||
logger.info("前端传递的分镜图URL: {}", storyboardImage != null ? storyboardImage.substring(0, Math.min(80, storyboardImage.length())) + "..." : "null");
|
||||
} else {
|
||||
logger.warn("请求体中没有storyboardImage参数,requestBody keys: {}", requestBody.keySet());
|
||||
}
|
||||
} else {
|
||||
logger.warn("请求体为空");
|
||||
}
|
||||
|
||||
// 开始生成视频,传递参数(包括参考图)
|
||||
storyboardVideoService.startVideoGeneration(taskId, duration, aspectRatio, hdMode, referenceImages);
|
||||
logger.info("调用startVideoGeneration: taskId={}, storyboardImage={}", taskId,
|
||||
storyboardImage != null ? "有值(长度:" + storyboardImage.length() + ")" : "null");
|
||||
|
||||
// 开始生成视频,传递参数(包括参考图和分镜图URL)
|
||||
storyboardVideoService.startVideoGeneration(taskId, duration, aspectRatio, hdMode, referenceImages, storyboardImage);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
|
||||
@@ -62,11 +62,11 @@ public class SystemSettings {
|
||||
@Column(length = 2000)
|
||||
private String storyboardSystemPrompt = "";
|
||||
|
||||
/** Token过期时间(小时),范围1-720小时,默认24小时 */
|
||||
/** Token过期时间(小时),范围1-720小时,默认720小时(30天) */
|
||||
@NotNull
|
||||
@Min(1)
|
||||
@Column(nullable = false)
|
||||
private Integer tokenExpireHours = 24;
|
||||
private Integer tokenExpireHours = 720;
|
||||
|
||||
/** AI API密钥(视频和图片生成共用) */
|
||||
@Column(length = 200)
|
||||
@@ -172,6 +172,13 @@ public class SystemSettings {
|
||||
// 限制范围在1-720小时(1小时到30天)
|
||||
if (tokenExpireHours != null && tokenExpireHours >= 1 && tokenExpireHours <= 720) {
|
||||
this.tokenExpireHours = tokenExpireHours;
|
||||
} else if (tokenExpireHours != null) {
|
||||
// 值超出范围时,强制设置为边界值
|
||||
if (tokenExpireHours < 1) {
|
||||
this.tokenExpireHours = 1;
|
||||
} else {
|
||||
this.tokenExpireHours = 720;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,10 @@ public class UserWork {
|
||||
private String tags; // 标签,用逗号分隔
|
||||
|
||||
@Column(name = "uploaded_images", columnDefinition = "LONGTEXT")
|
||||
private String uploadedImages; // 用户上传的参考图片(JSON数组),用于"做同款"功能恢复
|
||||
private String uploadedImages; // 用户上传的参考图片(JSON数组),用于"做同款"功能恢复(分镜图阶段)
|
||||
|
||||
@Column(name = "video_reference_images", columnDefinition = "LONGTEXT")
|
||||
private String videoReferenceImages; // 视频阶段用户上传的参考图片(JSON数组),用于分镜视频"做同款"功能恢复
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
@@ -390,6 +393,14 @@ public class UserWork {
|
||||
this.uploadedImages = uploadedImages;
|
||||
}
|
||||
|
||||
public String getVideoReferenceImages() {
|
||||
return videoReferenceImages;
|
||||
}
|
||||
|
||||
public void setVideoReferenceImages(String videoReferenceImages) {
|
||||
this.videoReferenceImages = videoReferenceImages;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@@ -216,10 +216,42 @@ public class RealAIService {
|
||||
// 根据参数选择可用的模型
|
||||
String modelName = selectAvailableImageToVideoModel(aspectRatio, duration, hdMode);
|
||||
|
||||
// 处理图片数据:如果是 URL,先下载转换为 base64
|
||||
String processedImageBase64 = imageBase64;
|
||||
if (imageBase64.startsWith("http://") || imageBase64.startsWith("https://")) {
|
||||
// 是 URL,需要下载图片
|
||||
logger.info("检测到图片URL,开始下载: {}", imageBase64.substring(0, Math.min(100, imageBase64.length())));
|
||||
try {
|
||||
java.net.URL imageUrl = new java.net.URL(imageBase64);
|
||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) imageUrl.openConnection();
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(60000);
|
||||
try (java.io.InputStream is = conn.getInputStream();
|
||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = is.read(buffer)) != -1) {
|
||||
baos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
byte[] imageData = baos.toByteArray();
|
||||
// 添加 data URI 前缀
|
||||
String mimeType = conn.getContentType();
|
||||
if (mimeType == null || !mimeType.startsWith("image/")) {
|
||||
mimeType = "image/png"; // 默认使用 PNG
|
||||
}
|
||||
processedImageBase64 = "data:" + mimeType + ";base64," + Base64.getEncoder().encodeToString(imageData);
|
||||
logger.info("图片下载成功,大小: {} KB", imageData.length / 1024);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("下载图片失败: {}", e.getMessage());
|
||||
throw new RuntimeException("下载参考图片失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 验证base64数据格式(提取纯Base64数据用于验证)
|
||||
String base64DataForValidation = imageBase64;
|
||||
if (imageBase64.contains(",")) {
|
||||
base64DataForValidation = imageBase64.substring(imageBase64.indexOf(",") + 1);
|
||||
String base64DataForValidation = processedImageBase64;
|
||||
if (processedImageBase64.contains(",")) {
|
||||
base64DataForValidation = processedImageBase64.substring(processedImageBase64.indexOf(",") + 1);
|
||||
}
|
||||
try {
|
||||
Base64.getDecoder().decode(base64DataForValidation);
|
||||
@@ -238,7 +270,7 @@ public class RealAIService {
|
||||
// 图生视频使用 images 数组(即使只有一张图片)
|
||||
// 验证并规范化图片格式(参考sora2实现)
|
||||
List<String> imagesList = new java.util.ArrayList<>();
|
||||
imagesList.add(imageBase64);
|
||||
imagesList.add(processedImageBase64);
|
||||
imagesList = validateImageFormat(imagesList);
|
||||
|
||||
Map<String, Object> requestMap = new HashMap<>();
|
||||
|
||||
@@ -372,15 +372,52 @@ public class StoryboardVideoService {
|
||||
logger.info("已保存优化后的提示词到任务: taskId={}", taskId);
|
||||
|
||||
} catch (Exception jsonException) {
|
||||
// JSON 解析失败,使用原始优化结果作为 imagePrompt
|
||||
// JSON 解析失败,使用原始优化结果作为 imagePrompt 和 videoPrompt
|
||||
logger.warn("JSON 解析失败,使用原始优化结果: {}", jsonException.getMessage());
|
||||
finalPrompt = optimizedResult;
|
||||
// 保存优化结果到 imagePrompt 和 videoPrompt
|
||||
final String optimizedPrompt = optimizedResult;
|
||||
asyncTransactionTemplate.executeWithoutResult(status -> {
|
||||
StoryboardVideoTask t = taskRepository.findByTaskId(taskId).orElse(null);
|
||||
if (t != null) {
|
||||
t.setImagePrompt(optimizedPrompt);
|
||||
t.setVideoPrompt(optimizedPrompt);
|
||||
taskRepository.save(t);
|
||||
}
|
||||
});
|
||||
logger.info("JSON解析失败,已保存优化结果到 imagePrompt 和 videoPrompt: taskId={}", taskId);
|
||||
}
|
||||
} else {
|
||||
logger.info("未配置系统引导词,使用原始提示词");
|
||||
// 没有系统引导词时,imagePrompt 和 videoPrompt 都使用原始提示词
|
||||
final String originalPrompt = prompt;
|
||||
asyncTransactionTemplate.executeWithoutResult(status -> {
|
||||
StoryboardVideoTask t = taskRepository.findByTaskId(taskId).orElse(null);
|
||||
if (t != null) {
|
||||
t.setImagePrompt(originalPrompt);
|
||||
t.setVideoPrompt(originalPrompt);
|
||||
taskRepository.save(t);
|
||||
}
|
||||
});
|
||||
logger.info("已保存原始提示词到 imagePrompt 和 videoPrompt: taskId={}", taskId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("提示词优化失败,使用原始提示词: {}", e.getMessage());
|
||||
// 优化失败时,imagePrompt 和 videoPrompt 都使用原始提示词
|
||||
final String originalPrompt = prompt;
|
||||
try {
|
||||
asyncTransactionTemplate.executeWithoutResult(status -> {
|
||||
StoryboardVideoTask t = taskRepository.findByTaskId(taskId).orElse(null);
|
||||
if (t != null) {
|
||||
t.setImagePrompt(originalPrompt);
|
||||
t.setVideoPrompt(originalPrompt);
|
||||
taskRepository.save(t);
|
||||
}
|
||||
});
|
||||
logger.info("优化失败,已保存原始提示词到 imagePrompt 和 videoPrompt: taskId={}", taskId);
|
||||
} catch (Exception saveEx) {
|
||||
logger.error("保存原始提示词失败: {}", saveEx.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("任务参数 - 原始提示词: {}, imagePrompt长度: {}, 目标比例: {}, 生成比例: {}, hdMode: {}, 有用户上传: {}, imageModel: {}",
|
||||
@@ -892,20 +929,55 @@ public class StoryboardVideoService {
|
||||
* @param aspectRatio 宽高比(可选,如果为null则使用任务中已有的值)
|
||||
* @param hdMode 高清模式(可选,如果为null则使用任务中已有的值)
|
||||
* @param referenceImages 参考图列表(可选)
|
||||
* @param storyboardImage 前端传递的分镜图URL(可选,用于恢复已被视频URL覆盖的分镜图)
|
||||
*/
|
||||
@Transactional
|
||||
public void startVideoGeneration(String taskId, Integer duration, String aspectRatio, Boolean hdMode, java.util.List<String> referenceImages) {
|
||||
public void startVideoGeneration(String taskId, Integer duration, String aspectRatio, Boolean hdMode, java.util.List<String> referenceImages, String storyboardImage) {
|
||||
try {
|
||||
logger.info("开始生成视频: taskId={}, storyboardImage={}", taskId,
|
||||
storyboardImage != null ? storyboardImage.substring(0, Math.min(80, storyboardImage.length())) + "..." : "null");
|
||||
|
||||
// 重新加载任务
|
||||
StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
|
||||
.orElseThrow(() -> new RuntimeException("任务不存在: " + taskId));
|
||||
|
||||
logger.info("任务当前状态: taskId={}, status={}, resultUrl={}", taskId, task.getStatus(),
|
||||
task.getResultUrl() != null ? task.getResultUrl().substring(0, Math.min(80, task.getResultUrl().length())) + "..." : "null");
|
||||
|
||||
// 检查分镜图是否已生成
|
||||
if (task.getResultUrl() == null || task.getResultUrl().isEmpty()) {
|
||||
// 优先使用前端传递的storyboardImage(因为任务的resultUrl可能已被视频URL覆盖)
|
||||
String effectiveStoryboardUrl = null;
|
||||
|
||||
// 首先检查前端是否传递了分镜图URL
|
||||
if (storyboardImage != null && !storyboardImage.isEmpty()) {
|
||||
effectiveStoryboardUrl = storyboardImage;
|
||||
logger.info("使用前端传递的分镜图URL: taskId={}", taskId);
|
||||
} else {
|
||||
// 前端没有传递,检查任务中的resultUrl是否是图片
|
||||
String currentResultUrl = task.getResultUrl();
|
||||
if (currentResultUrl != null && !currentResultUrl.isEmpty()) {
|
||||
boolean isImageUrl = currentResultUrl.contains(".png") || currentResultUrl.contains(".jpg")
|
||||
|| currentResultUrl.contains(".jpeg") || currentResultUrl.contains(".webp")
|
||||
|| currentResultUrl.startsWith("data:image");
|
||||
if (isImageUrl) {
|
||||
effectiveStoryboardUrl = currentResultUrl;
|
||||
logger.info("使用任务中的分镜图URL: taskId={}", taskId);
|
||||
} else {
|
||||
logger.warn("任务的resultUrl不是图片URL: taskId={}, url={}", taskId, currentResultUrl.substring(0, Math.min(50, currentResultUrl.length())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveStoryboardUrl == null || effectiveStoryboardUrl.isEmpty()) {
|
||||
throw new RuntimeException("分镜图尚未生成,无法生成视频");
|
||||
}
|
||||
|
||||
// 更新任务的resultUrl为有效的分镜图URL(确保后续处理能获取到)
|
||||
if (!effectiveStoryboardUrl.equals(task.getResultUrl())) {
|
||||
task.setResultUrl(effectiveStoryboardUrl);
|
||||
logger.info("已更新任务的resultUrl为分镜图URL: taskId={}", taskId);
|
||||
}
|
||||
|
||||
// 冻结分镜视频生成积分(30积分)
|
||||
try {
|
||||
userService.freezePoints(task.getUsername(), taskId + "_vid",
|
||||
@@ -919,12 +991,21 @@ public class StoryboardVideoService {
|
||||
|
||||
// 检查任务状态:允许从 PROCESSING、COMPLETED 或 FAILED(重试)状态生成视频
|
||||
// 只要分镜图已生成,就允许重试生成视频
|
||||
// 如果前端传递了storyboardImage,也允许PENDING状态(从"我的作品"页面跳转的情况)
|
||||
if (task.getStatus() != StoryboardVideoTask.TaskStatus.PROCESSING &&
|
||||
task.getStatus() != StoryboardVideoTask.TaskStatus.COMPLETED &&
|
||||
task.getStatus() != StoryboardVideoTask.TaskStatus.FAILED) {
|
||||
task.getStatus() != StoryboardVideoTask.TaskStatus.FAILED &&
|
||||
!(task.getStatus() == StoryboardVideoTask.TaskStatus.PENDING && storyboardImage != null && !storyboardImage.isEmpty())) {
|
||||
throw new RuntimeException("任务状态不正确,无法生成视频。当前状态: " + task.getStatus());
|
||||
}
|
||||
|
||||
// 如果是PENDING状态且有storyboardImage,更新状态为PROCESSING
|
||||
if (task.getStatus() == StoryboardVideoTask.TaskStatus.PENDING && storyboardImage != null && !storyboardImage.isEmpty()) {
|
||||
task.setStatus(StoryboardVideoTask.TaskStatus.PROCESSING);
|
||||
task.setProgress(50);
|
||||
logger.info("任务状态从PENDING更新为PROCESSING(有分镜图,开始视频生成): taskId={}", taskId);
|
||||
}
|
||||
|
||||
// 更新任务参数(如果提供了新的参数)
|
||||
boolean paramsUpdated = false;
|
||||
if (duration != null && !duration.equals(task.getDuration())) {
|
||||
|
||||
@@ -60,9 +60,12 @@ public class SystemSettingsService {
|
||||
updated.setAiApiKey(existing.getAiApiKey());
|
||||
}
|
||||
|
||||
// 记录保存前的tokenExpireHours值
|
||||
logger.info("保存前 tokenExpireHours: {}", updated.getTokenExpireHours());
|
||||
|
||||
// 保存更新
|
||||
SystemSettings saved = repository.save(updated);
|
||||
logger.info("系统设置保存成功: id={}", saved.getId());
|
||||
logger.info("系统设置保存成功: id={}, tokenExpireHours={}", saved.getId(), saved.getTokenExpireHours());
|
||||
|
||||
// 刷新运行时配置
|
||||
if (saved.getAiApiKey() != null && !saved.getAiApiKey().isEmpty()) {
|
||||
|
||||
@@ -294,7 +294,8 @@ public class UserWorkService {
|
||||
work.setPointsCost(task.getCostPoints());
|
||||
work.setStatus(UserWork.WorkStatus.COMPLETED);
|
||||
work.setCompletedAt(LocalDateTime.now());
|
||||
work.setUploadedImages(task.getUploadedImages()); // 同步用户上传的参考图,用于"做同款"
|
||||
work.setUploadedImages(task.getUploadedImages()); // 同步分镜图阶段用户上传的参考图
|
||||
work.setVideoReferenceImages(task.getVideoReferenceImages()); // 同步视频阶段用户上传的参考图,用于"做同款"
|
||||
|
||||
work = userWorkRepository.save(work);
|
||||
logger.info("创建分镜视频作品成功: {}, 用户: {}, 分镜图: {}", work.getId(), work.getUsername(), task.getResultUrl() != null ? "有" : "无");
|
||||
|
||||
@@ -20,7 +20,7 @@ public class JwtUtils {
|
||||
@Value("${jwt.secret:aigc-demo-secret-key-for-jwt-token-generation}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration:86400000}") // 24小时,单位毫秒
|
||||
@Value("${jwt.expiration:2592000000}") // 720小时(30天),单位毫秒
|
||||
private Long expiration;
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@ alipay.server-url=https\://openapi-sandbox.dl.alipaydev.com/gateway.do
|
||||
alipay.sign-type=RSA2
|
||||
app.ffmpeg.path=C\:/Users/UI/AppData/Local/Microsoft/WinGet/Packages/Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe/ffmpeg-8.0-full_build/bin/ffmpeg.exe
|
||||
app.temp.dir=./temp
|
||||
jwt.expiration=86400000
|
||||
jwt.expiration=2592000000
|
||||
jwt.secret=mySecretKey123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||
logging.level.com.example.demo=DEBUG
|
||||
logging.level.org.hibernate.SQL=WARN
|
||||
|
||||
@@ -72,7 +72,7 @@ alipay.return-url=https://vionow.com/payment/success
|
||||
|
||||
# JWT配置
|
||||
jwt.secret=mySecretKey123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||
jwt.expiration=604800000
|
||||
jwt.expiration=2592000000
|
||||
|
||||
# 腾讯云SES配置 (生产环境)
|
||||
tencent.ses.secret-id=AKIDoaEjFbqxxqZAcv8EE6oZCg2IQPG1fCxm
|
||||
@@ -82,10 +82,10 @@ tencent.ses.from-email=newletter@vionow.com
|
||||
tencent.ses.from-name=AIGC平台
|
||||
tencent.ses.template-id=154360
|
||||
|
||||
# PayPal配置 (生产环境)
|
||||
paypal.client-id=Adpi67TvppjhyyWhrALWwJhLFzv5S_vXoUHzWQchqZe48NaONSryg7QHKBubf0PRmkeJoaxGEKV5v9lT
|
||||
paypal.client-secret=EDzZl-hddwtt2pNt5RpBIICdlrUS8QtcmAttU_kuANL8Vd937SC4xel_K2hArTovVqEtyL2ZS5IcQcQV
|
||||
paypal.mode=sandbox
|
||||
# PayPal配置 (生产环境 - Live模式)
|
||||
paypal.client-id=AajQyk5afrKsLuDBpBKYsI3DdCUWC0B9puW3avt5SKJAaBtD73E1hYYCAK3GZFSEzsyLStyIPbuaXya4
|
||||
paypal.client-secret=EH8mA05ocZkOKRXbNGhAtkP0TgK9VVw7jqQad8SVVC82rChQ47E16SnCWpFISRyzE4xAlraDkfoh-4EB
|
||||
paypal.mode=live
|
||||
paypal.success-url=https://vionow.com/api/payment/paypal/success
|
||||
paypal.cancel-url=https://vionow.com/api/payment/paypal/cancel
|
||||
|
||||
@@ -132,9 +132,8 @@ app.ffmpeg.path=${FFMPEG_PATH:ffmpeg}
|
||||
app.upload.path=${UPLOAD_PATH:./uploads}
|
||||
|
||||
# SpringDoc OpenAPI (Swagger) 配置
|
||||
# 生产环境禁用以提高安全性和性能
|
||||
springdoc.api-docs.enabled=false
|
||||
springdoc.swagger-ui.enabled=false
|
||||
springdoc.api-docs.enabled=true
|
||||
springdoc.swagger-ui.enabled=true
|
||||
|
||||
# ============================================
|
||||
# Redis 配置(生产环境 - 已禁用)
|
||||
|
||||
@@ -39,8 +39,8 @@ app.upload.path=uploads
|
||||
app.video.output.path=outputs
|
||||
|
||||
# JWT配置
|
||||
jwt.secret=aigc-demo-secret-key-for-jwt-token-generation-2025
|
||||
jwt.expiration=86400000
|
||||
jwt.secret=mySecretKey123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||
jwt.expiration=2592000000
|
||||
|
||||
# ============================================
|
||||
# Redis配置
|
||||
|
||||
Reference in New Issue
Block a user