From a66bd806b2f723519ed919a9411987aa56f25044 Mon Sep 17 00:00:00 2001 From: AIGC Developer Date: Tue, 6 Jan 2026 14:33:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=99=9C=E5=99=9C?= =?UTF-8?q?=E6=94=AF=E4=BB=98SDK=E5=92=8C=E5=89=8D=E7=AB=AF=E6=87=92?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SDK_2.0/SDK/epayapi.php | 59 ++++++ SDK_2.0/SDK/index.php | 61 ++++++ SDK_2.0/SDK/lib/EpayCore.class.php | 184 ++++++++++++++++++ SDK_2.0/SDK/lib/epay.config.php | 19 ++ SDK_2.0/SDK/notify_url.php | 45 +++++ SDK_2.0/SDK/query.php | 17 ++ SDK_2.0/SDK/refund.php | 19 ++ SDK_2.0/SDK/return_url.php | 55 ++++++ demo/frontend/package-lock.json | 112 +++++++++++ demo/frontend/package.json | 26 +-- demo/frontend/src/directives/lazyLoad.js | 120 ++++++++++++ demo/frontend/src/locales/en.js | 4 +- demo/frontend/src/locales/zh.js | 4 +- demo/frontend/src/main.js | 2 + demo/frontend/src/views/ImageToVideo.vue | 27 ++- .../frontend/src/views/ImageToVideoCreate.vue | 27 ++- demo/frontend/src/views/MyWorks.vue | 153 ++++++++++++--- demo/frontend/src/views/Profile.vue | 92 +++++++-- demo/frontend/src/views/StoryboardVideo.vue | 27 ++- .../src/views/StoryboardVideoCreate.vue | 95 +++++++-- demo/init_database.sql | 2 +- .../StoryboardVideoApiController.java | 16 +- .../example/demo/model/SystemSettings.java | 11 +- .../java/com/example/demo/model/UserWork.java | 13 +- .../example/demo/service/RealAIService.java | 40 +++- .../demo/service/StoryboardVideoService.java | 89 ++++++++- .../demo/service/SystemSettingsService.java | 5 +- .../example/demo/service/UserWorkService.java | 3 +- .../java/com/example/demo/util/JwtUtils.java | 2 +- .../main/resources/application-dev.properties | 2 +- .../resources/application-prod.properties | 15 +- .../src/main/resources/application.properties | 4 +- 32 files changed, 1236 insertions(+), 114 deletions(-) create mode 100644 SDK_2.0/SDK/epayapi.php create mode 100644 SDK_2.0/SDK/index.php create mode 100644 SDK_2.0/SDK/lib/EpayCore.class.php create mode 100644 SDK_2.0/SDK/lib/epay.config.php create mode 100644 SDK_2.0/SDK/notify_url.php create mode 100644 SDK_2.0/SDK/query.php create mode 100644 SDK_2.0/SDK/refund.php create mode 100644 SDK_2.0/SDK/return_url.php create mode 100644 demo/frontend/src/directives/lazyLoad.js diff --git a/SDK_2.0/SDK/epayapi.php b/SDK_2.0/SDK/epayapi.php new file mode 100644 index 0000000..963146b --- /dev/null +++ b/SDK_2.0/SDK/epayapi.php @@ -0,0 +1,59 @@ + + + + + + 正在为您跳转到支付页面,请稍候... + + + + $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; + +?> +

正在为您跳转到支付页面,请稍候...

+ + \ No newline at end of file diff --git a/SDK_2.0/SDK/index.php b/SDK_2.0/SDK/index.php new file mode 100644 index 0000000..a15a4f4 --- /dev/null +++ b/SDK_2.0/SDK/index.php @@ -0,0 +1,61 @@ + + + + + + + 彩虹易支付接口测试 + + + + +
+
+ +
+
+ +
+
+ +
+ " autocomplete="off"> +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+   +   +   +   +
+
+
+
+

+
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/SDK_2.0/SDK/lib/EpayCore.class.php b/SDK_2.0/SDK/lib/EpayCore.class.php new file mode 100644 index 0000000..19f8426 --- /dev/null +++ b/SDK_2.0/SDK/lib/EpayCore.class.php @@ -0,0 +1,184 @@ +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 = '
'; + foreach ($param as $k=>$v) { + $html.= ''; + } + $html .= '
'; + + 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; + } +} diff --git a/SDK_2.0/SDK/lib/epay.config.php b/SDK_2.0/SDK/lib/epay.config.php new file mode 100644 index 0000000..0b2a500 --- /dev/null +++ b/SDK_2.0/SDK/lib/epay.config.php @@ -0,0 +1,19 @@ + '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=', +]; + diff --git a/SDK_2.0/SDK/notify_url.php b/SDK_2.0/SDK/notify_url.php new file mode 100644 index 0000000..4aafe6e --- /dev/null +++ b/SDK_2.0/SDK/notify_url.php @@ -0,0 +1,45 @@ +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"; +} +?> \ No newline at end of file diff --git a/SDK_2.0/SDK/query.php b/SDK_2.0/SDK/query.php new file mode 100644 index 0000000..49d11e7 --- /dev/null +++ b/SDK_2.0/SDK/query.php @@ -0,0 +1,17 @@ +queryOrder($trade_no); +}catch(Exception $e){ + echo $e->getMessage(); + exit; +} + +print_r($result); \ No newline at end of file diff --git a/SDK_2.0/SDK/refund.php b/SDK_2.0/SDK/refund.php new file mode 100644 index 0000000..05ce231 --- /dev/null +++ b/SDK_2.0/SDK/refund.php @@ -0,0 +1,19 @@ +refund($out_refund_no, $trade_no, $money); +}catch(Exception $e){ + echo $e->getMessage(); + exit; +} + +print_r($result); \ No newline at end of file diff --git a/SDK_2.0/SDK/return_url.php b/SDK_2.0/SDK/return_url.php new file mode 100644 index 0000000..3ae3367 --- /dev/null +++ b/SDK_2.0/SDK/return_url.php @@ -0,0 +1,55 @@ + + + + + + 支付返回页面 + + +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 "

验证成功


"; +} +else { + //验证失败 + echo "

验证失败

"; +} +?> + + \ No newline at end of file diff --git a/demo/frontend/package-lock.json b/demo/frontend/package-lock.json index e85f856..09cc787 100644 --- a/demo/frontend/package-lock.json +++ b/demo/frontend/package-lock.json @@ -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", diff --git a/demo/frontend/package.json b/demo/frontend/package.json index a22cfad..fd8873d 100644 --- a/demo/frontend/package.json +++ b/demo/frontend/package.json @@ -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" } - - - diff --git a/demo/frontend/src/directives/lazyLoad.js b/demo/frontend/src/directives/lazyLoad.js new file mode 100644 index 0000000..d83695f --- /dev/null +++ b/demo/frontend/src/directives/lazyLoad.js @@ -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) + } +} diff --git a/demo/frontend/src/locales/en.js b/demo/frontend/src/locales/en.js index 7376881..84fd24c 100644 --- a/demo/frontend/src/locales/en.js +++ b/demo/frontend/src/locales/en.js @@ -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: { diff --git a/demo/frontend/src/locales/zh.js b/demo/frontend/src/locales/zh.js index 9dea267..798673a 100644 --- a/demo/frontend/src/locales/zh.js +++ b/demo/frontend/src/locales/zh.js @@ -495,7 +495,9 @@ export default { videoFileNotExist: '视频文件可能不存在或已被删除', retry: '重试', deleteFailedWork: '删除此作品', - deleteFailedWorkConfirm: '此作品视频加载失败,确定要删除吗?删除后无法恢复。' + deleteFailedWorkConfirm: '此作品视频加载失败,确定要删除吗?删除后无法恢复。', + readyToGenerateVideo: '已填充分镜图,可以开始生成视频', + noDownloadUrl: '没有可下载的文件' }, subscription: { diff --git a/demo/frontend/src/main.js b/demo/frontend/src/main.js index 0ab42a7..c9d748f 100644 --- a/demo/frontend/src/main.js +++ b/demo/frontend/src/main.js @@ -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) diff --git a/demo/frontend/src/views/ImageToVideo.vue b/demo/frontend/src/views/ImageToVideo.vue index b0a7011..59fa78c 100644 --- a/demo/frontend/src/views/ImageToVideo.vue +++ b/demo/frontend/src/views/ImageToVideo.vue @@ -63,7 +63,7 @@ @@ -287,6 +287,31 @@ onMounted(() => {