merge: 合并远程仓库历史
29
.claude/settings.local.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(netstat:*)",
|
||||||
|
"Bash(findstr:*)",
|
||||||
|
"Bash(tasklist:*)",
|
||||||
|
"Bash(taskkill /F /PID 34568)",
|
||||||
|
"Bash(powershell \"Stop-Process -Id 34568 -Force\")",
|
||||||
|
"Bash(mvn clean:*)",
|
||||||
|
"Bash(cmd /c \"mvn clean\")",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(nul)",
|
||||||
|
"Bash(.mvnw.cmd clean package:*)",
|
||||||
|
"Bash(powershell -Command \"./mvnw.cmd clean package -DskipTests\")",
|
||||||
|
"Bash(powershell -Command:*)",
|
||||||
|
"Bash(timeout /t 3 /nobreak)",
|
||||||
|
"Bash(git rm:*)",
|
||||||
|
"Bash(if exist .dockerignore del .dockerignore)",
|
||||||
|
"Bash(if exist Dockerfile del Dockerfile)",
|
||||||
|
"Bash(if exist nginx.conf del nginx.conf)",
|
||||||
|
"Bash(if exist Dockerfile.java21 del Dockerfile.java21)",
|
||||||
|
"Bash(if exist Dockerfile.linux del Dockerfile.linux)",
|
||||||
|
"Bash(if exist docker-compose.example.yml del docker-compose.example.yml)",
|
||||||
|
"Bash(if exist nginx rmdir /s /q nginx)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
30
demo/.claude/settings.local.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(mvn clean package:*)",
|
||||||
|
"Bash(powershell.exe -ExecutionPolicy Bypass -File deploy.ps1)",
|
||||||
|
"Bash(mvn clean spring-boot:run:*)",
|
||||||
|
"Bash(where:*)",
|
||||||
|
"Bash(cmd.exe /c mvnw.cmd clean spring-boot:run -DskipTests)",
|
||||||
|
"Bash(tasklist:*)",
|
||||||
|
"Bash(netstat:*)",
|
||||||
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(powershell.exe -Command \"cd ''C:\\Users\\UI\\Desktop\\AIGC\\demo''; mvn clean compile -DskipTests\")",
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(npm cache clean:*)",
|
||||||
|
"Bash(npm run dev:*)",
|
||||||
|
"Bash(timeout:*)",
|
||||||
|
"Bash(powershell.exe -Command \"Start-Sleep -Seconds 5\")",
|
||||||
|
"Bash(findstr:*)",
|
||||||
|
"Bash(powershell.exe -Command \"Get-Process node | Stop-Process -Force\")",
|
||||||
|
"Bash(npm run build:*)",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(cmd.exe /c mvnw.cmd clean install -DskipTests)",
|
||||||
|
"Bash(powershell.exe -Command \"cd ''C:\\Users\\UI\\Desktop\\AIGC\\demo''; .\\mvnw.cmd clean compile -DskipTests\")",
|
||||||
|
"Bash(powershell.exe -Command \"Get-Process java | Stop-Process -Force\")",
|
||||||
|
"Bash(powershell.exe -Command \"Start-Sleep -Seconds 3\")"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
2
demo/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/mvnw text eol=lf
|
||||||
|
*.cmd text eol=crlf
|
||||||
82
demo/.gitignore
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### 敏感配置文件 ###
|
||||||
|
# 开发环境配置(包含敏感信息)
|
||||||
|
src/main/resources/application-dev.properties
|
||||||
|
# 生产环境配置(包含敏感信息)
|
||||||
|
src/main/resources/application-prod.properties
|
||||||
|
# PayPal配置文件
|
||||||
|
application.properties.paypal-config
|
||||||
|
# 环境变量文件
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
config/.env
|
||||||
|
|
||||||
|
### 上传文件和临时文件 ###
|
||||||
|
uploads/
|
||||||
|
temp/
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
### 构建产物 ###
|
||||||
|
target/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/node_modules/
|
||||||
|
|
||||||
|
### 数据库相关 ###
|
||||||
|
*.sql.backup
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
### IDE和编辑器 ###
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
### 测试文件 ###
|
||||||
|
test_*.py
|
||||||
|
test_*.java
|
||||||
|
test_*.html
|
||||||
|
test_*.sh
|
||||||
|
test_*.bat
|
||||||
|
*.test.*
|
||||||
|
|
||||||
|
### 备份文件 ###
|
||||||
|
*.backup
|
||||||
|
*.bak
|
||||||
|
*~
|
||||||
81
demo/config/examples/application-prod.properties.example
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# ============================================
|
||||||
|
# AIGC平台生产环境配置文件模板
|
||||||
|
# ============================================
|
||||||
|
# 使用说明:
|
||||||
|
# 1. 将此文件复制到服务器:/www/server/aigc-backend/application-prod.properties
|
||||||
|
# 2. 修改下面标记为【必改】的配置项
|
||||||
|
# 3. 根据需要修改【可选】配置项
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 服务器配置
|
||||||
|
# ============================================
|
||||||
|
server.port=8080
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 数据库配置【必改】
|
||||||
|
# ============================================
|
||||||
|
spring.datasource.url=jdbc:mysql://localhost:3306/aigc_platform?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||||
|
spring.datasource.username=aigc_platform
|
||||||
|
spring.datasource.password=YOUR_DB_PASSWORD
|
||||||
|
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# JPA配置
|
||||||
|
# ============================================
|
||||||
|
spring.jpa.hibernate.ddl-auto=none
|
||||||
|
spring.jpa.show-sql=false
|
||||||
|
spring.jpa.properties.hibernate.format_sql=false
|
||||||
|
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 日志配置
|
||||||
|
# ============================================
|
||||||
|
logging.level.root=INFO
|
||||||
|
logging.level.com.example=INFO
|
||||||
|
logging.file.name=/www/server/aigc-backend/logs/app.log
|
||||||
|
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
|
||||||
|
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 文件上传配置【可选修改】
|
||||||
|
# ============================================
|
||||||
|
spring.servlet.multipart.max-file-size=100MB
|
||||||
|
spring.servlet.multipart.max-request-size=100MB
|
||||||
|
|
||||||
|
# 文件存储路径
|
||||||
|
file.upload-dir=/www/server/aigc-backend/uploads
|
||||||
|
|
||||||
|
# 临时文件目录
|
||||||
|
app.temp.dir=/www/server/aigc-backend/temp
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 腾讯云配置【如使用腾讯云存储,必改】
|
||||||
|
# ============================================
|
||||||
|
# 腾讯云API密钥
|
||||||
|
# tencent.cloud.secret-id=YOUR_SECRET_ID
|
||||||
|
# tencent.cloud.secret-key=YOUR_SECRET_KEY
|
||||||
|
|
||||||
|
# 腾讯云区域
|
||||||
|
# tencent.cloud.region=ap-guangzhou
|
||||||
|
|
||||||
|
# COS对象存储配置
|
||||||
|
# tencent.cos.bucket-name=YOUR_BUCKET_NAME
|
||||||
|
# tencent.cos.region=ap-guangzhou
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# CORS跨域配置【根据前端域名修改】
|
||||||
|
# ============================================
|
||||||
|
# cors.allowed-origins=https://your-domain.com,http://your-domain.com
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 应用配置【可选】
|
||||||
|
# ============================================
|
||||||
|
# 应用名称
|
||||||
|
spring.application.name=aigc-platform
|
||||||
|
|
||||||
|
# 启用压缩
|
||||||
|
server.compression.enabled=true
|
||||||
|
server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
|
||||||
|
|
||||||
|
# Session配置
|
||||||
|
server.servlet.session.timeout=30m
|
||||||
43
demo/config/examples/env.example
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# 环境变量配置示例
|
||||||
|
# 复制此文件为 .env 并根据实际情况修改
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
DB_URL=jdbc:mysql://localhost:3306/aigc?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=your_database_password
|
||||||
|
|
||||||
|
# JWT配置
|
||||||
|
JWT_SECRET=your-very-long-and-secure-jwt-secret-key-at-least-256-bits-long
|
||||||
|
JWT_EXPIRATION=604800000
|
||||||
|
|
||||||
|
# 支付宝配置
|
||||||
|
ALIPAY_APP_ID=your_alipay_app_id
|
||||||
|
ALIPAY_PRIVATE_KEY=your_alipay_private_key
|
||||||
|
ALIPAY_PUBLIC_KEY=alipay_public_key
|
||||||
|
ALIPAY_NOTIFY_URL=https://yourdomain.com/api/payments/alipay/notify
|
||||||
|
ALIPAY_RETURN_URL=https://yourdomain.com/api/payments/alipay/return
|
||||||
|
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_FILE_PATH=./logs/application.log
|
||||||
|
|
||||||
|
# 服务器配置
|
||||||
|
SERVER_PORT=8080
|
||||||
|
SERVER_CONTEXT_PATH=/
|
||||||
|
|
||||||
|
# 邮件配置(可选)
|
||||||
|
MAIL_HOST=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USERNAME=your_email@gmail.com
|
||||||
|
MAIL_PASSWORD=your_email_password
|
||||||
|
|
||||||
|
# Redis配置(可选)
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=your_redis_password
|
||||||
|
|
||||||
|
# 文件上传配置
|
||||||
|
UPLOAD_PATH=./uploads
|
||||||
|
MAX_FILE_SIZE=10MB
|
||||||
|
|
||||||
|
|
||||||
29
demo/config/examples/frpc.ini.example
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# FRP 客户端配置文件示例
|
||||||
|
# 使用 OpenFrp 或其他免费 FRP 服务时使用此配置
|
||||||
|
|
||||||
|
[common]
|
||||||
|
# 服务器地址(从 FRP 服务提供商控制台获取)
|
||||||
|
server_addr = frp.example.com
|
||||||
|
# 服务器端口(通常是 7000)
|
||||||
|
server_port = 7000
|
||||||
|
# 认证 token(从 FRP 服务提供商控制台获取)
|
||||||
|
token = your_token_here
|
||||||
|
|
||||||
|
[payment]
|
||||||
|
# 隧道类型:http
|
||||||
|
type = http
|
||||||
|
# 本地 IP(通常是 127.0.0.1)
|
||||||
|
local_ip = 127.0.0.1
|
||||||
|
# 本地端口(Spring Boot 运行端口)
|
||||||
|
local_port = 8080
|
||||||
|
# 自定义域名(从 FRP 服务提供商控制台获取)
|
||||||
|
custom_domains = your-domain.openfrp.net
|
||||||
|
|
||||||
|
# 如果需要多个服务,可以添加更多配置段
|
||||||
|
# [other-service]
|
||||||
|
# type = http
|
||||||
|
# local_ip = 127.0.0.1
|
||||||
|
# local_port = 3000
|
||||||
|
# custom_domains = other-domain.openfrp.net
|
||||||
|
|
||||||
|
|
||||||
72
demo/config/examples/paypal-config.properties.example
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# ============================================
|
||||||
|
# PayPal支付配置示例
|
||||||
|
# ============================================
|
||||||
|
# 使用说明:
|
||||||
|
# 1. 将PayPal配置添加到 application-prod.properties 或 application-dev.properties 文件中
|
||||||
|
# 2. 从PayPal开发者平台获取Client ID和Client Secret
|
||||||
|
# 3. 根据环境选择sandbox(测试)或live(生产)模式
|
||||||
|
#
|
||||||
|
# PayPal开发者平台: https://developer.paypal.com/
|
||||||
|
# - 登录后在 Dashboard > My Apps & Credentials 中创建应用
|
||||||
|
# - 获取 Client ID 和 Secret
|
||||||
|
# - Sandbox环境用于测试,Live环境用于生产
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# PayPal基础配置
|
||||||
|
# ============================================
|
||||||
|
# PayPal Client ID(必填)
|
||||||
|
# 测试环境示例:
|
||||||
|
# paypal.client-id=AeXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
paypal.client-id=YOUR_PAYPAL_CLIENT_ID
|
||||||
|
|
||||||
|
# PayPal Client Secret(必填)
|
||||||
|
# 测试环境示例:
|
||||||
|
# paypal.client-secret=EXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
paypal.client-secret=YOUR_PAYPAL_CLIENT_SECRET
|
||||||
|
|
||||||
|
# PayPal模式(必填)
|
||||||
|
# sandbox: 测试环境(推荐先使用测试环境)
|
||||||
|
# live: 生产环境(正式上线后使用)
|
||||||
|
paypal.mode=sandbox
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# PayPal回调URL配置
|
||||||
|
# ============================================
|
||||||
|
# 支付成功后的返回URL(必填)
|
||||||
|
# 本地开发:
|
||||||
|
# paypal.success-url=http://localhost:8080/api/payment/paypal/success
|
||||||
|
# 生产环境:
|
||||||
|
paypal.success-url=https://your-domain.com/api/payment/paypal/success
|
||||||
|
|
||||||
|
# 支付取消后的返回URL(必填)
|
||||||
|
# 本地开发:
|
||||||
|
# paypal.cancel-url=http://localhost:8080/api/payment/paypal/cancel
|
||||||
|
# 生产环境:
|
||||||
|
paypal.cancel-url=https://your-domain.com/api/payment/paypal/cancel
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 重要提示
|
||||||
|
# ============================================
|
||||||
|
# 1. 测试账号:
|
||||||
|
# - 在PayPal Sandbox中可以创建测试买家和卖家账号
|
||||||
|
# - 测试账号信息在 Dashboard > Sandbox > Accounts 中查看
|
||||||
|
#
|
||||||
|
# 2. 货币支持:
|
||||||
|
# - PayPal不直接支持CNY(人民币)
|
||||||
|
# - 系统会自动将CNY转换为USD
|
||||||
|
# - 建议在前端显示时做汇率转换说明
|
||||||
|
#
|
||||||
|
# 3. 回调URL要求:
|
||||||
|
# - 必须是公网可访问的HTTPS地址(生产环境)
|
||||||
|
# - 本地测试可使用HTTP
|
||||||
|
# - 可使用ngrok等工具将本地服务暴露到公网进行测试
|
||||||
|
#
|
||||||
|
# 4. Webhook配置(可选但推荐):
|
||||||
|
# - 在PayPal应用设置中配置Webhook URL
|
||||||
|
# - 用于接收支付状态变更通知
|
||||||
|
# - URL格式: https://your-domain.com/api/payment/paypal/webhook
|
||||||
|
#
|
||||||
|
# 5. 安全建议:
|
||||||
|
# - 不要将此配置文件提交到版本控制系统
|
||||||
|
# - 生产环境的Client Secret必须妥善保管
|
||||||
|
# - 定期更新API凭证
|
||||||
42
demo/config/examples/tencent-config-template.properties
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 腾讯云邮件推送服务配置模板
|
||||||
|
# 请根据您的腾讯云账号信息填写以下配置
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 1. API密钥配置(必填)
|
||||||
|
# ===========================================
|
||||||
|
# 在腾讯云控制台 → 访问管理 → API密钥管理 中获取
|
||||||
|
tencent.cloud.secret-id=请填写您的SecretId
|
||||||
|
tencent.cloud.secret-key=请填写您的SecretKey
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 2. 邮件推送服务配置(必填)
|
||||||
|
# ===========================================
|
||||||
|
# 服务地域(通常使用北京)
|
||||||
|
tencent.cloud.ses.region=ap-beijing
|
||||||
|
|
||||||
|
# 发件人邮箱(需要在腾讯云SES中验证)
|
||||||
|
tencent.cloud.ses.from-email=请填写您的发件人邮箱
|
||||||
|
|
||||||
|
# 发件人名称
|
||||||
|
tencent.cloud.ses.from-name=AIGC Demo
|
||||||
|
|
||||||
|
# 邮件模板ID(可选,如不使用模板可留空)
|
||||||
|
tencent.cloud.ses.template-id=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 3. 使用说明
|
||||||
|
# ===========================================
|
||||||
|
# 1. 复制此文件为 application-tencent.properties
|
||||||
|
# 2. 填写上述配置信息
|
||||||
|
# 3. 在 application.properties 中设置 spring.profiles.active=tencent
|
||||||
|
# 4. 重启应用即可使用腾讯云邮件服务
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 4. 配置示例
|
||||||
|
# ===========================================
|
||||||
|
# tencent.cloud.secret-id=AKID1234567890abcdef1234567890abcdef
|
||||||
|
# tencent.cloud.secret-key=abcdef1234567890abcdef1234567890
|
||||||
|
# tencent.cloud.ses.region=ap-beijing
|
||||||
|
# tencent.cloud.ses.from-email=noreply@yourdomain.com
|
||||||
|
# tencent.cloud.ses.from-name=AIGC Demo
|
||||||
|
# tencent.cloud.ses.template-id=123456
|
||||||
BIN
demo/database/backups/database_baota_20251108_160249.zip
Normal file
BIN
demo/database/backups/database_baota_deploy_.zip
Normal file
0
demo/docs/DATABASE_SCHEMA_GUIDE.md
Normal file
0
demo/docs/PAYMENT_EVENTS_FLOW.md
Normal file
109
demo/docs/storyboard-prompt-optimizer.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# 分镜图提示词优化器 - 系统指令
|
||||||
|
|
||||||
|
你是一个专业的**分镜图生成提示词优化师**。你的任务是将用户的自然语言描述和系统预设要求,整合优化成适合AI绘图模型(Banana)的高质量英文提示词。
|
||||||
|
|
||||||
|
## 输入内容
|
||||||
|
|
||||||
|
你会收到以下信息:
|
||||||
|
|
||||||
|
1. **系统引导词** `[SYSTEM]`:平台预设的风格、画质要求等(可能为空)
|
||||||
|
2. **用户提示** `[USER]`:用户用自然语言描述的图片需求
|
||||||
|
|
||||||
|
## 处理规则
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
系统安全规则 > 系统画面要求 > 用户个人描述
|
||||||
|
|
||||||
|
### 从用户描述提取
|
||||||
|
- 主体(人物/物体/场景)
|
||||||
|
- 动作/姿态/表情
|
||||||
|
- 环境/背景
|
||||||
|
- 情绪/氛围
|
||||||
|
- 色彩基调
|
||||||
|
|
||||||
|
### 安全过滤
|
||||||
|
自动移除以下内容,不做提示:
|
||||||
|
- 色情/裸露/性暗示
|
||||||
|
- 暴力/血腥/恐怖
|
||||||
|
- 政治敏感/仇恨内容
|
||||||
|
- 真人肖像侵权
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
|
||||||
|
**仅输出以下JSON,不要任何额外文字:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "英文提示词,逗号分隔,包含主体、场景、风格、光影、画质、构图等",
|
||||||
|
"ratio": "推荐画幅比例:1:1 / 16:9 / 9:16",
|
||||||
|
"note": "简短备注(可选,中英文均可)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 提示词结构参考
|
||||||
|
|
||||||
|
按以下顺序组织 prompt:
|
||||||
|
1. **主体描述**:who/what,外貌特征、服装
|
||||||
|
2. **动作姿态**:doing what,表情
|
||||||
|
3. **场景环境**:where,背景细节
|
||||||
|
4. **风格媒介**:anime style / realistic photo / digital art / cinematic 等
|
||||||
|
5. **光影氛围**:lighting, mood, time of day
|
||||||
|
6. **画质标签**:masterpiece, best quality, highly detailed, 8k, sharp focus
|
||||||
|
|
||||||
|
## 默认画质标签
|
||||||
|
|
||||||
|
始终在 prompt 末尾添加:
|
||||||
|
```
|
||||||
|
masterpiece, best quality, highly detailed, sharp focus, professional
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
```
|
||||||
|
[SYSTEM] 风格:电影感,高质量
|
||||||
|
[USER] 一个女孩在樱花树下看书
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "a young girl reading a book under cherry blossom trees, soft pink petals falling, peaceful spring afternoon, warm sunlight filtering through branches, cinematic composition, soft bokeh background, gentle breeze, serene atmosphere, pastel color palette, masterpiece, best quality, highly detailed, sharp focus, professional",
|
||||||
|
"ratio": "16:9",
|
||||||
|
"note": "已优化为电影感构图,柔和春日氛围"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
```
|
||||||
|
[SYSTEM]
|
||||||
|
[USER] 科幻城市夜景
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "futuristic cyberpunk cityscape at night, towering skyscrapers with neon lights, flying vehicles in the sky, holographic advertisements, rain-slicked streets reflecting colorful lights, dramatic atmosphere, wide angle shot, sci-fi aesthetic, dark blue and purple color scheme, masterpiece, best quality, highly detailed, sharp focus, professional",
|
||||||
|
"ratio": "16:9",
|
||||||
|
"note": "赛博朋克风格夜景,宽屏构图"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**输入:**
|
||||||
|
```
|
||||||
|
[SYSTEM] 二次元插画风格
|
||||||
|
[USER] 可爱的猫咪
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "adorable fluffy cat, big sparkling eyes, cute expression, soft fur texture, anime illustration style, pastel colors, warm lighting, cozy atmosphere, kawaii aesthetic, simple clean background, masterpiece, best quality, highly detailed, sharp focus, professional",
|
||||||
|
"ratio": "1:1",
|
||||||
|
"note": "二次元可爱风格,方形构图适合头像"
|
||||||
|
}
|
||||||
|
```
|
||||||
114
demo/docs/task_status_cascade_trigger.sql
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 任务状态级联更新触发器
|
||||||
|
--
|
||||||
|
-- 功能:当 task_status 表状态更新时,自动同步到:
|
||||||
|
-- - task_queue
|
||||||
|
-- - user_works
|
||||||
|
-- - text_to_video_tasks
|
||||||
|
-- - image_to_video_tasks
|
||||||
|
-- - storyboard_video_tasks
|
||||||
|
--
|
||||||
|
-- 执行方式:在 MySQL 客户端中执行此脚本
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 选择数据库
|
||||||
|
USE aigc_platform;
|
||||||
|
|
||||||
|
-- 先删除已存在的触发器
|
||||||
|
DROP TRIGGER IF EXISTS trg_task_status_update;
|
||||||
|
|
||||||
|
-- 修改分隔符
|
||||||
|
DELIMITER //
|
||||||
|
|
||||||
|
-- 创建触发器
|
||||||
|
-- 状态枚举说明:
|
||||||
|
-- task_status/task_queue: PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED, TIMEOUT
|
||||||
|
-- user_works: PROCESSING, COMPLETED, FAILED, DELETED (缺少 PENDING, CANCELLED, TIMEOUT)
|
||||||
|
-- 业务表: PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED (缺少 TIMEOUT)
|
||||||
|
--
|
||||||
|
-- 注意:分镜图(_image后缀)在task_status表中没有独立记录,无需处理
|
||||||
|
CREATE TRIGGER trg_task_status_update
|
||||||
|
AFTER UPDATE ON task_status
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
IF NEW.status <> OLD.status THEN
|
||||||
|
|
||||||
|
-- 1. 更新 task_queue 表(状态枚举一致,直接同步)
|
||||||
|
UPDATE task_queue
|
||||||
|
SET status = NEW.status,
|
||||||
|
updated_at = NOW(),
|
||||||
|
error_message = CASE WHEN NEW.status IN ('FAILED', 'TIMEOUT') THEN NEW.error_message ELSE error_message END
|
||||||
|
WHERE task_id = NEW.task_id;
|
||||||
|
|
||||||
|
-- 2. 更新 user_works 表(需要状态映射)
|
||||||
|
-- PENDING -> PROCESSING, CANCELLED/TIMEOUT -> FAILED
|
||||||
|
UPDATE user_works
|
||||||
|
SET status = CASE
|
||||||
|
WHEN NEW.status IN ('PENDING', 'PROCESSING') THEN 'PROCESSING'
|
||||||
|
WHEN NEW.status = 'COMPLETED' THEN 'COMPLETED'
|
||||||
|
WHEN NEW.status IN ('FAILED', 'CANCELLED', 'TIMEOUT') THEN 'FAILED'
|
||||||
|
ELSE status
|
||||||
|
END,
|
||||||
|
updated_at = NOW(),
|
||||||
|
result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END
|
||||||
|
WHERE task_id = NEW.task_id;
|
||||||
|
|
||||||
|
-- 3. 更新业务任务表(TIMEOUT 映射为 FAILED)
|
||||||
|
|
||||||
|
-- 文生视频 (txt2vid_*)
|
||||||
|
IF NEW.task_id LIKE 'txt2vid_%' THEN
|
||||||
|
UPDATE text_to_video_tasks
|
||||||
|
SET status = CASE WHEN NEW.status = 'TIMEOUT' THEN 'FAILED' ELSE NEW.status END,
|
||||||
|
updated_at = NOW(),
|
||||||
|
progress = CASE WHEN NEW.status = 'COMPLETED' THEN 100 ELSE progress END,
|
||||||
|
completed_at = CASE WHEN NEW.status IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT') THEN NOW() ELSE completed_at END,
|
||||||
|
error_message = CASE WHEN NEW.status IN ('FAILED', 'TIMEOUT') THEN COALESCE(NEW.error_message, '任务超时') ELSE error_message END,
|
||||||
|
result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END
|
||||||
|
WHERE task_id = NEW.task_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 图生视频 (img2vid_*)
|
||||||
|
IF NEW.task_id LIKE 'img2vid_%' THEN
|
||||||
|
UPDATE image_to_video_tasks
|
||||||
|
SET status = CASE WHEN NEW.status = 'TIMEOUT' THEN 'FAILED' ELSE NEW.status END,
|
||||||
|
updated_at = NOW(),
|
||||||
|
progress = CASE WHEN NEW.status = 'COMPLETED' THEN 100 ELSE progress END,
|
||||||
|
completed_at = CASE WHEN NEW.status IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT') THEN NOW() ELSE completed_at END,
|
||||||
|
error_message = CASE WHEN NEW.status IN ('FAILED', 'TIMEOUT') THEN COALESCE(NEW.error_message, '任务超时') ELSE error_message END,
|
||||||
|
result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END
|
||||||
|
WHERE task_id = NEW.task_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 分镜视频 (storyboard_*)
|
||||||
|
IF NEW.task_id LIKE 'storyboard_%' THEN
|
||||||
|
UPDATE storyboard_video_tasks
|
||||||
|
SET status = CASE WHEN NEW.status = 'TIMEOUT' THEN 'FAILED' ELSE NEW.status END,
|
||||||
|
updated_at = NOW(),
|
||||||
|
progress = CASE WHEN NEW.status = 'COMPLETED' THEN 100 ELSE progress END,
|
||||||
|
completed_at = CASE WHEN NEW.status IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT') THEN NOW() ELSE completed_at END,
|
||||||
|
error_message = CASE WHEN NEW.status IN ('FAILED', 'TIMEOUT') THEN COALESCE(NEW.error_message, '任务超时') ELSE error_message END,
|
||||||
|
result_url = CASE WHEN NEW.status = 'COMPLETED' AND NEW.result_url IS NOT NULL THEN NEW.result_url ELSE result_url END
|
||||||
|
WHERE task_id = NEW.task_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END IF;
|
||||||
|
END//
|
||||||
|
|
||||||
|
-- 恢复分隔符
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
-- 验证触发器已创建
|
||||||
|
SHOW TRIGGERS LIKE 'task_status';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 使用说明:
|
||||||
|
--
|
||||||
|
-- 1. 连接到 MySQL 数据库
|
||||||
|
-- 2. 选择对应的数据库: USE your_database_name;
|
||||||
|
-- 3. 执行此脚本
|
||||||
|
--
|
||||||
|
-- 注意事项:
|
||||||
|
-- - 触发器会在 task_status 表的 status 字段更新时自动执行
|
||||||
|
-- - 确保所有表都有 task_id 字段作为关联键
|
||||||
|
-- - 状态值使用字符串存储(如 'COMPLETED', 'FAILED' 等)
|
||||||
|
-- ============================================
|
||||||
370
demo/docs/数据库完整结构-宝塔导入.sql
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- AIGC平台数据库完整结构
|
||||||
|
-- 适用于宝塔面板部署
|
||||||
|
-- 创建时间:2025
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 设置字符集
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 1. 基础表结构(schema.sql)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(100) NOT NULL,
|
||||||
|
role VARCHAR(30) NOT NULL DEFAULT 'ROLE_USER',
|
||||||
|
points INT NOT NULL DEFAULT 50,
|
||||||
|
frozen_points INT NOT NULL DEFAULT 0 COMMENT '冻结积分',
|
||||||
|
phone VARCHAR(20),
|
||||||
|
avatar VARCHAR(500),
|
||||||
|
nickname VARCHAR(100),
|
||||||
|
gender VARCHAR(10),
|
||||||
|
birthday DATE,
|
||||||
|
address TEXT,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
last_login_at TIMESTAMP NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_email (email),
|
||||||
|
INDEX idx_username (username)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||||
|
|
||||||
|
-- 支付表
|
||||||
|
CREATE TABLE IF NOT EXISTS payments (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
order_id VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
amount DECIMAL(10,2) NOT NULL,
|
||||||
|
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
|
||||||
|
payment_method VARCHAR(20) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
description VARCHAR(500),
|
||||||
|
external_transaction_id VARCHAR(100),
|
||||||
|
callback_url VARCHAR(1000),
|
||||||
|
return_url VARCHAR(1000),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
paid_at TIMESTAMP NULL,
|
||||||
|
user_id BIGINT,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
INDEX idx_order_id (order_id),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付表';
|
||||||
|
|
||||||
|
-- 订单表
|
||||||
|
CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
order_number VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
total_amount DECIMAL(10,2) NOT NULL,
|
||||||
|
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
order_type VARCHAR(20) NOT NULL DEFAULT 'PRODUCT',
|
||||||
|
description VARCHAR(500),
|
||||||
|
notes TEXT,
|
||||||
|
shipping_address TEXT,
|
||||||
|
billing_address TEXT,
|
||||||
|
contact_phone VARCHAR(20),
|
||||||
|
contact_email VARCHAR(100),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
paid_at TIMESTAMP NULL,
|
||||||
|
shipped_at TIMESTAMP NULL,
|
||||||
|
delivered_at TIMESTAMP NULL,
|
||||||
|
cancelled_at TIMESTAMP NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
INDEX idx_order_number (order_number),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';
|
||||||
|
|
||||||
|
-- 订单项表
|
||||||
|
CREATE TABLE IF NOT EXISTS order_items (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
product_name VARCHAR(100) NOT NULL,
|
||||||
|
product_description VARCHAR(500),
|
||||||
|
product_sku VARCHAR(200),
|
||||||
|
unit_price DECIMAL(10,2) NOT NULL,
|
||||||
|
quantity INT NOT NULL,
|
||||||
|
subtotal DECIMAL(10,2) NOT NULL,
|
||||||
|
product_image VARCHAR(100),
|
||||||
|
order_id BIGINT NOT NULL,
|
||||||
|
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_order_id (order_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单项表';
|
||||||
|
|
||||||
|
-- 会员等级表
|
||||||
|
CREATE TABLE IF NOT EXISTS membership_levels (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
display_name VARCHAR(50) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
price DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||||
|
duration_days INT NOT NULL DEFAULT 30,
|
||||||
|
points_bonus INT NOT NULL DEFAULT 0,
|
||||||
|
features JSON,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_name (name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会员等级表';
|
||||||
|
|
||||||
|
-- 用户会员信息表
|
||||||
|
CREATE TABLE IF NOT EXISTS user_memberships (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
membership_level_id BIGINT NOT NULL,
|
||||||
|
start_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
end_date TIMESTAMP NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (membership_level_id) REFERENCES membership_levels(id),
|
||||||
|
UNIQUE KEY unique_active_membership (user_id, status),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户会员信息表';
|
||||||
|
|
||||||
|
-- 视频生成任务表(基础表)
|
||||||
|
CREATE TABLE IF NOT EXISTS video_tasks (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
task_id VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
task_type VARCHAR(50) NOT NULL,
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
input_text TEXT,
|
||||||
|
input_image_url VARCHAR(500),
|
||||||
|
output_video_url VARCHAR(500),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
progress INT NOT NULL DEFAULT 0,
|
||||||
|
error_message TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
completed_at TIMESTAMP NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_task_id (task_id),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='视频生成任务表';
|
||||||
|
|
||||||
|
-- 用户作品表(基础版)
|
||||||
|
CREATE TABLE IF NOT EXISTS user_works_base (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
work_type VARCHAR(50) NOT NULL,
|
||||||
|
cover_image VARCHAR(500),
|
||||||
|
video_url VARCHAR(500),
|
||||||
|
tags VARCHAR(500),
|
||||||
|
is_public BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
view_count INT NOT NULL DEFAULT 0,
|
||||||
|
like_count INT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_work_type (work_type)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户作品表(基础版)';
|
||||||
|
|
||||||
|
-- 系统配置表
|
||||||
|
CREATE TABLE IF NOT EXISTS system_configs (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
config_value TEXT,
|
||||||
|
description VARCHAR(500),
|
||||||
|
config_type VARCHAR(50) NOT NULL DEFAULT 'STRING',
|
||||||
|
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_config_key (config_key)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 2. 迁移表结构(Flyway Migration)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- V3: 任务队列表
|
||||||
|
CREATE TABLE IF NOT EXISTS task_queue (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(100) NOT NULL COMMENT '用户名',
|
||||||
|
task_id VARCHAR(50) NOT NULL UNIQUE COMMENT '任务ID',
|
||||||
|
task_type ENUM('TEXT_TO_VIDEO', 'IMAGE_TO_VIDEO') NOT NULL COMMENT '任务类型',
|
||||||
|
status ENUM('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT') NOT NULL DEFAULT 'PENDING' COMMENT '队列状态',
|
||||||
|
priority INT NOT NULL DEFAULT 0 COMMENT '优先级,数字越小优先级越高',
|
||||||
|
real_task_id VARCHAR(100) COMMENT '外部API返回的真实任务ID',
|
||||||
|
last_check_time DATETIME COMMENT '最后一次检查时间',
|
||||||
|
check_count INT NOT NULL DEFAULT 0 COMMENT '检查次数',
|
||||||
|
max_check_count INT NOT NULL DEFAULT 30 COMMENT '最大检查次数(30次 * 2分钟 = 60分钟)',
|
||||||
|
error_message TEXT COMMENT '错误信息',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
completed_at DATETIME COMMENT '完成时间',
|
||||||
|
INDEX idx_username_status (username, status),
|
||||||
|
INDEX idx_status_priority (status, priority),
|
||||||
|
INDEX idx_last_check_time (last_check_time),
|
||||||
|
INDEX idx_created_at (created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务队列表';
|
||||||
|
|
||||||
|
-- V4: 积分冻结记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS points_freeze_records (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(100) NOT NULL COMMENT '用户名',
|
||||||
|
task_id VARCHAR(50) NOT NULL UNIQUE COMMENT '任务ID',
|
||||||
|
task_type ENUM('TEXT_TO_VIDEO', 'IMAGE_TO_VIDEO') NOT NULL COMMENT '任务类型',
|
||||||
|
freeze_points INT NOT NULL COMMENT '冻结的积分数量',
|
||||||
|
status ENUM('FROZEN', 'DEDUCTED', 'RETURNED', 'EXPIRED') NOT NULL DEFAULT 'FROZEN' COMMENT '冻结状态',
|
||||||
|
freeze_reason VARCHAR(200) COMMENT '冻结原因',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
completed_at DATETIME COMMENT '完成时间',
|
||||||
|
INDEX idx_username_status (username, status),
|
||||||
|
INDEX idx_task_id (task_id),
|
||||||
|
INDEX idx_created_at (created_at),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='积分冻结记录表';
|
||||||
|
|
||||||
|
-- V5: 用户作品表(详细版)
|
||||||
|
CREATE TABLE IF NOT EXISTS user_works (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(100) NOT NULL COMMENT '用户名',
|
||||||
|
task_id VARCHAR(50) NOT NULL UNIQUE COMMENT '任务ID',
|
||||||
|
work_type ENUM('TEXT_TO_VIDEO', 'IMAGE_TO_VIDEO') NOT NULL COMMENT '作品类型',
|
||||||
|
title VARCHAR(200) COMMENT '作品标题',
|
||||||
|
description TEXT COMMENT '作品描述',
|
||||||
|
prompt TEXT COMMENT '生成提示词',
|
||||||
|
result_url VARCHAR(500) COMMENT '结果视频URL',
|
||||||
|
thumbnail_url VARCHAR(500) COMMENT '缩略图URL',
|
||||||
|
duration VARCHAR(10) COMMENT '视频时长',
|
||||||
|
aspect_ratio VARCHAR(10) COMMENT '宽高比',
|
||||||
|
quality VARCHAR(20) COMMENT '画质',
|
||||||
|
file_size VARCHAR(20) COMMENT '文件大小',
|
||||||
|
points_cost INT NOT NULL DEFAULT 0 COMMENT '消耗积分',
|
||||||
|
status ENUM('PROCESSING', 'COMPLETED', 'FAILED', 'DELETED') NOT NULL DEFAULT 'PROCESSING' COMMENT '作品状态',
|
||||||
|
is_public BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否公开',
|
||||||
|
view_count INT NOT NULL DEFAULT 0 COMMENT '浏览次数',
|
||||||
|
like_count INT NOT NULL DEFAULT 0 COMMENT '点赞次数',
|
||||||
|
download_count INT NOT NULL DEFAULT 0 COMMENT '下载次数',
|
||||||
|
tags VARCHAR(500) COMMENT '标签',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
completed_at DATETIME COMMENT '完成时间',
|
||||||
|
INDEX idx_username_status (username, status),
|
||||||
|
INDEX idx_task_id (task_id),
|
||||||
|
INDEX idx_work_type (work_type),
|
||||||
|
INDEX idx_is_public_status (is_public, status),
|
||||||
|
INDEX idx_created_at (created_at),
|
||||||
|
INDEX idx_view_count (view_count),
|
||||||
|
INDEX idx_like_count (like_count),
|
||||||
|
INDEX idx_tags (tags),
|
||||||
|
INDEX idx_prompt (prompt(100))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户作品表';
|
||||||
|
|
||||||
|
-- V6: 任务状态表
|
||||||
|
CREATE TABLE IF NOT EXISTS task_status (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
task_id VARCHAR(255) NOT NULL COMMENT '任务ID',
|
||||||
|
username VARCHAR(255) NOT NULL COMMENT '用户名',
|
||||||
|
task_type VARCHAR(50) NOT NULL COMMENT '任务类型',
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态',
|
||||||
|
progress INT DEFAULT 0 COMMENT '进度百分比',
|
||||||
|
result_url TEXT COMMENT '结果URL',
|
||||||
|
error_message TEXT COMMENT '错误信息',
|
||||||
|
external_task_id VARCHAR(255) COMMENT '外部任务ID',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
completed_at TIMESTAMP NULL COMMENT '完成时间',
|
||||||
|
last_polled_at TIMESTAMP NULL COMMENT '最后轮询时间',
|
||||||
|
poll_count INT DEFAULT 0 COMMENT '轮询次数',
|
||||||
|
max_polls INT DEFAULT 60 COMMENT '最大轮询次数(2小时)',
|
||||||
|
INDEX idx_task_id (task_id),
|
||||||
|
INDEX idx_username (username),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_created_at (created_at),
|
||||||
|
INDEX idx_last_polled (last_polled_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务状态表';
|
||||||
|
|
||||||
|
-- V7: 任务清理表
|
||||||
|
-- 成功任务归档表
|
||||||
|
CREATE TABLE IF NOT EXISTS completed_tasks_archive (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
task_id VARCHAR(255) NOT NULL,
|
||||||
|
username VARCHAR(255) NOT NULL,
|
||||||
|
task_type VARCHAR(50) NOT NULL,
|
||||||
|
prompt TEXT,
|
||||||
|
aspect_ratio VARCHAR(20),
|
||||||
|
duration INT,
|
||||||
|
hd_mode BOOLEAN DEFAULT FALSE,
|
||||||
|
result_url TEXT,
|
||||||
|
real_task_id VARCHAR(255),
|
||||||
|
progress INT DEFAULT 100,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
completed_at TIMESTAMP NOT NULL,
|
||||||
|
archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
points_cost INT DEFAULT 0,
|
||||||
|
INDEX idx_username (username),
|
||||||
|
INDEX idx_task_type (task_type),
|
||||||
|
INDEX idx_created_at (created_at),
|
||||||
|
INDEX idx_completed_at (completed_at),
|
||||||
|
INDEX idx_archived_at (archived_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='成功任务归档表';
|
||||||
|
|
||||||
|
-- 失败任务清理日志表
|
||||||
|
CREATE TABLE IF NOT EXISTS failed_tasks_cleanup_log (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
task_id VARCHAR(255) NOT NULL,
|
||||||
|
username VARCHAR(255) NOT NULL,
|
||||||
|
task_type VARCHAR(50) NOT NULL,
|
||||||
|
error_message TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
failed_at TIMESTAMP NOT NULL,
|
||||||
|
cleaned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_username (username),
|
||||||
|
INDEX idx_task_type (task_type),
|
||||||
|
INDEX idx_cleaned_at (cleaned_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='失败任务清理日志表';
|
||||||
|
|
||||||
|
-- V8: 分镜视频任务表
|
||||||
|
CREATE TABLE IF NOT EXISTS storyboard_video_tasks (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
task_id VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
username VARCHAR(100) NOT NULL,
|
||||||
|
prompt TEXT,
|
||||||
|
image_url VARCHAR(500),
|
||||||
|
aspect_ratio VARCHAR(10) NOT NULL,
|
||||||
|
hd_mode BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
progress INT NOT NULL DEFAULT 0,
|
||||||
|
result_url VARCHAR(500),
|
||||||
|
real_task_id VARCHAR(255),
|
||||||
|
error_message TEXT,
|
||||||
|
cost_points INT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
completed_at TIMESTAMP,
|
||||||
|
INDEX idx_username (username),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_task_id (task_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分镜视频任务表';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 完成
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- 显示所有表
|
||||||
|
SHOW TABLES;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
299
demo/docs/环境变量配置说明-宝塔部署.md
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# 环境变量配置说明 - 宝塔部署
|
||||||
|
|
||||||
|
## 📋 必需的环境变量
|
||||||
|
|
||||||
|
### 1. 数据库配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# MySQL数据库主机地址(本地数据库使用127.0.0.1或localhost)
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
|
||||||
|
# MySQL数据库端口(默认3306)
|
||||||
|
DB_PORT=3306
|
||||||
|
|
||||||
|
# 数据库名称
|
||||||
|
DB_NAME=aigc_platform
|
||||||
|
|
||||||
|
# 数据库用户名
|
||||||
|
DB_USERNAME=aigc_user
|
||||||
|
|
||||||
|
# 数据库密码(请替换为你的实际密码)
|
||||||
|
DB_PASSWORD=your_secure_password_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. JWT配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JWT密钥(请生成一个足够长的随机字符串,至少64字符)
|
||||||
|
JWT_SECRET=your-very-long-secret-key-at-least-64-characters-for-production-security
|
||||||
|
|
||||||
|
# JWT过期时间(毫秒,默认604800000=7天)
|
||||||
|
JWT_EXPIRATION=604800000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. AI API配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# AI API基础URL
|
||||||
|
AI_API_BASE_URL=https://ai.comfly.chat
|
||||||
|
AI_API_KEY=your_ai_api_key
|
||||||
|
|
||||||
|
# 图片生成API配置
|
||||||
|
AI_IMAGE_API_BASE_URL=https://ai.comfly.chat
|
||||||
|
AI_IMAGE_API_KEY=your_ai_image_api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 支付宝配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 支付宝应用ID
|
||||||
|
ALIPAY_APP_ID=your_alipay_app_id
|
||||||
|
|
||||||
|
# 支付宝私钥
|
||||||
|
ALIPAY_PRIVATE_KEY=your_alipay_private_key
|
||||||
|
|
||||||
|
# 支付宝公钥
|
||||||
|
ALIPAY_PUBLIC_KEY=your_alipay_public_key
|
||||||
|
|
||||||
|
# 域名配置
|
||||||
|
ALIPAY_DOMAIN=https://yourdomain.com
|
||||||
|
|
||||||
|
# 回调URL
|
||||||
|
ALIPAY_NOTIFY_URL=https://yourdomain.com/api/payments/alipay/notify
|
||||||
|
ALIPAY_RETURN_URL=https://yourdomain.com/payment/success
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 腾讯云SES邮件服务配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 腾讯云SES Secret ID
|
||||||
|
TENCENT_SES_SECRET_ID=your_tencent_ses_secret_id
|
||||||
|
|
||||||
|
# 腾讯云SES Secret Key
|
||||||
|
TENCENT_SES_SECRET_KEY=your_tencent_ses_secret_key
|
||||||
|
|
||||||
|
# 发件邮箱
|
||||||
|
TENCENT_SES_FROM_EMAIL=noreply@yourdomain.com
|
||||||
|
|
||||||
|
# 邮件模板ID
|
||||||
|
TENCENT_SES_TEMPLATE_ID=your_template_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 文件路径配置(可选)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# FFmpeg可执行文件路径(Linux服务器)
|
||||||
|
FFMPEG_PATH=/usr/bin/ffmpeg
|
||||||
|
|
||||||
|
# 临时文件目录
|
||||||
|
TEMP_DIR=/app/temp
|
||||||
|
|
||||||
|
# 上传文件目录
|
||||||
|
UPLOAD_PATH=/app/uploads
|
||||||
|
|
||||||
|
# 日志文件路径
|
||||||
|
LOG_FILE_PATH=/app/logs/application.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 宝塔面板设置环境变量
|
||||||
|
|
||||||
|
### 方式一:通过启动脚本设置
|
||||||
|
|
||||||
|
在宝塔面板创建启动脚本 `start.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
export DB_HOST=127.0.0.1
|
||||||
|
export DB_PORT=3306
|
||||||
|
export DB_NAME=aigc_platform
|
||||||
|
export DB_USERNAME=aigc_user
|
||||||
|
export DB_PASSWORD=jRbHPZbbkdm24yTT
|
||||||
|
|
||||||
|
export JWT_SECRET=aigc-demo-secret-key-for-jwt-token-generation-2025-production-version-secure
|
||||||
|
|
||||||
|
export AI_API_BASE_URL=https://ai.comfly.chat
|
||||||
|
export AI_API_KEY=sk-xCX1X12e8Dpj4mRJKFMxFUnV29pzJQpPeuZFGqTwYOorjvOQ
|
||||||
|
|
||||||
|
export AI_IMAGE_API_BASE_URL=https://ai.comfly.chat
|
||||||
|
export AI_IMAGE_API_KEY=sk-xCX1X12e8Dpj4mRJKFMxFUnV29pzJQpPeuZFGqTwYOorjvOQ
|
||||||
|
|
||||||
|
export ALIPAY_APP_ID=your_app_id
|
||||||
|
export ALIPAY_PRIVATE_KEY=your_private_key
|
||||||
|
export ALIPAY_PUBLIC_KEY=your_public_key
|
||||||
|
export ALIPAY_DOMAIN=https://yourdomain.com
|
||||||
|
export ALIPAY_NOTIFY_URL=https://yourdomain.com/api/payments/alipay/notify
|
||||||
|
export ALIPAY_RETURN_URL=https://yourdomain.com/payment/success
|
||||||
|
|
||||||
|
export TENCENT_SES_SECRET_ID=your_secret_id
|
||||||
|
export TENCENT_SES_SECRET_KEY=your_secret_key
|
||||||
|
export TENCENT_SES_FROM_EMAIL=noreply@yourdomain.com
|
||||||
|
export TENCENT_SES_TEMPLATE_ID=your_template_id
|
||||||
|
|
||||||
|
export FFMPEG_PATH=/usr/bin/ffmpeg
|
||||||
|
export TEMP_DIR=/app/temp
|
||||||
|
export UPLOAD_PATH=/app/uploads
|
||||||
|
export LOG_FILE_PATH=/app/logs/application.log
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
java -jar -Dspring.profiles.active=prod demo-0.0.1-SNAPSHOT.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
赋予执行权限:
|
||||||
|
```bash
|
||||||
|
chmod +x start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:创建 .env 文件
|
||||||
|
|
||||||
|
创建 `.env` 文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 数据库配置
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=aigc_platform
|
||||||
|
DB_USERNAME=aigc_user
|
||||||
|
DB_PASSWORD=jRbHPZbbkdm24yTT
|
||||||
|
|
||||||
|
# JWT配置
|
||||||
|
JWT_SECRET=aigc-demo-secret-key-for-jwt-token-generation-2025-production-version-secure
|
||||||
|
JWT_EXPIRATION=604800000
|
||||||
|
|
||||||
|
# AI API配置
|
||||||
|
AI_API_BASE_URL=https://ai.comfly.chat
|
||||||
|
AI_API_KEY=sk-xCX1X12e8Dpj4mRJKFMxFUnV29pzJQpPeuZFGqTwYOorjvOQ
|
||||||
|
AI_IMAGE_API_BASE_URL=https://ai.comfly.chat
|
||||||
|
AI_IMAGE_API_KEY=sk-xCX1X12e8Dpj4mRJKFMxFUnV29pzJQpPeuZFGqTwYOorjvOQ
|
||||||
|
|
||||||
|
# 支付宝配置
|
||||||
|
ALIPAY_APP_ID=your_app_id
|
||||||
|
ALIPAY_PRIVATE_KEY=your_private_key
|
||||||
|
ALIPAY_PUBLIC_KEY=your_public_key
|
||||||
|
ALIPAY_DOMAIN=https://yourdomain.com
|
||||||
|
ALIPAY_NOTIFY_URL=https://yourdomain.com/api/payments/alipay/notify
|
||||||
|
ALIPAY_RETURN_URL=https://yourdomain.com/payment/success
|
||||||
|
|
||||||
|
# 腾讯云SES配置
|
||||||
|
TENCENT_SES_SECRET_ID=your_secret_id
|
||||||
|
TENCENT_SES_SECRET_KEY=your_secret_key
|
||||||
|
TENCENT_SES_FROM_EMAIL=noreply@yourdomain.com
|
||||||
|
TENCENT_SES_TEMPLATE_ID=your_template_id
|
||||||
|
|
||||||
|
# 文件路径配置
|
||||||
|
FFMPEG_PATH=/usr/bin/ffmpeg
|
||||||
|
TEMP_DIR=/app/temp
|
||||||
|
UPLOAD_PATH=/app/uploads
|
||||||
|
LOG_FILE_PATH=/app/logs/application.log
|
||||||
|
```
|
||||||
|
|
||||||
|
使用 `source` 加载环境变量后启动:
|
||||||
|
```bash
|
||||||
|
source .env && java -jar -Dspring.profiles.active=prod demo-0.0.1-SNAPSHOT.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式三:systemd服务配置
|
||||||
|
|
||||||
|
创建 systemd 服务文件 `/etc/systemd/system/aigc-platform.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=AIGC Platform Service
|
||||||
|
After=network.target mysql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=www
|
||||||
|
WorkingDirectory=/www/wwwroot/aigc-platform
|
||||||
|
ExecStart=/usr/bin/java -jar -Dspring.profiles.active=prod demo-0.0.1-SNAPSHOT.jar
|
||||||
|
|
||||||
|
# 环境变量配置
|
||||||
|
Environment="DB_HOST=127.0.0.1"
|
||||||
|
Environment="DB_PORT=3306"
|
||||||
|
Environment="DB_NAME=aigc_platform"
|
||||||
|
Environment="DB_USERNAME=aigc_user"
|
||||||
|
Environment="DB_PASSWORD=jRbHPZbbkdm24yTT"
|
||||||
|
Environment="JWT_SECRET=your-jwt-secret-key"
|
||||||
|
Environment="AI_API_BASE_URL=https://ai.comfly.chat"
|
||||||
|
Environment="AI_API_KEY=your_api_key"
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
启动服务:
|
||||||
|
```bash
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable aigc-platform
|
||||||
|
systemctl start aigc-platform
|
||||||
|
systemctl status aigc-platform
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 配置验证
|
||||||
|
|
||||||
|
启动应用后,检查日志确认数据库连接成功:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看启动日志
|
||||||
|
tail -f logs/application.log
|
||||||
|
|
||||||
|
# 应该看到类似的日志:
|
||||||
|
# INFO - HikariPool-1 - Starting...
|
||||||
|
# INFO - HikariPool-1 - Start completed.
|
||||||
|
# INFO - Started DemoApplication in X.XX seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
测试数据库连接:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/health/ping
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 安全建议
|
||||||
|
|
||||||
|
1. **不要将 .env 文件或包含密钥的启动脚本提交到Git**
|
||||||
|
```bash
|
||||||
|
echo ".env" >> .gitignore
|
||||||
|
echo "start.sh" >> .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **定期更换JWT密钥和数据库密码**
|
||||||
|
|
||||||
|
3. **生产环境使用强密码**
|
||||||
|
- 数据库密码:至少16位,包含大小写字母、数字、特殊字符
|
||||||
|
- JWT密钥:至少64字符随机字符串
|
||||||
|
|
||||||
|
4. **限制数据库访问**
|
||||||
|
- 仅允许应用服务器IP访问MySQL
|
||||||
|
- 不要使用root用户,创建专用数据库用户
|
||||||
|
|
||||||
|
5. **启用HTTPS**
|
||||||
|
- 生产环境务必配置SSL证书
|
||||||
|
- 在Nginx/宝塔面板配置HTTPS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 快速配置模板(本地测试用)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 最小化配置(使用默认值)
|
||||||
|
export DB_HOST=127.0.0.1
|
||||||
|
export DB_USERNAME=aigc_user
|
||||||
|
export DB_PASSWORD=jRbHPZbbkdm24yTT
|
||||||
|
export JWT_SECRET=aigc-demo-secret-key-for-jwt-token-generation-2025
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
java -jar -Dspring.profiles.active=prod demo-0.0.1-SNAPSHOT.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
16
demo/frontend/.claude/settings.local.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(if exist dist rmdir /s /q dist)",
|
||||||
|
"Bash(npm run build:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(npm run dev:*)",
|
||||||
|
"Bash(npm install)",
|
||||||
|
"Bash(npm cache clean:*)",
|
||||||
|
"Bash(git rm:*)",
|
||||||
|
"Bash(git checkout:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
3
demo/frontend/.env.development
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 开发环境配置(Vue CLI)
|
||||||
|
VUE_APP_API_URL=http://localhost:8080/api
|
||||||
|
NODE_ENV=development
|
||||||
3
demo/frontend/.env.development.vite
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 开发环境配置(Vite - 项目当前使用)
|
||||||
|
VITE_APP_API_URL=http://localhost:8080/api
|
||||||
|
NODE_ENV=development
|
||||||
2
demo/frontend/.env.production
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VUE_APP_API_URL=/api
|
||||||
|
NODE_ENV=production
|
||||||
4
demo/frontend/.env.production.vite
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Vite 环境变量(项目当前使用 Vite)
|
||||||
|
# Vite 使用 VITE_ 前缀,而不是 VUE_APP_
|
||||||
|
VITE_APP_API_URL=/api
|
||||||
|
NODE_ENV=production
|
||||||
40
demo/frontend/build-for-baota.bat
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
echo ========================================
|
||||||
|
echo 前端打包脚本 - Windows
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [1/3] 安装依赖...
|
||||||
|
call npm install
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 错误: 依赖安装失败
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [2/3] 构建生产版本...
|
||||||
|
call npm run build
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 错误: 构建失败
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo 打包完成!
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo 打包文件位置: frontend\dist\
|
||||||
|
echo.
|
||||||
|
echo 部署到宝塔步骤:
|
||||||
|
echo 1. 直接上传 frontend\dist 目录下的所有文件到服务器
|
||||||
|
echo 2. 上传到网站根目录(如:/www/wwwroot/your-domain)
|
||||||
|
echo 3. 在宝塔面板配置 Nginx 为 Vue Router 模式
|
||||||
|
echo.
|
||||||
|
echo 提示:可以使用宝塔面板的文件管理器,选择 dist 目录下所有文件上传
|
||||||
|
echo 或使用 FTP/SFTP 工具上传整个 dist 目录的内容
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
44
demo/frontend/build-for-baota.sh
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 前端打包脚本 - Linux
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " 前端打包脚本 - Linux"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo "[1/3] 安装依赖..."
|
||||||
|
npm install
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${RED}错误: 依赖安装失败${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[2/3] 构建生产版本..."
|
||||||
|
npm run build
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${RED}错误: 构建失败${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo " 打包完成!"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}打包文件位置: frontend/dist/${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "部署到宝塔步骤:"
|
||||||
|
echo "1. 直接上传 frontend/dist 目录下的所有文件到服务器"
|
||||||
|
echo "2. 上传到网站根目录(如:/www/wwwroot/your-domain)"
|
||||||
|
echo "3. 在宝塔面板配置 Nginx 为 Vue Router 模式"
|
||||||
|
echo ""
|
||||||
|
echo "上传命令示例:"
|
||||||
|
echo " scp -r dist/* root@your-server:/www/wwwroot/your-domain/"
|
||||||
|
echo ""
|
||||||
81
demo/frontend/dev-server.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
console.log(`${req.method} ${req.url}`);
|
||||||
|
|
||||||
|
// 设置CORS头
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filePath = req.url === '/' ? '/index.html' : req.url;
|
||||||
|
|
||||||
|
// 如果是API请求,代理到后端
|
||||||
|
if (req.url.startsWith('/api')) {
|
||||||
|
const http = require('http');
|
||||||
|
const options = {
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 8080,
|
||||||
|
path: req.url,
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyReq = http.request(options, (proxyRes) => {
|
||||||
|
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||||
|
proxyRes.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.pipe(proxyReq);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态文件服务
|
||||||
|
const fullPath = path.join(__dirname, 'dist', filePath);
|
||||||
|
|
||||||
|
fs.readFile(fullPath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>开发服务器</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>开发服务器正在运行</h1>
|
||||||
|
<p>端口: ${PORT}</p>
|
||||||
|
<p>请先运行 <code>npm run build</code> 构建项目</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(fullPath);
|
||||||
|
const contentType = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.js': 'text/javascript',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.json': 'application/json'
|
||||||
|
}[ext] || 'text/plain';
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
|
res.end(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`🚀 开发服务器运行在 http://localhost:${PORT}`);
|
||||||
|
console.log(`🌐 网络访问: http://0.0.0.0:${PORT}`);
|
||||||
|
});
|
||||||
BIN
demo/frontend/frontend-dist.zip
Normal file
17
demo/frontend/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AIGC Demo - Vue.js Frontend</title>
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
2331
demo/frontend/package-lock.json
generated
Normal file
35
demo/frontend/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "aigc-demo-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "AIGC Demo Frontend with Vue.js",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"serve": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.1.0",
|
||||||
|
"axios": "^1.5.0",
|
||||||
|
"element-plus": "^2.3.8",
|
||||||
|
"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",
|
||||||
|
"sass": "^1.66.1",
|
||||||
|
"terser": "^5.44.1",
|
||||||
|
"vite": "^4.4.9"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"vue",
|
||||||
|
"frontend",
|
||||||
|
"aigc"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
BIN
demo/frontend/public/fonts/TaipeiSansTC.ttf
Normal file
BIN
demo/frontend/public/images/backgrounds/1.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
16
demo/frontend/public/images/backgrounds/avatar-default.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<svg width="104" height="104" viewBox="0 0 104 104" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_601_168)">
|
||||||
|
<circle cx="52" cy="52" r="51.5" fill="#1BAFFF" stroke="white"/>
|
||||||
|
<path d="M16.9548 9.33656C10.1123 10.3489 3.30243 13.5493 -1.94346 18.2193C-7.90619 23.5097 -12.1746 31.8372 -13.2498 40.3607C-13.5431 42.6467 -13.7386 43.2672 -14.1622 43.2672C-15.4003 43.2672 -20.2226 45.2592 -22.5686 46.7288C-34.7873 54.3379 -38.6647 70.601 -31.2032 82.8801C-27.1303 89.5094 -20.5485 94.0161 -13.2173 95.1917L-10.7409 95.5836L-9.6331 98.0329C-5.69053 106.818 1.83619 113.12 10.9595 115.243C16.3031 116.484 22.0378 116.19 27.3488 114.427C32.8554 112.565 37.7755 109.136 41.1967 104.728L43.1843 102.148L45.1067 102.899C48.3976 104.27 51.6559 104.76 55.8591 104.564C60.2904 104.368 62.5061 103.813 66.4487 101.919C75.018 97.739 80.4594 89.4115 81.1437 79.2878L81.3718 76.218L83.4897 74.1933C87.3345 70.5031 89.7131 66.4536 91.0816 61.3918C92.0591 57.7342 92.0591 51.9866 91.0816 48.329C88.5727 38.8585 81.665 31.8372 72.2485 29.1593C70.1631 28.5715 68.925 28.4409 65.3083 28.4735C61.4634 28.4735 60.5511 28.6042 58.14 29.3553C56.6085 29.8451 55.1423 30.3023 54.8816 30.3677C54.5558 30.4983 54.1648 30.0084 53.6435 28.8981C47.4527 15.574 31.715 7.18121 16.9548 9.33656ZM20.6693 43.5937C23.113 44.2795 24.7748 45.6838 25.9152 48.0351C26.644 49.5175 27.023 51.1474 27.023 52.7993V62.5348V72.2702C27.023 73.9221 26.644 75.552 25.9152 77.0345C24.7422 79.4511 23.0804 80.8227 20.5064 81.5085C18.8772 81.9657 15.4885 81.6718 14.0875 80.9533C12.2954 80.0716 10.6011 78.1775 9.91682 76.3487C9.29774 74.7158 9.26515 73.7688 9.26515 62.7307C9.26515 49.472 9.3629 48.6556 11.4156 46.2716C13.6313 43.659 17.2155 42.614 20.6693 43.5937ZM42.0438 43.757C44.1943 44.3775 46.1493 46.1083 47.0942 48.231C47.8762 49.9618 47.8762 49.9618 47.8762 62.5348C47.8762 75.1077 47.8762 75.1077 47.0942 76.8385C45.3022 80.8227 40.7405 82.6841 36.1137 81.3125C33.9306 80.6594 32.3015 79.2225 31.1936 76.9692L30.2813 75.1077V62.5348V49.9618L31.1936 48.1004C33.1486 44.0836 37.417 42.3854 42.0438 43.757Z" fill="#151515"/>
|
||||||
|
<path d="M20.6693 43.5937C23.113 44.2795 24.7748 45.6838 25.9152 48.0351C26.644 49.5175 27.023 51.1474 27.023 52.7993V62.5348V72.2702C27.023 73.9221 26.644 75.552 25.9152 77.0345C24.7422 79.4511 23.0804 80.8227 20.5064 81.5085C18.8772 81.9657 15.4885 81.6718 14.0875 80.9533C12.2954 80.0716 10.6011 78.1775 9.91682 76.3487C9.29774 74.7158 9.26515 73.7688 9.26515 62.7307C9.26515 49.472 9.3629 48.6556 11.4156 46.2716C13.6313 43.659 17.2155 42.614 20.6693 43.5937Z" fill="#151515"/>
|
||||||
|
<path d="M42.0438 43.757C44.1943 44.3775 46.1493 46.1083 47.0942 48.231C47.8762 49.9618 47.8762 49.9618 47.8762 62.5348C47.8762 75.1077 47.8762 75.1077 47.0942 76.8385C45.3022 80.8227 40.7405 82.6841 36.1137 81.3125C33.9306 80.6594 32.3015 79.2225 31.1936 76.9692L30.2813 75.1077V62.5348V49.9618L31.1936 48.1004C33.1486 44.0836 37.417 42.3854 42.0438 43.757Z" fill="#151515"/>
|
||||||
|
<rect x="36.3332" y="45.9877" width="7.92593" height="17.1728" rx="3.96296" fill="white"/>
|
||||||
|
<rect x="49.543" y="45.9875" width="7.92593" height="17.1728" rx="3.96296" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<rect x="0.5" y="0.5" width="103" height="103" rx="51.5" stroke="white"/>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_601_168">
|
||||||
|
<rect width="104" height="104" rx="52" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
233
demo/frontend/public/images/backgrounds/login-bg.svg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
demo/frontend/public/images/backgrounds/login_bg.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
15
demo/frontend/public/images/backgrounds/logo-admin.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="194" height="41" viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM93.1291 38.6957C91.2536 38.6957 89.6317 38.2973 88.2633 37.5004C86.903 36.6955 85.8526 35.5766 85.112 34.1439C84.3715 32.7031 84.0012 31.0328 84.0012 29.1332C84.0012 27.2175 84.3715 25.5432 85.112 24.1105C85.8526 22.6697 86.903 21.5508 88.2633 20.7539C89.6317 19.949 91.2536 19.5466 93.1291 19.5466C95.0046 19.5466 96.6225 19.949 97.9828 20.7539C99.3511 21.5508 100.406 22.6697 101.146 24.1105C101.887 25.5432 102.257 27.2175 102.257 29.1332C102.257 31.0328 101.887 32.7031 101.146 34.1439C100.406 35.5766 99.3511 36.6955 97.9828 37.5004C96.6225 38.2973 95.0046 38.6957 93.1291 38.6957ZM93.1532 34.7113C94.0065 34.7113 94.7188 34.4699 95.2903 33.9869C95.8618 33.4959 96.2924 32.8278 96.5822 31.9826C96.88 31.1375 97.0289 30.1756 97.0289 29.097C97.0289 28.0184 96.88 27.0565 96.5822 26.2113C96.2924 25.3662 95.8618 24.6981 95.2903 24.2071C94.7188 23.7161 94.0065 23.4706 93.1532 23.4706C92.292 23.4706 91.5675 23.7161 90.9799 24.2071C90.4004 24.6981 89.9617 25.3662 89.6639 26.2113C89.3741 27.0565 89.2292 28.0184 89.2292 29.097C89.2292 30.1756 89.3741 31.1375 89.6639 31.9826C89.9617 32.8278 90.4004 33.4959 90.9799 33.9869C91.5675 34.4699 92.292 34.7113 93.1532 34.7113ZM110.745 27.6119V38.3335H105.601V19.788H110.503V23.0601H110.721C111.131 21.9815 111.819 21.1282 112.785 20.5004C113.751 19.8645 114.922 19.5466 116.299 19.5466C117.587 19.5466 118.71 19.8283 119.667 20.3917C120.625 20.9552 121.37 21.7601 121.901 22.8065C122.432 23.8449 122.698 25.0844 122.698 26.5253V38.3335H117.555V27.4429C117.563 26.3079 117.273 25.4225 116.685 24.7866C116.098 24.1427 115.289 23.8207 114.258 23.8207C113.566 23.8207 112.954 23.9696 112.423 24.2674C111.9 24.5653 111.489 24.9999 111.192 25.5714C110.902 26.1349 110.753 26.815 110.745 27.6119ZM135.131 38.6957C133.256 38.6957 131.634 38.2973 130.265 37.5004C128.905 36.6955 127.855 35.5766 127.114 34.1439C126.373 32.7031 126.003 31.0328 126.003 29.1332C126.003 27.2175 126.373 25.5432 127.114 24.1105C127.855 22.6697 128.905 21.5508 130.265 20.7539C131.634 19.949 133.256 19.5466 135.131 19.5466C137.007 19.5466 138.624 19.949 139.985 20.7539C141.353 21.5508 142.408 22.6697 143.148 24.1105C143.889 25.5432 144.259 27.2175 144.259 29.1332C144.259 31.0328 143.889 32.7031 143.148 34.1439C142.408 35.5766 141.353 36.6955 139.985 37.5004C138.624 38.2973 137.007 38.6957 135.131 38.6957ZM135.155 34.7113C136.008 34.7113 136.721 34.4699 137.292 33.9869C137.864 33.4959 138.294 32.8278 138.584 31.9826C138.882 31.1375 139.031 30.1756 139.031 29.097C139.031 28.0184 138.882 27.0565 138.584 26.2113C138.294 25.3662 137.864 24.6981 137.292 24.2071C136.721 23.7161 136.008 23.4706 135.155 23.4706C134.294 23.4706 133.569 23.7161 132.982 24.2071C132.402 24.6981 131.964 25.3662 131.666 26.2113C131.376 27.0565 131.231 28.0184 131.231 29.097C131.231 30.1756 131.376 31.1375 131.666 31.9826C131.964 32.8278 132.402 33.4959 132.982 33.9869C133.569 34.4699 134.294 34.7113 135.155 34.7113ZM150.797 38.3335L145.75 19.788H150.954L153.827 32.2483H153.996L156.991 19.788H162.098L165.141 32.1758H165.298L168.123 19.788H173.315L168.28 38.3335H162.835L159.647 26.6701H159.418L156.23 38.3335H150.797Z" fill="#1D2129"/>
|
||||||
|
<g clip-path="url(#clip0_445_10776)">
|
||||||
|
<path d="M5.74048 1.6455C2.43981 1.6455 0.0100346 1.57286 0.000244138 1.6455C0.000244144 1.71889 2.11281 5.37372 4.7063 9.58593C7.28518 13.8128 12.3555 22.0903 15.9543 27.9609L20.8684 35.9883C21.8795 37.6395 23.6773 38.6455 25.6135 38.6455L26.4299 38.6455C26.4299 38.6455 31.0566 38.7202 31.9963 37.2227C32.2253 36.8286 32.3459 36.3806 32.3459 35.9248L32.3459 21.8994L31.1799 21.8994L31.1799 26.5967C31.1799 31.1021 31.1657 31.3081 30.8889 31.5869C30.7286 31.7483 30.4661 31.8799 30.3059 31.8799C29.9854 31.8798 29.7815 31.5857 27.1448 27.2568C26.256 25.8038 24.0844 22.237 22.2922 19.3311C20.5147 16.4251 17.3532 11.2594 15.2698 7.85449L11.467 1.64551L5.74048 1.6455ZM31.6614 2.99609C31.4428 4.17012 30.5249 6.06336 29.6653 7.10547C28.456 8.60245 26.4303 9.83514 24.5071 10.2314C23.4726 10.4516 23.1959 10.5986 23.8079 10.5986C24.0266 10.5987 24.58 10.701 25.0315 10.833C28.2661 11.7576 31.0493 14.7672 31.6467 17.9814L31.8362 18.9795L32.011 18.1133C32.5647 15.3247 34.4441 12.8149 36.9065 11.5674C38.0138 10.995 39.2814 10.5986 39.9807 10.5986C40.6216 10.5986 40.4029 10.4956 39.2083 10.2461C37.4599 9.87917 36.1048 9.11605 34.6624 7.66309C33.2637 6.26884 32.5495 5.02109 32.0833 3.17187L31.8215 2.11523L31.6614 2.99609Z" fill="url(#paint0_linear_445_10776)"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_445_10776" x1="23.4864" y1="5.39407" x2="32.9094" y2="11.1241" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||||
|
<stop offset="1" stop-color="#0F9CFF"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_445_10776">
|
||||||
|
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
BIN
demo/frontend/public/images/backgrounds/logo.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
15
demo/frontend/public/images/backgrounds/logo.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="101" height="21" viewBox="0 0 101 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M28.4343 7.58893L31.5459 17.3698H31.6653L34.7831 7.58893H37.8003L33.3625 20.4624H29.855L25.4108 7.58893H28.4343ZM39.2995 20.4624V10.8073H41.9773V20.4624H39.2995ZM40.6447 9.56269C40.2466 9.56269 39.905 9.43069 39.6201 9.16668C39.3393 8.89848 39.1989 8.5779 39.1989 8.20494C39.1989 7.83617 39.3393 7.51978 39.6201 7.25577C39.905 6.98758 40.2466 6.85348 40.6447 6.85348C41.0428 6.85348 41.3822 6.98758 41.663 7.25577C41.9479 7.51978 42.0904 7.83617 42.0904 8.20494C42.0904 8.5779 41.9479 8.89848 41.663 9.16668C41.3822 9.43069 41.0428 9.56269 40.6447 9.56269ZM48.4847 20.651C47.5083 20.651 46.6639 20.4435 45.9515 20.0287C45.2433 19.6096 44.6964 19.0271 44.3109 18.2812C43.9254 17.5311 43.7326 16.6615 43.7326 15.6726C43.7326 14.6752 43.9254 13.8036 44.3109 13.0576C44.6964 12.3075 45.2433 11.725 45.9515 11.3102C46.6639 10.8911 47.5083 10.6816 48.4847 10.6816C49.4611 10.6816 50.3034 10.8911 51.0116 11.3102C51.724 11.725 52.273 12.3075 52.6585 13.0576C53.0441 13.8036 53.2368 14.6752 53.2368 15.6726C53.2368 16.6615 53.0441 17.5311 52.6585 18.2812C52.273 19.0271 51.724 19.6096 51.0116 20.0287C50.3034 20.4435 49.4611 20.651 48.4847 20.651ZM48.4973 18.5766C48.9415 18.5766 49.3124 18.4509 49.6099 18.1995C49.9074 17.9439 50.1316 17.596 50.2825 17.156C50.4375 16.716 50.5151 16.2152 50.5151 15.6537C50.5151 15.0922 50.4375 14.5914 50.2825 14.1514C50.1316 13.7114 49.9074 13.3636 49.6099 13.1079C49.3124 12.8523 48.9415 12.7245 48.4973 12.7245C48.0489 12.7245 47.6717 12.8523 47.3658 13.1079C47.0641 13.3636 46.8357 13.7114 46.6807 14.1514C46.5298 14.5914 46.4544 15.0922 46.4544 15.6537C46.4544 16.2152 46.5298 16.716 46.6807 17.156C46.8357 17.596 47.0641 17.9439 47.3658 18.1995C47.6717 18.4509 48.0489 18.5766 48.4973 18.5766ZM57.6558 14.8805V20.4624H54.978V10.8073H57.5301V12.5108H57.6432C57.857 11.9492 58.2153 11.505 58.7181 11.1782C59.221 10.8471 59.8307 10.6816 60.5473 10.6816C61.2178 10.6816 61.8024 10.8282 62.3011 11.1216C62.7998 11.4149 63.1874 11.834 63.464 12.3788C63.7405 12.9193 63.8788 13.5647 63.8788 14.3148V20.4624H61.2011V14.7925C61.2052 14.2017 61.0544 13.7407 60.7485 13.4096C60.4426 13.0744 60.0214 12.9068 59.485 12.9068C59.1246 12.9068 58.8061 12.9843 58.5296 13.1394C58.2572 13.2944 58.0434 13.5207 57.8884 13.8182C57.7375 14.1116 57.66 14.4657 57.6558 14.8805ZM70.3517 20.651C69.3753 20.651 68.5309 20.4435 67.8185 20.0287C67.1103 19.6096 66.5634 19.0271 66.1779 18.2812C65.7924 17.5311 65.5996 16.6615 65.5996 15.6726C65.5996 14.6752 65.7924 13.8036 66.1779 13.0576C66.5634 12.3075 67.1103 11.725 67.8185 11.3102C68.5309 10.8911 69.3753 10.6816 70.3517 10.6816C71.3281 10.6816 72.1704 10.8911 72.8786 11.3102C73.591 11.725 74.14 12.3075 74.5255 13.0576C74.9111 13.8036 75.1038 14.6752 75.1038 15.6726C75.1038 16.6615 74.9111 17.5311 74.5255 18.2812C74.14 19.0271 73.591 19.6096 72.8786 20.0287C72.1704 20.4435 71.3281 20.651 70.3517 20.651ZM70.3643 18.5766C70.8085 18.5766 71.1794 18.4509 71.4769 18.1995C71.7744 17.9439 71.9986 17.596 72.1495 17.156C72.3045 16.716 72.3821 16.2152 72.3821 15.6537C72.3821 15.0922 72.3045 14.5914 72.1495 14.1514C71.9986 13.7114 71.7744 13.3636 71.4769 13.1079C71.1794 12.8523 70.8085 12.7245 70.3643 12.7245C69.9159 12.7245 69.5387 12.8523 69.2328 13.1079C68.9311 13.3636 68.7027 13.7114 68.5477 14.1514C68.3968 14.5914 68.3214 15.0922 68.3214 15.6537C68.3214 16.2152 68.3968 16.716 68.5477 17.156C68.7027 17.596 68.9311 17.9439 69.2328 18.1995C69.5387 18.4509 69.9159 18.5766 70.3643 18.5766ZM78.5076 20.4624L75.8801 10.8073H78.5894L80.0854 17.2943H80.1734L81.7323 10.8073H84.3912L85.9753 17.2566H86.057L87.5279 10.8073H90.2308L87.6096 20.4624H84.7747L83.1152 14.3902H82.9958L81.3363 20.4624H78.5076Z" fill="white"/>
|
||||||
|
<g clip-path="url(#clip0_287_18768)">
|
||||||
|
<path d="M2.98853 0.856444C1.27414 0.856441 0.0111275 0.81906 0.000244139 0.856443C0.000244136 0.894647 1.10026 2.79734 2.45044 4.99023C3.79308 7.19083 6.43228 11.5012 8.30591 14.5576L10.8645 18.7363C11.3909 19.5959 12.3272 20.1201 13.3352 20.1201L13.76 20.1201C13.76 20.1201 16.1679 20.1583 16.6575 19.3789C16.7767 19.1737 16.8401 18.9404 16.8401 18.7031L16.8401 11.4014L16.2327 11.4014L16.2327 13.8467C16.2327 16.1919 16.2254 16.2992 16.0813 16.4443C15.9979 16.5284 15.861 16.5977 15.7776 16.5977C15.6109 16.5975 15.5042 16.4431 14.1321 14.1904C13.6694 13.434 12.5387 11.5774 11.6057 10.0645C10.6803 8.55154 9.03419 5.86157 7.94946 4.08887L5.96997 0.856444L2.98853 0.856444ZM16.4836 1.55957C16.3699 2.17078 15.8921 3.15667 15.4446 3.69922C14.815 4.47857 13.7603 5.12082 12.759 5.32715C12.2205 5.44176 12.0762 5.51758 12.3948 5.51758C12.5085 5.51759 12.7965 5.57095 13.0315 5.63965C14.7155 6.12103 16.1648 7.68795 16.4758 9.36133L16.5745 9.88086L16.6653 9.42969C16.9536 7.97804 17.9323 6.67195 19.2141 6.02246C19.7906 5.72446 20.4506 5.51758 20.8147 5.51758C21.1484 5.51757 21.0343 5.46388 20.4124 5.33398C19.5023 5.14293 18.797 4.74562 18.0461 3.98926C17.318 3.26338 16.9461 2.61411 16.7034 1.65137L16.5667 1.10156L16.4836 1.55957Z" fill="url(#paint0_linear_287_18768)"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_287_18768" x1="12.2274" y1="2.80849" x2="17.1333" y2="5.79162" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||||
|
<stop offset="1" stop-color="#0F9CFF"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_287_18768">
|
||||||
|
<rect width="20.9983" height="20.9983" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
9
demo/frontend/public/images/backgrounds/welcome-bg.svg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
demo/frontend/public/images/backgrounds/welcome_bg.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
demo/frontend/public/images/login.png
Normal file
|
After Width: | Height: | Size: 848 KiB |
BIN
demo/frontend/public/images/welcome.jpg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
703
demo/frontend/src/App.vue
Normal file
@@ -0,0 +1,703 @@
|
|||||||
|
<template>
|
||||||
|
<el-config-provider :locale="elementLocale">
|
||||||
|
<div id="app" :data-route="route.name">
|
||||||
|
<!-- 全屏背景层 -->
|
||||||
|
<div class="fullscreen-background" :class="route.name"></div>
|
||||||
|
|
||||||
|
<!-- 导航栏 - 根据路由条件显示 -->
|
||||||
|
<NavBar v-if="shouldShowNavBar" />
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<main :class="{ 'with-navbar': shouldShowNavBar }">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 页脚 - 根据路由条件显示 -->
|
||||||
|
<Footer v-if="shouldShowFooter" />
|
||||||
|
</div>
|
||||||
|
</el-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
import en from 'element-plus/dist/locale/en.mjs'
|
||||||
|
import NavBar from '@/components/NavBar.vue'
|
||||||
|
import Footer from '@/components/Footer.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const { locale } = useI18n()
|
||||||
|
|
||||||
|
// 动态计算 Element Plus 的语言配置
|
||||||
|
const elementLocale = computed(() => {
|
||||||
|
console.log('[App.vue] 当前语言切换为:', locale.value)
|
||||||
|
return locale.value === 'zh' ? zhCn : en
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算是否显示导航栏和页脚
|
||||||
|
const shouldShowNavBar = computed(() => {
|
||||||
|
// 所有页面都不显示导航栏
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const shouldShowFooter = computed(() => {
|
||||||
|
// 所有页面都是全屏固定布局,不显示页脚
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听路由变化,动态设置页面样式
|
||||||
|
watch(route, (newRoute) => {
|
||||||
|
console.log('路由变化:', newRoute.name)
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
console.log('App.vue 加载成功')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 全局样式重置 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000510;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000510;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全屏背景层 */
|
||||||
|
.fullscreen-background {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
z-index: -10; /* 确保在最底层 */
|
||||||
|
pointer-events: none;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.with-navbar {
|
||||||
|
padding-top: 0; /* NavBar 是 fixed 定位,不需要 padding-top */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保登录页面内容正确显示 */
|
||||||
|
#app[data-route="Login"] main {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录页面特殊背景处理 */
|
||||||
|
#app[data-route="Login"] .fullscreen-background {
|
||||||
|
background: url('/images/backgrounds/login.png') center/cover no-repeat;
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 页面特殊样式 ========== */
|
||||||
|
|
||||||
|
/* 欢迎页面 - 参考Welcome页面样式 */
|
||||||
|
.fullscreen-background.Welcome {
|
||||||
|
background: url('/images/backgrounds/welcome.jpg') center/cover no-repeat;
|
||||||
|
background-attachment: fixed;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 首页 - 深色科技风全屏覆盖 */
|
||||||
|
.fullscreen-background.Home {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(20, 40, 80, 0.8) 0%, rgba(10, 20, 40, 0.9) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
|
||||||
|
animation: homeGlow 6s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.Home::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 70%;
|
||||||
|
height: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(100, 150, 255, 0.4) 0%, rgba(50, 100, 200, 0.2) 40%, transparent 70%);
|
||||||
|
animation: homeCentralGlow 7s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes homeGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes homeCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translate(-50%, -50%) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 个人主页 - 深色科技风全屏覆盖 */
|
||||||
|
.fullscreen-background.Profile {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(0, 30, 60, 0.9) 0%, rgba(0, 10, 20, 0.95) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #000000 0%, #0a0a0a 50%, #1a1a1a 100%);
|
||||||
|
animation: profileGlow 6s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.Profile::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 60%;
|
||||||
|
height: 40%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(64, 158, 255, 0.2) 0%, rgba(103, 194, 58, 0.1) 30%, transparent 70%);
|
||||||
|
animation: profileCentralGlow 8s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes profileGlow {
|
||||||
|
0% { opacity: 0.9; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes profileCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.2;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: translate(-50%, -50%) scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单管理 - 深色商务风全屏覆盖 */
|
||||||
|
.fullscreen-background.Orders {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(80, 20, 60, 0.8) 0%, rgba(40, 10, 30, 0.9) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #0a0a0a 0%, #1a0a1a 50%, #2a0a2a 100%);
|
||||||
|
animation: ordersGlow 5s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.Orders::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 75%;
|
||||||
|
height: 55%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(240, 147, 251, 0.3) 0%, rgba(245, 87, 108, 0.2) 40%, transparent 70%);
|
||||||
|
animation: ordersCentralGlow 6s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ordersGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ordersCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translate(-50%, -50%) scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 支付记录 - 深色金色风全屏覆盖 */
|
||||||
|
.fullscreen-background.Payments {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(100, 80, 20, 0.8) 0%, rgba(50, 40, 10, 0.9) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #0a0a0a 0%, #1a1a0a 50%, #2a2a0a 100%);
|
||||||
|
animation: paymentsGlow 4s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.Payments::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 70%;
|
||||||
|
height: 45%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(246, 211, 101, 0.3) 0%, rgba(253, 160, 133, 0.2) 40%, transparent 70%);
|
||||||
|
animation: paymentsCentralGlow 5s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes paymentsGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes paymentsCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: translate(-50%, -50%) scale(1.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 我的作品 - 深色创意风全屏覆盖 */
|
||||||
|
.fullscreen-background.MyWorks {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(20, 60, 40, 0.8) 0%, rgba(10, 30, 20, 0.9) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #0a0a0a 0%, #0a1a0a 50%, #0a2a0a 100%);
|
||||||
|
animation: worksGlow 7s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.MyWorks::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 80%;
|
||||||
|
height: 60%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(168, 237, 234, 0.3) 0%, rgba(254, 214, 227, 0.2) 40%, transparent 70%);
|
||||||
|
animation: worksCentralGlow 8s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes worksGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes worksCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(-50%, -50%) scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translate(-50%, -50%) scale(1.05) rotate(2deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文生视频 - 深色蓝色科技风全屏覆盖 */
|
||||||
|
.fullscreen-background.TextToVideo {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(20, 40, 80, 0.8) 0%, rgba(10, 20, 40, 0.9) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #0a0a0a 0%, #0a1a2e 50%, #1a2a4e 100%);
|
||||||
|
animation: textVideoGlow 6s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.TextToVideo::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 75%;
|
||||||
|
height: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(102, 126, 234, 0.4) 0%, rgba(118, 75, 162, 0.3) 40%, transparent 70%);
|
||||||
|
animation: textVideoCentralGlow 7s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes textVideoGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes textVideoCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: translate(-50%, -50%) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图生视频 - 深色紫色梦幻风全屏覆盖 */
|
||||||
|
.fullscreen-background.ImageToVideo {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(40, 20, 60, 0.8) 0%, rgba(20, 10, 30, 0.9) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #0a0a0a 0%, #1a0a2e 50%, #2a0a4e 100%);
|
||||||
|
animation: imageVideoGlow 8s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.ImageToVideo::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 80%;
|
||||||
|
height: 60%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(168, 192, 255, 0.3) 0%, rgba(63, 43, 150, 0.3) 40%, transparent 70%);
|
||||||
|
animation: imageVideoCentralGlow 9s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes imageVideoGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes imageVideoCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.2;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translate(-50%, -50%) scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分镜视频 - 深色橙色活力风全屏覆盖 */
|
||||||
|
.fullscreen-background.StoryboardVideo {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(80, 40, 20, 0.8) 0%, rgba(40, 20, 10, 0.9) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #0a0a0a 0%, #2a0a0a 50%, #4a0a0a 100%);
|
||||||
|
animation: storyboardGlow 5s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.StoryboardVideo::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 70%;
|
||||||
|
height: 45%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(255, 154, 158, 0.3) 0%, rgba(254, 207, 239, 0.2) 40%, transparent 70%);
|
||||||
|
animation: storyboardCentralGlow 6s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes storyboardGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes storyboardCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: translate(-50%, -50%) scale(1.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 会员订阅 - 深色奢华金色风全屏覆盖 */
|
||||||
|
.fullscreen-background.Subscription {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(100, 80, 40, 0.8) 0%, rgba(50, 40, 20, 0.9) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #0a0a0a 0%, #2a2a0a 50%, #4a4a0a 100%);
|
||||||
|
animation: subscriptionGlow 6s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.Subscription::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 75%;
|
||||||
|
height: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(255, 215, 0, 0.3) 0%, rgba(252, 182, 159, 0.2) 40%, transparent 70%);
|
||||||
|
animation: subscriptionCentralGlow 7s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes subscriptionGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes subscriptionCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: translate(-50%, -50%) scale(1.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 管理员页面 - 深色专业科技风全屏覆盖 */
|
||||||
|
.fullscreen-background.AdminDashboard,
|
||||||
|
.fullscreen-background.AdminOrders {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(0, 20, 40, 0.9) 0%, rgba(0, 10, 20, 0.95) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #000000 0%, #0a0a0a 50%, #1a1a1a 100%);
|
||||||
|
animation: adminGlow 8s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.AdminDashboard::before,
|
||||||
|
.fullscreen-background.AdminOrders::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 60%;
|
||||||
|
height: 40%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(0, 150, 255, 0.2) 0%, rgba(255, 0, 150, 0.1) 30%, rgba(0, 255, 150, 0.1) 60%, transparent 80%);
|
||||||
|
animation: adminCentralGlow 9s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes adminGlow {
|
||||||
|
0% { opacity: 0.9; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes adminCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.2;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: translate(-50%, -50%) scale(1.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 注册页面 - 深色清新风全屏覆盖 */
|
||||||
|
.fullscreen-background.Register {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(20, 40, 80, 0.8) 0%, rgba(10, 20, 40, 0.9) 50%, rgba(0, 0, 0, 1) 100%),
|
||||||
|
linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
|
||||||
|
animation: registerGlow 4s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-background.Register::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 70%;
|
||||||
|
height: 45%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, rgba(255, 255, 255, 0.1) 0%, rgba(100, 150, 255, 0.1) 40%, transparent 70%);
|
||||||
|
animation: registerCentralGlow 5s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes registerGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes registerCentralGlow {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(-50%, -50%) scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translate(-50%, -50%) scale(1.02) rotate(1deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容层级确保在所有背景效果之上 */
|
||||||
|
#app main > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保导航栏在背景之上 */
|
||||||
|
#app .navbar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保页脚在背景之上 */
|
||||||
|
#app .footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
main {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端减少动画效果 */
|
||||||
|
#app[data-route]::before {
|
||||||
|
animation-duration: 10s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus 样式覆盖 */
|
||||||
|
.el-button {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复 el-select 选中值不显示的问题 */
|
||||||
|
.el-select .el-select__wrapper .el-select__selected-item {
|
||||||
|
color: inherit !important;
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select .el-select__wrapper .el-select__selection {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select .el-select__wrapper .el-select__input-wrapper {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除 el-dialog 的所有可能的白色边框 */
|
||||||
|
.payment-modal-dialog,
|
||||||
|
.payment-modal-dialog.el-dialog,
|
||||||
|
.payment-modal-dialog.el-dialog--center,
|
||||||
|
.payment-modal-dialog.el-dialog--center.payment-modal,
|
||||||
|
.el-overlay-dialog .payment-modal-dialog,
|
||||||
|
.el-overlay-dialog .payment-modal-dialog.el-dialog,
|
||||||
|
.el-overlay-dialog .payment-modal-dialog.el-dialog--center,
|
||||||
|
.el-overlay-dialog .payment-modal-dialog.el-dialog--center.payment-modal,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .payment-modal-dialog,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .payment-modal-dialog.el-dialog,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .payment-modal-dialog.el-dialog--center,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .payment-modal-dialog.el-dialog--center.payment-modal {
|
||||||
|
background: #000000 !important;
|
||||||
|
background-color: #000000 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-width: 0 !important;
|
||||||
|
border-style: none !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
outline: none !important;
|
||||||
|
outline-width: 0 !important;
|
||||||
|
outline-style: none !important;
|
||||||
|
outline-color: transparent !important;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-modal-dialog .el-dialog__body,
|
||||||
|
.payment-modal-dialog .el-dialog__header {
|
||||||
|
background: #000000 !important;
|
||||||
|
background-color: #000000 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-width: 0 !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-right: none !important;
|
||||||
|
border-top: none !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局覆盖所有可能的对话框背景 */
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .el-dialog,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .el-dialog.el-dialog--center,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .el-dialog.el-dialog--center.payment-modal,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .payment-modal-dialog,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .payment-modal-dialog.el-dialog,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .payment-modal-dialog.el-dialog--center,
|
||||||
|
.el-overlay.payment-modal-overlay.el-modal-dialog .payment-modal-dialog.el-dialog--center.payment-modal,
|
||||||
|
.el-overlay-dialog .el-dialog.el-dialog--center.payment-modal,
|
||||||
|
.el-overlay-dialog .payment-modal-dialog.el-dialog--center.payment-modal,
|
||||||
|
.el-dialog.el-dialog--center.payment-modal,
|
||||||
|
.payment-modal-dialog.el-dialog.el-dialog--center.payment-modal,
|
||||||
|
/* 使用属性选择器覆盖所有包含 payment-modal 的对话框 */
|
||||||
|
[class*="payment-modal"][class*="el-dialog"],
|
||||||
|
[class*="payment-modal"][class*="el-dialog--center"],
|
||||||
|
.el-dialog[class*="payment-modal"],
|
||||||
|
.payment-modal-dialog[class*="el-dialog"] {
|
||||||
|
background: #000000 !important;
|
||||||
|
background-color: #000000 !important;
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #050515;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
demo/frontend/src/api/analytics.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 获取日活用户趋势数据
|
||||||
|
export const getDailyActiveUsersTrend = (year = '2024', granularity = 'monthly') => {
|
||||||
|
return api.get('/analytics/daily-active-users', {
|
||||||
|
params: { year, granularity }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户活跃度概览
|
||||||
|
export const getUserActivityOverview = () => {
|
||||||
|
return api.get('/analytics/user-activity-overview')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户活跃度热力图数据
|
||||||
|
export const getUserActivityHeatmap = (year = '2024') => {
|
||||||
|
return api.get('/analytics/user-activity-heatmap', {
|
||||||
|
params: { year }
|
||||||
|
})
|
||||||
|
}
|
||||||
83
demo/frontend/src/api/auth.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 认证相关API
|
||||||
|
// 注意:用户名密码登录已禁用,仅支持邮箱验证码登录
|
||||||
|
|
||||||
|
// 邮箱验证码登录(唯一登录方式)
|
||||||
|
export const loginWithEmail = (credentials) => {
|
||||||
|
return api.post('/auth/login/email', credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向后兼容(已禁用)
|
||||||
|
export const login = (credentials) => {
|
||||||
|
return api.post('/auth/login', credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const register = (userData) => {
|
||||||
|
return api.post('/auth/register', userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
return api.post('/auth/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCurrentUser = () => {
|
||||||
|
return api.get('/auth/me')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改当前登录用户密码
|
||||||
|
export const changePassword = (data) => {
|
||||||
|
return api.post('/auth/change-password', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户相关API
|
||||||
|
export const getUsers = (params) => {
|
||||||
|
return api.get('/users', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserById = (id) => {
|
||||||
|
return api.get(`/users/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUser = (userData) => {
|
||||||
|
return api.post('/users', userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateUser = (id, userData) => {
|
||||||
|
return api.put(`/users/${id}`, userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteUser = (id) => {
|
||||||
|
return api.delete(`/users/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户名是否存在
|
||||||
|
export const checkUsernameExists = (username) => {
|
||||||
|
return api.get(`/public/users/exists/username`, {
|
||||||
|
params: { value: username }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否存在
|
||||||
|
export const checkEmailExists = (email) => {
|
||||||
|
return api.get(`/public/users/exists/email`, {
|
||||||
|
params: { value: email }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送邮箱验证码
|
||||||
|
export const sendEmailCode = (email) => {
|
||||||
|
return api.post('/verification/email/send', { email })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开发环境:设置验证码(用于开发测试)
|
||||||
|
export const setDevEmailCode = (email, code) => {
|
||||||
|
return api.post('/verification/email/dev-set', { email, code })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
88
demo/frontend/src/api/cleanup.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// 任务清理API服务
|
||||||
|
import request from './request'
|
||||||
|
import { getApiBaseURL } from '@/utils/apiHelper'
|
||||||
|
|
||||||
|
export const cleanupApi = {
|
||||||
|
// 获取清理统计信息
|
||||||
|
getCleanupStats() {
|
||||||
|
return request({
|
||||||
|
url: '/cleanup/cleanup-stats',
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 执行完整清理
|
||||||
|
performFullCleanup() {
|
||||||
|
return request({
|
||||||
|
url: '/cleanup/full-cleanup',
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清理指定用户任务
|
||||||
|
cleanupUserTasks(username) {
|
||||||
|
return request({
|
||||||
|
url: `/cleanup/user-tasks/${username}`,
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取清理统计信息(原始fetch方式,用于测试)
|
||||||
|
async getCleanupStatsRaw() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getApiBaseURL()}/cleanup/cleanup-stats`)
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json()
|
||||||
|
} else {
|
||||||
|
throw new Error('获取统计信息失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计信息失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 执行完整清理(原始fetch方式,用于测试)
|
||||||
|
async performFullCleanupRaw() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getApiBaseURL()}/cleanup/full-cleanup`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json()
|
||||||
|
} else {
|
||||||
|
throw new Error('执行完整清理失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('执行完整清理失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清理指定用户任务(原始fetch方式,用于测试)
|
||||||
|
async cleanupUserTasksRaw(username) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/cleanup/user-tasks/${username}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json()
|
||||||
|
} else {
|
||||||
|
throw new Error('清理用户任务失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清理用户任务失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default cleanupApi
|
||||||
38
demo/frontend/src/api/dashboard.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 获取仪表盘概览数据
|
||||||
|
export const getDashboardOverview = () => {
|
||||||
|
return api.get('/dashboard/overview')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取月度收入趋势数据
|
||||||
|
export const getMonthlyRevenue = (year = '2024') => {
|
||||||
|
return api.get('/dashboard/monthly-revenue', {
|
||||||
|
params: { year }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户转化率数据
|
||||||
|
export const getConversionRate = (year = null) => {
|
||||||
|
const params = year ? { year } : {}
|
||||||
|
return api.get('/dashboard/conversion-rate', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最近订单数据
|
||||||
|
export const getRecentOrders = (limit = 10) => {
|
||||||
|
return api.get('/dashboard/recent-orders', {
|
||||||
|
params: { limit }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取系统状态
|
||||||
|
export const getSystemStatus = () => {
|
||||||
|
return api.get('/dashboard/system-status')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日活用户趋势数据
|
||||||
|
export const getDailyActiveUsersTrend = (year = '2024', granularity = 'monthly') => {
|
||||||
|
return api.get('/analytics/daily-active-users', {
|
||||||
|
params: { year, granularity }
|
||||||
|
})
|
||||||
|
}
|
||||||
247
demo/frontend/src/api/imageToVideo.js
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图生视频API服务
|
||||||
|
*/
|
||||||
|
export const imageToVideoApi = {
|
||||||
|
/**
|
||||||
|
* 创建图生视频任务
|
||||||
|
* @param {Object} params - 任务参数
|
||||||
|
* @param {File} params.firstFrame - 首帧图片
|
||||||
|
* @param {File} params.lastFrame - 尾帧图片(可选)
|
||||||
|
* @param {string} params.prompt - 描述文字
|
||||||
|
* @param {string} params.aspectRatio - 视频比例
|
||||||
|
* @param {number} params.duration - 视频时长
|
||||||
|
* @param {boolean} params.hdMode - 是否高清模式
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
createTask(params) {
|
||||||
|
// 参数验证
|
||||||
|
if (!params) {
|
||||||
|
throw new Error('参数不能为空')
|
||||||
|
}
|
||||||
|
if (!params.firstFrame) {
|
||||||
|
throw new Error('首帧图片不能为空')
|
||||||
|
}
|
||||||
|
if (!params.prompt || params.prompt.trim() === '') {
|
||||||
|
throw new Error('描述文字不能为空')
|
||||||
|
}
|
||||||
|
if (!params.aspectRatio) {
|
||||||
|
throw new Error('视频比例不能为空')
|
||||||
|
}
|
||||||
|
if (!params.duration || params.duration < 1 || params.duration > 60) {
|
||||||
|
throw new Error('视频时长必须在1-60秒之间')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
// 添加必填参数
|
||||||
|
formData.append('firstFrame', params.firstFrame)
|
||||||
|
formData.append('prompt', params.prompt.trim())
|
||||||
|
formData.append('aspectRatio', params.aspectRatio)
|
||||||
|
formData.append('duration', params.duration.toString())
|
||||||
|
formData.append('hdMode', params.hdMode.toString())
|
||||||
|
|
||||||
|
// 添加可选参数
|
||||||
|
if (params.lastFrame) {
|
||||||
|
formData.append('lastFrame', params.lastFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
return request({
|
||||||
|
url: '/image-to-video/create',
|
||||||
|
method: 'POST',
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过图片URL创建图生视频任务(用于"做同款"功能)
|
||||||
|
* @param {Object} params - 任务参数
|
||||||
|
* @param {string} params.imageUrl - 图片URL
|
||||||
|
* @param {string} params.prompt - 描述文字
|
||||||
|
* @param {string} params.aspectRatio - 视频比例
|
||||||
|
* @param {number} params.duration - 视频时长
|
||||||
|
* @param {boolean} params.hdMode - 是否高清模式
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
createTaskByUrl(params) {
|
||||||
|
if (!params) {
|
||||||
|
throw new Error('参数不能为空')
|
||||||
|
}
|
||||||
|
if (!params.imageUrl) {
|
||||||
|
throw new Error('图片URL不能为空')
|
||||||
|
}
|
||||||
|
if (!params.prompt || params.prompt.trim() === '') {
|
||||||
|
throw new Error('描述文字不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
return request({
|
||||||
|
url: '/image-to-video/create-by-url',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
imageUrl: params.imageUrl,
|
||||||
|
prompt: params.prompt.trim(),
|
||||||
|
aspectRatio: params.aspectRatio || '16:9',
|
||||||
|
duration: params.duration || 5,
|
||||||
|
hdMode: params.hdMode || false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户任务列表
|
||||||
|
* @param {number} page - 页码
|
||||||
|
* @param {number} size - 每页数量
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
getTasks(page = 0, size = 10) {
|
||||||
|
return request({
|
||||||
|
url: '/image-to-video/tasks',
|
||||||
|
method: 'GET',
|
||||||
|
params: { page, size }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务详情
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
getTaskDetail(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/image-to-video/tasks/${taskId}`,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务状态
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
getTaskStatus(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/image-to-video/tasks/${taskId}/status`,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除任务
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
deleteTask(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/image-to-video/tasks/${taskId}`,
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试失败的任务
|
||||||
|
* 复用原task_id和已上传的图片,重新提交至外部API
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
retryTask(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/image-to-video/tasks/${taskId}/retry`,
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询任务状态
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @param {Function} onProgress - 进度回调
|
||||||
|
* @param {Function} onComplete - 完成回调
|
||||||
|
* @param {Function} onError - 错误回调
|
||||||
|
* @returns {Function} 停止轮询的函数
|
||||||
|
*/
|
||||||
|
pollTaskStatus(taskId, onProgress, onComplete, onError) {
|
||||||
|
let isPolling = true
|
||||||
|
let pollCount = 0
|
||||||
|
const maxPolls = 30 // 最大轮询次数(1小时,每2分钟一次)
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
if (!isPolling || pollCount >= maxPolls) {
|
||||||
|
if (pollCount >= maxPolls) {
|
||||||
|
onError && onError(new Error('任务超时'))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request({
|
||||||
|
url: `/image-to-video/tasks/${taskId}/status`,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查响应是否有效
|
||||||
|
if (!response || !response.data || !response.data.success) {
|
||||||
|
onError && onError(new Error('获取任务状态失败'))
|
||||||
|
isPolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskData = response.data.data
|
||||||
|
|
||||||
|
// 检查taskData是否有效
|
||||||
|
if (!taskData || !taskData.status) {
|
||||||
|
onError && onError(new Error('无效的任务数据'))
|
||||||
|
isPolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskData.status === 'COMPLETED') {
|
||||||
|
onComplete && onComplete(taskData)
|
||||||
|
isPolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskData.status === 'FAILED' || taskData.status === 'CANCELLED') {
|
||||||
|
console.error('任务失败:', {
|
||||||
|
taskId: taskId,
|
||||||
|
status: taskData.status,
|
||||||
|
errorMessage: taskData.errorMessage,
|
||||||
|
pollCount: pollCount
|
||||||
|
})
|
||||||
|
onError && onError(new Error(taskData.errorMessage || '任务失败'))
|
||||||
|
isPolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用进度回调
|
||||||
|
onProgress && onProgress({
|
||||||
|
status: taskData.status,
|
||||||
|
progress: taskData.progress || 0,
|
||||||
|
resultUrl: taskData.resultUrl
|
||||||
|
})
|
||||||
|
|
||||||
|
pollCount++
|
||||||
|
|
||||||
|
// 继续轮询
|
||||||
|
setTimeout(poll, 120000) // 每2分钟轮询一次
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('轮询任务状态失败:', error)
|
||||||
|
onError && onError(error)
|
||||||
|
isPolling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始轮询
|
||||||
|
poll()
|
||||||
|
|
||||||
|
// 返回停止轮询的函数
|
||||||
|
return () => {
|
||||||
|
isPolling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default imageToVideoApi
|
||||||
46
demo/frontend/src/api/members.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 获取会员列表
|
||||||
|
export const getMembers = (params) => {
|
||||||
|
return api.get('/members', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新会员信息
|
||||||
|
export const updateMember = (id, data) => {
|
||||||
|
return api.put(`/members/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除会员
|
||||||
|
export const deleteMember = (id) => {
|
||||||
|
return api.delete(`/members/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除会员
|
||||||
|
export const deleteMembers = (ids) => {
|
||||||
|
return api.delete('/members/batch', { data: { ids } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会员详情
|
||||||
|
export const getMemberDetail = (id) => {
|
||||||
|
return api.get(`/members/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有会员等级配置
|
||||||
|
export const getMembershipLevels = () => {
|
||||||
|
return api.get('/members/levels')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新会员等级配置
|
||||||
|
export const updateMembershipLevel = (id, data) => {
|
||||||
|
return api.put(`/members/levels/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 封禁/解封会员
|
||||||
|
export const toggleBanMember = (id, isActive) => {
|
||||||
|
return api.put(`/members/${id}/ban`, { isActive })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置用户角色(仅超级管理员可用)
|
||||||
|
export const setUserRole = (id, role) => {
|
||||||
|
return api.put(`/members/${id}/role`, { role })
|
||||||
|
}
|
||||||
63
demo/frontend/src/api/orders.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 订单相关API
|
||||||
|
export const getOrders = (params) => {
|
||||||
|
return api.get('/orders', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOrderById = (id) => {
|
||||||
|
return api.get(`/orders/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createOrder = (orderData) => {
|
||||||
|
return api.post('/orders/create', orderData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateOrderStatus = (id, status, notes) => {
|
||||||
|
return api.post(`/orders/${id}/status`, {
|
||||||
|
status,
|
||||||
|
notes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cancelOrder = (id, reason) => {
|
||||||
|
return api.post(`/orders/${id}/cancel`, {
|
||||||
|
reason
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shipOrder = (id, trackingNumber) => {
|
||||||
|
return api.post(`/orders/${id}/ship`, {
|
||||||
|
trackingNumber
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const completeOrder = (id) => {
|
||||||
|
return api.post(`/orders/${id}/complete`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createOrderPayment = (id, paymentMethod) => {
|
||||||
|
return api.post(`/orders/${id}/pay`, {
|
||||||
|
paymentMethod
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员订单API(使用普通订单接口,后端会根据用户角色返回相应数据)
|
||||||
|
export const getAdminOrders = (params) => {
|
||||||
|
return api.get('/orders', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单统计API
|
||||||
|
export const getOrderStats = () => {
|
||||||
|
return api.get('/orders/stats')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除订单
|
||||||
|
export const deleteOrders = (orderIds) => {
|
||||||
|
return api.delete('/orders/batch', { data: orderIds })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除单个订单
|
||||||
|
export const deleteOrder = (id) => {
|
||||||
|
return api.delete(`/orders/${id}`)
|
||||||
|
}
|
||||||
82
demo/frontend/src/api/payments.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 支付相关API
|
||||||
|
export const getPayments = (params) => {
|
||||||
|
return api.get('/payments', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPaymentById = (id) => {
|
||||||
|
return api.get(`/payments/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPayment = (paymentData) => {
|
||||||
|
return api.post('/payments/create', paymentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTestPayment = (paymentData) => {
|
||||||
|
return api.post('/payments/create-test', paymentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updatePaymentStatus = (id, status) => {
|
||||||
|
return api.put(`/payments/${id}/status`, { status })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const confirmPaymentSuccess = (id, externalTransactionId) => {
|
||||||
|
return api.post(`/payments/${id}/success`, {
|
||||||
|
externalTransactionId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const confirmPaymentFailure = (id, failureReason) => {
|
||||||
|
return api.post(`/payments/${id}/failure`, {
|
||||||
|
failureReason
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试支付完成API
|
||||||
|
export const testPaymentComplete = (id) => {
|
||||||
|
return api.post(`/payments/${id}/test-complete`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付宝支付API
|
||||||
|
export const createAlipayPayment = (paymentData) => {
|
||||||
|
return api.post(`/payments/alipay/create`, paymentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleAlipayCallback = (params) => {
|
||||||
|
return api.post('/payments/alipay/callback', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 噜噜支付(彩虹易支付)API
|
||||||
|
export const createLuluPayment = (paymentData) => {
|
||||||
|
return api.post('/payments/lulupay/create', paymentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayPal支付API
|
||||||
|
export const createPayPalPayment = (paymentData) => {
|
||||||
|
return api.post('/payment/paypal/create', paymentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPayPalPaymentStatus = (paymentId) => {
|
||||||
|
return api.get(`/payment/paypal/status/${paymentId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付统计API
|
||||||
|
export const getPaymentStats = () => {
|
||||||
|
return api.get('/payments/stats')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户订阅信息
|
||||||
|
export const getUserSubscriptionInfo = () => {
|
||||||
|
return api.get('/payments/subscription/info')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除单个支付记录
|
||||||
|
export const deletePayment = (id) => {
|
||||||
|
return api.delete(`/payments/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除支付记录
|
||||||
|
export const deletePayments = (paymentIds) => {
|
||||||
|
return api.delete('/payments/batch', { data: paymentIds })
|
||||||
|
}
|
||||||
23
demo/frontend/src/api/points.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 积分相关API
|
||||||
|
export const getPointsInfo = () => {
|
||||||
|
return api.get('/points/info')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPointsHistory = (params = {}) => {
|
||||||
|
return api.get('/points/history', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPointsFreezeRecords = () => {
|
||||||
|
return api.get('/points/freeze-records')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const processExpiredRecords = () => {
|
||||||
|
return api.post('/points/process-expired')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
31
demo/frontend/src/api/promptOptimizer.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优化提示词
|
||||||
|
* @param {string} prompt - 原始提示词
|
||||||
|
* @param {string} type - 优化类型: 'text-to-video' | 'image-to-video' | 'storyboard'
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
export const optimizePrompt = async (prompt, type = 'text-to-video') => {
|
||||||
|
// 参数验证
|
||||||
|
if (!prompt || !prompt.trim()) {
|
||||||
|
throw new Error('提示词不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prompt.length > 2000) {
|
||||||
|
throw new Error('提示词过长,请控制在2000字符以内')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置较长的超时时间(30秒),因为AI优化可能需要较长时间
|
||||||
|
return api.post('/prompt/optimize', {
|
||||||
|
prompt: prompt.trim(),
|
||||||
|
type
|
||||||
|
}, {
|
||||||
|
timeout: 30000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
optimizePrompt
|
||||||
|
}
|
||||||
|
|
||||||
194
demo/frontend/src/api/request.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import router from '@/router'
|
||||||
|
import { getApiBaseURL } from '@/utils/apiHelper'
|
||||||
|
|
||||||
|
// 创建axios实例
|
||||||
|
// 自动检测:如果通过 Nginx 访问(包含 ngrok),使用相对路径;否则使用完整 URL
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: getApiBaseURL(),
|
||||||
|
timeout: 900000, // 增加到15分钟,适应视频生成时间
|
||||||
|
withCredentials: true,
|
||||||
|
maxRedirects: 0, // 不自动跟随重定向,手动处理302
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
validateStatus: function (status) {
|
||||||
|
// 允许所有状态码,包括302,让拦截器处理
|
||||||
|
return status >= 200 && status < 600
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 登录相关的接口不需要添加token
|
||||||
|
const loginUrls = [
|
||||||
|
'/auth/login',
|
||||||
|
'/auth/login/email',
|
||||||
|
'/auth/register',
|
||||||
|
'/verification/email/send',
|
||||||
|
'/verification/email/verify',
|
||||||
|
'/verification/email/dev-set',
|
||||||
|
'/public/'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 检查当前请求是否是登录相关接口
|
||||||
|
const isLoginRequest = loginUrls.some(url => config.url.includes(url))
|
||||||
|
|
||||||
|
if (!isLoginRequest) {
|
||||||
|
// 非登录请求才添加Authorization头
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token && token !== 'null' && token.trim() !== '') {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
// 打印token前30字符用于调试
|
||||||
|
console.log('请求拦截器:添加Authorization头,token前30字符:', token.substring(0, 30), '请求URL:', config.url)
|
||||||
|
} else {
|
||||||
|
console.warn('请求拦截器:未找到有效的token,请求URL:', config.url)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('请求拦截器:登录相关请求,不添加token:', config.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('请求拦截器错误:', error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// 检查是否是HTML响应(可能是302重定向的结果)
|
||||||
|
if (response.data && typeof response.data === 'string' && response.data.trim().startsWith('<!DOCTYPE')) {
|
||||||
|
console.error('收到HTML响应,可能是认证失败:', response.config.url)
|
||||||
|
|
||||||
|
// 只有非登录请求才清除token并跳转
|
||||||
|
const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||||
|
const isLoginRequest = loginUrls.some(url => response.config.url.includes(url))
|
||||||
|
|
||||||
|
if (!isLoginRequest) {
|
||||||
|
// 清除无效的token并跳转到欢迎页
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
// 避免重复跳转
|
||||||
|
if (router.currentRoute.value.path !== '/' && router.currentRoute.value.path !== '/login') {
|
||||||
|
ElMessage.warning('登录已过期,请重新登录')
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 返回错误,让调用方知道这是认证失败
|
||||||
|
return Promise.reject(new Error('认证失败:收到HTML响应'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查401未授权(Token过期)
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.error('收到401,Token已过期:', response.config.url)
|
||||||
|
|
||||||
|
const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||||
|
const isLoginRequest = loginUrls.some(url => response.config.url.includes(url))
|
||||||
|
|
||||||
|
if (!isLoginRequest) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
ElMessage.warning('登录已过期,请重新登录')
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('认证失败:Token已过期'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查302重定向
|
||||||
|
if (response.status === 302) {
|
||||||
|
console.error('收到302重定向,可能是认证失败:', response.config.url)
|
||||||
|
|
||||||
|
const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||||
|
const isLoginRequest = loginUrls.some(url => response.config.url.includes(url))
|
||||||
|
|
||||||
|
if (!isLoginRequest) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
ElMessage.warning('登录已过期,请重新登录')
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('认证失败:302重定向'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回response,让调用方处理data
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error.response) {
|
||||||
|
const { status, data } = error.response
|
||||||
|
|
||||||
|
// 检查响应数据是否是HTML(302重定向的结果)
|
||||||
|
if (data && typeof data === 'string' && data.trim().startsWith('<!DOCTYPE')) {
|
||||||
|
console.error('收到HTML响应(可能是302重定向):', error.config.url)
|
||||||
|
|
||||||
|
// 只有非登录请求才清除token并跳转
|
||||||
|
const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||||
|
const isLoginRequest = loginUrls.some(url => error.config.url.includes(url))
|
||||||
|
|
||||||
|
if (!isLoginRequest) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
if (router.currentRoute.value.path !== '/' && router.currentRoute.value.path !== '/login') {
|
||||||
|
ElMessage.warning('登录已过期,请重新登录')
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
case 302:
|
||||||
|
// 只有非登录请求才清除token并跳转
|
||||||
|
const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||||
|
const isLoginRequest = loginUrls.some(url => error.config.url.includes(url))
|
||||||
|
|
||||||
|
if (!isLoginRequest) {
|
||||||
|
// 302也可能是认证失败导致的
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
// 跳转到欢迎页
|
||||||
|
if (router.currentRoute.value.path !== '/' && router.currentRoute.value.path !== '/login') {
|
||||||
|
ElMessage.warning('登录已过期,请重新登录')
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
// 403可能是权限不足或CORS问题
|
||||||
|
// 如果是登录请求的403,不要显示"权限不足",而是显示具体错误信息
|
||||||
|
const loginUrls403 = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||||
|
const isLoginRequest403 = loginUrls403.some(url => error.config.url.includes(url))
|
||||||
|
|
||||||
|
if (!isLoginRequest403) {
|
||||||
|
ElMessage.error('权限不足')
|
||||||
|
} else {
|
||||||
|
// 登录请求的403,显示具体错误或网络问题
|
||||||
|
ElMessage.error(data?.message || '请求失败,请检查网络连接')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
ElMessage.error('请求的资源不存在')
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
ElMessage.error('服务器内部错误')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
ElMessage.error(data?.message || '请求失败')
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
ElMessage.error('网络错误,请检查网络连接')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请求配置错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default api
|
||||||
56
demo/frontend/src/api/storyboardVideo.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分镜视频任务
|
||||||
|
*/
|
||||||
|
export const createStoryboardTask = async (data) => {
|
||||||
|
return api.post('/storyboard-video/create', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接使用上传的分镜图创建视频任务(跳过分镜图生成)
|
||||||
|
* @param {object} data - 包含 storyboardImage, prompt, aspectRatio, hdMode, duration, referenceImages
|
||||||
|
*/
|
||||||
|
export const createVideoDirectTask = async (data) => {
|
||||||
|
return api.post('/storyboard-video/create-video-direct', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务详情
|
||||||
|
*/
|
||||||
|
export const getStoryboardTask = async (taskId) => {
|
||||||
|
return api.get(`/storyboard-video/task/${taskId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户任务列表
|
||||||
|
*/
|
||||||
|
export const getUserStoryboardTasks = async (page = 0, size = 10) => {
|
||||||
|
return api.get('/storyboard-video/tasks', { params: { page, size } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始生成视频(从分镜图生成视频)
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @param {object} params - 视频参数(duration, aspectRatio, hdMode)
|
||||||
|
*/
|
||||||
|
export const startVideoGeneration = async (taskId, params = {}) => {
|
||||||
|
return api.post(`/storyboard-video/task/${taskId}/start-video`, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接多张图片为六宫格(2×3)
|
||||||
|
* @param {Array<string>} images - Base64图片数组
|
||||||
|
* @param {number} cols - 列数,默认3(2×3布局)
|
||||||
|
*/
|
||||||
|
export const mergeImagesToGrid = async (images, cols = 3) => {
|
||||||
|
return api.post('/image-grid/merge', { images, cols })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试失败的分镜视频任务
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
*/
|
||||||
|
export const retryStoryboardTask = async (taskId) => {
|
||||||
|
return api.post(`/storyboard-video/task/${taskId}/retry`)
|
||||||
|
}
|
||||||
30
demo/frontend/src/api/taskStatus.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
export const taskStatusApi = {
|
||||||
|
// 获取任务状态
|
||||||
|
getTaskStatus(taskId) {
|
||||||
|
return api.get(`/task-status/${taskId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取用户的所有任务状态
|
||||||
|
getUserTaskStatuses(username) {
|
||||||
|
return api.get(`/task-status/user/${username}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 取消任务
|
||||||
|
cancelTask(taskId) {
|
||||||
|
return api.post(`/task-status/${taskId}/cancel`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 手动触发轮询(管理员功能)
|
||||||
|
triggerPolling() {
|
||||||
|
return api.post('/task-status/poll')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取所有任务记录(管理员功能)
|
||||||
|
getAllTaskRecords(params) {
|
||||||
|
return api.get('/task-status/admin/all', { params })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
179
demo/frontend/src/api/textToVideo.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文生视频API服务
|
||||||
|
*/
|
||||||
|
export const textToVideoApi = {
|
||||||
|
/**
|
||||||
|
* 创建文生视频任务
|
||||||
|
* @param {Object} params - 任务参数
|
||||||
|
* @param {string} params.prompt - 文本描述
|
||||||
|
* @param {string} params.aspectRatio - 视频比例
|
||||||
|
* @param {number} params.duration - 视频时长
|
||||||
|
* @param {boolean} params.hdMode - 是否高清模式
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
createTask(params) {
|
||||||
|
// 参数验证
|
||||||
|
if (!params) {
|
||||||
|
throw new Error('参数不能为空')
|
||||||
|
}
|
||||||
|
if (!params.prompt || params.prompt.trim() === '') {
|
||||||
|
throw new Error('文本描述不能为空')
|
||||||
|
}
|
||||||
|
if (!params.aspectRatio) {
|
||||||
|
throw new Error('视频比例不能为空')
|
||||||
|
}
|
||||||
|
if (!params.duration || params.duration < 1 || params.duration > 60) {
|
||||||
|
throw new Error('视频时长必须在1-60秒之间')
|
||||||
|
}
|
||||||
|
|
||||||
|
return request({
|
||||||
|
url: '/text-to-video/create',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
prompt: params.prompt.trim(),
|
||||||
|
aspectRatio: params.aspectRatio,
|
||||||
|
duration: params.duration,
|
||||||
|
hdMode: params.hdMode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的所有文生视频任务
|
||||||
|
* @param {number} page - 页码
|
||||||
|
* @param {number} size - 每页数量
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
getTasks(page = 0, size = 10) {
|
||||||
|
return request({
|
||||||
|
url: '/text-to-video/tasks',
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个文生视频任务详情
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
getTaskDetail(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/text-to-video/tasks/${taskId}`,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文生视频任务状态
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
getTaskStatus(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/text-to-video/tasks/${taskId}/status`,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试失败的任务
|
||||||
|
* 复用原task_id,重新提交至外部API
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @returns {Promise} API响应
|
||||||
|
*/
|
||||||
|
retryTask(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/text-to-video/tasks/${taskId}/retry`,
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询任务状态
|
||||||
|
* @param {string} taskId - 任务ID
|
||||||
|
* @param {Function} onProgress - 进度回调
|
||||||
|
* @param {Function} onComplete - 完成回调
|
||||||
|
* @param {Function} onError - 错误回调
|
||||||
|
* @returns {Function} 停止轮询的函数
|
||||||
|
*/
|
||||||
|
pollTaskStatus(taskId, onProgress, onComplete, onError) {
|
||||||
|
let isPolling = true
|
||||||
|
let pollCount = 0
|
||||||
|
const maxPolls = 30 // 最大轮询次数(1小时,每2分钟一次)
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
if (!isPolling || pollCount >= maxPolls) {
|
||||||
|
if (pollCount >= maxPolls) {
|
||||||
|
onError && onError(new Error('任务超时'))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request({
|
||||||
|
url: `/text-to-video/tasks/${taskId}/status`,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查响应是否有效
|
||||||
|
if (!response || !response.data || !response.data.success) {
|
||||||
|
onError && onError(new Error('获取任务状态失败'))
|
||||||
|
isPolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskData = response.data.data
|
||||||
|
|
||||||
|
// 检查taskData是否有效
|
||||||
|
if (!taskData || !taskData.status) {
|
||||||
|
onError && onError(new Error('无效的任务数据'))
|
||||||
|
isPolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskData.status === 'COMPLETED') {
|
||||||
|
onComplete && onComplete(taskData)
|
||||||
|
isPolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskData.status === 'FAILED' || taskData.status === 'CANCELLED') {
|
||||||
|
onError && onError(new Error(taskData.errorMessage || '任务失败'))
|
||||||
|
isPolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用进度回调
|
||||||
|
onProgress && onProgress({
|
||||||
|
status: taskData.status,
|
||||||
|
progress: taskData.progress || 0,
|
||||||
|
resultUrl: taskData.resultUrl
|
||||||
|
})
|
||||||
|
|
||||||
|
pollCount++
|
||||||
|
|
||||||
|
// 继续轮询
|
||||||
|
setTimeout(poll, 120000) // 每2分钟轮询一次
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('轮询任务状态失败:', error)
|
||||||
|
onError && onError(error)
|
||||||
|
isPolling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始轮询
|
||||||
|
poll()
|
||||||
|
|
||||||
|
// 返回停止轮询的函数
|
||||||
|
return () => {
|
||||||
|
isPolling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
demo/frontend/src/api/userWorks.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import api from './request'
|
||||||
|
|
||||||
|
// 获取我的作品列表
|
||||||
|
export const getMyWorks = (params = {}) => {
|
||||||
|
return api.get('/works/my-works', {
|
||||||
|
params: {
|
||||||
|
page: params.page || 0,
|
||||||
|
size: params.size || 10,
|
||||||
|
includeProcessing: params.includeProcessing !== false, // 默认包含正在处理中的作品
|
||||||
|
workType: params.workType || null // 按作品类型筛选
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按类型获取我的作品(用于历史记录)
|
||||||
|
export const getMyWorksByType = (workType, params = {}) => {
|
||||||
|
return api.get('/works/my-works', {
|
||||||
|
params: {
|
||||||
|
page: params.page || 0,
|
||||||
|
size: params.size || 1000,
|
||||||
|
includeProcessing: true,
|
||||||
|
workType: workType // TEXT_TO_VIDEO, IMAGE_TO_VIDEO, STORYBOARD_VIDEO, STORYBOARD_IMAGE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取正在进行中的作品
|
||||||
|
export const getProcessingWorks = () => {
|
||||||
|
return api.get('/works/processing')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取作品详情
|
||||||
|
export const getWorkDetail = (workId) => {
|
||||||
|
return api.get(`/works/${workId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除作品
|
||||||
|
export const deleteWork = (workId) => {
|
||||||
|
return api.delete(`/works/${workId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除作品
|
||||||
|
export const batchDeleteWorks = (workIds) => {
|
||||||
|
return api.post('/works/batch-delete', {
|
||||||
|
workIds: workIds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新作品信息
|
||||||
|
export const updateWork = (workId, data) => {
|
||||||
|
return api.put(`/works/${workId}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取作品统计信息
|
||||||
|
export const getWorkStats = () => {
|
||||||
|
return api.get('/works/stats')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录下载(增加下载次数)
|
||||||
|
export const recordDownload = (workId) => {
|
||||||
|
return api.post(`/works/${workId}/download`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取作品文件下载URL
|
||||||
|
export const getWorkFileUrl = (workId, download = false) => {
|
||||||
|
// 构建URL,直接返回完整路径用于浏览器打开
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||||
|
|
||||||
|
// 去掉 /api 前缀,因为 nginx 会自动转发
|
||||||
|
let url = `${baseUrl}/works/${workId}/file`
|
||||||
|
|
||||||
|
// 添加 download 参数(可选)
|
||||||
|
if (download) {
|
||||||
|
url += '?download=true'
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
398
demo/frontend/src/components/DailyActiveUsersChart.vue
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
<template>
|
||||||
|
<div class="daily-active-users-chart">
|
||||||
|
<div class="chart-header">
|
||||||
|
<h3 class="chart-title">日活用户趋势</h3>
|
||||||
|
<div class="chart-controls">
|
||||||
|
<el-select v-model="selectedYear" @change="loadChartData" placeholder="选择年份">
|
||||||
|
<el-option
|
||||||
|
v-for="year in availableYears"
|
||||||
|
:key="year"
|
||||||
|
:label="`${year}年`"
|
||||||
|
:value="year">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container" ref="chartContainer"></div>
|
||||||
|
|
||||||
|
<div class="chart-footer">
|
||||||
|
<div class="chart-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">今日日活:</span>
|
||||||
|
<span class="stat-value">{{ formatNumber(todayDAU) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">日增长率:</span>
|
||||||
|
<span class="stat-value" :class="dayGrowthRate >= 0 ? 'positive' : 'negative'">
|
||||||
|
{{ dayGrowthRate >= 0 ? '+' : '' }}{{ dayGrowthRate.toFixed(1) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">月均日活:</span>
|
||||||
|
<span class="stat-value">{{ formatNumber(monthlyAvgDAU) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">月增长率:</span>
|
||||||
|
<span class="stat-value" :class="monthGrowthRate >= 0 ? 'positive' : 'negative'">
|
||||||
|
{{ monthGrowthRate >= 0 ? '+' : '' }}{{ monthGrowthRate.toFixed(1) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import * as analyticsAPI from '@/api/analytics'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const chartContainer = ref(null)
|
||||||
|
const selectedYear = ref(2024)
|
||||||
|
const availableYears = ref([2023, 2024, 2025])
|
||||||
|
const chartInstance = ref(null)
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const todayDAU = ref(0)
|
||||||
|
const dayGrowthRate = ref(0)
|
||||||
|
const monthlyAvgDAU = ref(0)
|
||||||
|
const monthGrowthRate = ref(0)
|
||||||
|
|
||||||
|
// 动态加载ECharts
|
||||||
|
const loadECharts = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (window.echarts) {
|
||||||
|
resolve(window.echarts)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js'
|
||||||
|
script.onload = () => resolve(window.echarts)
|
||||||
|
script.onerror = reject
|
||||||
|
document.head.appendChild(script)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载图表数据
|
||||||
|
const loadChartData = async () => {
|
||||||
|
try {
|
||||||
|
// 并行加载图表数据和概览数据
|
||||||
|
const [chartRes, overviewRes] = await Promise.all([
|
||||||
|
analyticsAPI.getDailyActiveUsersTrend(selectedYear.value, 'monthly'),
|
||||||
|
analyticsAPI.getUserActivityOverview()
|
||||||
|
])
|
||||||
|
|
||||||
|
// 处理图表数据
|
||||||
|
if (chartRes && chartRes.monthlyData) {
|
||||||
|
await nextTick()
|
||||||
|
initChart(chartRes.monthlyData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理概览数据
|
||||||
|
if (overviewRes) {
|
||||||
|
todayDAU.value = overviewRes.todayDAU || 0
|
||||||
|
dayGrowthRate.value = overviewRes.dayGrowthRate || 0
|
||||||
|
monthlyAvgDAU.value = overviewRes.monthlyAvgDAU || 0
|
||||||
|
monthGrowthRate.value = overviewRes.monthGrowthRate || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载图表数据失败:', error)
|
||||||
|
ElMessage.error('加载图表数据失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
const initChart = async (data) => {
|
||||||
|
try {
|
||||||
|
const echarts = await loadECharts()
|
||||||
|
|
||||||
|
if (!chartContainer.value) return
|
||||||
|
|
||||||
|
// 销毁现有图表实例
|
||||||
|
if (chartInstance.value) {
|
||||||
|
chartInstance.value.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新图表实例
|
||||||
|
chartInstance.value = echarts.init(chartContainer.value)
|
||||||
|
|
||||||
|
// 准备数据
|
||||||
|
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||||
|
const values = data.map(item => item.avgDailyActive || 0)
|
||||||
|
const maxValues = data.map(item => item.maxDailyActive || 0)
|
||||||
|
const minValues = data.map(item => item.minDailyActive || 0)
|
||||||
|
|
||||||
|
// 图表配置
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
formatter: function(params) {
|
||||||
|
const dataIndex = params[0].dataIndex
|
||||||
|
const month = months[dataIndex]
|
||||||
|
const avgValue = values[dataIndex]
|
||||||
|
const maxValue = maxValues[dataIndex]
|
||||||
|
const minValue = minValues[dataIndex]
|
||||||
|
|
||||||
|
return `${month}<br/>
|
||||||
|
平均日活: ${formatNumber(avgValue)}<br/>
|
||||||
|
最高日活: ${formatNumber(maxValue)}<br/>
|
||||||
|
最低日活: ${formatNumber(minValue)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
top: '10%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: months,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontSize: 12,
|
||||||
|
formatter: function(value) {
|
||||||
|
return formatNumber(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#f0f0f0',
|
||||||
|
type: 'dashed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '日活用户',
|
||||||
|
type: 'line',
|
||||||
|
data: values,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#3b82f6',
|
||||||
|
width: 3
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#3b82f6',
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(59, 130, 246, 0.3)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(59, 130, 246, 0.05)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#1d4ed8',
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 3,
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: 'rgba(59, 130, 246, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
animation: true,
|
||||||
|
animationDuration: 1000,
|
||||||
|
animationEasing: 'cubicOut'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置图表配置
|
||||||
|
chartInstance.value.setOption(option)
|
||||||
|
|
||||||
|
// 响应式调整
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化图表失败:', error)
|
||||||
|
ElMessage.error('图表初始化失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理窗口大小变化
|
||||||
|
const handleResize = () => {
|
||||||
|
if (chartInstance.value) {
|
||||||
|
chartInstance.value.resize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数字
|
||||||
|
const formatNumber = (num) => {
|
||||||
|
if (num >= 10000) {
|
||||||
|
return (num / 10000).toFixed(1) + '万'
|
||||||
|
}
|
||||||
|
return Math.round(num).toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadChartData()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (chartInstance.value) {
|
||||||
|
chartInstance.value.dispose()
|
||||||
|
chartInstance.value = null
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.daily-active-users-chart {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-footer {
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.positive {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.negative {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chart-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stats {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.daily-active-users-chart {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
112
demo/frontend/src/components/Footer.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<el-footer class="footer">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-info">
|
||||||
|
<p>© 2024 AIGC Demo. All rights reserved.</p>
|
||||||
|
<p>基于 Vue.js 3 + Element Plus 构建</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="#" class="footer-link">关于我们</a>
|
||||||
|
<a href="#" class="footer-link">联系我们</a>
|
||||||
|
<a href="#" class="footer-link">隐私政策</a>
|
||||||
|
<a href="#" class="footer-link">服务条款</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// Footer组件逻辑
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
height: 60px;
|
||||||
|
background-color: transparent;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-info {
|
||||||
|
color: #e5e7ff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-info p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
color: #e5e7ff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
71
demo/frontend/src/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<button class="language-switcher" @click="toggleLanguage" :title="currentLanguage === 'zh' ? '切换到英文' : 'Switch to Chinese'">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||||
|
<path d="M4.16602 12.4998V14.1665C4.16602 15.0452 4.84592 15.765 5.7083 15.8286L5.83268 15.8332H8.33268V17.4998H5.83268C3.99173 17.4998 2.49935 16.0074 2.49935 14.1665V12.4998H4.16602ZM14.9993 8.33317L18.666 17.4998H16.8702L15.8694 14.9998H12.461L11.4618 17.4998H9.66685L13.3327 8.33317H14.9993ZM14.166 10.7375L13.1268 13.3332H15.2035L14.166 10.7375ZM6.66602 1.6665V3.33317H9.99935V9.1665H6.66602V11.6665H4.99935V9.1665H1.66602V3.33317H4.99935V1.6665H6.66602ZM14.166 2.49984C16.0069 2.49984 17.4993 3.99222 17.4993 5.83317V7.49984H15.8327V5.83317C15.8327 4.9127 15.0865 4.1665 14.166 4.1665H11.666V2.49984H14.166ZM4.99935 4.99984H3.33268V7.49984H4.99935V4.99984ZM8.33268 4.99984H6.66602V7.49984H8.33268V4.99984Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<span class="lang-text">{{ currentLanguage === 'zh' ? '中' : 'EN' }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { locale } = useI18n()
|
||||||
|
|
||||||
|
const currentLanguage = computed(() => locale.value)
|
||||||
|
|
||||||
|
const toggleLanguage = () => {
|
||||||
|
console.log('[LanguageSwitcher] 当前语言:', locale.value)
|
||||||
|
|
||||||
|
// 切换语言
|
||||||
|
const newLang = locale.value === 'zh' ? 'en' : 'zh'
|
||||||
|
console.log('[LanguageSwitcher] 切换到:', newLang)
|
||||||
|
|
||||||
|
// 直接更新 locale(响应式切换)
|
||||||
|
locale.value = newLang
|
||||||
|
|
||||||
|
// 保存到 localStorage 以便下次刷新时使用
|
||||||
|
localStorage.setItem('language', newLang)
|
||||||
|
console.log('[LanguageSwitcher] localStorage 已保存:', localStorage.getItem('language'))
|
||||||
|
console.log('[LanguageSwitcher] 语言切换完成(无刷新)')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.language-switcher {
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switcher:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switcher:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switcher svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-text {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
275
demo/frontend/src/components/NavBar.vue
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<template>
|
||||||
|
<el-header class="navbar">
|
||||||
|
<div class="navbar-container">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<router-link to="/" class="brand-link">
|
||||||
|
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" class="brand-logo" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 导航菜单 -->
|
||||||
|
<el-menu
|
||||||
|
mode="horizontal"
|
||||||
|
class="navbar-menu"
|
||||||
|
background-color="#409EFF"
|
||||||
|
text-color="#fff"
|
||||||
|
active-text-color="#ffd04b"
|
||||||
|
router
|
||||||
|
@select="handleMenuSelect"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/welcome">
|
||||||
|
<span>{{ $t('common.welcome') }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item index="/admin/dashboard">
|
||||||
|
<span>{{ $t('common.home') }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAuthenticated" index="/profile">
|
||||||
|
<span>{{ $t('common.profile') }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAuthenticated" index="/admin/orders">
|
||||||
|
<span>{{ $t('common.orders') }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAuthenticated" index="/payments">
|
||||||
|
<span>{{ $t('common.payments') }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item v-if="userStore.isAdmin" index="/admin/dashboard">
|
||||||
|
<span>{{ $t('common.adminPanel') }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
|
||||||
|
<!-- 快速切换提示(暂时隐藏) -->
|
||||||
|
<!-- <div class="quick-switch-hint" v-if="showShortcutHint">
|
||||||
|
<el-tooltip content="使用 Alt + 数字键快速切换页面" placement="bottom">
|
||||||
|
<el-icon><Keyboard /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- 用户菜单 -->
|
||||||
|
<div class="navbar-user">
|
||||||
|
<template v-if="userStore.isAuthenticated">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<el-dropdown @command="handleUserCommand">
|
||||||
|
<span class="user-dropdown">
|
||||||
|
<span>{{ userStore.username }}</span>
|
||||||
|
<el-tag v-if="userStore.availablePoints > 0" size="small" type="success" class="points-tag">
|
||||||
|
{{ userStore.availablePoints }}{{ $t('common.points') }}
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="profile">
|
||||||
|
{{ $t('common.userProfile') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="userStore.isAdmin" command="admin">
|
||||||
|
{{ $t('common.adminPanel') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="settings">
|
||||||
|
{{ $t('common.settings') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided command="logout">
|
||||||
|
{{ $t('common.logout') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<el-button type="primary" plain @click="$router.push('/login')">
|
||||||
|
{{ $t('common.login') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" plain @click="$router.push('/register')">
|
||||||
|
{{ $t('common.register') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import LanguageSwitcher from './LanguageSwitcher.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 显示快捷键提示(暂时禁用)
|
||||||
|
// const showShortcutHint = ref(true)
|
||||||
|
|
||||||
|
// 快速切换处理函数
|
||||||
|
const handleMenuSelect = (index) => {
|
||||||
|
// 使用replace而不是push,避免浏览器历史记录堆积
|
||||||
|
router.replace(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂时禁用快捷键功能,确保基本功能正常
|
||||||
|
// const handleKeydown = (event) => {
|
||||||
|
// // 快捷键功能暂时禁用
|
||||||
|
// }
|
||||||
|
|
||||||
|
// onMounted(() => {
|
||||||
|
// // 暂时不添加键盘事件监听
|
||||||
|
// })
|
||||||
|
|
||||||
|
// onUnmounted(() => {
|
||||||
|
// // 暂时不移除键盘事件监听
|
||||||
|
// })
|
||||||
|
|
||||||
|
const handleUserCommand = async (command) => {
|
||||||
|
switch (command) {
|
||||||
|
case 'profile':
|
||||||
|
ElMessage.info(t('common.profileDevMsg'))
|
||||||
|
break
|
||||||
|
case 'admin':
|
||||||
|
// 检查管理员权限
|
||||||
|
if (userStore.isAdmin) {
|
||||||
|
router.push('/admin/dashboard')
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(t('common.noPermissionMsg'))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'settings':
|
||||||
|
ElMessage.info(t('common.settingsDevMsg'))
|
||||||
|
break
|
||||||
|
case 'logout':
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(t('common.logoutConfirm'), t('common.tip'), {
|
||||||
|
confirmButtonText: t('common.confirm'),
|
||||||
|
cancelButtonText: t('common.cancel'),
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
await userStore.logoutUser()
|
||||||
|
ElMessage.success(t('common.logoutSuccess'))
|
||||||
|
router.push('/')
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.navbar {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-menu {
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-menu .el-menu-item {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-menu .el-menu-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-user {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-tag {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown .el-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-user .el-button {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-hint {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-switch-hint {
|
||||||
|
margin-right: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-switch-hint:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1211
demo/frontend/src/components/PaymentModal.vue
Normal file
435
demo/frontend/src/components/TaskStatusDisplay.vue
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
<template>
|
||||||
|
<div class="task-status-display">
|
||||||
|
<div class="status-header">
|
||||||
|
<h3>任务状态</h3>
|
||||||
|
<div class="status-badge" :class="statusClass">
|
||||||
|
{{ statusText }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-section" v-if="taskStatus">
|
||||||
|
<!-- 排队中:不确定进度条 -->
|
||||||
|
<div v-if="taskStatus.status === 'PENDING'" class="progress-bar indeterminate">
|
||||||
|
<div class="progress-fill-indeterminate"></div>
|
||||||
|
</div>
|
||||||
|
<!-- 生成中:动态进度条 -->
|
||||||
|
<div v-else class="progress-bar">
|
||||||
|
<div
|
||||||
|
class="progress-fill animated"
|
||||||
|
:style="{ width: taskStatus.progress + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text" v-if="taskStatus.status !== 'PENDING'">{{ taskStatus.progress }}%</div>
|
||||||
|
<div class="progress-text" v-else>排队中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">任务ID:</span>
|
||||||
|
<span class="value">{{ taskStatus?.taskId }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">创建时间:</span>
|
||||||
|
<span class="value">{{ formatDate(taskStatus?.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item" v-if="taskStatus?.completedAt">
|
||||||
|
<span class="label">完成时间:</span>
|
||||||
|
<span class="value">{{ formatDate(taskStatus.completedAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item" v-if="taskStatus?.resultUrl">
|
||||||
|
<span class="label">结果URL:</span>
|
||||||
|
<a :href="taskStatus.resultUrl" target="_blank" class="result-link">
|
||||||
|
查看结果
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="info-item" v-if="taskStatus?.errorMessage">
|
||||||
|
<span class="label">错误信息:</span>
|
||||||
|
<span class="value error">{{ taskStatus.errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons" v-if="showActions">
|
||||||
|
<button
|
||||||
|
v-if="canCancel"
|
||||||
|
@click="cancelTask"
|
||||||
|
class="btn-cancel"
|
||||||
|
:disabled="cancelling"
|
||||||
|
>
|
||||||
|
{{ cancelling ? '取消中...' : '取消任务' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="canRetry"
|
||||||
|
@click="retryTask"
|
||||||
|
class="btn-retry"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { taskStatusApi } from '@/api/taskStatus'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
taskId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
autoRefresh: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
refreshInterval: {
|
||||||
|
type: Number,
|
||||||
|
default: 30000 // 30秒
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['statusChanged', 'taskCompleted', 'taskFailed'])
|
||||||
|
|
||||||
|
const taskStatus = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const cancelling = ref(false)
|
||||||
|
const refreshTimer = ref(null)
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
if (!taskStatus.value) return 'status-pending'
|
||||||
|
|
||||||
|
switch (taskStatus.value.status) {
|
||||||
|
case 'PENDING':
|
||||||
|
return 'status-pending'
|
||||||
|
case 'PROCESSING':
|
||||||
|
return 'status-processing'
|
||||||
|
case 'COMPLETED':
|
||||||
|
return 'status-completed'
|
||||||
|
case 'FAILED':
|
||||||
|
return 'status-failed'
|
||||||
|
case 'CANCELLED':
|
||||||
|
return 'status-cancelled'
|
||||||
|
case 'TIMEOUT':
|
||||||
|
return 'status-timeout'
|
||||||
|
default:
|
||||||
|
return 'status-pending'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
if (!taskStatus.value) return '未知'
|
||||||
|
return taskStatus.value.statusDescription || taskStatus.value.status
|
||||||
|
})
|
||||||
|
|
||||||
|
const showActions = computed(() => {
|
||||||
|
if (!taskStatus.value) return false
|
||||||
|
return ['PENDING', 'PROCESSING'].includes(taskStatus.value.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
const canCancel = computed(() => {
|
||||||
|
if (!taskStatus.value) return false
|
||||||
|
return taskStatus.value.status === 'PROCESSING'
|
||||||
|
})
|
||||||
|
|
||||||
|
const canRetry = computed(() => {
|
||||||
|
if (!taskStatus.value) return false
|
||||||
|
return ['FAILED', 'TIMEOUT'].includes(taskStatus.value.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const fetchTaskStatus = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await taskStatusApi.getTaskStatus(props.taskId)
|
||||||
|
taskStatus.value = response.data
|
||||||
|
|
||||||
|
// 触发状态变化事件
|
||||||
|
emit('statusChanged', taskStatus.value)
|
||||||
|
|
||||||
|
// 检查任务是否完成
|
||||||
|
if (taskStatus.value.status === 'COMPLETED') {
|
||||||
|
emit('taskCompleted', taskStatus.value)
|
||||||
|
} else if (['FAILED', 'TIMEOUT', 'CANCELLED'].includes(taskStatus.value.status)) {
|
||||||
|
emit('taskFailed', taskStatus.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务状态失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelTask = async () => {
|
||||||
|
try {
|
||||||
|
cancelling.value = true
|
||||||
|
const response = await taskStatusApi.cancelTask(props.taskId)
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
await fetchTaskStatus() // 刷新状态
|
||||||
|
} else {
|
||||||
|
alert('取消失败: ' + response.data.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消任务失败:', error)
|
||||||
|
alert('取消任务失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
cancelling.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryTask = () => {
|
||||||
|
// 重试逻辑,这里可以触发重新创建任务
|
||||||
|
emit('retryTask', props.taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-'
|
||||||
|
return new Date(dateString).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
const startAutoRefresh = () => {
|
||||||
|
if (props.autoRefresh && !refreshTimer.value) {
|
||||||
|
refreshTimer.value = setInterval(fetchTaskStatus, props.refreshInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAutoRefresh = () => {
|
||||||
|
if (refreshTimer.value) {
|
||||||
|
clearInterval(refreshTimer.value)
|
||||||
|
refreshTimer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTaskStatus()
|
||||||
|
startAutoRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAutoRefresh()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.task-status-display {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-processing {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-completed {
|
||||||
|
background: #10b981;
|
||||||
|
color: #064e3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-failed {
|
||||||
|
background: #ef4444;
|
||||||
|
color: #7f1d1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background: #6b7280;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-timeout {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: #78350f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #374151;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #3b82f6, #1d4ed8);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动态进度条动画 */
|
||||||
|
.progress-fill.animated {
|
||||||
|
background: linear-gradient(90deg, #3b82f6, #60a5fa, #3b82f6);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: progress-gradient 2s ease infinite, progress-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill.animated::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||||||
|
animation: progress-shine 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-gradient {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.85; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-shine {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 不确定进度条(排队中) */
|
||||||
|
.progress-bar.indeterminate {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill-indeterminate {
|
||||||
|
width: 30%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, #3b82f6, #60a5fa, #3b82f6, transparent);
|
||||||
|
border-radius: 4px;
|
||||||
|
animation: indeterminate-slide 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes indeterminate-slide {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(400%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
text-align: center;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.error {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-link {
|
||||||
|
color: #3b82f6;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel,
|
||||||
|
.btn-retry {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover:not(:disabled) {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-retry {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-retry:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
919
demo/frontend/src/locales/en.js
Normal file
@@ -0,0 +1,919 @@
|
|||||||
|
export default {
|
||||||
|
common: {
|
||||||
|
confirm: 'Confirm',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
save: 'Save',
|
||||||
|
delete: 'Delete',
|
||||||
|
edit: 'Edit',
|
||||||
|
search: 'Search',
|
||||||
|
reset: 'Reset',
|
||||||
|
view: 'View',
|
||||||
|
loading: 'Loading...',
|
||||||
|
searchPlaceholder: 'Search for content',
|
||||||
|
welcome: 'Welcome',
|
||||||
|
home: 'Home',
|
||||||
|
profile: 'Profile',
|
||||||
|
orders: 'Orders',
|
||||||
|
payments: 'Payments',
|
||||||
|
adminPanel: 'Admin Panel',
|
||||||
|
login: 'Login',
|
||||||
|
register: 'Register',
|
||||||
|
logout: 'Logout',
|
||||||
|
settings: 'Settings',
|
||||||
|
userProfile: 'User Profile',
|
||||||
|
points: 'Points',
|
||||||
|
profileDevMsg: 'Profile feature under development',
|
||||||
|
settingsDevMsg: 'Settings feature under development',
|
||||||
|
noPermissionMsg: 'Insufficient permission, only administrators can access admin panel',
|
||||||
|
logoutConfirm: 'Are you sure you want to logout?',
|
||||||
|
logoutSuccess: 'Logout successful',
|
||||||
|
tip: 'Tip',
|
||||||
|
submit: 'Submit',
|
||||||
|
back: 'Back',
|
||||||
|
next: 'Next',
|
||||||
|
previous: 'Previous',
|
||||||
|
close: 'Close',
|
||||||
|
success: 'Success',
|
||||||
|
error: 'Error',
|
||||||
|
warning: 'Warning',
|
||||||
|
info: 'Info'
|
||||||
|
},
|
||||||
|
|
||||||
|
login: {
|
||||||
|
title: 'Login',
|
||||||
|
emailCodeLogin: 'Email Verification Login',
|
||||||
|
emailPasswordLogin: 'Email Password Login',
|
||||||
|
emailPlaceholder: 'Enter email address',
|
||||||
|
codePlaceholder: 'Enter verification code',
|
||||||
|
passwordPlaceholder: 'Enter password',
|
||||||
|
getCode: 'Get Code',
|
||||||
|
loginButton: 'Login',
|
||||||
|
loginOrRegister: 'Login/Register',
|
||||||
|
loggingIn: 'Logging in...',
|
||||||
|
agreement: 'By logging in, you agree to the Terms of Service and Privacy Policy',
|
||||||
|
testAccounts: 'Test Accounts',
|
||||||
|
admin: 'Admin',
|
||||||
|
normalUser: 'Normal User',
|
||||||
|
quickEmail: 'Quick Email'
|
||||||
|
},
|
||||||
|
|
||||||
|
home: {
|
||||||
|
title: 'Home',
|
||||||
|
exploreCreativity: 'Explore Unlimited Creativity',
|
||||||
|
subtitle: 'Transform Your Imagination into Reality with AI',
|
||||||
|
getStarted: 'Get Started',
|
||||||
|
learnMore: 'Learn More',
|
||||||
|
features: 'Core Features',
|
||||||
|
textToVideo: 'Text to Video',
|
||||||
|
textToVideoDesc: 'Enter text descriptions and AI generates high-quality videos',
|
||||||
|
imageToVideo: 'Image to Video',
|
||||||
|
imageToVideoDesc: 'Upload images and AI generates dynamic videos',
|
||||||
|
storyboardVideo: 'Storyboard Video',
|
||||||
|
storyboardVideoDesc: 'Professional storyboarding for cinematic effects',
|
||||||
|
myWorks: 'My Works',
|
||||||
|
myWorksDesc: 'Manage and view all your creations',
|
||||||
|
subscription: 'Subscription',
|
||||||
|
subscriptionDesc: 'Unlock more advanced features and resources'
|
||||||
|
},
|
||||||
|
|
||||||
|
profile: {
|
||||||
|
title: 'Profile',
|
||||||
|
userInfo: 'User Information',
|
||||||
|
username: 'Username',
|
||||||
|
email: 'Email',
|
||||||
|
memberLevel: 'Membership Level',
|
||||||
|
expiryDate: 'Expiry Date',
|
||||||
|
availablePoints: 'Available Points',
|
||||||
|
frozenPoints: 'Frozen Points',
|
||||||
|
accountStats: 'Account Statistics',
|
||||||
|
totalWorks: 'Total Works',
|
||||||
|
videoGenerated: 'Videos Generated',
|
||||||
|
storageUsed: 'Storage Used',
|
||||||
|
editProfile: 'Edit Profile',
|
||||||
|
changePassword: 'Change Password',
|
||||||
|
viewWorks: 'View Works',
|
||||||
|
upgradeMembership: 'Upgrade Membership',
|
||||||
|
subscription: 'Subscription',
|
||||||
|
myWorks: 'My Works',
|
||||||
|
tools: 'Tools',
|
||||||
|
noUsername: 'No username set',
|
||||||
|
published: 'Portfolio',
|
||||||
|
userId: 'ID',
|
||||||
|
noWorksYet: 'No works yet, start creating!',
|
||||||
|
createSimilar: 'Create Similar',
|
||||||
|
workDetail: 'Work Details',
|
||||||
|
category: 'Category',
|
||||||
|
inputDetails: 'Input Details',
|
||||||
|
createTime: 'Create Time',
|
||||||
|
workId: 'Work ID',
|
||||||
|
date: 'Date',
|
||||||
|
duration: 'Duration',
|
||||||
|
quality: 'Quality',
|
||||||
|
aspectRatio: 'Aspect Ratio',
|
||||||
|
unknown: 'Unknown',
|
||||||
|
anonymousUser: 'Anonymous User',
|
||||||
|
browserNotSupport: 'Your browser does not support video playback',
|
||||||
|
noPrompt: 'No prompt available',
|
||||||
|
dashboard: 'Dashboard',
|
||||||
|
orderManagement: 'Order Management',
|
||||||
|
memberManagement: 'Member Management',
|
||||||
|
systemSettings: 'System Settings',
|
||||||
|
insufficientPermission: 'Insufficient permission, only administrators can access',
|
||||||
|
logoutConfirm: 'Are you sure you want to logout?',
|
||||||
|
logoutSuccess: 'Logged out successfully',
|
||||||
|
logoutFailed: 'Logout failed',
|
||||||
|
loadWorksFailed: 'Failed to load works',
|
||||||
|
loadUserInfoFailed: 'Failed to load user info',
|
||||||
|
loadDetailFailed: 'Failed to load work details',
|
||||||
|
profileEditDevMsg: 'Profile edit feature under development'
|
||||||
|
},
|
||||||
|
|
||||||
|
video: {
|
||||||
|
create: 'Create',
|
||||||
|
generating: 'Generating',
|
||||||
|
completed: 'Completed',
|
||||||
|
failed: 'Failed',
|
||||||
|
failReason: 'Failure Reason',
|
||||||
|
prompt: 'Prompt',
|
||||||
|
promptPlaceholder: 'Enter video description...',
|
||||||
|
optimizePrompt: 'Optimize Prompt',
|
||||||
|
uploadImage: 'Upload Image',
|
||||||
|
selectImage: 'Select Image',
|
||||||
|
generate: 'Generate Video',
|
||||||
|
duration: 'Duration',
|
||||||
|
resolution: 'Resolution',
|
||||||
|
style: 'Style',
|
||||||
|
aspectRatio: 'Aspect Ratio',
|
||||||
|
advancedSettings: 'Advanced Settings',
|
||||||
|
cost: 'Points Cost',
|
||||||
|
estimatedTime: 'Estimated Time',
|
||||||
|
result: 'Result',
|
||||||
|
download: 'Download',
|
||||||
|
share: 'Share',
|
||||||
|
regenerate: 'Regenerate',
|
||||||
|
saveToWorks: 'Save to Works',
|
||||||
|
videoUrl: 'Video URL',
|
||||||
|
status: 'Status',
|
||||||
|
createdAt: 'Created At',
|
||||||
|
|
||||||
|
// Text to Video translations
|
||||||
|
textToVideo: {
|
||||||
|
aspectRatio: 'Aspect Ratio',
|
||||||
|
dateFormat: '{year}-{month}-{day} {hours}:{minutes}',
|
||||||
|
pointsUpdated: 'User points updated',
|
||||||
|
pointsUpdateFailed: 'Failed to update user points',
|
||||||
|
textInputPlaceholder: 'Enter video description, e.g.: horses running on the grassland...',
|
||||||
|
userAvatar: 'User Avatar',
|
||||||
|
optimizing: 'Optimizing...',
|
||||||
|
oneClickOptimize: 'One-Click Optimize',
|
||||||
|
hdMode: 'HD Mode',
|
||||||
|
hdModeCost: '+20 Points',
|
||||||
|
startGenerate: 'Start Generation',
|
||||||
|
pleaseLogin: 'Please Login First',
|
||||||
|
loginRequired: 'Login required to start creating',
|
||||||
|
loginNow: 'Login Now',
|
||||||
|
inProgress: 'In Progress',
|
||||||
|
noVideoUrl: 'No Video Available',
|
||||||
|
withWatermark: 'With Watermark',
|
||||||
|
withoutWatermark: 'Without Watermark',
|
||||||
|
createSimilar: 'Create Similar',
|
||||||
|
downloadVideo: 'Download Video',
|
||||||
|
deleteWork: 'Delete Work',
|
||||||
|
generationFailed: 'Generation Failed',
|
||||||
|
checkInputOrRetry: 'Please check input or retry',
|
||||||
|
regenerate: 'Regenerate',
|
||||||
|
clearAndCreateNew: 'Clear and Create New',
|
||||||
|
failedTaskCleared: 'Failed task cleared',
|
||||||
|
startCreating: 'Start creating your video!',
|
||||||
|
noDescription: 'No Description',
|
||||||
|
queuing: 'Queuing...',
|
||||||
|
subscribeToSpeedUp: 'Subscribe to speed up',
|
||||||
|
noResult: 'No Result',
|
||||||
|
pleaseLoginFirst: 'Please login first',
|
||||||
|
taskInProgress: 'Task already in progress, please wait',
|
||||||
|
pleaseEnterText: 'Please enter video description',
|
||||||
|
creatingTask: 'Creating task...',
|
||||||
|
taskCreated: 'Task created successfully, generating...',
|
||||||
|
createTaskFailed: 'Failed to create task',
|
||||||
|
videoCompleted: 'Video generation completed!',
|
||||||
|
videoFailed: 'Video generation failed:',
|
||||||
|
statusPending: 'Pending',
|
||||||
|
statusProcessing: 'Processing',
|
||||||
|
statusCancelled: 'Cancelled',
|
||||||
|
statusUnknown: 'Unknown Status',
|
||||||
|
pleaseEnterPrompt: 'Please enter prompt first',
|
||||||
|
promptTooLong: 'Prompt too long, max 2000 characters',
|
||||||
|
optimizingPrompt: 'Optimizing prompt...',
|
||||||
|
optimizeSuccess: 'Prompt optimized successfully!',
|
||||||
|
optimizeNoChange: 'Prompt is already optimal',
|
||||||
|
optimizeFailed: 'Optimization failed, please retry',
|
||||||
|
requestParamError: 'Invalid request parameters',
|
||||||
|
requestTimeout: 'Request timeout, check network',
|
||||||
|
serverError: 'Server error, please retry later',
|
||||||
|
networkError: 'Network error, check connection',
|
||||||
|
networkConnectionError: 'Network connection failed',
|
||||||
|
downloadStarted: 'Download started',
|
||||||
|
videoUrlNotAvailable: 'Video URL not available',
|
||||||
|
noWorkToDelete: 'No work to delete',
|
||||||
|
deleteConfirm: 'Are you sure to delete this work? This cannot be undone.',
|
||||||
|
confirmDelete: 'Confirm Delete',
|
||||||
|
workDeleted: 'Work deleted',
|
||||||
|
deleteCancelled: 'Delete cancelled',
|
||||||
|
historyLoadSuccess: 'History loaded: {count} items',
|
||||||
|
historyLoadFailed: 'Failed to load history',
|
||||||
|
historyParamsFilled: 'History parameters filled, ready to generate',
|
||||||
|
cancelFunctionTBD: 'Cancel function under development',
|
||||||
|
unfinishedTaskDetected: 'Unfinished task detected, restoring...'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Image to Video translations
|
||||||
|
imageToVideo: {
|
||||||
|
userAvatar: 'User Avatar',
|
||||||
|
firstFrame: 'First Frame',
|
||||||
|
promptPlaceholder: 'Describe the content you want to generate with the image',
|
||||||
|
tipWarning: '⚠️ Do not upload images of real people or anime IP',
|
||||||
|
tip1: '🎬 Supports camera movements, character actions, scene transitions, etc.',
|
||||||
|
optimizing: 'Optimizing...',
|
||||||
|
optimizePrompt: 'Optimize',
|
||||||
|
hdMode: 'HD Mode',
|
||||||
|
hdModeCost: 'Costs 20 points when enabled',
|
||||||
|
startGenerate: 'Start Generate',
|
||||||
|
pleaseLogin: 'Please Login',
|
||||||
|
loginRequired: 'Login required to submit task',
|
||||||
|
loginNow: 'Login Now',
|
||||||
|
inProgress: 'In Progress',
|
||||||
|
statusPending: 'Pending',
|
||||||
|
statusProcessing: 'Processing',
|
||||||
|
statusCompleted: 'Completed',
|
||||||
|
statusFailed: 'Failed',
|
||||||
|
statusCancelled: 'Cancelled',
|
||||||
|
noVideoUrl: 'Video generated but URL not available',
|
||||||
|
withWatermark: 'With Watermark',
|
||||||
|
withoutWatermark: 'Without Watermark (Member Exclusive)',
|
||||||
|
createSimilar: 'Create Similar',
|
||||||
|
downloadVideo: 'Download Video',
|
||||||
|
deleteWork: 'Delete Work',
|
||||||
|
retry: 'Retry',
|
||||||
|
generateFailed: 'Generation Failed',
|
||||||
|
generateFailedDesc: 'Please check input or retry',
|
||||||
|
startCreating: 'Start creating your first work!',
|
||||||
|
tip1: 'Upload first frame image',
|
||||||
|
tip2: 'Enter description text',
|
||||||
|
tip3: 'Select video parameters',
|
||||||
|
tip4: 'Click to start generating',
|
||||||
|
noDescription: 'No Description',
|
||||||
|
queuing: 'Queuing',
|
||||||
|
subscribeToSpeedUp: 'Subscribe to speed up generation',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
firstFrameImage: 'First Frame Image',
|
||||||
|
noResult: 'No Result',
|
||||||
|
year: '',
|
||||||
|
month: '',
|
||||||
|
day: '',
|
||||||
|
fileSizeLimit: 'Image file size cannot exceed 100MB',
|
||||||
|
invalidImageFile: 'Please select a valid image file',
|
||||||
|
pleaseLoginFirst: 'Please login before submitting task',
|
||||||
|
taskInProgress: 'A task is already in progress, please wait or cancel it',
|
||||||
|
uploadFirstFrameRequired: 'Please upload first frame image',
|
||||||
|
enterDescriptionRequired: 'Please enter description text',
|
||||||
|
creatingTask: 'Creating task...',
|
||||||
|
taskCreatedSuccess: 'Task created successfully, processing...',
|
||||||
|
createTaskFailed: 'Failed to create task',
|
||||||
|
createTaskFailedRetry: 'Failed to create task, please retry',
|
||||||
|
videoGenerateCompleted: 'Video generation completed!',
|
||||||
|
videoGenerateFailed: 'Video generation failed: ',
|
||||||
|
enterPromptFirst: 'Please enter prompt',
|
||||||
|
promptTooLong: 'Prompt too long, please keep within 2000 characters',
|
||||||
|
optimizingPrompt: 'Optimizing prompt, please wait...',
|
||||||
|
promptOptimizedSuccess: 'Prompt optimized successfully!',
|
||||||
|
promptAlreadyOptimized: 'Prompt already optimized, may not have obvious changes',
|
||||||
|
optimizeFailed: 'Optimization failed',
|
||||||
|
optimizePromptFailed: 'Failed to optimize prompt',
|
||||||
|
requestParameterError: 'Request parameter error',
|
||||||
|
requestTimeout: 'Request timeout, please retry later',
|
||||||
|
serverError: 'Server error, please retry later',
|
||||||
|
networkError: 'Network error, please check connection',
|
||||||
|
networkConnectionError: 'Network connection error, please check your network',
|
||||||
|
startDownload: 'Starting video download',
|
||||||
|
videoUrlNotAvailable: 'Video URL not available',
|
||||||
|
noWorkToDelete: 'No work to delete',
|
||||||
|
confirmDeleteWork: 'Are you sure to delete this work?',
|
||||||
|
confirmDelete: 'Confirm Delete',
|
||||||
|
workDeleted: 'Work deleted',
|
||||||
|
deleteCancelled: 'Deletion cancelled',
|
||||||
|
historyParamsFilled: 'History parameters filled, ready to generate',
|
||||||
|
cancelFeatureTodo: 'Cancel feature coming soon',
|
||||||
|
resumingTask: 'Unfinished task detected, resuming...',
|
||||||
|
unfinishedTaskDetected: 'Unfinished task detected, restoring...'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Storyboard Video translations
|
||||||
|
storyboard: {
|
||||||
|
userAvatar: 'User Avatar',
|
||||||
|
generateStoryboard: 'Generate Storyboard',
|
||||||
|
generateVideo: 'Generate Video',
|
||||||
|
uploadStoryboard: 'Upload Storyboard (can generate video directly)',
|
||||||
|
uploadImage: 'Upload Image',
|
||||||
|
imageLabel: 'Image ',
|
||||||
|
uploadHint: 'Upload reference images, AI will generate 6-grid storyboard based on images',
|
||||||
|
addMore: 'Add More',
|
||||||
|
uploadedCount: 'Uploaded {count}/9',
|
||||||
|
uploadLimit: 'Limit reached',
|
||||||
|
uploadedImage: 'Uploaded image {index}',
|
||||||
|
maxImages: 'Maximum 3 images allowed',
|
||||||
|
maxImagesWarning: 'Maximum 3 images allowed',
|
||||||
|
fileSizeLimit: 'Image file size cannot exceed 100MB',
|
||||||
|
invalidFileType: 'Please select valid image files',
|
||||||
|
uploadSuccess: 'Successfully uploaded {count} images',
|
||||||
|
imageRemoved: 'Image removed',
|
||||||
|
promptPlaceholder: 'Example: a coffee advertisement\n\nTip: Simple description is enough, AI will automatically optimize it into professional storyboard\nSupports Chinese or English input, the system will automatically translate and optimize it into professional storyboard description',
|
||||||
|
tipWarning: '⚠️ Do not upload images of real people or anime IP',
|
||||||
|
tip1: '🎬 Supports camera movements, character actions, scene transitions, etc.',
|
||||||
|
imageCost: 'Storyboard generation costs 30 points',
|
||||||
|
videoCost: 'Video generation costs 30 points',
|
||||||
|
videoPromptLabel: 'Video Description',
|
||||||
|
videoPromptPlaceholder: 'Describe actions and camera movements in the video, e.g.: camera slowly zooms in, person turns around and smiles',
|
||||||
|
videoTip1: '💡 Describe actions and camera movements for more vivid video generation',
|
||||||
|
videoTip2: '🎬 Supports camera movements, character actions, scene transitions, etc.',
|
||||||
|
storyboardReadyHint: 'Storyboard is ready, enter video description and click generate',
|
||||||
|
optimizing: 'Optimizing...',
|
||||||
|
enterPrompt: 'Please enter prompt',
|
||||||
|
promptTooLong: 'Prompt is too long, please keep it within 2000 characters',
|
||||||
|
optimizingPrompt: 'Optimizing prompt, please wait...',
|
||||||
|
optimizeSuccess: 'Prompt optimized successfully!',
|
||||||
|
alreadyOptimized: 'Prompt already optimized, but may have no obvious changes',
|
||||||
|
optimizeFailed: 'Failed to optimize prompt',
|
||||||
|
paramError: 'Request parameter error',
|
||||||
|
timeout: 'Request timeout, please try again later',
|
||||||
|
serverError: 'Server error, please try again later',
|
||||||
|
networkError: 'Network error, please check your connection',
|
||||||
|
storyboardImage: 'Storyboard',
|
||||||
|
referenceImage: 'Reference Image',
|
||||||
|
noStoryboard: 'No storyboard yet',
|
||||||
|
hdMode: 'HD Mode (1080P)',
|
||||||
|
hdCost: 'Costs 20 points when enabled',
|
||||||
|
imageModel: 'Image Generation Model',
|
||||||
|
pleaseLogin: 'Please login first',
|
||||||
|
loginRequired: 'Login required to submit task',
|
||||||
|
loginNow: 'Login Now',
|
||||||
|
loginBeforeSubmit: 'Please login before submitting task',
|
||||||
|
inProgress: 'In Progress',
|
||||||
|
generatingVideo: 'Generating video, please wait...',
|
||||||
|
progress: 'Progress: {progress}%',
|
||||||
|
generatingStoryboardText: 'Generating storyboard, please wait...',
|
||||||
|
generatingVideoText: 'Generating video, please wait...',
|
||||||
|
generatingText: 'Generating',
|
||||||
|
startCreating: 'Start creating your first work!',
|
||||||
|
noDescription: 'No description',
|
||||||
|
queuing: 'Queuing',
|
||||||
|
subscribeToSpeed: 'Subscribe to improve generation speed',
|
||||||
|
noResult: 'No result yet',
|
||||||
|
noStoryboardImage: 'Please wait for storyboard generation to complete, or upload a storyboard image',
|
||||||
|
uploadOrGenerateFirst: 'Please upload or generate storyboard first',
|
||||||
|
uploadOrInputPrompt: 'Please upload storyboard or enter prompt',
|
||||||
|
startGenerateVideo: 'Start Generate Video',
|
||||||
|
generateVideoWithUpload: 'Generate Video with Uploaded Image',
|
||||||
|
startGenerateStoryboard: 'Start Generate Storyboard',
|
||||||
|
startGenerate: 'Start Generate',
|
||||||
|
generating: 'Generating...',
|
||||||
|
enterDescription: 'Please enter description',
|
||||||
|
enterDescriptionForImage: 'Please enter description, AI will generate storyboard based on reference image and description',
|
||||||
|
startingGenerate: 'Starting to generate storyboard...',
|
||||||
|
taskCreated: 'Storyboard task created successfully!',
|
||||||
|
createTaskFailed: 'Failed to create task',
|
||||||
|
generateFailed: 'Failed to generate storyboard',
|
||||||
|
taskTimeout: 'Task timeout, please check later',
|
||||||
|
storyboardCompleted: 'Storyboard generation completed! Please click "Start Generate" button to generate video',
|
||||||
|
videoCompleted: 'Video generation completed!',
|
||||||
|
taskFailed: 'Task failed',
|
||||||
|
checkInputOrRetry: 'Please check input or retry',
|
||||||
|
unknownError: 'Unknown error',
|
||||||
|
startingVideoGenerate: 'Starting to generate video...',
|
||||||
|
videoTaskStarted: 'Video generation task started, please wait...',
|
||||||
|
videoStartFailed: 'Failed to start video generation',
|
||||||
|
defaultPrompt: 'Generate video from image',
|
||||||
|
videoTaskCreated: 'Video task created successfully, processing...',
|
||||||
|
createVideoTaskFailed: 'Failed to create video task',
|
||||||
|
generateVideoFailed: 'Failed to generate video',
|
||||||
|
videoTaskTimeout: 'Video task timeout, please check later',
|
||||||
|
videoGenerateFailed: 'Video generation failed',
|
||||||
|
paramsFilled: 'Parameters filled from history, ready to generate',
|
||||||
|
cancelFeaturePending: 'Cancel feature pending implementation',
|
||||||
|
taskCompleted: 'Task completed!',
|
||||||
|
resumingVideoTask: 'Detected unfinished video generation task, resuming...',
|
||||||
|
resumingStoryboardTask: 'Detected unfinished storyboard generation task, resuming...',
|
||||||
|
resumingTask: 'Detected unfinished task, resuming...',
|
||||||
|
storyboardReady: 'Storyboard generated, click button below to generate video',
|
||||||
|
readyForVideo: 'Storyboard ready, you can generate video now',
|
||||||
|
uploadStoryboardFirst: 'Please upload storyboard first',
|
||||||
|
regenerate: 'Regenerate',
|
||||||
|
regenerateConfirm: 'Regenerating will consume points and create a new task. Continue?',
|
||||||
|
regenerateTitle: 'Regenerate Storyboard',
|
||||||
|
generateVideo: 'Generate Video',
|
||||||
|
readyToGenerateVideo: 'Storyboard parameters filled, ready to generate video',
|
||||||
|
downloadImage: 'Download Storyboard',
|
||||||
|
imageUrlNotAvailable: 'Storyboard image URL not available',
|
||||||
|
statusPending: 'Pending',
|
||||||
|
statusProcessing: 'Processing',
|
||||||
|
statusCompleted: 'Completed',
|
||||||
|
statusFailed: 'Failed',
|
||||||
|
statusCancelled: 'Cancelled',
|
||||||
|
statusUnknown: 'Unknown Status'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
works: {
|
||||||
|
title: 'My Works',
|
||||||
|
all: 'All',
|
||||||
|
textToVideo: 'Text to Video',
|
||||||
|
imageToVideo: 'Image to Video',
|
||||||
|
storyboardVideo: 'Storyboard Video',
|
||||||
|
filter: 'Filter',
|
||||||
|
sortBy: 'Sort By',
|
||||||
|
newest: 'Newest',
|
||||||
|
oldest: 'Oldest',
|
||||||
|
noWorks: 'No Works Yet',
|
||||||
|
noWorksDesc: 'Start creating your first work!',
|
||||||
|
viewDetail: 'View Details',
|
||||||
|
deleteConfirm: 'Are you sure you want to delete this work?',
|
||||||
|
deleteSuccess: 'Deleted successfully',
|
||||||
|
deleteFailed: 'Delete failed',
|
||||||
|
video: 'Video',
|
||||||
|
image: 'Image',
|
||||||
|
dateFilter: 'Date',
|
||||||
|
today: 'Today',
|
||||||
|
thisWeek: 'This Week',
|
||||||
|
thisMonth: 'This Month',
|
||||||
|
taskType: 'Task Type',
|
||||||
|
resolution: 'Resolution',
|
||||||
|
sd: 'SD',
|
||||||
|
hd: 'HD',
|
||||||
|
uhd: 'UHD',
|
||||||
|
ratio: 'Ratio',
|
||||||
|
time: 'Time',
|
||||||
|
popular: 'Popular',
|
||||||
|
searchPlaceholder: 'Name/Prompt/ID',
|
||||||
|
selectItems: 'Select {count} items',
|
||||||
|
selectAll: 'Select All',
|
||||||
|
selectedCount: '{count} selected',
|
||||||
|
favorite: 'Favorite',
|
||||||
|
downloadWithWatermark: 'Download with Watermark',
|
||||||
|
downloadWithoutWatermark: 'Download without Watermark',
|
||||||
|
memberOnly: 'Member Only',
|
||||||
|
rename: 'Rename',
|
||||||
|
referenceImagePrompt: 'Image 1 running in Image 2 video',
|
||||||
|
allLoaded: '✓ All content loaded',
|
||||||
|
noContent: 'No content found',
|
||||||
|
backToTop: 'Back to Top',
|
||||||
|
createSimilarInfo: 'Create similar based on "{title}"',
|
||||||
|
goToCreate: 'Go to create page',
|
||||||
|
downloadStart: 'Download started: {title}',
|
||||||
|
shareComingSoon: 'Share feature coming soon',
|
||||||
|
downloadWithWatermarkStart: 'Starting download with watermark',
|
||||||
|
downloadWithoutWatermarkStart: 'Starting download without watermark (Member exclusive)',
|
||||||
|
renameDevMsg: 'Rename feature under development',
|
||||||
|
deleteWorkConfirm: 'Are you sure to delete this work?',
|
||||||
|
deleteConfirmTitle: 'Delete Confirmation',
|
||||||
|
bulkDownloadStart: 'Starting download of {count} files',
|
||||||
|
bulkDeleteConfirm: 'Are you sure to delete {count} selected items?',
|
||||||
|
bulkDeleteSuccess: 'Selected items deleted',
|
||||||
|
filtersReset: 'Filters reset',
|
||||||
|
processing: 'Processing...',
|
||||||
|
queuing: 'Queuing...',
|
||||||
|
pleaseWait: 'Please wait, video is being generated',
|
||||||
|
noPreview: 'No Preview',
|
||||||
|
videoLoadFailed: 'Video Load Failed',
|
||||||
|
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.',
|
||||||
|
readyToGenerateVideo: 'Storyboard image loaded, ready to generate video',
|
||||||
|
noDownloadUrl: 'No downloadable file available'
|
||||||
|
},
|
||||||
|
|
||||||
|
subscription: {
|
||||||
|
title: 'Subscription',
|
||||||
|
choosePlan: 'Choose Plan',
|
||||||
|
currentPlan: 'Current Plan',
|
||||||
|
free: 'Free',
|
||||||
|
standard: 'Standard',
|
||||||
|
professional: 'Professional',
|
||||||
|
perMonth: '/year',
|
||||||
|
subscribe: 'Subscribe Now',
|
||||||
|
renew: 'Renew',
|
||||||
|
upgrade: 'Upgrade',
|
||||||
|
features: 'Features',
|
||||||
|
unlimited: 'Unlimited',
|
||||||
|
limited: 'Limited',
|
||||||
|
pointsPerMonth: 'Points/Year',
|
||||||
|
videoQuality: 'Video Quality',
|
||||||
|
support: 'Support',
|
||||||
|
priorityQueue: 'Priority Queue',
|
||||||
|
advancedFeatures: 'Advanced Features',
|
||||||
|
|
||||||
|
// New keys
|
||||||
|
userAvatar: 'User Avatar',
|
||||||
|
loading: 'Loading...',
|
||||||
|
pointsDetails: 'Points Details',
|
||||||
|
myOrders: 'My Orders',
|
||||||
|
currentActivePlan: 'Current Active Plan',
|
||||||
|
expiryTime: 'Expiry Time',
|
||||||
|
permanent: 'Permanent',
|
||||||
|
remainingPoints: 'Remaining Points',
|
||||||
|
plans: 'Plans',
|
||||||
|
pointsPerYear: 'points/year',
|
||||||
|
currentPackage: 'Current Plan',
|
||||||
|
firstPurchaseDiscount: 'First Purchase Discount up to 15% off',
|
||||||
|
bestValue: 'Best Value',
|
||||||
|
standardPoints: '6000 points/year',
|
||||||
|
premiumPoints: '12000 points/year',
|
||||||
|
freeNewUserBonus: 'New users get 50 points free on first login',
|
||||||
|
fastGeneration: 'Fast Generation',
|
||||||
|
superFastGeneration: 'Super Fast Generation',
|
||||||
|
commercialUse: 'Commercial Use',
|
||||||
|
noWatermark: 'No Watermark',
|
||||||
|
earlyAccess: 'Early Access to New Features',
|
||||||
|
|
||||||
|
// Points and items
|
||||||
|
points: 'points',
|
||||||
|
items: 'items',
|
||||||
|
pointsValidOneYear: 'Points valid for one year',
|
||||||
|
textToVideo30Points: 'Text to Video: 30 points/item',
|
||||||
|
imageToVideo30Points: 'Image to Video: 30 points/item',
|
||||||
|
storyboardImage30Points: 'Storyboard Image: 30 points/time',
|
||||||
|
storyboardVideo30Points: 'Storyboard Video: 30 points/item',
|
||||||
|
maxTextToVideo: 'Max Text to Video {count} items',
|
||||||
|
maxImageToVideo: 'Max Image to Video {count} items',
|
||||||
|
maxStoryboardImage: 'Max Storyboard Image {count} times',
|
||||||
|
maxStoryboardVideo: 'Max Storyboard Video {count} items',
|
||||||
|
textToVideoItems: 'Text to Video {count} items',
|
||||||
|
imageToVideoItems: 'or Image to Video {count} items',
|
||||||
|
storyboardImageTimes: 'or Storyboard Image {count} times',
|
||||||
|
storyboardVideoItems: 'or Storyboard Video {count} items',
|
||||||
|
|
||||||
|
// Points history related
|
||||||
|
pointsUsageHistory: 'Points Usage History',
|
||||||
|
pointsUsageOverview: 'Points Usage Overview',
|
||||||
|
totalRecharge: 'Total Recharge',
|
||||||
|
totalConsumption: 'Total Consumption',
|
||||||
|
currentPoints: 'Current Points',
|
||||||
|
noPointsHistory: 'No points usage history',
|
||||||
|
description: 'Description',
|
||||||
|
time: 'Time',
|
||||||
|
orderNumber: 'Order Number',
|
||||||
|
taskId: 'Task ID',
|
||||||
|
recharge: 'Recharge',
|
||||||
|
consume: 'Consume',
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
pleaseLogin: 'Please login first',
|
||||||
|
loadUserInfoFailed: 'Failed to load user info, using defaults',
|
||||||
|
loadUserInfoError: 'Failed to load user info: {message}',
|
||||||
|
loadPointsHistoryFailed: 'Failed to load points history',
|
||||||
|
generatingQRCode: 'Generating payment QR code...',
|
||||||
|
subscriptionFailed: 'Subscription failed, please try again',
|
||||||
|
qrCodeGenerated: 'QR code generated, please scan with Alipay',
|
||||||
|
qrCodeGenerationFailed: 'Failed to generate QR code, please try again',
|
||||||
|
qrCodeEmpty: 'QR code generation failed: QR code is empty',
|
||||||
|
createPaymentFailed: 'Failed to create payment order',
|
||||||
|
qrCodeGenerationError: 'QR code generation failed: {message}',
|
||||||
|
pleaseTryAgain: 'Please try again',
|
||||||
|
refreshPage: 'Please refresh and try again',
|
||||||
|
paymentSuccess: 'Payment successful! Updating information...',
|
||||||
|
infoUpdated: 'Information updated!',
|
||||||
|
paymentProcessingFailed: 'Payment successful but order processing failed, please contact support',
|
||||||
|
paymentFailed: 'Payment failed, please try again',
|
||||||
|
creatingOrder: 'Creating order...',
|
||||||
|
paymentPageLoadFailed: 'Failed to load payment page',
|
||||||
|
createAlipayPaymentFailed: 'Failed to create Alipay payment',
|
||||||
|
createPaymentOrderFailed: 'Failed to create payment order, please try again',
|
||||||
|
|
||||||
|
// Plan descriptions
|
||||||
|
standardDescription: 'Standard Subscription - 200 points per month',
|
||||||
|
premiumDescription: 'Premium Subscription - 1000 points per month'
|
||||||
|
},
|
||||||
|
|
||||||
|
welcome: {
|
||||||
|
textToVideo: 'Text to Video',
|
||||||
|
imageToVideo: 'Image to Video',
|
||||||
|
storyboardVideo: 'Storyboard Video',
|
||||||
|
pricing: 'Pricing Plans',
|
||||||
|
startExperience: 'Get Started',
|
||||||
|
title1: 'Create',
|
||||||
|
title2: 'Unlimited,',
|
||||||
|
title3: 'Ideas',
|
||||||
|
title4: 'Realized.',
|
||||||
|
subtitle: 'Secure and convenient login with email verification',
|
||||||
|
tryNow: 'Try Now',
|
||||||
|
coreFeatures: 'Core Features',
|
||||||
|
textToVideoDesc: 'Enter text description, AI automatically generates high-quality video content',
|
||||||
|
imageToVideoDesc: 'Upload images, AI intelligently analyzes and generates dynamic videos',
|
||||||
|
storyboardVideoDesc: 'Professional storyboarding for cinematic video effects',
|
||||||
|
pricingDesc: 'Flexible pricing plans to meet different creative needs',
|
||||||
|
startCreating: 'Start Creating'
|
||||||
|
},
|
||||||
|
|
||||||
|
nav: {
|
||||||
|
dashboard: 'Dashboard',
|
||||||
|
members: 'Member Management',
|
||||||
|
orders: 'Order Management',
|
||||||
|
apiManagement: 'API Management',
|
||||||
|
tasks: 'Task Records',
|
||||||
|
systemSettings: 'System Settings',
|
||||||
|
onlineUsers: 'Online Users',
|
||||||
|
systemUptime: 'System Uptime',
|
||||||
|
todayVisitors: 'Today Visitors',
|
||||||
|
loading: 'Loading...',
|
||||||
|
memberManagement: 'Member Management',
|
||||||
|
orderManagement: 'Order Management',
|
||||||
|
taskRecord: 'Task Records',
|
||||||
|
errorStats: 'Error Statistics'
|
||||||
|
},
|
||||||
|
|
||||||
|
admin: {
|
||||||
|
exitAdmin: 'Exit Admin'
|
||||||
|
},
|
||||||
|
|
||||||
|
dashboard: {
|
||||||
|
title: 'Dashboard',
|
||||||
|
totalUsers: 'Total Users',
|
||||||
|
paidUsers: 'Paid Users',
|
||||||
|
todayRevenue: "Today's Revenue",
|
||||||
|
dailyActive: 'Daily Active User Trend',
|
||||||
|
conversionRate: 'User Conversion Rate',
|
||||||
|
comparedToLastMonth: 'vs last month',
|
||||||
|
comparedToYesterday: 'vs yesterday',
|
||||||
|
year2025: '2025',
|
||||||
|
year2024: '2024',
|
||||||
|
year2023: '2023',
|
||||||
|
userAvatar: 'User Avatar',
|
||||||
|
month1: 'Jan',
|
||||||
|
month2: 'Feb',
|
||||||
|
month3: 'Mar',
|
||||||
|
month4: 'Apr',
|
||||||
|
month5: 'May',
|
||||||
|
month6: 'Jun',
|
||||||
|
month7: 'Jul',
|
||||||
|
month8: 'Aug',
|
||||||
|
month9: 'Sep',
|
||||||
|
month10: 'Oct',
|
||||||
|
month11: 'Nov',
|
||||||
|
month12: 'Dec',
|
||||||
|
pleaseLogin: 'Please login first',
|
||||||
|
loadDataFailed: 'Failed to load dashboard data',
|
||||||
|
unknownError: 'Unknown error'
|
||||||
|
},
|
||||||
|
|
||||||
|
orders: {
|
||||||
|
title: 'Order Management',
|
||||||
|
orderNumber: 'Order Number',
|
||||||
|
username: 'Username',
|
||||||
|
amount: 'Amount',
|
||||||
|
paymentMethod: 'Payment Method',
|
||||||
|
status: 'Status',
|
||||||
|
createTime: 'Create Time',
|
||||||
|
operation: 'Operation',
|
||||||
|
allStatus: 'All Status',
|
||||||
|
allTypes: 'All Types',
|
||||||
|
allPaymentMethods: 'All Payment Methods',
|
||||||
|
pending: 'Pending',
|
||||||
|
confirmed: 'Confirmed',
|
||||||
|
paid: 'Paid',
|
||||||
|
processing: 'Processing',
|
||||||
|
shipped: 'Shipped',
|
||||||
|
delivered: 'Delivered',
|
||||||
|
completed: 'Completed',
|
||||||
|
cancelled: 'Cancelled',
|
||||||
|
refunded: 'Refunded',
|
||||||
|
unpaid: 'Unpaid',
|
||||||
|
alipay: 'Alipay',
|
||||||
|
wechat: 'WeChat Pay',
|
||||||
|
paypal: 'PayPal',
|
||||||
|
selected: '{count} selected',
|
||||||
|
orderDetail: 'Order Detail',
|
||||||
|
basicInfo: 'Basic Info',
|
||||||
|
orderType: 'Order Type',
|
||||||
|
paymentInfo: 'Payment Info',
|
||||||
|
confirmDeleteOrder: 'Are you sure to delete order {orderNumber}?',
|
||||||
|
confirmDeleteTitle: 'Confirm Delete',
|
||||||
|
deleteSuccess: 'Deleted successfully',
|
||||||
|
deleteFailed: 'Delete failed',
|
||||||
|
pleaseSelectOrders: 'Please select orders to delete first',
|
||||||
|
confirmBatchDelete: 'Are you sure to delete the selected {count} orders?',
|
||||||
|
batchDeleteTitle: 'Batch Delete',
|
||||||
|
batchDeleteSuccess: 'Batch delete successful',
|
||||||
|
batchDeleteFailed: 'Batch delete failed',
|
||||||
|
loadOrdersFailed: 'Failed to load orders',
|
||||||
|
apiDataFormatError: 'API data format error',
|
||||||
|
productOrder: 'Product Order',
|
||||||
|
serviceOrder: 'Service Order',
|
||||||
|
subscriptionOrder: 'Subscription Order',
|
||||||
|
digitalProduct: 'Digital Product',
|
||||||
|
physicalProduct: 'Physical Product'
|
||||||
|
},
|
||||||
|
|
||||||
|
tasks: {
|
||||||
|
title: 'Task Records',
|
||||||
|
taskId: 'Task ID',
|
||||||
|
username: 'Username',
|
||||||
|
type: 'Type',
|
||||||
|
resources: 'Resources Used',
|
||||||
|
status: 'Status',
|
||||||
|
createTime: 'Create Time',
|
||||||
|
operation: 'Operation',
|
||||||
|
allStatus: 'All Status',
|
||||||
|
completed: 'Completed',
|
||||||
|
processing: 'Processing',
|
||||||
|
failed: 'Failed',
|
||||||
|
cancelled: 'Cancelled',
|
||||||
|
pending: 'Pending',
|
||||||
|
textToVideo: 'Text to Video',
|
||||||
|
imageToVideo: 'Image to Video',
|
||||||
|
storyboardVideo: 'Storyboard Video',
|
||||||
|
taskDetail: 'Task Detail',
|
||||||
|
unknown: 'Unknown',
|
||||||
|
pointsUnit: ' points',
|
||||||
|
basicInfo: 'Basic Info',
|
||||||
|
timeInfo: 'Time Info',
|
||||||
|
progressInfo: 'Progress Info',
|
||||||
|
progress: 'Progress',
|
||||||
|
result: 'Result',
|
||||||
|
resultLink: 'Result Link',
|
||||||
|
viewResult: 'View Result',
|
||||||
|
errorInfo: 'Error Info',
|
||||||
|
close: 'Close',
|
||||||
|
updateTime: 'Update Time',
|
||||||
|
completeTime: 'Complete Time',
|
||||||
|
taskType: 'Task Type',
|
||||||
|
resourcesConsumed: 'Resources Consumed',
|
||||||
|
defaultPoints: '0 points'
|
||||||
|
},
|
||||||
|
|
||||||
|
members: {
|
||||||
|
title: 'Member List',
|
||||||
|
userId: 'User ID',
|
||||||
|
username: 'Username',
|
||||||
|
level: 'Membership Level',
|
||||||
|
points: 'Remaining Points',
|
||||||
|
expiryDate: 'Expiry Date',
|
||||||
|
operation: 'Edit',
|
||||||
|
allLevels: 'All Levels',
|
||||||
|
professional: 'Professional',
|
||||||
|
standard: 'Standard',
|
||||||
|
editMember: 'Edit Member',
|
||||||
|
usernamePlaceholder: 'Enter username',
|
||||||
|
levelPlaceholder: 'Select level',
|
||||||
|
pointsPlaceholder: 'Enter points',
|
||||||
|
expiryPlaceholder: 'Select expiry date',
|
||||||
|
memberLevel: 'Membership Level',
|
||||||
|
freeMember: 'Free Member',
|
||||||
|
standardMember: 'Standard Member',
|
||||||
|
professionalMember: 'Professional Member',
|
||||||
|
userStatus: 'User Status',
|
||||||
|
activeUsers: 'Active Users',
|
||||||
|
bannedUsers: 'Banned Users',
|
||||||
|
allUsers: 'All Users',
|
||||||
|
role: 'Role',
|
||||||
|
status: 'Status',
|
||||||
|
setAdmin: 'Set as Admin',
|
||||||
|
revokeAdmin: 'Revoke Admin',
|
||||||
|
ban: 'Ban',
|
||||||
|
unban: 'Unban',
|
||||||
|
active: 'Active',
|
||||||
|
banned: 'Banned',
|
||||||
|
superAdmin: 'Super Admin',
|
||||||
|
admin: 'Admin',
|
||||||
|
normalUser: 'User',
|
||||||
|
userRole: 'User Role',
|
||||||
|
selectRole: 'Select role',
|
||||||
|
confirmRoleChange: 'Are you sure to {action} user {username}?',
|
||||||
|
confirmBanAction: 'Are you sure to {action} user {username}?',
|
||||||
|
confirmAction: 'Confirm {action}',
|
||||||
|
actionSuccess: '{action} successful',
|
||||||
|
actionFailed: '{action} failed'
|
||||||
|
},
|
||||||
|
|
||||||
|
apiManagement: {
|
||||||
|
title: 'API Management',
|
||||||
|
apiKey: 'API Key',
|
||||||
|
apiKeyPlaceholder: 'Enter API key',
|
||||||
|
apiBaseUrl: 'API Base URL',
|
||||||
|
apiBaseUrlPlaceholder: 'Enter API base URL, e.g. https://ai.comfly.chat',
|
||||||
|
apiBaseUrlHint: 'Currently using',
|
||||||
|
tokenExpiration: 'Token Expiration',
|
||||||
|
tokenPlaceholder: 'Enter hours (1-720)',
|
||||||
|
hours: 'hours',
|
||||||
|
days: 'days',
|
||||||
|
rangeHint: 'Range: 1-720 hours (1 hour - 30 days)',
|
||||||
|
atLeastOneRequired: 'Please enter at least one configuration',
|
||||||
|
saveSuccess: 'Saved successfully',
|
||||||
|
saveFailed: 'Save failed'
|
||||||
|
},
|
||||||
|
|
||||||
|
errorStats: {
|
||||||
|
title: 'Error Statistics',
|
||||||
|
userAvatar: 'User Avatar',
|
||||||
|
totalErrors: 'Total Errors',
|
||||||
|
todayErrors: 'Today Errors',
|
||||||
|
weekErrors: 'Week Errors',
|
||||||
|
errorTypeDistribution: 'Error Type Distribution',
|
||||||
|
last7Days: 'Last 7 Days',
|
||||||
|
last30Days: 'Last 30 Days',
|
||||||
|
last90Days: 'Last 90 Days',
|
||||||
|
times: 'times',
|
||||||
|
noErrorData: 'No error data',
|
||||||
|
recentErrors: 'Recent Errors',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
time: 'Time',
|
||||||
|
errorType: 'Error Type',
|
||||||
|
user: 'User',
|
||||||
|
taskId: 'Task ID',
|
||||||
|
errorMessage: 'Error Message'
|
||||||
|
},
|
||||||
|
|
||||||
|
systemSettings: {
|
||||||
|
title: 'System Settings',
|
||||||
|
membership: 'Membership Pricing',
|
||||||
|
cleanup: 'Task Cleanup',
|
||||||
|
membershipLevels: 'Membership Levels',
|
||||||
|
editLevel: 'Edit Level',
|
||||||
|
price: 'Price',
|
||||||
|
description: 'Description',
|
||||||
|
cleanupStats: 'Cleanup Statistics',
|
||||||
|
manualCleanup: 'Manual Cleanup',
|
||||||
|
autoCleanup: 'Auto Cleanup',
|
||||||
|
perMonth: '/month',
|
||||||
|
includesPoints: 'Includes {points} points/month',
|
||||||
|
cleanupStatsInfo: 'Cleanup Statistics',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
currentTotalTasks: 'Current Total Tasks',
|
||||||
|
completedTasks: 'Completed Tasks',
|
||||||
|
failedTasks: 'Failed Tasks',
|
||||||
|
archivedTasks: 'Archived Tasks',
|
||||||
|
cleanupLogsCount: 'Cleanup Logs',
|
||||||
|
retentionDays: 'Retention Days',
|
||||||
|
days: 'days',
|
||||||
|
cleanupActions: 'Cleanup Actions',
|
||||||
|
performFullCleanup: 'Perform Full Cleanup',
|
||||||
|
cleanupUserTasks: 'Cleanup User Tasks',
|
||||||
|
fullCleanupDesc: 'Full Cleanup',
|
||||||
|
fullCleanupDescDetail: 'Export successful tasks to archive, delete failed tasks',
|
||||||
|
userCleanupDesc: 'User Cleanup',
|
||||||
|
userCleanupDescDetail: 'Cleanup all tasks for specified user',
|
||||||
|
cleanupConfig: 'Cleanup Configuration',
|
||||||
|
taskRetentionDays: 'Task Retention Days',
|
||||||
|
taskRetentionTip: 'Days to retain completed tasks',
|
||||||
|
archiveRetentionDays: 'Archive Retention Days',
|
||||||
|
archiveRetentionTip: 'Days to retain archived data',
|
||||||
|
membershipLevel: 'Membership Level',
|
||||||
|
selectLevelPlaceholder: 'Select membership level',
|
||||||
|
freeMembership: 'Free Membership',
|
||||||
|
standardMembership: 'Standard Membership',
|
||||||
|
professionalMembership: 'Professional Membership',
|
||||||
|
membershipPrice: 'Membership Price',
|
||||||
|
resourcePointsAmount: 'Resource Points',
|
||||||
|
validityPeriod: 'Validity Period',
|
||||||
|
monthly: 'Monthly',
|
||||||
|
quarterly: 'Quarterly',
|
||||||
|
yearly: 'Yearly',
|
||||||
|
enterUsername: 'Enter username to cleanup',
|
||||||
|
warning: 'Warning',
|
||||||
|
cleanupWarning: 'This operation will cleanup all tasks for this user, including:',
|
||||||
|
successTasksArchived: 'Successful tasks will be exported to archive',
|
||||||
|
failedTasksLogged: 'Failed tasks will be logged to cleanup logs',
|
||||||
|
originalTasksDeleted: 'Original task records will be deleted',
|
||||||
|
irreversibleWarning: 'This operation is irreversible, please proceed with caution!',
|
||||||
|
confirmCleanup: 'Confirm Cleanup',
|
||||||
|
selectLevelRequired: 'Please select membership level',
|
||||||
|
enterPriceRequired: 'Please enter price',
|
||||||
|
enterValidNumber: 'Please enter a valid number',
|
||||||
|
enterResourcePointsRequired: 'Please enter resource points',
|
||||||
|
selectValidityRequired: 'Please select validity period',
|
||||||
|
enterUsernameRequired: 'Please enter username',
|
||||||
|
usernameLengthLimit: 'Username must be 2-50 characters',
|
||||||
|
membershipUpdateSuccess: 'Membership level updated successfully',
|
||||||
|
membershipUpdateFailed: 'Failed to update membership level',
|
||||||
|
loadMembershipFailed: 'Failed to load membership configuration',
|
||||||
|
usingDefaultConfig: 'Using default configuration',
|
||||||
|
statsRefreshSuccess: 'Statistics refreshed successfully',
|
||||||
|
statsRefreshFailed: 'Failed to get statistics',
|
||||||
|
fullCleanupSuccess: 'Full cleanup executed successfully',
|
||||||
|
fullCleanupFailed: 'Failed to execute full cleanup',
|
||||||
|
userCleanupSuccess: 'User tasks cleaned up successfully',
|
||||||
|
userCleanupFailed: 'Failed to cleanup user tasks',
|
||||||
|
configSaveSuccess: 'Cleanup configuration saved successfully',
|
||||||
|
configSaveFailed: 'Failed to save cleanup configuration',
|
||||||
|
aiModelSaveSuccess: 'AI model settings saved successfully',
|
||||||
|
aiModelSaveFailed: 'Failed to save AI model settings',
|
||||||
|
aiModelLoadFailed: 'Failed to load AI model settings',
|
||||||
|
includesPointsPerMonth: 'Includes {points} points/month',
|
||||||
|
unknown: 'Unknown',
|
||||||
|
aiModel: 'AI Model Settings',
|
||||||
|
promptOptimization: 'Prompt Optimization Settings',
|
||||||
|
promptOptimizationModel: 'Model Name',
|
||||||
|
promptOptimizationModelTip: 'Enter the model name for prompt optimization, e.g., gpt-4o, gemini-pro',
|
||||||
|
storyboardSystemPrompt: 'Storyboard System Prompt',
|
||||||
|
storyboardSystemPromptTip: 'This prompt will be prepended to user prompts for consistent storyboard generation style',
|
||||||
|
storyboardSystemPromptPlaceholder: 'E.g., high quality cinematic shot, professional photography, film tone...'
|
||||||
|
}
|
||||||
|
}
|
||||||
23
demo/frontend/src/locales/index.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import zh from './zh'
|
||||||
|
import en from './en'
|
||||||
|
|
||||||
|
// 从localStorage获取保存的语言设置,默认中文
|
||||||
|
const savedLanguage = localStorage.getItem('language') || 'zh'
|
||||||
|
|
||||||
|
console.log('[i18n] 从 localStorage 读取的语言:', savedLanguage)
|
||||||
|
console.log('[i18n] 可用的语言:', Object.keys({ zh, en }))
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false, // 使用Composition API模式
|
||||||
|
locale: savedLanguage, // 默认语言
|
||||||
|
fallbackLocale: 'zh', // 回退语言
|
||||||
|
messages: {
|
||||||
|
zh,
|
||||||
|
en
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('[i18n] i18n 初始化完成,当前语言:', i18n.global.locale.value)
|
||||||
|
|
||||||
|
export default i18n
|
||||||
906
demo/frontend/src/locales/zh.js
Normal file
@@ -0,0 +1,906 @@
|
|||||||
|
export default {
|
||||||
|
common: {
|
||||||
|
confirm: '确认',
|
||||||
|
cancel: '取消',
|
||||||
|
save: '保存',
|
||||||
|
delete: '删除',
|
||||||
|
edit: '编辑',
|
||||||
|
search: '搜索',
|
||||||
|
reset: '重置',
|
||||||
|
view: '查看',
|
||||||
|
loading: '加载中...',
|
||||||
|
searchPlaceholder: '搜索你想要的内容',
|
||||||
|
welcome: '欢迎页',
|
||||||
|
home: '首页',
|
||||||
|
profile: '个人主页',
|
||||||
|
orders: '订单管理',
|
||||||
|
payments: '支付记录',
|
||||||
|
adminPanel: '后台管理',
|
||||||
|
login: '登录',
|
||||||
|
register: '注册',
|
||||||
|
logout: '退出登录',
|
||||||
|
settings: '设置',
|
||||||
|
userProfile: '个人资料',
|
||||||
|
points: '积分',
|
||||||
|
profileDevMsg: '个人资料功能开发中',
|
||||||
|
settingsDevMsg: '设置功能开发中',
|
||||||
|
noPermissionMsg: '权限不足,只有管理员才能访问后台管理',
|
||||||
|
logoutConfirm: '确定要退出登录吗?',
|
||||||
|
logoutSuccess: '退出登录成功',
|
||||||
|
tip: '提示',
|
||||||
|
submit: '提交',
|
||||||
|
back: '返回',
|
||||||
|
next: '下一步',
|
||||||
|
previous: '上一步',
|
||||||
|
close: '关闭',
|
||||||
|
success: '成功',
|
||||||
|
error: '错误',
|
||||||
|
warning: '警告',
|
||||||
|
info: '信息'
|
||||||
|
},
|
||||||
|
|
||||||
|
login: {
|
||||||
|
title: '登录',
|
||||||
|
emailCodeLogin: '邮箱验证码登录',
|
||||||
|
emailPasswordLogin: '邮箱密码登录',
|
||||||
|
emailPlaceholder: '请输入邮箱地址',
|
||||||
|
codePlaceholder: '请输入验证码',
|
||||||
|
passwordPlaceholder: '请输入密码',
|
||||||
|
getCode: '获取验证码',
|
||||||
|
loginButton: '登录',
|
||||||
|
loginOrRegister: '登陆/注册',
|
||||||
|
loggingIn: '登录中...',
|
||||||
|
agreement: '登录即表示您同意遵守用户协议和隐私政策',
|
||||||
|
testAccounts: '测试邮箱',
|
||||||
|
admin: '管理员',
|
||||||
|
normalUser: '普通用户',
|
||||||
|
quickEmail: '快捷输入'
|
||||||
|
},
|
||||||
|
|
||||||
|
home: {
|
||||||
|
title: '首页',
|
||||||
|
exploreCreativity: '探索无限创意',
|
||||||
|
subtitle: '用 AI 将你的想象变为现实',
|
||||||
|
getStarted: '开始创作',
|
||||||
|
learnMore: '了解更多',
|
||||||
|
features: '核心功能',
|
||||||
|
textToVideo: '文生视频',
|
||||||
|
textToVideoDesc: '输入文字描述,AI 自动生成高质量视频',
|
||||||
|
imageToVideo: '图生视频',
|
||||||
|
imageToVideoDesc: '上传图片,AI 智能生成动态视频',
|
||||||
|
storyboardVideo: '分镜视频',
|
||||||
|
storyboardVideoDesc: '专业分镜制作,打造电影级效果',
|
||||||
|
myWorks: '我的作品',
|
||||||
|
myWorksDesc: '管理和查看你的所有创作',
|
||||||
|
subscription: '会员订阅',
|
||||||
|
subscriptionDesc: '解锁更多高级功能和资源'
|
||||||
|
},
|
||||||
|
|
||||||
|
profile: {
|
||||||
|
title: '个人主页',
|
||||||
|
userInfo: '用户信息',
|
||||||
|
username: '用户名',
|
||||||
|
email: '邮箱',
|
||||||
|
memberLevel: '会员等级',
|
||||||
|
expiryDate: '到期时间',
|
||||||
|
availablePoints: '可用积分',
|
||||||
|
frozenPoints: '冻结积分',
|
||||||
|
accountStats: '账户统计',
|
||||||
|
totalWorks: '作品总数',
|
||||||
|
videoGenerated: '生成视频数',
|
||||||
|
storageUsed: '已用存储',
|
||||||
|
editProfile: '编辑资料',
|
||||||
|
changePassword: '修改密码',
|
||||||
|
viewWorks: '查看作品',
|
||||||
|
upgradeMembership: '升级会员',
|
||||||
|
subscription: '会员订阅',
|
||||||
|
myWorks: '我的作品',
|
||||||
|
tools: '工具',
|
||||||
|
noUsername: '未设置用户名',
|
||||||
|
published: '作品集',
|
||||||
|
userId: 'ID',
|
||||||
|
noWorksYet: '暂无作品,开始创作吧!',
|
||||||
|
createSimilar: '做同款',
|
||||||
|
workDetail: '作品详情',
|
||||||
|
videoDetail: '视频详情',
|
||||||
|
description: '描述',
|
||||||
|
category: '分类',
|
||||||
|
inputDetails: '输入详情',
|
||||||
|
createTime: '创建时间',
|
||||||
|
workId: '作品 ID',
|
||||||
|
date: '日期',
|
||||||
|
duration: '时长',
|
||||||
|
quality: '清晰度',
|
||||||
|
aspectRatio: '宽高比',
|
||||||
|
unknown: '未知',
|
||||||
|
anonymousUser: '匿名用户',
|
||||||
|
browserNotSupport: '您的浏览器不支持视频播放',
|
||||||
|
noPrompt: '暂无提示词',
|
||||||
|
dashboard: '数据仪表盘',
|
||||||
|
orderManagement: '订单管理',
|
||||||
|
memberManagement: '会员管理',
|
||||||
|
systemSettings: '系统设置',
|
||||||
|
insufficientPermission: '权限不足,只有管理员才能访问',
|
||||||
|
logoutConfirm: '确定要退出登录吗?',
|
||||||
|
logoutSuccess: '已退出登录',
|
||||||
|
logoutFailed: '退出登录失败',
|
||||||
|
loadWorksFailed: '加载作品列表失败',
|
||||||
|
loadUserInfoFailed: '获取用户信息失败',
|
||||||
|
loadDetailFailed: '加载作品详情失败',
|
||||||
|
profileEditDevMsg: '个人简介编辑功能待实现'
|
||||||
|
},
|
||||||
|
|
||||||
|
video: {
|
||||||
|
create: '创建',
|
||||||
|
generating: '生成中',
|
||||||
|
completed: '已完成',
|
||||||
|
failed: '失败',
|
||||||
|
failReason: '失败原因',
|
||||||
|
prompt: '提示词',
|
||||||
|
promptPlaceholder: '请输入视频描述...',
|
||||||
|
optimizePrompt: '优化提示词',
|
||||||
|
uploadImage: '上传图片',
|
||||||
|
selectImage: '选择图片',
|
||||||
|
generate: '生成视频',
|
||||||
|
duration: '时长',
|
||||||
|
resolution: '分辨率',
|
||||||
|
style: '风格',
|
||||||
|
aspectRatio: '宽高比',
|
||||||
|
advancedSettings: '高级设置',
|
||||||
|
cost: '消耗积分',
|
||||||
|
estimatedTime: '预计用时',
|
||||||
|
result: '生成结果',
|
||||||
|
download: '下载',
|
||||||
|
share: '分享',
|
||||||
|
regenerate: '重新生成',
|
||||||
|
saveToWorks: '保存到作品',
|
||||||
|
videoUrl: '视频链接',
|
||||||
|
status: '状态',
|
||||||
|
createdAt: '创建时间',
|
||||||
|
|
||||||
|
// 文生视频专用翻译
|
||||||
|
textToVideo: {
|
||||||
|
aspectRatio: '宽高比',
|
||||||
|
dateFormat: '{year}年{month}月{day}日 {hours}:{minutes}',
|
||||||
|
pointsUpdated: '用户积分已更新',
|
||||||
|
pointsUpdateFailed: '更新用户积分失败',
|
||||||
|
textInputPlaceholder: '请输入视频描述,例如:在草原上奔跑的骏马...',
|
||||||
|
userAvatar: '用户头像',
|
||||||
|
optimizing: '优化中...',
|
||||||
|
oneClickOptimize: '一键优化',
|
||||||
|
hdMode: '高清模式',
|
||||||
|
hdModeCost: '+20积分',
|
||||||
|
startGenerate: '开始生成',
|
||||||
|
pleaseLogin: '请先登录',
|
||||||
|
loginRequired: '登录后才能开始创作',
|
||||||
|
loginNow: '立即登录',
|
||||||
|
inProgress: '进行中',
|
||||||
|
noVideoUrl: '暂无视频',
|
||||||
|
withWatermark: '带水印',
|
||||||
|
withoutWatermark: '不带水印',
|
||||||
|
createSimilar: '做同款',
|
||||||
|
downloadVideo: '下载视频',
|
||||||
|
deleteWork: '删除作品',
|
||||||
|
generationFailed: '生成失败',
|
||||||
|
checkInputOrRetry: '请检查输入或重新尝试',
|
||||||
|
regenerate: '重新生成',
|
||||||
|
clearAndCreateNew: '清除并创作新内容',
|
||||||
|
failedTaskCleared: '失败任务已清除',
|
||||||
|
startCreating: '开始创作你的视频吧!',
|
||||||
|
noDescription: '无描述',
|
||||||
|
queuing: '排队中...',
|
||||||
|
subscribeToSpeedUp: '订阅会员可加速',
|
||||||
|
noResult: '暂无结果',
|
||||||
|
pleaseLoginFirst: '请先登录后再创建任务',
|
||||||
|
taskInProgress: '当前已有任务正在生成中,请稍候',
|
||||||
|
pleaseEnterText: '请输入视频描述文本',
|
||||||
|
creatingTask: '正在创建任务...',
|
||||||
|
taskCreated: '任务创建成功,开始生成',
|
||||||
|
createTaskFailed: '创建任务失败',
|
||||||
|
videoCompleted: '视频生成完成!',
|
||||||
|
videoFailed: '视频生成失败:',
|
||||||
|
statusPending: '排队中',
|
||||||
|
statusProcessing: '生成中',
|
||||||
|
statusCancelled: '已取消',
|
||||||
|
statusUnknown: '未知状态',
|
||||||
|
pleaseEnterPrompt: '请先输入提示词',
|
||||||
|
promptTooLong: '提示词过长,最多2000字符',
|
||||||
|
optimizingPrompt: '正在优化提示词...',
|
||||||
|
optimizeSuccess: '提示词优化成功!',
|
||||||
|
optimizeNoChange: '提示词已是最优,无需优化',
|
||||||
|
optimizeFailed: '优化失败,请稍后重试',
|
||||||
|
requestParamError: '请求参数错误',
|
||||||
|
requestTimeout: '请求超时,请检查网络',
|
||||||
|
serverError: '服务器错误,请稍后重试',
|
||||||
|
networkError: '网络错误,请检查连接',
|
||||||
|
networkConnectionError: '网络连接失败',
|
||||||
|
downloadStarted: '开始下载',
|
||||||
|
videoUrlNotAvailable: '视频链接不可用',
|
||||||
|
noWorkToDelete: '没有可删除的作品',
|
||||||
|
deleteConfirm: '确定要删除这个作品吗?删除后将无法恢复。',
|
||||||
|
confirmDelete: '确认删除',
|
||||||
|
workDeleted: '作品已删除',
|
||||||
|
deleteCancelled: '已取消删除',
|
||||||
|
historyLoadSuccess: '历史记录加载成功: {count}条',
|
||||||
|
historyLoadFailed: '加载历史记录失败',
|
||||||
|
historyParamsFilled: '已填充历史参数,可以开始生成',
|
||||||
|
cancelFunctionTBD: '取消功能开发中',
|
||||||
|
unfinishedTaskDetected: '检测到未完成任务,正在恢复...'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 图生视频专用翻译
|
||||||
|
imageToVideo: {
|
||||||
|
userAvatar: '用户头像',
|
||||||
|
firstFrame: '首帧',
|
||||||
|
promptPlaceholder: '结合图片,描述想要生成的内容',
|
||||||
|
tipWarning: '⚠️ 图片不能上传真人、涉及动漫IP等类型的图片',
|
||||||
|
tip1: '🎬 支持描述镜头推拉、人物动作、场景变化等',
|
||||||
|
optimizing: '优化中...',
|
||||||
|
optimizePrompt: '一键优化',
|
||||||
|
hdMode: '高清模式',
|
||||||
|
hdModeCost: '开启消耗20积分',
|
||||||
|
startGenerate: '开始生成',
|
||||||
|
pleaseLogin: '请先登录',
|
||||||
|
loginRequired: '需要登录后才能提交任务',
|
||||||
|
loginNow: '立即登录',
|
||||||
|
inProgress: '进行中',
|
||||||
|
statusPending: '等待中',
|
||||||
|
statusProcessing: '处理中',
|
||||||
|
statusCompleted: '已完成',
|
||||||
|
statusFailed: '失败',
|
||||||
|
statusCancelled: '已取消',
|
||||||
|
noVideoUrl: '视频生成完成,但未获取到视频链接',
|
||||||
|
withWatermark: '带水印',
|
||||||
|
withoutWatermark: '不带水印 会员专享',
|
||||||
|
createSimilar: '做同款',
|
||||||
|
downloadVideo: '下载视频',
|
||||||
|
deleteWork: '删除作品',
|
||||||
|
retry: '重新生成',
|
||||||
|
generateFailed: '生成失败',
|
||||||
|
generateFailedDesc: '请检查输入内容或重试',
|
||||||
|
startCreating: '开始创作您的第一个作品吧!',
|
||||||
|
tip1: '上传首帧图片',
|
||||||
|
tip2: '输入描述文字',
|
||||||
|
tip3: '选择视频参数',
|
||||||
|
tip4: '点击开始生成',
|
||||||
|
noDescription: '无描述',
|
||||||
|
queuing: '排队中',
|
||||||
|
subscribeToSpeedUp: '订阅套餐以提升生成速度',
|
||||||
|
cancel: '取消',
|
||||||
|
firstFrameImage: '首帧图片',
|
||||||
|
noResult: '暂无结果',
|
||||||
|
year: '年',
|
||||||
|
month: '月',
|
||||||
|
day: '日',
|
||||||
|
fileSizeLimit: '图片文件大小不能超过100MB',
|
||||||
|
invalidImageFile: '请选择有效的图片文件',
|
||||||
|
pleaseLoginFirst: '请先登录后再提交任务',
|
||||||
|
taskInProgress: '已有任务在进行中,请等待完成或取消当前任务',
|
||||||
|
uploadFirstFrameRequired: '请上传首帧图片',
|
||||||
|
enterDescriptionRequired: '请输入描述文字',
|
||||||
|
creatingTask: '正在创建任务...',
|
||||||
|
taskCreatedSuccess: '任务创建成功,开始处理...',
|
||||||
|
createTaskFailed: '创建任务失败',
|
||||||
|
createTaskFailedRetry: '创建任务失败,请重试',
|
||||||
|
videoGenerateCompleted: '视频生成完成!',
|
||||||
|
videoGenerateFailed: '视频生成失败:',
|
||||||
|
enterPromptFirst: '请输入提示词',
|
||||||
|
promptTooLong: '提示词过长,请控制在2000字符以内',
|
||||||
|
optimizingPrompt: '正在优化提示词,请稍候...',
|
||||||
|
promptOptimizedSuccess: '提示词优化成功!',
|
||||||
|
promptAlreadyOptimized: '提示词已优化,但可能无明显变化',
|
||||||
|
optimizeFailed: '优化失败',
|
||||||
|
optimizePromptFailed: '优化提示词失败',
|
||||||
|
requestParameterError: '请求参数错误',
|
||||||
|
requestTimeout: '请求超时,请稍后重试',
|
||||||
|
serverError: '服务器错误,请稍后重试',
|
||||||
|
networkError: '网络错误,请检查网络连接',
|
||||||
|
networkConnectionError: '网络连接错误,请检查您的网络',
|
||||||
|
startDownload: '开始下载视频',
|
||||||
|
videoUrlNotAvailable: '视频链接不可用',
|
||||||
|
noWorkToDelete: '没有可删除的作品',
|
||||||
|
confirmDeleteWork: '确定要删除这个作品吗?',
|
||||||
|
confirmDelete: '确认删除',
|
||||||
|
workDeleted: '作品已删除',
|
||||||
|
deleteCancelled: '已取消删除',
|
||||||
|
historyParamsFilled: '已填充历史记录参数,可以开始生成',
|
||||||
|
cancelFeatureTodo: '取消功能待实现',
|
||||||
|
resumingTask: '检测到未完成的任务,继续处理中...',
|
||||||
|
resumingStoryboardTask: '检测到分镜图生成任务,继续处理中...',
|
||||||
|
unfinishedTaskDetected: '检测到未完成任务,正在恢复...'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 分镜视频专用翻译
|
||||||
|
storyboard: {
|
||||||
|
userAvatar: '用户头像',
|
||||||
|
generateStoryboard: '生成分镜图',
|
||||||
|
generateVideo: '生成视频',
|
||||||
|
uploadStoryboard: '上传参考图片',
|
||||||
|
uploadImage: '上传图片',
|
||||||
|
imageLabel: '图',
|
||||||
|
uploadHint: '上传参考图片,AI将根据图片生成6宫格分镜图',
|
||||||
|
addMore: '重新上传',
|
||||||
|
uploadedCount: '已上传图片',
|
||||||
|
uploadLimit: '已上传',
|
||||||
|
uploadedImage: '参考图片',
|
||||||
|
maxImages: '最多只能上传3张参考图片',
|
||||||
|
maxImagesWarning: '最多只能上传3张参考图片',
|
||||||
|
fileSizeLimit: '图片文件大小不能超过100MB',
|
||||||
|
invalidFileType: '请选择有效的图片文件',
|
||||||
|
uploadSuccess: '成功上传 {count} 张图片',
|
||||||
|
imageRemoved: '已删除图片',
|
||||||
|
promptPlaceholder: '例如:一个咖啡的广告\n\n提示:简单描述即可,AI会自动优化成专业的分镜图\n支持中文或英文输入,系统会自动翻译并优化为专业的分镜图描述',
|
||||||
|
tipWarning: '⚠️ 图片不能上传真人、涉及动漫IP等类型的图片',
|
||||||
|
tip1: '🎬 支持描述镜头推拉、人物动作、场景变化等',
|
||||||
|
imageCost: '生成分镜图消耗30积分',
|
||||||
|
videoCost: '生成视频消耗30积分',
|
||||||
|
videoPromptLabel: '视频描述',
|
||||||
|
videoPromptPlaceholder: '描述视频中的动作、镜头运动等,例如:镜头缓慢推进,人物转身微笑',
|
||||||
|
videoTip1: '⚠️ 图片不能上传真人、涉及动漫IP等类型的图片',
|
||||||
|
videoTip2: '🎬 支持描述镜头推拉、人物动作、场景变化等',
|
||||||
|
storyboardReadyHint: '分镜图已准备就绪,输入视频描述后点击生成视频',
|
||||||
|
optimizing: '优化中...',
|
||||||
|
enterPrompt: '请输入提示词',
|
||||||
|
promptTooLong: '提示词过长,请控制在2000字符以内',
|
||||||
|
optimizingPrompt: '正在优化提示词,请稍候...',
|
||||||
|
optimizeSuccess: '提示词优化成功!',
|
||||||
|
alreadyOptimized: '提示词已优化,但可能无明显变化',
|
||||||
|
optimizeFailed: '优化提示词失败',
|
||||||
|
paramError: '请求参数错误',
|
||||||
|
timeout: '请求超时,请稍后重试',
|
||||||
|
serverError: '服务器错误,请稍后重试',
|
||||||
|
networkError: '网络错误,请检查网络连接',
|
||||||
|
storyboardImage: '分镜图',
|
||||||
|
referenceImage: '参考图',
|
||||||
|
noStoryboard: '暂无分镜图',
|
||||||
|
hdMode: '高清模式 (1080P)',
|
||||||
|
hdCost: '开启消耗20积分',
|
||||||
|
imageModel: '图像生成模型',
|
||||||
|
pleaseLogin: '请先登录',
|
||||||
|
loginRequired: '需要登录后才能提交任务',
|
||||||
|
loginNow: '立即登录',
|
||||||
|
loginBeforeSubmit: '请先登录后再提交任务',
|
||||||
|
inProgress: '进行中',
|
||||||
|
generatingVideo: '正在生成视频,请稍候...',
|
||||||
|
progress: '进度: {progress}%',
|
||||||
|
generatingStoryboardText: '正在生成分镜图,请稍候...',
|
||||||
|
generatingVideoText: '正在生成视频,请稍候...',
|
||||||
|
generatingText: '生成中',
|
||||||
|
startCreating: '开始创作您的第一个作品吧!',
|
||||||
|
noDescription: '无描述',
|
||||||
|
queuing: '排队中',
|
||||||
|
subscribeToSpeed: '订阅套餐以提升生成速度',
|
||||||
|
noResult: '暂无结果',
|
||||||
|
noStoryboardImage: '请等待分镜图生成完成,或上传一张分镜图',
|
||||||
|
uploadOrGenerateFirst: '请先上传参考图片或输入描述生成分镜图',
|
||||||
|
uploadOrInputPrompt: '请上传参考图片或输入提示词',
|
||||||
|
startGenerateVideo: '开始生成视频',
|
||||||
|
generateVideoWithUpload: '使用上传图片生成视频',
|
||||||
|
generateStoryboardWithImage: '使用图片生成分镜图',
|
||||||
|
startGenerateStoryboard: '开始生成分镜图',
|
||||||
|
startGenerate: '开始生成',
|
||||||
|
generating: '生成中...',
|
||||||
|
enterDescription: '请输入描述文字',
|
||||||
|
enterDescriptionForImage: '请输入描述文字,AI将根据参考图和描述生成分镜图',
|
||||||
|
startingGenerate: '开始生成分镜图...',
|
||||||
|
taskCreated: '分镜图任务创建成功!',
|
||||||
|
createTaskFailed: '创建任务失败',
|
||||||
|
generateFailed: '生成分镜图失败',
|
||||||
|
taskTimeout: '任务超时,请稍后查看',
|
||||||
|
storyboardCompleted: '分镜图生成完成!请点击"开始生成"按钮生成视频',
|
||||||
|
videoCompleted: '视频生成完成!',
|
||||||
|
taskFailed: '任务失败',
|
||||||
|
checkInputOrRetry: '请检查输入或重新尝试',
|
||||||
|
unknownError: '未知错误',
|
||||||
|
startingVideoGenerate: '开始生成视频...',
|
||||||
|
videoTaskStarted: '视频生成任务已启动,请稍候...',
|
||||||
|
videoStartFailed: '启动视频生成失败',
|
||||||
|
defaultPrompt: '根据图片生成视频',
|
||||||
|
videoTaskCreated: '视频任务创建成功,开始处理...',
|
||||||
|
createVideoTaskFailed: '创建视频任务失败',
|
||||||
|
generateVideoFailed: '生成视频失败',
|
||||||
|
videoTaskTimeout: '视频任务超时,请稍后查看',
|
||||||
|
videoGenerateFailed: '视频生成失败',
|
||||||
|
paramsFilled: '已填充历史记录参数,可以开始生成',
|
||||||
|
cancelFeaturePending: '取消功能待实现',
|
||||||
|
taskCompleted: '任务已完成!',
|
||||||
|
resumingVideoTask: '检测到未完成的视频生成任务,继续处理中...',
|
||||||
|
resumingStoryboardTask: '检测到未完成的分镜图生成任务,继续处理中...',
|
||||||
|
resumingTask: '检测到未完成的任务,继续处理中...',
|
||||||
|
storyboardReady: '分镜图已生成,点击下方按钮生成视频',
|
||||||
|
readyForVideo: '分镜图已就绪,可以生成视频',
|
||||||
|
uploadStoryboardFirst: '请先上传分镜图',
|
||||||
|
regenerate: '重新生成',
|
||||||
|
regenerateConfirm: '重新生成将消耗积分并创建新任务,确定要继续吗?',
|
||||||
|
regenerateTitle: '重新生成分镜图',
|
||||||
|
generateVideo: '生成视频',
|
||||||
|
readyToGenerateVideo: '已填充分镜图参数,可以生成视频',
|
||||||
|
downloadImage: '下载分镜图',
|
||||||
|
imageUrlNotAvailable: '分镜图地址不可用',
|
||||||
|
statusPending: '排队中',
|
||||||
|
statusProcessing: '生成中',
|
||||||
|
statusCompleted: '已完成',
|
||||||
|
statusFailed: '生成失败',
|
||||||
|
statusCancelled: '已取消',
|
||||||
|
statusUnknown: '未知状态'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
works: {
|
||||||
|
title: '我的作品',
|
||||||
|
all: '全部',
|
||||||
|
videoDetail: '视频详情',
|
||||||
|
description: '描述',
|
||||||
|
createSimilar: '做同款',
|
||||||
|
deleteFailedWork: '删除作品',
|
||||||
|
retry: '重试',
|
||||||
|
videoLoadFailed: '视频加载失败',
|
||||||
|
videoFileNotExist: '视频文件不存在或已删除',
|
||||||
|
referenceImagePrompt: '基于参考图片生成',
|
||||||
|
textToVideo: '文生视频',
|
||||||
|
imageToVideo: '图生视频',
|
||||||
|
storyboardVideo: '分镜视频',
|
||||||
|
filter: '筛选',
|
||||||
|
sortBy: '排序',
|
||||||
|
newest: '最新',
|
||||||
|
oldest: '最旧',
|
||||||
|
noWorks: '暂无作品',
|
||||||
|
noWorksDesc: '开始创作你的第一个作品吧!',
|
||||||
|
viewDetail: '查看详情',
|
||||||
|
deleteConfirm: '确定要删除这个作品吗?',
|
||||||
|
deleteSuccess: '删除成功',
|
||||||
|
deleteFailed: '删除失败',
|
||||||
|
video: '视频',
|
||||||
|
image: '图片',
|
||||||
|
dateFilter: '日期',
|
||||||
|
today: '今天',
|
||||||
|
thisWeek: '本周',
|
||||||
|
thisMonth: '本月',
|
||||||
|
taskType: '任务类型',
|
||||||
|
resolution: '清晰度',
|
||||||
|
sd: '标清',
|
||||||
|
hd: '高清',
|
||||||
|
uhd: '超清',
|
||||||
|
ratio: '比例',
|
||||||
|
time: '时间',
|
||||||
|
popular: '热门',
|
||||||
|
searchPlaceholder: '名字/提示词/ID',
|
||||||
|
selectItems: '选择{count}个项目',
|
||||||
|
selectAll: '全选',
|
||||||
|
selectedCount: '已选 {count} 个项目',
|
||||||
|
favorite: '收藏',
|
||||||
|
downloadWithWatermark: '带水印下载',
|
||||||
|
downloadWithoutWatermark: '不带水印下载',
|
||||||
|
memberOnly: '会员',
|
||||||
|
rename: '重命名',
|
||||||
|
referenceImagePrompt: '图1在图2中奔跑视频',
|
||||||
|
allLoaded: '✓ 已加载全部内容',
|
||||||
|
noContent: '没有找到相关内容',
|
||||||
|
backToTop: '回到顶部',
|
||||||
|
createSimilarInfo: '基于作品"{title}"创建同款',
|
||||||
|
goToCreate: '跳转到创作页面',
|
||||||
|
downloadStart: '开始下载:{title}',
|
||||||
|
downloadComplete: '下载完成',
|
||||||
|
shareComingSoon: '分享链接功能即将上线',
|
||||||
|
downloadWithWatermarkStart: '开始下载带水印版本',
|
||||||
|
downloadWithoutWatermarkStart: '开始下载不带水印版本(会员专享)',
|
||||||
|
renameDevMsg: '重命名功能开发中',
|
||||||
|
deleteWorkConfirm: '确定删除该作品吗?',
|
||||||
|
deleteConfirmTitle: '删除确认',
|
||||||
|
bulkDownloadStart: '开始下载 {count} 个文件',
|
||||||
|
bulkDeleteConfirm: '确定删除选中的 {count} 个项目吗?',
|
||||||
|
bulkDeleteSuccess: '已删除选中项目',
|
||||||
|
filtersReset: '筛选器已重置',
|
||||||
|
processing: '生成中...',
|
||||||
|
queuing: '排队中...',
|
||||||
|
pleaseWait: '请耐心等待,视频正在生成中',
|
||||||
|
noPreview: '无预览',
|
||||||
|
videoLoadFailed: '视频加载失败',
|
||||||
|
videoFileNotExist: '视频文件可能不存在或已被删除',
|
||||||
|
retry: '重试',
|
||||||
|
deleteFailedWork: '删除此作品',
|
||||||
|
deleteFailedWorkConfirm: '此作品视频加载失败,确定要删除吗?删除后无法恢复。',
|
||||||
|
readyToGenerateVideo: '已填充分镜图,可以开始生成视频',
|
||||||
|
noDownloadUrl: '没有可下载的文件'
|
||||||
|
},
|
||||||
|
|
||||||
|
subscription: {
|
||||||
|
title: '会员订阅',
|
||||||
|
choosePlan: '选择套餐',
|
||||||
|
currentPlan: '当前套餐',
|
||||||
|
free: '免费版',
|
||||||
|
standard: '标准版',
|
||||||
|
professional: '专业版',
|
||||||
|
perMonth: '年',
|
||||||
|
perYear: '年',
|
||||||
|
subscribe: '立即订阅',
|
||||||
|
renew: '续费',
|
||||||
|
upgrade: '升级',
|
||||||
|
features: '功能特性',
|
||||||
|
unlimited: '无限',
|
||||||
|
limited: '有限',
|
||||||
|
pointsPerMonth: '积分/年',
|
||||||
|
pointsPerYear: '积分/年',
|
||||||
|
videoQuality: '视频质量',
|
||||||
|
support: '客服支持',
|
||||||
|
priorityQueue: '优先队列',
|
||||||
|
advancedFeatures: '高级功能',
|
||||||
|
|
||||||
|
// 新增键
|
||||||
|
userAvatar: '用户头像',
|
||||||
|
loading: '加载中...',
|
||||||
|
pointsDetails: '积分详情',
|
||||||
|
myOrders: '我的订单',
|
||||||
|
currentActivePlan: '当前生效权益',
|
||||||
|
expiryTime: '到期时间',
|
||||||
|
permanent: '永久',
|
||||||
|
remainingPoints: '剩余积分',
|
||||||
|
plans: '套餐',
|
||||||
|
currentPackage: '当前套餐',
|
||||||
|
firstPurchaseDiscount: '首购低至8.5折',
|
||||||
|
bestValue: '超值之选',
|
||||||
|
standardPoints: '6000积分/年',
|
||||||
|
premiumPoints: '12000积分/年',
|
||||||
|
freeNewUserBonus: '新用户首次登陆免费获得50积分',
|
||||||
|
fastGeneration: '快速通道生成',
|
||||||
|
superFastGeneration: '极速通道生成',
|
||||||
|
commercialUse: '支持商用',
|
||||||
|
noWatermark: '下载去水印',
|
||||||
|
earlyAccess: '新功能优先体验',
|
||||||
|
|
||||||
|
// 积分和条数
|
||||||
|
points: '积分',
|
||||||
|
items: '条',
|
||||||
|
pointsValidOneYear: '积分一年有效',
|
||||||
|
textToVideo30Points: '文生视频:30积分/条',
|
||||||
|
imageToVideo30Points: '图生视频:30积分/条',
|
||||||
|
storyboardImage30Points: '分镜图生成:30积分/次',
|
||||||
|
storyboardVideo30Points: '分镜视频生成:30积分/条',
|
||||||
|
maxTextToVideo: '最多文生视频 {count}条',
|
||||||
|
maxImageToVideo: '最多图生视频 {count}条',
|
||||||
|
maxStoryboardImage: '最多分镜图 {count}次',
|
||||||
|
maxStoryboardVideo: '最多分镜视频 {count}条',
|
||||||
|
textToVideoItems: '文生视频 {count}条',
|
||||||
|
imageToVideoItems: '或图生视频 {count}条',
|
||||||
|
storyboardImageTimes: '或分镜图 {count}次',
|
||||||
|
storyboardVideoItems: '或分镜视频 {count}条',
|
||||||
|
|
||||||
|
// 积分历史相关
|
||||||
|
pointsUsageHistory: '积分使用情况',
|
||||||
|
pointsUsageOverview: '积分使用总览',
|
||||||
|
totalRecharge: '总充值',
|
||||||
|
totalConsumption: '总消耗',
|
||||||
|
currentPoints: '当前积分',
|
||||||
|
noPointsHistory: '暂无积分使用记录',
|
||||||
|
description: '描述',
|
||||||
|
time: '时间',
|
||||||
|
orderNumber: '订单号',
|
||||||
|
taskId: '任务ID',
|
||||||
|
recharge: '充值',
|
||||||
|
consume: '消耗',
|
||||||
|
|
||||||
|
// 消息提示
|
||||||
|
pleaseLogin: '请先登录',
|
||||||
|
loadUserInfoFailed: '获取用户信息失败,使用默认值',
|
||||||
|
loadUserInfoError: '加载用户信息失败: {message}',
|
||||||
|
loadPointsHistoryFailed: '获取积分使用历史失败',
|
||||||
|
generatingQRCode: '正在生成支付二维码...',
|
||||||
|
subscriptionFailed: '订阅处理失败,请重试',
|
||||||
|
qrCodeGenerated: '二维码已生成,请使用支付宝扫码支付',
|
||||||
|
qrCodeGenerationFailed: '生成二维码失败,请重试',
|
||||||
|
qrCodeEmpty: '二维码生成失败:二维码为空',
|
||||||
|
createPaymentFailed: '创建支付订单失败',
|
||||||
|
qrCodeGenerationError: '二维码生成失败:{message}',
|
||||||
|
pleaseTryAgain: '请重试',
|
||||||
|
refreshPage: '请刷新页面重试',
|
||||||
|
paymentSuccess: '支付成功!积分已到账',
|
||||||
|
paymentPending: '支付处理中,请稍候...',
|
||||||
|
paymentCancelled: '支付已取消',
|
||||||
|
paymentError: '支付处理异常,如有疑问请联系客服',
|
||||||
|
infoUpdated: '信息已更新!',
|
||||||
|
paymentProcessingFailed: '支付成功但处理订单失败,请联系客服',
|
||||||
|
paymentFailed: '支付失败,请重试',
|
||||||
|
creatingOrder: '正在创建订单...',
|
||||||
|
paymentPageLoadFailed: '支付页面加载失败',
|
||||||
|
createAlipayPaymentFailed: '创建支付宝支付失败',
|
||||||
|
createPaymentOrderFailed: '创建支付订单失败,请重试',
|
||||||
|
|
||||||
|
// 套餐描述
|
||||||
|
standardDescription: '标准版订阅 - 每月200积分',
|
||||||
|
premiumDescription: '专业版订阅 - 每月1000积分'
|
||||||
|
},
|
||||||
|
|
||||||
|
welcome: {
|
||||||
|
textToVideo: '文生视频',
|
||||||
|
imageToVideo: '图生视频',
|
||||||
|
storyboardVideo: '分镜视频',
|
||||||
|
pricing: '订阅套餐',
|
||||||
|
startExperience: '开始体验',
|
||||||
|
title1: '智创',
|
||||||
|
title2: '无限,',
|
||||||
|
title3: '灵感',
|
||||||
|
title4: '变现。',
|
||||||
|
subtitle: '使用邮箱验证码登录,安全便捷',
|
||||||
|
tryNow: '立即体验',
|
||||||
|
coreFeatures: '核心功能',
|
||||||
|
textToVideoDesc: '输入文字描述,AI自动生成高质量视频内容',
|
||||||
|
imageToVideoDesc: '上传图片,AI智能分析并生成动态视频',
|
||||||
|
storyboardVideoDesc: '专业分镜制作,打造电影级视频效果',
|
||||||
|
pricingDesc: '灵活的价格方案,满足不同创作需求',
|
||||||
|
startCreating: '开始创作'
|
||||||
|
},
|
||||||
|
|
||||||
|
nav: {
|
||||||
|
dashboard: '数据仪表台',
|
||||||
|
members: '会员管理',
|
||||||
|
orders: '订单管理',
|
||||||
|
apiManagement: 'API管理',
|
||||||
|
tasks: '生成任务记录',
|
||||||
|
systemSettings: '系统设置',
|
||||||
|
onlineUsers: '当前在线用户',
|
||||||
|
systemUptime: '系统运行时间',
|
||||||
|
todayVisitors: '今日访客',
|
||||||
|
memberManagement: '会员管理',
|
||||||
|
orderManagement: '订单管理',
|
||||||
|
taskRecord: '生成任务记录',
|
||||||
|
errorStats: '错误统计'
|
||||||
|
},
|
||||||
|
|
||||||
|
admin: {
|
||||||
|
exitAdmin: '退出后台'
|
||||||
|
},
|
||||||
|
|
||||||
|
dashboard: {
|
||||||
|
title: '数据仪表台',
|
||||||
|
totalUsers: '用户总数',
|
||||||
|
paidUsers: '付费用户数',
|
||||||
|
todayRevenue: '今日收入',
|
||||||
|
dailyActive: '日活用户趋势',
|
||||||
|
conversionRate: '用户转化率',
|
||||||
|
comparedToLastMonth: '较上月同期',
|
||||||
|
comparedToYesterday: '较昨日',
|
||||||
|
year2025: '2025年',
|
||||||
|
year2024: '2024年',
|
||||||
|
year2023: '2023年',
|
||||||
|
userAvatar: '用户头像',
|
||||||
|
month1: '1月',
|
||||||
|
month2: '2月',
|
||||||
|
month3: '3月',
|
||||||
|
month4: '4月',
|
||||||
|
month5: '5月',
|
||||||
|
month6: '6月',
|
||||||
|
month7: '7月',
|
||||||
|
month8: '8月',
|
||||||
|
month9: '9月',
|
||||||
|
month10: '10月',
|
||||||
|
month11: '11月',
|
||||||
|
month12: '12月',
|
||||||
|
pleaseLogin: '请先登录',
|
||||||
|
loadDataFailed: '加载仪表盘数据失败'
|
||||||
|
},
|
||||||
|
|
||||||
|
orders: {
|
||||||
|
title: '订单管理',
|
||||||
|
orderNumber: '订单编号',
|
||||||
|
username: '用户名',
|
||||||
|
amount: '金额',
|
||||||
|
paymentMethod: '支付方式',
|
||||||
|
status: '状态',
|
||||||
|
createTime: '创建时间',
|
||||||
|
operation: '操作',
|
||||||
|
allStatus: '全部状态',
|
||||||
|
allTypes: '全部类型',
|
||||||
|
pending: '待支付',
|
||||||
|
confirmed: '已确认',
|
||||||
|
paid: '已支付',
|
||||||
|
processing: '处理中',
|
||||||
|
shipped: '已发货',
|
||||||
|
delivered: '已送达',
|
||||||
|
completed: '已完成',
|
||||||
|
cancelled: '已取消',
|
||||||
|
refunded: '已退款',
|
||||||
|
unpaid: '未支付',
|
||||||
|
allPaymentMethods: '全部支付方式',
|
||||||
|
alipay: '支付宝',
|
||||||
|
wechat: '微信支付',
|
||||||
|
paypal: 'PayPal',
|
||||||
|
selected: '已选择{count}项',
|
||||||
|
orderDetail: '订单详情',
|
||||||
|
basicInfo: '基本信息',
|
||||||
|
orderType: '订单类型',
|
||||||
|
paymentInfo: '支付信息',
|
||||||
|
productOrder: '商品订单',
|
||||||
|
serviceOrder: '服务订单',
|
||||||
|
subscriptionOrder: '订阅订单',
|
||||||
|
digitalProduct: '数字商品',
|
||||||
|
physicalProduct: '实体商品'
|
||||||
|
},
|
||||||
|
|
||||||
|
tasks: {
|
||||||
|
title: '生成任务记录',
|
||||||
|
taskId: '任务ID',
|
||||||
|
username: '用户名',
|
||||||
|
type: '类型',
|
||||||
|
resources: '消耗资源',
|
||||||
|
status: '状态',
|
||||||
|
createTime: '创建时间',
|
||||||
|
operation: '操作',
|
||||||
|
allStatus: '全部状态',
|
||||||
|
completed: '已完成',
|
||||||
|
processing: '处理中',
|
||||||
|
failed: '失败',
|
||||||
|
cancelled: '已取消',
|
||||||
|
pending: '待处理',
|
||||||
|
textToVideo: '文生视频',
|
||||||
|
imageToVideo: '图生视频',
|
||||||
|
storyboardVideo: '分镜视频',
|
||||||
|
taskDetail: '任务详情',
|
||||||
|
unknown: '未知',
|
||||||
|
pointsUnit: '积分',
|
||||||
|
basicInfo: '基本信息',
|
||||||
|
timeInfo: '时间信息',
|
||||||
|
progressInfo: '进度信息',
|
||||||
|
progress: '进度',
|
||||||
|
result: '结果',
|
||||||
|
resultLink: '结果链接',
|
||||||
|
viewResult: '查看结果',
|
||||||
|
errorInfo: '错误信息',
|
||||||
|
close: '关闭',
|
||||||
|
updateTime: '更新时间',
|
||||||
|
completeTime: '完成时间',
|
||||||
|
taskType: '任务类型',
|
||||||
|
resourcesConsumed: '消耗资源',
|
||||||
|
defaultPoints: '0积分'
|
||||||
|
},
|
||||||
|
|
||||||
|
members: {
|
||||||
|
title: '会员列表',
|
||||||
|
userId: '用户ID',
|
||||||
|
username: '用户名',
|
||||||
|
level: '会员等级',
|
||||||
|
points: '剩余资源点',
|
||||||
|
expiryDate: '到期时间',
|
||||||
|
operation: '编辑',
|
||||||
|
allLevels: '全部等级',
|
||||||
|
professional: '专业会员',
|
||||||
|
standard: '标准会员',
|
||||||
|
editMember: '编辑会员信息',
|
||||||
|
usernamePlaceholder: '请输入用户名',
|
||||||
|
levelPlaceholder: '请选择会员等级',
|
||||||
|
pointsPlaceholder: '请输入资源点',
|
||||||
|
expiryPlaceholder: '请选择到期时间',
|
||||||
|
memberLevel: '会员等级',
|
||||||
|
freeMember: '免费会员',
|
||||||
|
standardMember: '标准会员',
|
||||||
|
professionalMember: '专业会员',
|
||||||
|
userStatus: '用户状态',
|
||||||
|
activeUsers: '活跃用户',
|
||||||
|
bannedUsers: '封禁用户',
|
||||||
|
allUsers: '全部用户',
|
||||||
|
role: '角色',
|
||||||
|
status: '状态',
|
||||||
|
setAdmin: '设为管理员',
|
||||||
|
revokeAdmin: '取消管理员',
|
||||||
|
ban: '封禁',
|
||||||
|
unban: '解封',
|
||||||
|
active: '活跃',
|
||||||
|
banned: '封禁',
|
||||||
|
superAdmin: '超级管理员',
|
||||||
|
admin: '管理员',
|
||||||
|
normalUser: '普通用户',
|
||||||
|
userRole: '用户角色',
|
||||||
|
selectRole: '请选择用户角色',
|
||||||
|
confirmRoleChange: '确定要将用户 {username} {action}吗?',
|
||||||
|
confirmBanAction: '确定要{action}用户 {username} 吗?',
|
||||||
|
confirmAction: '确认{action}',
|
||||||
|
actionSuccess: '{action}成功',
|
||||||
|
actionFailed: '{action}失败'
|
||||||
|
},
|
||||||
|
|
||||||
|
apiManagement: {
|
||||||
|
title: 'API管理',
|
||||||
|
apiKey: 'API密钥',
|
||||||
|
apiKeyPlaceholder: '请输入API密钥',
|
||||||
|
apiBaseUrl: 'API基础URL',
|
||||||
|
apiBaseUrlPlaceholder: '请输入API基础URL,如 https://ai.comfly.chat',
|
||||||
|
apiBaseUrlHint: '当前使用',
|
||||||
|
tokenExpiration: 'Token过期时间',
|
||||||
|
tokenPlaceholder: '请输入小时数(1-720)',
|
||||||
|
hours: '小时',
|
||||||
|
days: '天',
|
||||||
|
rangeHint: '范围:1-720小时(1小时-30天)',
|
||||||
|
atLeastOneRequired: '请至少输入一个配置项',
|
||||||
|
saveSuccess: '保存成功',
|
||||||
|
saveFailed: '保存失败'
|
||||||
|
},
|
||||||
|
|
||||||
|
errorStats: {
|
||||||
|
title: '错误类型统计',
|
||||||
|
userAvatar: '用户头像',
|
||||||
|
totalErrors: '总错误数',
|
||||||
|
todayErrors: '今日错误',
|
||||||
|
weekErrors: '本周错误',
|
||||||
|
errorTypeDistribution: '错误类型分布',
|
||||||
|
last7Days: '最近7天',
|
||||||
|
last30Days: '最近30天',
|
||||||
|
last90Days: '最近90天',
|
||||||
|
times: '次',
|
||||||
|
noErrorData: '暂无错误数据',
|
||||||
|
recentErrors: '最近错误',
|
||||||
|
refresh: '刷新',
|
||||||
|
time: '时间',
|
||||||
|
errorType: '错误类型',
|
||||||
|
user: '用户',
|
||||||
|
taskId: '任务ID',
|
||||||
|
errorMessage: '错误信息'
|
||||||
|
},
|
||||||
|
|
||||||
|
systemSettings: {
|
||||||
|
title: '系统设置',
|
||||||
|
membership: '会员收费标准',
|
||||||
|
cleanup: '任务清理管理',
|
||||||
|
membershipLevels: '会员等级',
|
||||||
|
editLevel: '编辑等级',
|
||||||
|
price: '价格',
|
||||||
|
description: '描述',
|
||||||
|
cleanupStats: '清理统计',
|
||||||
|
manualCleanup: '手动清理',
|
||||||
|
autoCleanup: '自动清理',
|
||||||
|
perMonth: '/月',
|
||||||
|
includesPoints: '包含{points}资源点/月',
|
||||||
|
includesPointsPerMonth: '包含{points}资源点/月',
|
||||||
|
cleanupStatsInfo: '清理统计信息',
|
||||||
|
refresh: '刷新',
|
||||||
|
currentTotalTasks: '当前任务总数',
|
||||||
|
completedTasks: '已完成任务',
|
||||||
|
failedTasks: '失败任务',
|
||||||
|
archivedTasks: '已归档任务',
|
||||||
|
cleanupLogsCount: '清理日志数',
|
||||||
|
retentionDays: '保留天数',
|
||||||
|
days: '天',
|
||||||
|
cleanupActions: '清理操作',
|
||||||
|
performFullCleanup: '执行完整清理',
|
||||||
|
cleanupUserTasks: '清理指定用户任务',
|
||||||
|
fullCleanupDesc: '完整清理',
|
||||||
|
fullCleanupDescDetail: '将成功任务导出到归档表,删除失败任务',
|
||||||
|
userCleanupDesc: '用户清理',
|
||||||
|
userCleanupDescDetail: '清理指定用户的所有任务',
|
||||||
|
cleanupConfig: '清理配置',
|
||||||
|
taskRetentionDays: '任务保留天数',
|
||||||
|
taskRetentionTip: '任务完成后保留的天数',
|
||||||
|
archiveRetentionDays: '归档保留天数',
|
||||||
|
archiveRetentionTip: '归档数据保留的天数',
|
||||||
|
membershipLevel: '会员等级',
|
||||||
|
selectLevelPlaceholder: '请选择会员等级',
|
||||||
|
freeMembership: '免费版会员',
|
||||||
|
standardMembership: '标准版会员',
|
||||||
|
professionalMembership: '专业版会员',
|
||||||
|
membershipPrice: '会员价格',
|
||||||
|
resourcePointsAmount: '资源点数量',
|
||||||
|
validityPeriod: '会员有效期',
|
||||||
|
monthly: '月付',
|
||||||
|
quarterly: '季付',
|
||||||
|
yearly: '年付',
|
||||||
|
enterUsername: '请输入要清理的用户名',
|
||||||
|
warning: '警告',
|
||||||
|
cleanupWarning: '此操作将清理该用户的所有任务,包括:',
|
||||||
|
successTasksArchived: '成功任务将导出到归档表',
|
||||||
|
failedTasksLogged: '失败任务将记录到清理日志',
|
||||||
|
originalTasksDeleted: '原始任务记录将被删除',
|
||||||
|
irreversibleWarning: '此操作不可撤销,请谨慎操作!',
|
||||||
|
confirmCleanup: '确认清理',
|
||||||
|
aiModel: 'AI模型设置',
|
||||||
|
promptOptimization: '提示词优化设置',
|
||||||
|
promptOptimizationModel: '模型名称',
|
||||||
|
promptOptimizationModelTip: '输入用于优化提示词的模型名称,如 gpt-4o、gemini-pro 等',
|
||||||
|
storyboardSystemPrompt: '分镜图系统引导词',
|
||||||
|
storyboardSystemPromptTip: '此引导词会自动添加到用户提示词前面,用于统一分镜图生成风格',
|
||||||
|
storyboardSystemPromptPlaceholder: '例如:高质量电影级画面,专业摄影,电影色调...',
|
||||||
|
membershipUpdateSuccess: '会员等级配置更新成功',
|
||||||
|
membershipUpdateFailed: '会员等级配置更新失败',
|
||||||
|
loadMembershipFailed: '加载会员配置失败',
|
||||||
|
usingDefaultConfig: '使用默认配置',
|
||||||
|
enterValidNumber: '请输入有效的数字',
|
||||||
|
unknown: '未知错误'
|
||||||
|
}
|
||||||
|
}
|
||||||
21
demo/frontend/src/main-backup.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import { useUserStore } from './stores/user'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 立即挂载应用
|
||||||
|
app.mount('#app')
|
||||||
26
demo/frontend/src/main.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(router)
|
||||||
|
app.use(i18n)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.use(lazyLoadDirective)
|
||||||
|
|
||||||
|
console.log('[main.js] i18n 当前语言:', i18n.global.locale.value)
|
||||||
|
|
||||||
|
// 立即挂载应用
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
|
console.log('[main.js] 应用已挂载,当前语言:', i18n.global.locale.value)
|
||||||
319
demo/frontend/src/router/index.js
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// 路由组件 - 使用懒加载优化性能
|
||||||
|
const Login = () => import('@/views/Login.vue')
|
||||||
|
const Register = () => import('@/views/Register.vue')
|
||||||
|
const OrderDetail = () => import('@/views/OrderDetail.vue')
|
||||||
|
const OrderCreate = () => import('@/views/OrderCreate.vue')
|
||||||
|
const Payments = () => import('@/views/Payments.vue')
|
||||||
|
const PaymentCreate = () => import('@/views/PaymentCreate.vue')
|
||||||
|
const AdminOrders = () => import('@/views/AdminOrders.vue')
|
||||||
|
const AdminDashboard = () => import('@/views/AdminDashboard.vue')
|
||||||
|
const Welcome = () => import('@/views/Welcome.vue')
|
||||||
|
const Profile = () => import('@/views/Profile.vue')
|
||||||
|
const Subscription = () => import('@/views/Subscription.vue')
|
||||||
|
const MyWorks = () => import('@/views/MyWorks.vue')
|
||||||
|
const VideoDetail = () => import('@/views/VideoDetail.vue')
|
||||||
|
const TextToVideo = () => import('@/views/TextToVideo.vue')
|
||||||
|
const TextToVideoCreate = () => import('@/views/TextToVideoCreate.vue')
|
||||||
|
const ImageToVideo = () => import('@/views/ImageToVideo.vue')
|
||||||
|
const ImageToVideoCreate = () => import('@/views/ImageToVideoCreate.vue')
|
||||||
|
const ImageToVideoDetail = () => import('@/views/ImageToVideoDetail.vue')
|
||||||
|
const StoryboardVideo = () => import('@/views/StoryboardVideo.vue')
|
||||||
|
const StoryboardVideoCreate = () => import('@/views/StoryboardVideoCreate.vue')
|
||||||
|
const MemberManagement = () => import('@/views/MemberManagement.vue')
|
||||||
|
const SystemSettings = () => import('@/views/SystemSettings.vue')
|
||||||
|
const GenerateTaskRecord = () => import('@/views/GenerateTaskRecord.vue')
|
||||||
|
const HelloWorld = () => import('@/views/HelloWorld.vue')
|
||||||
|
const TaskStatusPage = () => import('@/views/TaskStatusPage.vue')
|
||||||
|
const TermsOfService = () => import('@/views/TermsOfService.vue')
|
||||||
|
const UserAgreement = () => import('@/views/UserAgreement.vue')
|
||||||
|
const PrivacyPolicy = () => import('@/views/PrivacyPolicy.vue')
|
||||||
|
const ChangePassword = () => import('@/views/ChangePassword.vue')
|
||||||
|
const SetPassword = () => import('@/views/SetPassword.vue')
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/works',
|
||||||
|
name: 'MyWorks',
|
||||||
|
component: MyWorks,
|
||||||
|
meta: { title: '我的作品', requiresAuth: true, keepAlive: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/task-status',
|
||||||
|
name: 'TaskStatus',
|
||||||
|
component: TaskStatusPage,
|
||||||
|
meta: { title: '任务状态', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/video/:id',
|
||||||
|
name: 'VideoDetail',
|
||||||
|
component: VideoDetail,
|
||||||
|
meta: { title: '视频详情', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/text-to-video',
|
||||||
|
name: 'TextToVideo',
|
||||||
|
component: TextToVideo,
|
||||||
|
meta: { title: '文生视频', requiresAuth: true, keepAlive: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/text-to-video/create',
|
||||||
|
name: 'TextToVideoCreate',
|
||||||
|
component: TextToVideoCreate,
|
||||||
|
meta: { title: '文生视频创作' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/image-to-video',
|
||||||
|
name: 'ImageToVideo',
|
||||||
|
component: ImageToVideo,
|
||||||
|
meta: { title: '图生视频', requiresAuth: true, keepAlive: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/image-to-video/create',
|
||||||
|
name: 'ImageToVideoCreate',
|
||||||
|
component: ImageToVideoCreate,
|
||||||
|
meta: { title: '图生视频创作' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/image-to-video/detail/:taskId',
|
||||||
|
name: 'ImageToVideoDetail',
|
||||||
|
component: ImageToVideoDetail,
|
||||||
|
meta: { title: '图生视频详情', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/storyboard-video',
|
||||||
|
name: 'StoryboardVideo',
|
||||||
|
component: StoryboardVideo,
|
||||||
|
meta: { title: '分镜视频', requiresAuth: true, keepAlive: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/storyboard-video/create',
|
||||||
|
name: 'StoryboardVideoCreate',
|
||||||
|
component: StoryboardVideoCreate,
|
||||||
|
meta: { title: '分镜视频创作' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Root',
|
||||||
|
redirect: '/welcome' // 默认重定向到欢迎页面
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/welcome',
|
||||||
|
name: 'Welcome',
|
||||||
|
component: Welcome,
|
||||||
|
meta: { title: '欢迎', guest: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
name: 'Profile',
|
||||||
|
component: Profile,
|
||||||
|
meta: { title: '个人主页', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/subscription',
|
||||||
|
name: 'Subscription',
|
||||||
|
component: Subscription,
|
||||||
|
meta: { title: '会员订阅', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: Login,
|
||||||
|
meta: { title: '登录', guest: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'Register',
|
||||||
|
component: Register,
|
||||||
|
meta: { title: '注册', guest: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/orders/:id',
|
||||||
|
name: 'OrderDetail',
|
||||||
|
component: OrderDetail,
|
||||||
|
meta: { title: '订单详情', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/orders/create',
|
||||||
|
name: 'OrderCreate',
|
||||||
|
component: OrderCreate,
|
||||||
|
meta: { title: '创建订单', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/payments',
|
||||||
|
name: 'Payments',
|
||||||
|
component: Payments,
|
||||||
|
meta: { title: '支付记录', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/payments/create',
|
||||||
|
name: 'PaymentCreate',
|
||||||
|
component: PaymentCreate,
|
||||||
|
meta: { title: '创建支付', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/orders',
|
||||||
|
name: 'AdminOrders',
|
||||||
|
component: AdminOrders,
|
||||||
|
meta: { title: '订单管理', requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/dashboard',
|
||||||
|
name: 'AdminDashboard',
|
||||||
|
component: AdminDashboard,
|
||||||
|
meta: { title: '后台管理', requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/member-management',
|
||||||
|
name: 'MemberManagement',
|
||||||
|
component: MemberManagement,
|
||||||
|
meta: { title: '会员管理', requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/system-settings',
|
||||||
|
name: 'SystemSettings',
|
||||||
|
component: SystemSettings,
|
||||||
|
meta: { title: '系统设置', requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/generate-task-record',
|
||||||
|
name: 'GenerateTaskRecord',
|
||||||
|
component: GenerateTaskRecord,
|
||||||
|
meta: { title: '生成任务记录', requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/api-management',
|
||||||
|
name: 'ApiManagement',
|
||||||
|
component: () => import('@/views/ApiManagement.vue'),
|
||||||
|
meta: { title: 'API管理', requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/error-statistics',
|
||||||
|
name: 'ErrorStatistics',
|
||||||
|
component: () => import('@/views/ErrorStatistics.vue'),
|
||||||
|
meta: { title: '错误统计', requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/hello',
|
||||||
|
name: 'HelloWorld',
|
||||||
|
component: HelloWorld,
|
||||||
|
meta: { title: 'Hello World' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/terms-of-service',
|
||||||
|
name: 'TermsOfService',
|
||||||
|
component: TermsOfService,
|
||||||
|
meta: { title: 'Vionow 服务条款' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user-agreement',
|
||||||
|
name: 'UserAgreement',
|
||||||
|
component: UserAgreement,
|
||||||
|
meta: { title: '用户协议' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/privacy-policy',
|
||||||
|
name: 'PrivacyPolicy',
|
||||||
|
component: PrivacyPolicy,
|
||||||
|
meta: { title: '隐私政策' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/change-password',
|
||||||
|
name: 'ChangePassword',
|
||||||
|
component: ChangePassword,
|
||||||
|
meta: { title: '修改密码', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/set-password',
|
||||||
|
name: 'SetPassword',
|
||||||
|
component: SetPassword,
|
||||||
|
meta: { title: '设置密码', requiresAuth: true }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
// 添加路由缓存配置
|
||||||
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition
|
||||||
|
} else {
|
||||||
|
return { top: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
try {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 检查localStorage中的token是否被清除(例如JWT过期后被request.js清除)
|
||||||
|
// 如果token被清除但store中仍有用户信息,则同步清除store
|
||||||
|
const storedToken = localStorage.getItem('token')
|
||||||
|
if (!storedToken && userStore.isAuthenticated) {
|
||||||
|
userStore.clearUserData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优化:只在首次访问时初始化用户状态
|
||||||
|
if (!userStore.initialized) {
|
||||||
|
await userStore.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理根路径:如果已登录,重定向到个人主页;否则重定向到欢迎页面
|
||||||
|
if (to.path === '/' || to.path === '/welcome') {
|
||||||
|
if (userStore.isAuthenticated && to.path === '/') {
|
||||||
|
next('/profile')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 未登录用户访问欢迎页面,允许访问
|
||||||
|
if (!userStore.isAuthenticated && to.path === '/welcome') {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要认证
|
||||||
|
if (to.meta.requiresAuth) {
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
// 未登录,跳转到登录页
|
||||||
|
next({
|
||||||
|
path: '/login',
|
||||||
|
query: { redirect: to.fullPath }
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查管理员权限
|
||||||
|
if (to.meta.requiresAdmin && !userStore.isAdmin) {
|
||||||
|
// 权限不足,跳转到个人主页并显示警告
|
||||||
|
ElMessage.warning('权限不足,只有管理员才能访问此页面')
|
||||||
|
next('/profile')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已登录用户访问登录页,重定向到个人主页
|
||||||
|
if (to.meta.guest && userStore.isAuthenticated) {
|
||||||
|
next('/profile')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置页面标题
|
||||||
|
if (to.meta.title) {
|
||||||
|
document.title = `${to.meta.title} - AIGC Demo`
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('路由守卫错误:', error)
|
||||||
|
// 发生错误时,允许访问但显示错误信息
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
238
demo/frontend/src/stores/orders.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { getOrders, getOrderById, createOrder, updateOrderStatus, cancelOrder, shipOrder, completeOrder } from '@/api/orders'
|
||||||
|
|
||||||
|
export const useOrderStore = defineStore('orders', () => {
|
||||||
|
// 状态
|
||||||
|
const orders = ref([])
|
||||||
|
const currentOrder = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const pagination = ref({
|
||||||
|
page: 0,
|
||||||
|
size: 10,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取订单列表
|
||||||
|
const fetchOrders = async (params = {}) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
console.log('OrderStore: 开始获取订单,参数:', params)
|
||||||
|
|
||||||
|
const response = await getOrders(params)
|
||||||
|
console.log('OrderStore: API原始响应:', response)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
orders.value = response.data.content || response.data
|
||||||
|
pagination.value = {
|
||||||
|
page: response.data.number || 0,
|
||||||
|
size: response.data.size || 10,
|
||||||
|
total: response.data.totalElements || response.data.length,
|
||||||
|
totalPages: response.data.totalPages || 1
|
||||||
|
}
|
||||||
|
console.log('OrderStore: 处理后的订单数据:', orders.value)
|
||||||
|
console.log('OrderStore: 分页信息:', pagination.value)
|
||||||
|
} else {
|
||||||
|
console.error('OrderStore: API返回失败:', response.message)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OrderStore: 获取订单异常:', error)
|
||||||
|
return { success: false, message: '获取订单列表失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单详情
|
||||||
|
const fetchOrderById = async (id) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await getOrderById(id)
|
||||||
|
|
||||||
|
console.log('OrderStore: 获取订单详情响应:', response)
|
||||||
|
|
||||||
|
// axios会将响应包装在response.data中
|
||||||
|
const responseData = response?.data || response || {}
|
||||||
|
console.log('OrderStore: 解析后的响应数据:', responseData)
|
||||||
|
|
||||||
|
if (responseData.success && responseData.data) {
|
||||||
|
currentOrder.value = responseData.data
|
||||||
|
console.log('OrderStore: 设置后的订单详情:', currentOrder.value)
|
||||||
|
return { success: true, data: responseData.data }
|
||||||
|
} else if (responseData.success === false) {
|
||||||
|
console.error('OrderStore: API返回失败:', responseData.message)
|
||||||
|
return { success: false, message: responseData.message || '获取订单详情失败' }
|
||||||
|
} else {
|
||||||
|
// 如果没有success字段,尝试直接使用data
|
||||||
|
if (responseData.id || responseData.orderNumber) {
|
||||||
|
currentOrder.value = responseData
|
||||||
|
return { success: true, data: responseData }
|
||||||
|
} else {
|
||||||
|
console.error('OrderStore: API返回数据格式错误:', responseData)
|
||||||
|
return { success: false, message: 'API返回数据格式错误' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OrderStore: 获取订单详情异常:', error)
|
||||||
|
return { success: false, message: error.response?.data?.message || error.message || '获取订单详情失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
const createNewOrder = async (orderData) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await createOrder(orderData)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 刷新订单列表
|
||||||
|
await fetchOrders()
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create order error:', error)
|
||||||
|
return { success: false, message: '创建订单失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
const updateOrder = async (id, status, notes) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await updateOrderStatus(id, status, notes)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 更新本地订单状态
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.status = status
|
||||||
|
order.updatedAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前订单
|
||||||
|
if (currentOrder.value && currentOrder.value.id === id) {
|
||||||
|
currentOrder.value.status = status
|
||||||
|
currentOrder.value.updatedAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update order error:', error)
|
||||||
|
return { success: false, message: '更新订单状态失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消订单
|
||||||
|
const cancelOrderById = async (id, reason) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await cancelOrder(id, reason)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 更新本地订单状态
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.status = 'CANCELLED'
|
||||||
|
order.cancelledAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前订单
|
||||||
|
if (currentOrder.value && currentOrder.value.id === id) {
|
||||||
|
currentOrder.value.status = 'CANCELLED'
|
||||||
|
currentOrder.value.cancelledAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Cancel order error:', error)
|
||||||
|
return { success: false, message: '取消订单失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发货
|
||||||
|
const shipOrderById = async (id, trackingNumber) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await shipOrder(id, trackingNumber)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 更新本地订单状态
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.status = 'SHIPPED'
|
||||||
|
order.shippedAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前订单
|
||||||
|
if (currentOrder.value && currentOrder.value.id === id) {
|
||||||
|
currentOrder.value.status = 'SHIPPED'
|
||||||
|
currentOrder.value.shippedAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ship order error:', error)
|
||||||
|
return { success: false, message: '发货失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成订单
|
||||||
|
const completeOrderById = async (id) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await completeOrder(id)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 更新本地订单状态
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.status = 'COMPLETED'
|
||||||
|
order.deliveredAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前订单
|
||||||
|
if (currentOrder.value && currentOrder.value.id === id) {
|
||||||
|
currentOrder.value.status = 'COMPLETED'
|
||||||
|
currentOrder.value.deliveredAt = new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Complete order error:', error)
|
||||||
|
return { success: false, message: '完成订单失败' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
orders,
|
||||||
|
currentOrder,
|
||||||
|
loading,
|
||||||
|
pagination,
|
||||||
|
// 方法
|
||||||
|
fetchOrders,
|
||||||
|
fetchOrderById,
|
||||||
|
createNewOrder,
|
||||||
|
updateOrder,
|
||||||
|
cancelOrderById,
|
||||||
|
shipOrderById,
|
||||||
|
completeOrderById
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
182
demo/frontend/src/stores/user.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { login, register, logout, getCurrentUser } from '@/api/auth'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// 状态 - 从 localStorage 尝试恢复用户信息
|
||||||
|
const user = ref(null)
|
||||||
|
const token = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const initialized = ref(false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cachedUser = localStorage.getItem('user')
|
||||||
|
const cachedToken = localStorage.getItem('token')
|
||||||
|
if (cachedUser && cachedToken) {
|
||||||
|
user.value = JSON.parse(cachedUser)
|
||||||
|
token.value = cachedToken
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore localStorage parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const isAuthenticated = computed(() => !!user.value)
|
||||||
|
const isAdmin = computed(() => user.value?.role === 'ROLE_ADMIN' || user.value?.role === 'ROLE_SUPER_ADMIN')
|
||||||
|
const isSuperAdmin = computed(() => user.value?.role === 'ROLE_SUPER_ADMIN')
|
||||||
|
const username = computed(() => user.value?.username || '')
|
||||||
|
|
||||||
|
// 可用积分(总积分 - 冻结积分)
|
||||||
|
const availablePoints = computed(() => {
|
||||||
|
if (!user.value) return 0
|
||||||
|
const total = user.value.points || 0
|
||||||
|
const frozen = user.value.frozenPoints || 0
|
||||||
|
return Math.max(0, total - frozen)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
const loginUser = async (credentials) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await login(credentials)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// 使用JWT认证,保存token和用户信息
|
||||||
|
user.value = response.data.user
|
||||||
|
token.value = response.data.token
|
||||||
|
|
||||||
|
// 保存到localStorage,关闭浏览器后仍保持登录
|
||||||
|
localStorage.setItem('token', response.data.token)
|
||||||
|
localStorage.setItem('user', JSON.stringify(user.value))
|
||||||
|
return { success: true }
|
||||||
|
} else {
|
||||||
|
return { success: false, message: response.message }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
return { success: false, message: '登录失败,请检查网络连接' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
const registerUser = async (userData) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await register(userData)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return { success: true, message: '注册成功,请登录' }
|
||||||
|
} else {
|
||||||
|
return { success: false, message: response.message }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Register error:', error)
|
||||||
|
return { success: false, message: '注册失败,请检查网络连接' }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
const logoutUser = async () => {
|
||||||
|
try {
|
||||||
|
// JWT无状态,直接清除localStorage即可
|
||||||
|
token.value = null
|
||||||
|
user.value = null
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
const fetchCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getCurrentUser()
|
||||||
|
// 统一使用 response.data 格式
|
||||||
|
const data = response.data || response
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
user.value = data.data
|
||||||
|
localStorage.setItem('user', JSON.stringify(user.value))
|
||||||
|
} else {
|
||||||
|
console.warn('获取用户信息失败:', data.message)
|
||||||
|
// 不要立即清除用户数据,保持当前登录状态
|
||||||
|
// 只在明确的401/认证失败时才由axios拦截器处理登出
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch user error:', error)
|
||||||
|
// 请求失败时不强制清除,保持现有本地态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除用户数据
|
||||||
|
const clearUserData = () => {
|
||||||
|
token.value = null
|
||||||
|
user.value = null
|
||||||
|
// 清除 localStorage 中的用户数据
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
const init = async () => {
|
||||||
|
if (initialized.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 localStorage 恢复用户状态
|
||||||
|
const savedToken = localStorage.getItem('token')
|
||||||
|
const savedUser = localStorage.getItem('user')
|
||||||
|
|
||||||
|
console.log('Store init - savedToken:', savedToken ? savedToken.substring(0, 30) + '...' : 'null')
|
||||||
|
|
||||||
|
if (savedToken && savedUser) {
|
||||||
|
try {
|
||||||
|
token.value = savedToken
|
||||||
|
user.value = JSON.parse(savedUser)
|
||||||
|
|
||||||
|
console.log('恢复用户状态:', user.value?.username)
|
||||||
|
|
||||||
|
// 刷新用户信息(确保角色等信息是最新的)
|
||||||
|
await fetchCurrentUser()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore user state:', error)
|
||||||
|
clearUserData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置初始化状态(登录成功后调用)
|
||||||
|
const resetInitialized = () => {
|
||||||
|
initialized.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
loading,
|
||||||
|
// 计算属性
|
||||||
|
isAuthenticated,
|
||||||
|
isAdmin,
|
||||||
|
isSuperAdmin,
|
||||||
|
username,
|
||||||
|
availablePoints,
|
||||||
|
// 方法
|
||||||
|
loginUser,
|
||||||
|
registerUser,
|
||||||
|
logoutUser,
|
||||||
|
fetchCurrentUser,
|
||||||
|
clearUserData,
|
||||||
|
init,
|
||||||
|
initialized,
|
||||||
|
resetInitialized
|
||||||
|
}
|
||||||
|
})
|
||||||
41
demo/frontend/src/utils/apiHelper.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* API 基础路径工具函数
|
||||||
|
* 自动适配 ngrok 内网穿透和本地开发环境
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 API 基础路径
|
||||||
|
* @returns {string} API 基础路径
|
||||||
|
*/
|
||||||
|
export function getApiBaseURL() {
|
||||||
|
// 检查是否在浏览器环境中
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const hostname = window.location.hostname
|
||||||
|
|
||||||
|
// 如果当前域名包含 ngrok 或通过 Nginx 访问,使用相对路径
|
||||||
|
if (hostname.includes('ngrok') ||
|
||||||
|
hostname === 'localhost' ||
|
||||||
|
hostname === '127.0.0.1' ||
|
||||||
|
hostname.startsWith('172.22.') ||
|
||||||
|
window.location.port === '') { // 通过 Nginx 代理访问时没有端口号
|
||||||
|
// 通过 Nginx 访问,使用相对路径(自动适配当前域名)
|
||||||
|
return '/api'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认开发环境,使用相对路径(通过 Vite 代理)
|
||||||
|
return '/api'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建完整的 API URL
|
||||||
|
* @param {string} path - API 路径(如 '/users' 或 'users')
|
||||||
|
* @returns {string} 完整的 API URL
|
||||||
|
*/
|
||||||
|
export function buildApiURL(path) {
|
||||||
|
const baseURL = getApiBaseURL()
|
||||||
|
// 确保路径以 / 开头
|
||||||
|
const cleanPath = path.startsWith('/') ? path : `/${path}`
|
||||||
|
return `${baseURL}${cleanPath}`
|
||||||
|
}
|
||||||
|
|
||||||
193
demo/frontend/src/utils/download.js
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* 跨浏览器兼容的文件下载工具
|
||||||
|
* 特别针对 Safari/iOS 进行了优化
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否为 Safari 浏览器
|
||||||
|
*/
|
||||||
|
export const isSafari = () => {
|
||||||
|
const ua = navigator.userAgent.toLowerCase()
|
||||||
|
return ua.includes('safari') && !ua.includes('chrome') && !ua.includes('android')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否为 iOS 设备
|
||||||
|
*/
|
||||||
|
export const isIOS = () => {
|
||||||
|
return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||||
|
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用文件下载函数
|
||||||
|
* @param {string} url - 文件URL
|
||||||
|
* @param {string} filename - 下载文件名
|
||||||
|
* @param {string} mimeType - 文件MIME类型(可选)
|
||||||
|
* @returns {Promise<boolean>} - 下载是否成功
|
||||||
|
*/
|
||||||
|
export const downloadFile = async (url, filename, mimeType = '') => {
|
||||||
|
try {
|
||||||
|
// Safari 和 iOS 特殊处理
|
||||||
|
if (isSafari() || isIOS()) {
|
||||||
|
return await downloadForSafari(url, filename, mimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他浏览器使用标准方式
|
||||||
|
return await downloadStandard(url, filename)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败,尝试备用方案:', error)
|
||||||
|
// 最终备用方案:新窗口打开
|
||||||
|
window.open(url, '_blank')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safari/iOS 专用下载方法
|
||||||
|
*/
|
||||||
|
const downloadForSafari = async (url, filename, mimeType) => {
|
||||||
|
try {
|
||||||
|
// 方案1:尝试使用 fetch + blob
|
||||||
|
const response = await fetch(url, {
|
||||||
|
mode: 'cors',
|
||||||
|
credentials: 'omit'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
|
||||||
|
// 创建带正确 MIME 类型的 blob
|
||||||
|
const finalMimeType = mimeType || blob.type || getMimeType(filename)
|
||||||
|
const finalBlob = new Blob([blob], { type: finalMimeType })
|
||||||
|
|
||||||
|
// Safari 需要使用 FileReader 转换为 data URL
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const dataUrl = reader.result
|
||||||
|
|
||||||
|
// 创建临时链接
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = dataUrl
|
||||||
|
link.download = filename
|
||||||
|
link.style.display = 'none'
|
||||||
|
|
||||||
|
// Safari 需要将链接添加到 DOM
|
||||||
|
document.body.appendChild(link)
|
||||||
|
|
||||||
|
// 使用 setTimeout 确保 Safari 能正确处理
|
||||||
|
setTimeout(() => {
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
// 延迟移除链接
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
resolve(true)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
// FileReader 失败,尝试直接打开
|
||||||
|
window.open(url, '_blank')
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(finalBlob)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Safari 下载失败:', error)
|
||||||
|
|
||||||
|
// 备用方案:直接打开新窗口
|
||||||
|
// 对于视频文件,Safari 会显示播放器,用户可以长按保存
|
||||||
|
window.open(url, '_blank')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准浏览器下载方法
|
||||||
|
*/
|
||||||
|
const downloadStandard = async (url, filename) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
mode: 'cors',
|
||||||
|
credentials: 'omit'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = blobUrl
|
||||||
|
link.download = filename
|
||||||
|
link.style.display = 'none'
|
||||||
|
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('标准下载失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件名获取 MIME 类型
|
||||||
|
*/
|
||||||
|
const getMimeType = (filename) => {
|
||||||
|
const ext = filename.split('.').pop()?.toLowerCase()
|
||||||
|
const mimeTypes = {
|
||||||
|
'mp4': 'video/mp4',
|
||||||
|
'webm': 'video/webm',
|
||||||
|
'mov': 'video/quicktime',
|
||||||
|
'avi': 'video/x-msvideo',
|
||||||
|
'png': 'image/png',
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'webp': 'image/webp'
|
||||||
|
}
|
||||||
|
return mimeTypes[ext] || 'application/octet-stream'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载视频文件
|
||||||
|
*/
|
||||||
|
export const downloadVideo = async (url, taskId) => {
|
||||||
|
const filename = `video_${taskId || Date.now()}.mp4`
|
||||||
|
return downloadFile(url, filename, 'video/mp4')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载图片文件
|
||||||
|
*/
|
||||||
|
export const downloadImage = async (url, taskId, prefix = 'image') => {
|
||||||
|
// 根据 URL 判断图片格式
|
||||||
|
let ext = 'png'
|
||||||
|
if (url.includes('.jpg') || url.includes('.jpeg')) {
|
||||||
|
ext = 'jpg'
|
||||||
|
} else if (url.includes('.webp')) {
|
||||||
|
ext = 'webp'
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `${prefix}_${taskId || Date.now()}.${ext}`
|
||||||
|
const mimeType = ext === 'jpg' ? 'image/jpeg' : `image/${ext}`
|
||||||
|
return downloadFile(url, filename, mimeType)
|
||||||
|
}
|
||||||
898
demo/frontend/src/views/AdminDashboard.vue
Normal file
@@ -0,0 +1,898 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-dashboard">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/backgrounds/logo-admin.svg" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item active">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
<span>{{ $t('nav.dashboard') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToMembers">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>{{ $t('nav.members') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToOrders">
|
||||||
|
<el-icon><ShoppingCart /></el-icon>
|
||||||
|
<span>{{ $t('nav.orders') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToAPI">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>{{ $t('nav.apiManagement') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToTasks">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>{{ $t('nav.tasks') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToErrorStats">
|
||||||
|
<el-icon><Warning /></el-icon>
|
||||||
|
<span>错误统计</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToSettings">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<span>{{ $t('nav.systemSettings') }}</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="online-users">
|
||||||
|
{{ $t('nav.todayVisitors') }}: <span class="highlight">{{ onlineUsers }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="system-uptime">
|
||||||
|
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<h2>{{ $t('nav.dashboard') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<el-dropdown @command="handleUserCommand">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||||
|
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="exitAdmin">
|
||||||
|
{{ $t('admin.exitAdmin') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-cards" v-loading="loading">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon users">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">{{ $t('dashboard.totalUsers') }}</div>
|
||||||
|
<div class="stat-number">{{ formatNumber(stats.totalUsers) }}</div>
|
||||||
|
<div class="stat-change" :class="stats.totalUsersChange >= 0 ? 'positive' : 'negative'">
|
||||||
|
{{ stats.totalUsersChange >= 0 ? '+' : '' }}{{ stats.totalUsersChange }}% {{ $t('dashboard.comparedToLastMonth') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon paid-users">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">{{ $t('dashboard.paidUsers') }}</div>
|
||||||
|
<div class="stat-number">{{ formatNumber(stats.paidUsers) }}</div>
|
||||||
|
<div class="stat-change" :class="stats.paidUsersChange >= 0 ? 'positive' : 'negative'">
|
||||||
|
{{ stats.paidUsersChange >= 0 ? '+' : '' }}{{ stats.paidUsersChange }}% {{ $t('dashboard.comparedToLastMonth') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon revenue">
|
||||||
|
<el-icon><Money /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">{{ $t('dashboard.todayRevenue') }}</div>
|
||||||
|
<div class="stat-number">{{ formatCurrency(stats.todayRevenue) }}</div>
|
||||||
|
<div class="stat-change" :class="stats.todayRevenueChange >= 0 ? 'positive' : 'negative'">
|
||||||
|
{{ stats.todayRevenueChange >= 0 ? '+' : '' }}{{ stats.todayRevenueChange }}% {{ $t('dashboard.comparedToYesterday') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="charts-section">
|
||||||
|
<!-- 日活用户趋势 -->
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-header">
|
||||||
|
<h3>{{ $t('dashboard.dailyActive') }}</h3>
|
||||||
|
<el-select v-model="selectedYear" @change="loadDailyActiveChart" class="year-select">
|
||||||
|
<el-option :label="$t('dashboard.year2025')" value="2025"></el-option>
|
||||||
|
<el-option :label="$t('dashboard.year2024')" value="2024"></el-option>
|
||||||
|
<el-option :label="$t('dashboard.year2023')" value="2023"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="chart-content">
|
||||||
|
<div ref="dailyActiveChart" style="width: 100%; height: 100%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户转化率 -->
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-header">
|
||||||
|
<h3>{{ $t('dashboard.conversionRate') }}</h3>
|
||||||
|
<el-select v-model="selectedYear2" @change="loadConversionChart" class="year-select">
|
||||||
|
<el-option :label="$t('dashboard.year2025')" value="2025"></el-option>
|
||||||
|
<el-option :label="$t('dashboard.year2024')" value="2024"></el-option>
|
||||||
|
<el-option :label="$t('dashboard.year2023')" value="2023"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="chart-content">
|
||||||
|
<div ref="conversionChart" style="width: 100%; height: 100%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
Grid,
|
||||||
|
User,
|
||||||
|
ShoppingCart,
|
||||||
|
Document,
|
||||||
|
User as Briefcase,
|
||||||
|
Setting,
|
||||||
|
User as Search,
|
||||||
|
User as Avatar,
|
||||||
|
ArrowDown,
|
||||||
|
Money,
|
||||||
|
Warning
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { getDashboardOverview, getConversionRate, getDailyActiveUsersTrend, getSystemStatus } from '@/api/dashboard'
|
||||||
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 年份选择
|
||||||
|
const selectedYear = ref('2025')
|
||||||
|
const selectedYear2 = ref('2025')
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = ref({
|
||||||
|
totalUsers: 0,
|
||||||
|
paidUsers: 0,
|
||||||
|
todayRevenue: 0,
|
||||||
|
totalUsersChange: 0,
|
||||||
|
paidUsersChange: 0,
|
||||||
|
todayRevenueChange: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 系统状态数据
|
||||||
|
const onlineUsers = ref('0/500')
|
||||||
|
const systemUptime = ref(t('nav.loading'))
|
||||||
|
|
||||||
|
// 图表相关
|
||||||
|
const dailyActiveChart = ref(null)
|
||||||
|
const conversionChart = ref(null)
|
||||||
|
let dailyActiveChartInstance = null
|
||||||
|
let conversionChartInstance = null
|
||||||
|
|
||||||
|
// 动态加载ECharts
|
||||||
|
const loadECharts = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (window.echarts) {
|
||||||
|
resolve(window.echarts)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js'
|
||||||
|
script.onload = () => resolve(window.echarts)
|
||||||
|
script.onerror = reject
|
||||||
|
document.head.appendChild(script)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goToMembers = () => {
|
||||||
|
router.push('/member-management')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToOrders = () => {
|
||||||
|
router.push('/admin/orders')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToAPI = () => {
|
||||||
|
router.push('/api-management')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTasks = () => {
|
||||||
|
router.push('/generate-task-record')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToErrorStats = () => {
|
||||||
|
router.push('/admin/error-statistics')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToSettings = () => {
|
||||||
|
router.push('/system-settings')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理用户头像下拉菜单
|
||||||
|
const handleUserCommand = (command) => {
|
||||||
|
if (command === 'exitAdmin') {
|
||||||
|
// 退出后台,返回个人首页
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数字
|
||||||
|
const formatNumber = (num) => {
|
||||||
|
if (num >= 10000) {
|
||||||
|
return (num / 10000).toFixed(1) + '万'
|
||||||
|
}
|
||||||
|
return num.toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化货币
|
||||||
|
const formatCurrency = (amount) => {
|
||||||
|
if (amount >= 10000) {
|
||||||
|
return '¥' + (amount / 10000).toFixed(1) + '万'
|
||||||
|
}
|
||||||
|
return '¥' + amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载仪表盘数据
|
||||||
|
const loadDashboardData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 获取概览数据
|
||||||
|
const overviewRes = await getDashboardOverview()
|
||||||
|
console.log('仪表盘概览数据响应:', overviewRes)
|
||||||
|
|
||||||
|
// 后端直接返回Map,没有success/data包装
|
||||||
|
const data = overviewRes?.data || overviewRes || {}
|
||||||
|
console.log('解析后的数据:', data)
|
||||||
|
|
||||||
|
if (data && !data.error) {
|
||||||
|
stats.value = {
|
||||||
|
totalUsers: data.totalUsers || 0,
|
||||||
|
paidUsers: data.paidUsers || 0,
|
||||||
|
todayRevenue: data.todayRevenue || 0,
|
||||||
|
totalUsersChange: data.totalUsersChange ?? 0,
|
||||||
|
paidUsersChange: data.paidUsersChange ?? 0,
|
||||||
|
todayRevenueChange: data.todayRevenueChange ?? 0
|
||||||
|
}
|
||||||
|
console.log('设置后的统计数据:', stats.value)
|
||||||
|
} else {
|
||||||
|
console.error('Get dashboard data failed:', data.error || data.message)
|
||||||
|
ElMessage.error(t('dashboard.loadDataFailed') + ': ' + (data.message || t('dashboard.unknownError')))
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load dashboard data failed:', error)
|
||||||
|
ElMessage.error(t('dashboard.loadDataFailed') + ': ' + (error.message || t('dashboard.unknownError')))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载日活用户趋势图
|
||||||
|
const loadDailyActiveChart = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getDailyActiveUsersTrend(selectedYear.value, 'monthly')
|
||||||
|
const data = response.data || {}
|
||||||
|
|
||||||
|
if (!dailyActiveChart.value) return
|
||||||
|
|
||||||
|
const echarts = await loadECharts()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
if (dailyActiveChartInstance) {
|
||||||
|
dailyActiveChartInstance.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
dailyActiveChartInstance = echarts.init(dailyActiveChart.value)
|
||||||
|
|
||||||
|
const monthlyData = data.monthlyData || []
|
||||||
|
const months = monthlyData.map(item => `${item.month}月`)
|
||||||
|
const values = monthlyData.map(item => item.avgDailyActive || item.dailyActiveUsers || 0)
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: months,
|
||||||
|
axisLabel: {
|
||||||
|
color: '#6b7280'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
color: '#6b7280',
|
||||||
|
formatter: '{value}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '日活用户',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
data: values,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#3b82f6'
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(59, 130, 246, 0.3)'
|
||||||
|
}, {
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(59, 130, 246, 0.1)'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
dailyActiveChartInstance.setOption(option)
|
||||||
|
|
||||||
|
// 响应式调整
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (dailyActiveChartInstance) {
|
||||||
|
dailyActiveChartInstance.resize()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载日活用户趋势图失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载用户转化率图
|
||||||
|
const loadConversionChart = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getConversionRate(selectedYear2.value)
|
||||||
|
const data = response.data || {}
|
||||||
|
|
||||||
|
if (!conversionChart.value) return
|
||||||
|
|
||||||
|
const echarts = await loadECharts()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
if (conversionChartInstance) {
|
||||||
|
conversionChartInstance.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
conversionChartInstance = echarts.init(conversionChart.value)
|
||||||
|
|
||||||
|
const monthlyData = data.monthlyData || []
|
||||||
|
const months = monthlyData.map(item => `${item.month}月`)
|
||||||
|
const conversionRates = monthlyData.map(item => item.conversionRate || 0)
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
},
|
||||||
|
formatter: (params) => {
|
||||||
|
const item = params[0]
|
||||||
|
const monthData = monthlyData[item.dataIndex]
|
||||||
|
return `${item.name}<br/>转化率: ${item.value}%<br/>总用户: ${monthData?.totalUsers || 0}<br/>付费用户: ${monthData?.paidUsers || 0}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: months,
|
||||||
|
axisLabel: {
|
||||||
|
color: '#6b7280'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
color: '#6b7280',
|
||||||
|
formatter: '{value}%'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '转化率',
|
||||||
|
type: 'bar',
|
||||||
|
data: conversionRates,
|
||||||
|
itemStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [{
|
||||||
|
offset: 0,
|
||||||
|
color: '#8b5cf6'
|
||||||
|
}, {
|
||||||
|
offset: 1,
|
||||||
|
color: '#3b82f6'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
conversionChartInstance.setOption(option)
|
||||||
|
|
||||||
|
// 响应式调整
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (conversionChartInstance) {
|
||||||
|
conversionChartInstance.resize()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户转化率图失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定时器
|
||||||
|
let systemStatsTimer = null
|
||||||
|
|
||||||
|
// 页面加载时获取数据
|
||||||
|
onMounted(async () => {
|
||||||
|
console.log('后台管理页面加载完成')
|
||||||
|
fetchSystemStats()
|
||||||
|
await loadDashboardData()
|
||||||
|
await nextTick()
|
||||||
|
await loadDailyActiveChart()
|
||||||
|
await loadConversionChart()
|
||||||
|
|
||||||
|
// 每30秒刷新一次系统状态
|
||||||
|
systemStatsTimer = setInterval(() => {
|
||||||
|
fetchSystemStats()
|
||||||
|
}, 30000)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取系统统计数据(当天访问人数和系统运行时间)
|
||||||
|
const fetchSystemStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/online-stats', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success) {
|
||||||
|
onlineUsers.value = data.todayVisitors || 0
|
||||||
|
systemUptime.value = data.uptime || t('systemSettings.unknown')
|
||||||
|
} else {
|
||||||
|
onlineUsers.value = '0'
|
||||||
|
systemUptime.value = t('systemSettings.unknown')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get online stats failed:', error)
|
||||||
|
onlineUsers.value = '0'
|
||||||
|
systemUptime.value = t('systemSettings.unknown')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清理图表和定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (dailyActiveChartInstance) {
|
||||||
|
dailyActiveChartInstance.dispose()
|
||||||
|
dailyActiveChartInstance = null
|
||||||
|
}
|
||||||
|
if (conversionChartInstance) {
|
||||||
|
conversionChartInstance.dispose()
|
||||||
|
conversionChartInstance = null
|
||||||
|
}
|
||||||
|
// 清理定时器
|
||||||
|
if (systemStatsTimer) {
|
||||||
|
clearInterval(systemStatsTimer)
|
||||||
|
systemStatsTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-dashboard {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 0;
|
||||||
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-width: 180px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: rgba(64, 158, 255, 0.15);
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
background: #f9fafb;
|
||||||
|
margin-top: auto;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer .highlight {
|
||||||
|
color: #409EFF;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-users,
|
||||||
|
.system-uptime {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-count {
|
||||||
|
color: #3b82f6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-uptime {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部搜索栏 */
|
||||||
|
.top-header {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar .arrow-down {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片 */
|
||||||
|
.stats-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.users {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.paid-users {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.revenue {
|
||||||
|
background: #fce7f3;
|
||||||
|
color: #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-change {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-change.positive {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-change.negative {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表区域 */
|
||||||
|
.charts-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 0 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-select {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
padding: 24px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-placeholder {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px dashed #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.charts-section {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-dashboard {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-section {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.stat-card {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
padding: 16px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1260
demo/frontend/src/views/AdminOrders.vue
Normal file
605
demo/frontend/src/views/ApiManagement.vue
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
<template>
|
||||||
|
<div class="api-management">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/backgrounds/logo-admin.svg" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item" @click="goToDashboard">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
<span>{{ $t('nav.dashboard') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToMembers">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>{{ $t('nav.members') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToOrders">
|
||||||
|
<el-icon><ShoppingCart /></el-icon>
|
||||||
|
<span>{{ $t('nav.orders') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item active">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>{{ $t('nav.apiManagement') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToTasks">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>{{ $t('nav.tasks') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToErrorStats">
|
||||||
|
<el-icon><Warning /></el-icon>
|
||||||
|
<span>错误统计</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToSettings">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<span>{{ $t('nav.systemSettings') }}</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="online-users">
|
||||||
|
{{ $t('nav.todayVisitors') }}: <span class="highlight">{{ onlineUsers }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="system-uptime">
|
||||||
|
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<h2>{{ $t('nav.apiManagement') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<el-dropdown @command="handleUserCommand">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||||
|
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="exitAdmin">
|
||||||
|
{{ $t('admin.exitAdmin') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- API密钥输入内容 -->
|
||||||
|
<section class="api-content">
|
||||||
|
<div class="content-header">
|
||||||
|
<h2>{{ $t('apiManagement.title') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前配置展示 -->
|
||||||
|
<div class="current-config">
|
||||||
|
<h3>当前配置</h3>
|
||||||
|
<div class="config-item">
|
||||||
|
<span class="config-label">API密钥:</span>
|
||||||
|
<span class="config-value">{{ currentMaskedKey || '未配置' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<span class="config-label">API端点:</span>
|
||||||
|
<span class="config-value">{{ currentApiBaseUrl || '未配置' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<span class="config-label">Token过期时间:</span>
|
||||||
|
<span class="config-value">{{ apiForm.tokenExpireHours ? apiForm.tokenExpireHours + ' 小时' : '未配置' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-form-container">
|
||||||
|
<h3 style="margin-bottom: 16px; color: #374151;">修改配置</h3>
|
||||||
|
<el-form :model="apiForm" label-width="120px" class="api-form">
|
||||||
|
<el-form-item :label="$t('apiManagement.apiKey')">
|
||||||
|
<el-input
|
||||||
|
v-model="apiForm.apiKey"
|
||||||
|
type="password"
|
||||||
|
:placeholder="$t('apiManagement.apiKeyPlaceholder')"
|
||||||
|
show-password
|
||||||
|
style="width: 100%; max-width: 600px;"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('apiManagement.apiBaseUrl')">
|
||||||
|
<el-input
|
||||||
|
v-model="apiForm.apiBaseUrl"
|
||||||
|
:placeholder="$t('apiManagement.apiBaseUrlPlaceholder')"
|
||||||
|
style="width: 100%; max-width: 600px;"
|
||||||
|
/>
|
||||||
|
<div style="margin-top: 8px; color: #6b7280; font-size: 12px;">
|
||||||
|
{{ $t('apiManagement.apiBaseUrlHint') }}: {{ currentApiBaseUrl || $t('common.notConfigured') || '未配置' }}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('apiManagement.tokenExpiration')">
|
||||||
|
<div style="display: flex; align-items: center; gap: 12px; width: 100%; max-width: 600px;">
|
||||||
|
<el-input
|
||||||
|
v-model.number="apiForm.tokenExpireHours"
|
||||||
|
type="number"
|
||||||
|
:placeholder="$t('apiManagement.tokenPlaceholder')"
|
||||||
|
style="flex: 1;"
|
||||||
|
:min="1"
|
||||||
|
:max="720"
|
||||||
|
/>
|
||||||
|
<span style="color: #6b7280; font-size: 14px;">{{ $t('apiManagement.hours') }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 8px; color: #6b7280; font-size: 12px;">
|
||||||
|
{{ $t('apiManagement.rangeHint') }}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="saveApiKey" :loading="saving">{{ $t('common.save') }}</el-button>
|
||||||
|
<el-button @click="resetForm">{{ $t('common.reset') }}</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
Grid,
|
||||||
|
User,
|
||||||
|
ShoppingCart,
|
||||||
|
Document,
|
||||||
|
Setting,
|
||||||
|
Search,
|
||||||
|
ArrowDown,
|
||||||
|
Warning
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import api from '@/api/request'
|
||||||
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const saving = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 系统状态数据
|
||||||
|
const onlineUsers = ref('0/500')
|
||||||
|
const systemUptime = ref(t('common.loading'))
|
||||||
|
const apiForm = reactive({
|
||||||
|
apiKey: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
tokenExpireHours: null // 从数据库加载
|
||||||
|
})
|
||||||
|
const currentApiBaseUrl = ref('')
|
||||||
|
const currentMaskedKey = ref('')
|
||||||
|
|
||||||
|
// 导航功能
|
||||||
|
const goToDashboard = () => {
|
||||||
|
router.push('/admin/dashboard')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToMembers = () => {
|
||||||
|
router.push('/member-management')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToOrders = () => {
|
||||||
|
router.push('/admin/orders')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTasks = () => {
|
||||||
|
router.push('/generate-task-record')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToErrorStats = () => {
|
||||||
|
router.push('/admin/error-statistics')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToSettings = () => {
|
||||||
|
router.push('/system-settings')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理用户头像下拉菜单
|
||||||
|
const handleUserCommand = (command) => {
|
||||||
|
if (command === 'exitAdmin') {
|
||||||
|
// 退出后台,返回个人首页
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化JWT过期时间显示
|
||||||
|
const formatJwtExpiration = (hours) => {
|
||||||
|
if (!hours) return ''
|
||||||
|
if (hours < 24) {
|
||||||
|
return `${hours}${t('apiManagement.hours')}`
|
||||||
|
} else if (hours < 720) {
|
||||||
|
const days = Math.floor(hours / 24)
|
||||||
|
const remainingHours = hours % 24
|
||||||
|
if (remainingHours === 0) {
|
||||||
|
return `${days}${t('apiManagement.days')}`
|
||||||
|
}
|
||||||
|
return `${days}${t('apiManagement.days')}${remainingHours}${t('apiManagement.hours')}`
|
||||||
|
} else {
|
||||||
|
return `30${t('apiManagement.days')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载当前API配置
|
||||||
|
const loadApiKey = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await api.get('/api-key')
|
||||||
|
if (response.data?.maskedKey) {
|
||||||
|
currentMaskedKey.value = response.data.maskedKey
|
||||||
|
console.log('当前API密钥已配置')
|
||||||
|
}
|
||||||
|
// 加载当前API基础URL
|
||||||
|
if (response.data?.apiBaseUrl) {
|
||||||
|
currentApiBaseUrl.value = response.data.apiBaseUrl
|
||||||
|
}
|
||||||
|
// 加载当前Token过期时间
|
||||||
|
if (response.data?.tokenExpireHours) {
|
||||||
|
apiForm.tokenExpireHours = response.data.tokenExpireHours
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载配置失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存API配置到数据库
|
||||||
|
const saveApiKey = async () => {
|
||||||
|
// 检查是否有任何输入
|
||||||
|
const hasApiKey = apiForm.apiKey && apiForm.apiKey.trim() !== ''
|
||||||
|
const hasApiBaseUrl = apiForm.apiBaseUrl && apiForm.apiBaseUrl.trim() !== ''
|
||||||
|
const hasTokenExpire = apiForm.tokenExpireHours && apiForm.tokenExpireHours >= 1 && apiForm.tokenExpireHours <= 720
|
||||||
|
|
||||||
|
// 验证输入:至少需要提供一个配置项
|
||||||
|
if (!hasApiKey && !hasApiBaseUrl && !hasTokenExpire) {
|
||||||
|
ElMessage.warning(t('apiManagement.atLeastOneRequired') || '请至少输入一个配置项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const requestData = {}
|
||||||
|
|
||||||
|
// 如果提供了API密钥,添加到请求中
|
||||||
|
if (hasApiKey) {
|
||||||
|
requestData.apiKey = apiForm.apiKey.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了API基础URL,添加到请求中
|
||||||
|
if (hasApiBaseUrl) {
|
||||||
|
requestData.apiBaseUrl = apiForm.apiBaseUrl.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了Token过期时间,添加到请求中
|
||||||
|
if (hasTokenExpire) {
|
||||||
|
requestData.tokenExpireHours = apiForm.tokenExpireHours
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.put('/api-key', requestData)
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
ElMessage.success(response.data.message || '配置已保存到数据库,立即生效')
|
||||||
|
// 清空输入框
|
||||||
|
apiForm.apiKey = ''
|
||||||
|
apiForm.apiBaseUrl = ''
|
||||||
|
// 重新加载当前配置
|
||||||
|
loadApiKey()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data?.error || '保存失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存配置失败:', error)
|
||||||
|
ElMessage.error('保存失败: ' + (error.response?.data?.message || error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
apiForm.apiKey = ''
|
||||||
|
apiForm.apiBaseUrl = ''
|
||||||
|
loadApiKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取当前API密钥状态
|
||||||
|
onMounted(() => {
|
||||||
|
loadApiKey()
|
||||||
|
fetchSystemStats()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取系统统计数据(当天访问人数和系统运行时间)
|
||||||
|
const fetchSystemStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/online-stats', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success) {
|
||||||
|
onlineUsers.value = data.todayVisitors || 0
|
||||||
|
systemUptime.value = data.uptime || $t('systemSettings.unknown')
|
||||||
|
} else {
|
||||||
|
onlineUsers.value = '0'
|
||||||
|
systemUptime.value = $t('systemSettings.unknown')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get online stats failed:', error)
|
||||||
|
onlineUsers.value = '0'
|
||||||
|
systemUptime.value = $t('systemSettings.unknown')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.api-management {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: white;
|
||||||
|
border-right: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 0;
|
||||||
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-width: 180px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
background: #f8f9fa;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-users,
|
||||||
|
.system-uptime {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: #3b82f6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部搜索栏 */
|
||||||
|
.top-header {
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding: 16px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar .arrow-down {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* API内容区域 */
|
||||||
|
.api-content {
|
||||||
|
padding: 24px;
|
||||||
|
flex: 1;
|
||||||
|
background: white;
|
||||||
|
margin: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-form-container {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-config {
|
||||||
|
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-config h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid rgba(186, 230, 253, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
width: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1e293b;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-form {
|
||||||
|
background: #f9fafb;
|
||||||
|
padding: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.api-management {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 16px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
382
demo/frontend/src/views/ChangePassword.vue
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改密码卡片 -->
|
||||||
|
<div class="login-card">
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="page-title">修改密码</div>
|
||||||
|
|
||||||
|
<!-- 表单 -->
|
||||||
|
<div class="password-form">
|
||||||
|
<!-- 当前密码(可选) -->
|
||||||
|
<div class="input-group">
|
||||||
|
<el-input
|
||||||
|
v-model="form.currentPassword"
|
||||||
|
placeholder="输入当前密码(可选)"
|
||||||
|
class="password-input"
|
||||||
|
show-password
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
/>
|
||||||
|
<div class="input-error" v-if="errors.currentPassword">{{ errors.currentPassword }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新密码 -->
|
||||||
|
<div class="input-group">
|
||||||
|
<el-input
|
||||||
|
v-model="form.newPassword"
|
||||||
|
placeholder="输入新密码"
|
||||||
|
class="password-input"
|
||||||
|
show-password
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
/>
|
||||||
|
<div class="input-error" v-if="errors.newPassword">{{ errors.newPassword }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认新密码 -->
|
||||||
|
<div class="input-group">
|
||||||
|
<el-input
|
||||||
|
v-model="form.confirmPassword"
|
||||||
|
placeholder="确认新密码"
|
||||||
|
class="password-input"
|
||||||
|
show-password
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
/>
|
||||||
|
<div class="input-error" v-if="errors.confirmPassword">{{ errors.confirmPassword }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 确定修改按钮 -->
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="submit-button"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ loading ? '提交中...' : '确定修改' }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<div class="back-button-wrapper">
|
||||||
|
<el-button
|
||||||
|
class="back-button"
|
||||||
|
@click="handleBack"
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import request from '@/api/request'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 判断是否首次设置密码
|
||||||
|
const isFirstTimeSetup = computed(() => {
|
||||||
|
return localStorage.getItem('needSetPassword') === '1'
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const errors = reactive({
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 验证表单
|
||||||
|
const validateForm = () => {
|
||||||
|
let valid = true
|
||||||
|
errors.currentPassword = ''
|
||||||
|
errors.newPassword = ''
|
||||||
|
errors.confirmPassword = ''
|
||||||
|
|
||||||
|
// 当前密码为可选,不强制必填
|
||||||
|
|
||||||
|
// 新密码必填,且必须包含英文字母和数字,不少于8位
|
||||||
|
if (!form.newPassword) {
|
||||||
|
errors.newPassword = '请输入新密码'
|
||||||
|
valid = false
|
||||||
|
} else if (form.newPassword.length < 8) {
|
||||||
|
errors.newPassword = '密码长度至少8位'
|
||||||
|
valid = false
|
||||||
|
} else if (!/[a-zA-Z]/.test(form.newPassword)) {
|
||||||
|
errors.newPassword = '密码必须包含英文字母'
|
||||||
|
valid = false
|
||||||
|
} else if (!/[0-9]/.test(form.newPassword)) {
|
||||||
|
errors.newPassword = '密码必须包含数字'
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认密码必填且必须与新密码一致
|
||||||
|
if (!form.confirmPassword) {
|
||||||
|
errors.confirmPassword = '请确认新密码'
|
||||||
|
valid = false
|
||||||
|
} else if (form.newPassword !== form.confirmPassword) {
|
||||||
|
errors.confirmPassword = '两次输入的密码不一致'
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交修改
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!validateForm()) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request({
|
||||||
|
url: '/auth/change-password',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
oldPassword: form.currentPassword || null,
|
||||||
|
newPassword: form.newPassword
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('修改密码响应:', response)
|
||||||
|
|
||||||
|
// response.data 是后端返回的数据
|
||||||
|
const result = response.data
|
||||||
|
if (result && result.success) {
|
||||||
|
ElMessage.success('密码修改成功')
|
||||||
|
|
||||||
|
// 清除首次设置标记
|
||||||
|
localStorage.removeItem('needSetPassword')
|
||||||
|
|
||||||
|
// 跳转到首页或之前的页面
|
||||||
|
const redirect = route.query.redirect || '/profile'
|
||||||
|
router.replace(redirect)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '修改失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码失败:', error)
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '修改失败,请重试'
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回
|
||||||
|
const handleBack = () => {
|
||||||
|
if (isFirstTimeSetup.value) {
|
||||||
|
// 首次设置时返回到首页
|
||||||
|
router.replace('/')
|
||||||
|
} else {
|
||||||
|
// 非首次设置时返回上一页
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查用户是否已登录
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
router.replace('/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0e1a url('/images/backgrounds/login_bg.png') center/cover no-repeat;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左上角Logo */
|
||||||
|
.logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
left: 30px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.login-card {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 550px;
|
||||||
|
max-width: 90vw;
|
||||||
|
background: rgba(121, 121, 121, 0.1);
|
||||||
|
backdrop-filter: blur(50px);
|
||||||
|
-webkit-backdrop-filter: blur(50px);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 60px 80px;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面标题 */
|
||||||
|
.page-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单 */
|
||||||
|
.password-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入组 */
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__wrapper) {
|
||||||
|
background: rgba(217, 217, 217, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 60px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__wrapper:hover) {
|
||||||
|
background: rgba(217, 217, 217, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__wrapper.is-focus) {
|
||||||
|
background: rgba(217, 217, 217, 0.3);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__inner) {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
background: transparent;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__inner::placeholder) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-error {
|
||||||
|
color: #ff7875;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确定修改按钮 */
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
background: #0DC0FF;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover {
|
||||||
|
background: #4DD4FF;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 返回按钮 */
|
||||||
|
.back-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
background: rgba(217, 217, 217, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: rgba(217, 217, 217, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button-wrapper .back-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.login-card {
|
||||||
|
width: 90%;
|
||||||
|
padding: 40px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-card {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
754
demo/frontend/src/views/ErrorStatistics.vue
Normal file
@@ -0,0 +1,754 @@
|
|||||||
|
<template>
|
||||||
|
<div class="error-statistics-page">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/backgrounds/logo-admin.svg" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item" @click="goToDashboard">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
<span>{{ $t('nav.dashboard') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToMembers">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>{{ $t('nav.members') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToOrders">
|
||||||
|
<el-icon><ShoppingCart /></el-icon>
|
||||||
|
<span>{{ $t('nav.orders') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToAPI">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>{{ $t('nav.apiManagement') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToTasks">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>{{ $t('nav.tasks') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item active">
|
||||||
|
<el-icon><Warning /></el-icon>
|
||||||
|
<span>{{ $t('nav.errorStats') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToSettings">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<span>{{ $t('nav.systemSettings') }}</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="online-users">
|
||||||
|
{{ $t('nav.todayVisitors') }}: <span class="highlight">{{ onlineUsers }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="system-uptime">
|
||||||
|
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 顶部搜索栏 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<h2>{{ $t('errorStats.title') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<el-dropdown @command="handleUserCommand">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<img src="/images/backgrounds/avatar-default.svg" :alt="$t('errorStats.userAvatar')" />
|
||||||
|
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="exitAdmin">
|
||||||
|
{{ $t('admin.exitAdmin') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 内容包装器 -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-cards" v-loading="loading">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon error">
|
||||||
|
<el-icon><Warning /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">{{ $t('errorStats.totalErrors') }}</div>
|
||||||
|
<div class="stat-number">{{ statistics.totalErrors || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon today">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">{{ $t('errorStats.todayErrors') }}</div>
|
||||||
|
<div class="stat-number">{{ statistics.todayErrors || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon week">
|
||||||
|
<el-icon><Calendar /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-title">{{ $t('errorStats.weekErrors') }}</div>
|
||||||
|
<div class="stat-number">{{ statistics.weekErrors || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误类型分布 -->
|
||||||
|
<div class="charts-section">
|
||||||
|
<div class="chart-card full-width">
|
||||||
|
<div class="chart-header">
|
||||||
|
<h3>{{ $t('errorStats.errorTypeDistribution') }}</h3>
|
||||||
|
<el-select v-model="selectedDays" @change="loadStatistics" class="days-select">
|
||||||
|
<el-option :label="$t('errorStats.last7Days')" :value="7"></el-option>
|
||||||
|
<el-option :label="$t('errorStats.last30Days')" :value="30"></el-option>
|
||||||
|
<el-option :label="$t('errorStats.last90Days')" :value="90"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="error-type-list">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in errorTypeStats"
|
||||||
|
:key="item.type"
|
||||||
|
class="error-type-item"
|
||||||
|
>
|
||||||
|
<div class="type-info">
|
||||||
|
<span class="type-name">{{ item.description || item.type }}</span>
|
||||||
|
<span class="type-count">{{ item.count }} {{ $t('errorStats.times') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="type-bar">
|
||||||
|
<div
|
||||||
|
class="type-bar-fill"
|
||||||
|
:style="{ width: getBarWidth(item.count) + '%', backgroundColor: getBarColor(index) }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="type-percentage">{{ getPercentage(item.count) }}%</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="errorTypeStats.length === 0" :description="$t('errorStats.noErrorData')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近错误列表 -->
|
||||||
|
<div class="recent-errors-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>{{ $t('errorStats.recentErrors') }}</h3>
|
||||||
|
<el-button type="primary" size="small" @click="loadRecentErrors">{{ $t('errorStats.refresh') }}</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="recentErrors" v-loading="tableLoading" stripe>
|
||||||
|
<el-table-column prop="createdAt" :label="$t('errorStats.time')" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.createdAt) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="errorType" :label="$t('errorStats.errorType')" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getTagType(row.errorType)">{{ row.errorType }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="username" :label="$t('errorStats.user')" width="120" />
|
||||||
|
<el-table-column prop="taskId" :label="$t('errorStats.taskId')" width="200" />
|
||||||
|
<el-table-column prop="errorMessage" :label="$t('errorStats.errorMessage')" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:total="totalErrors"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@size-change="loadErrorLogs"
|
||||||
|
@current-change="loadErrorLogs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Grid,
|
||||||
|
User,
|
||||||
|
ShoppingCart,
|
||||||
|
Document,
|
||||||
|
Setting,
|
||||||
|
ArrowDown,
|
||||||
|
Warning,
|
||||||
|
Clock,
|
||||||
|
Calendar
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import request from '@/api/request'
|
||||||
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableLoading = ref(false)
|
||||||
|
const selectedDays = ref(7)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const totalErrors = ref(0)
|
||||||
|
const onlineUsers = ref('0')
|
||||||
|
const systemUptime = ref('--')
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const statistics = ref({})
|
||||||
|
const errorTypeStats = ref([])
|
||||||
|
const recentErrors = ref([])
|
||||||
|
const errorTypes = ref({})
|
||||||
|
|
||||||
|
// 计算总数
|
||||||
|
const totalCount = computed(() => {
|
||||||
|
return errorTypeStats.value.reduce((sum, item) => sum + item.count, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载统计数据
|
||||||
|
const loadStatistics = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await request.get('/admin/error-logs/statistics', {
|
||||||
|
params: { days: selectedDays.value }
|
||||||
|
})
|
||||||
|
if (res.data.success) {
|
||||||
|
const data = res.data.data || {}
|
||||||
|
statistics.value = {
|
||||||
|
totalErrors: data.totalErrors || 0,
|
||||||
|
todayErrors: data.todayErrors || 0,
|
||||||
|
weekErrors: data.weekErrors || 0
|
||||||
|
}
|
||||||
|
// 处理错误类型统计 - 后端返回的是 errorsByType
|
||||||
|
if (data.errorsByType) {
|
||||||
|
errorTypeStats.value = Object.entries(data.errorsByType).map(([type, count]) => ({
|
||||||
|
type,
|
||||||
|
description: errorTypes.value[type] || type,
|
||||||
|
count
|
||||||
|
})).sort((a, b) => b.count - a.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载统计失败:', error)
|
||||||
|
ElMessage.error('加载统计数据失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载错误类型定义
|
||||||
|
const loadErrorTypes = async () => {
|
||||||
|
try {
|
||||||
|
const res = await request.get('/admin/error-logs/types')
|
||||||
|
if (res.data.success) {
|
||||||
|
errorTypes.value = res.data.data || {}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载错误类型失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载错误日志列表
|
||||||
|
const loadErrorLogs = async () => {
|
||||||
|
tableLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await request.get('/admin/error-logs', {
|
||||||
|
params: {
|
||||||
|
page: currentPage.value - 1,
|
||||||
|
size: pageSize.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.data.success) {
|
||||||
|
recentErrors.value = res.data.data || []
|
||||||
|
totalErrors.value = res.data.totalElements || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载错误日志失败:', error)
|
||||||
|
ElMessage.error('加载错误日志失败')
|
||||||
|
} finally {
|
||||||
|
tableLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载最近错误
|
||||||
|
const loadRecentErrors = async () => {
|
||||||
|
tableLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await request.get('/admin/error-logs/recent', {
|
||||||
|
params: { limit: 20 }
|
||||||
|
})
|
||||||
|
if (res.data.success) {
|
||||||
|
recentErrors.value = res.data.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载最近错误失败:', error)
|
||||||
|
} finally {
|
||||||
|
tableLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取进度条宽度
|
||||||
|
const getBarWidth = (count) => {
|
||||||
|
if (totalCount.value === 0) return 0
|
||||||
|
return Math.min((count / totalCount.value) * 100, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取百分比
|
||||||
|
const getPercentage = (count) => {
|
||||||
|
if (totalCount.value === 0) return 0
|
||||||
|
return ((count / totalCount.value) * 100).toFixed(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取进度条颜色
|
||||||
|
const getBarColor = (index) => {
|
||||||
|
const colors = ['#f56c6c', '#e6a23c', '#409eff', '#67c23a', '#909399', '#b88230', '#8e44ad', '#16a085']
|
||||||
|
return colors[index % colors.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return date.toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标签类型
|
||||||
|
const getTagType = (errorType) => {
|
||||||
|
const typeMap = {
|
||||||
|
'API_ERROR': 'danger',
|
||||||
|
'TASK_FAILED': 'warning',
|
||||||
|
'PAYMENT_ERROR': 'danger',
|
||||||
|
'AUTH_ERROR': 'info',
|
||||||
|
'SYSTEM_ERROR': 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[errorType] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goToDashboard = () => router.push('/admin/dashboard')
|
||||||
|
const goToMembers = () => router.push('/member-management')
|
||||||
|
const goToOrders = () => router.push('/admin/orders')
|
||||||
|
const goToAPI = () => router.push('/api-management')
|
||||||
|
const goToTasks = () => router.push('/generate-task-record')
|
||||||
|
const goToSettings = () => router.push('/system-settings')
|
||||||
|
|
||||||
|
const handleUserCommand = (command) => {
|
||||||
|
if (command === 'exitAdmin') {
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取系统统计数据
|
||||||
|
const fetchSystemStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/online-stats', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success) {
|
||||||
|
onlineUsers.value = data.todayVisitors || 0
|
||||||
|
systemUptime.value = data.uptime || '--'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取系统统计失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
fetchSystemStats()
|
||||||
|
await loadErrorTypes()
|
||||||
|
await loadStatistics()
|
||||||
|
await loadErrorLogs()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.error-statistics-page {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 0;
|
||||||
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-width: 180px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: rgba(64, 158, 255, 0.15);
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
background: #f9fafb;
|
||||||
|
margin-top: auto;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer .highlight {
|
||||||
|
color: #409EFF;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-users,
|
||||||
|
.system-uptime {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f8f9fa;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部搜索栏 */
|
||||||
|
.top-header {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-down {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容包装器 */
|
||||||
|
.content-wrapper {
|
||||||
|
padding: 24px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片 */
|
||||||
|
.stats-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.error {
|
||||||
|
background: rgba(245, 108, 108, 0.2);
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.today {
|
||||||
|
background: rgba(230, 162, 60, 0.2);
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.week {
|
||||||
|
background: rgba(64, 158, 255, 0.2);
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.users {
|
||||||
|
background: rgba(103, 194, 58, 0.2);
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表区域 */
|
||||||
|
.charts-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-select {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误类型列表 */
|
||||||
|
.error-type-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-type-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-info {
|
||||||
|
width: 200px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-name {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-count {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 8px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-percentage {
|
||||||
|
width: 60px;
|
||||||
|
text-align: right;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 最近错误区域 */
|
||||||
|
.recent-errors-section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus 样式覆盖 */
|
||||||
|
:deep(.el-table) {
|
||||||
|
background: transparent;
|
||||||
|
--el-table-bg-color: transparent;
|
||||||
|
--el-table-tr-bg-color: transparent;
|
||||||
|
--el-table-header-bg-color: #f5f5f5;
|
||||||
|
--el-table-row-hover-bg-color: #f5f5f5;
|
||||||
|
--el-table-border-color: #e5e7eb;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table th) {
|
||||||
|
background: #f5f5f5 !important;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select) {
|
||||||
|
--el-select-input-focus-border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-pagination) {
|
||||||
|
--el-pagination-bg-color: transparent;
|
||||||
|
--el-pagination-text-color: #9ca3af;
|
||||||
|
--el-pagination-button-disabled-bg-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.stats-cards {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1308
demo/frontend/src/views/GenerateTaskRecord.vue
Normal file
10
demo/frontend/src/views/HelloWorld.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Hello World!</h1>
|
||||||
|
<p>Vue is working!</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
console.log('Vue component loaded!')
|
||||||
|
</script>
|
||||||
893
demo/frontend/src/views/ImageToVideo.vue
Normal file
@@ -0,0 +1,893 @@
|
|||||||
|
<template>
|
||||||
|
<div class="image-to-video-page">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<div class="nav-item" @click="goToProfile">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>个人主页</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToSubscription">
|
||||||
|
<el-icon><Compass /></el-icon>
|
||||||
|
<span>会员订阅</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToMyWorks">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>我的作品</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-divider"></div>
|
||||||
|
<div class="nav-item" @click="goToTextToVideo">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>文生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item active">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>图生视频</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item storyboard-item" @click="goToStoryboardVideo">
|
||||||
|
<el-icon><Film /></el-icon>
|
||||||
|
<span>分镜视频</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 顶部用户信息卡片 -->
|
||||||
|
<div class="user-info-card">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="avatar-image" />
|
||||||
|
</div>
|
||||||
|
<div class="user-details">
|
||||||
|
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
|
||||||
|
<div class="profile-prompt">还没有设置个人简介,点击填写</div>
|
||||||
|
<div class="user-id">ID 2994509784706419</div>
|
||||||
|
</div>
|
||||||
|
<div class="edit-profile-btn">
|
||||||
|
<el-button type="primary">编辑资料</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已发布作品区域 -->
|
||||||
|
<div class="published-works">
|
||||||
|
<div class="works-tabs">
|
||||||
|
<div class="tab active">已发布</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="works-grid">
|
||||||
|
<div class="work-item" v-for="(work, index) in publishedWorks" :key="work.taskId || work.id" @click="openDetail(work)">
|
||||||
|
<div class="work-thumbnail">
|
||||||
|
<!-- 优先使用首帧作为封面,如果没有则使用视频 -->
|
||||||
|
<img
|
||||||
|
v-if="work.firstFrameUrl"
|
||||||
|
v-lazy:loading="work.firstFrameUrl"
|
||||||
|
:alt="work.title || work.prompt"
|
||||||
|
class="work-image-thumbnail"
|
||||||
|
/>
|
||||||
|
<video
|
||||||
|
v-else-if="work.resultUrl"
|
||||||
|
:src="work.resultUrl"
|
||||||
|
class="work-video-thumbnail"
|
||||||
|
preload="metadata"
|
||||||
|
muted
|
||||||
|
@mouseenter="playPreview($event)"
|
||||||
|
@mouseleave="pausePreview($event)"
|
||||||
|
></video>
|
||||||
|
<!-- 如果都没有,显示占位符 -->
|
||||||
|
<div v-else class="work-placeholder">
|
||||||
|
<div class="play-icon">▶</div>
|
||||||
|
</div>
|
||||||
|
<!-- 鼠标悬停时显示的做同款按钮 -->
|
||||||
|
<div class="hover-create-btn" @click.stop="goToCreate(work)">
|
||||||
|
<el-button type="primary" size="small" round>
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
做同款
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-info">
|
||||||
|
<div class="work-title">{{ work.prompt || work.title || '图生视频' }}</div>
|
||||||
|
<div class="work-meta">{{ work.date || '未知日期' }} · {{ work.taskId || work.id }} · {{ formatSize(work) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="work-actions" v-if="index === 0">
|
||||||
|
<el-button type="primary" class="create-similar-btn" @click.stop="goToCreate(work)">做同款</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="work-director" v-else>
|
||||||
|
<span>DIRECTED BY VANNOCENT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 作品详情模态框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
:title="selectedItem?.title"
|
||||||
|
width="60%"
|
||||||
|
class="detail-dialog"
|
||||||
|
:modal="true"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:close-on-press-escape="true"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="detail-content">
|
||||||
|
<div class="detail-left">
|
||||||
|
<div class="video-player">
|
||||||
|
<img :src="selectedItem?.cover" :alt="selectedItem?.title" class="video-thumbnail" />
|
||||||
|
<div class="play-overlay">
|
||||||
|
<div class="play-button">▶</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-right">
|
||||||
|
<div class="metadata-section">
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">作品 ID</span>
|
||||||
|
<span class="value">{{ selectedItem?.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">文件大小</span>
|
||||||
|
<span class="value">{{ selectedItem?.size }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">创建时间</span>
|
||||||
|
<span class="value">{{ selectedItem?.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">分类</span>
|
||||||
|
<span class="value">{{ selectedItem?.category }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description-section">
|
||||||
|
<h3 class="section-title">描述</h3>
|
||||||
|
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-section">
|
||||||
|
<button class="create-similar-btn" @click="createSimilar">
|
||||||
|
做同款
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElIcon, ElButton, ElTag, ElMessage, ElDialog } from 'element-plus'
|
||||||
|
import { User, Document, VideoPlay, Picture, Film, Compass } from '@element-plus/icons-vue'
|
||||||
|
import { imageToVideoApi } from '@/api/imageToVideo'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 模态框状态
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
|
||||||
|
// 已发布作品数据
|
||||||
|
const publishedWorks = ref([])
|
||||||
|
|
||||||
|
// 导航函数
|
||||||
|
const goToProfile = () => {
|
||||||
|
router.push('/profile')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToSubscription = () => {
|
||||||
|
router.push('/subscription')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToMyWorks = () => {
|
||||||
|
router.push('/works')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTextToVideo = () => {
|
||||||
|
router.push('/text-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToStoryboardVideo = () => {
|
||||||
|
router.push('/storyboard-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToCreate = (work) => {
|
||||||
|
// 跳转到图生视频创作页面
|
||||||
|
router.push('/image-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模态框相关函数
|
||||||
|
const openDetail = (work) => {
|
||||||
|
selectedItem.value = work
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
selectedItem.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDescription = (item) => {
|
||||||
|
if (!item) return ''
|
||||||
|
return `这是一个${item.category}作品,展现了"What Does it Mean To You"的主题。作品通过AI技术生成,具有独特的视觉风格和创意表达。`
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSimilar = () => {
|
||||||
|
// 关闭模态框并跳转到创作页面
|
||||||
|
handleClose()
|
||||||
|
router.push('/image-to-video/create')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatSize = (work) => {
|
||||||
|
if (work.size) return work.size
|
||||||
|
return '未知大小'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放预览(鼠标悬停时)
|
||||||
|
const playPreview = (event) => {
|
||||||
|
const video = event.target
|
||||||
|
if (video && video.tagName === 'VIDEO') {
|
||||||
|
video.currentTime = 0
|
||||||
|
video.play().catch(() => {
|
||||||
|
// 忽略自动播放失败
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂停预览(鼠标离开时)
|
||||||
|
const pausePreview = (event) => {
|
||||||
|
const video = event.target
|
||||||
|
if (video && video.tagName === 'VIDEO') {
|
||||||
|
video.pause()
|
||||||
|
video.currentTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载任务列表
|
||||||
|
const loadTasks = async () => {
|
||||||
|
try {
|
||||||
|
const response = await imageToVideoApi.getTasks(0, 20)
|
||||||
|
if (response.data && response.data.success && response.data.data) {
|
||||||
|
// 只显示已完成的任务
|
||||||
|
publishedWorks.value = response.data.data
|
||||||
|
.filter(task => task.status === 'COMPLETED' && (task.resultUrl || task.firstFrameUrl))
|
||||||
|
.map(task => ({
|
||||||
|
taskId: task.taskId,
|
||||||
|
prompt: task.prompt,
|
||||||
|
resultUrl: task.resultUrl,
|
||||||
|
firstFrameUrl: task.firstFrameUrl,
|
||||||
|
status: task.status,
|
||||||
|
createdAt: task.createdAt,
|
||||||
|
id: task.taskId,
|
||||||
|
title: task.prompt || '图生视频',
|
||||||
|
text: task.prompt || '图生视频',
|
||||||
|
category: '图生视频',
|
||||||
|
createTime: task.createdAt ? new Date(task.createdAt).toLocaleString('zh-CN') : '',
|
||||||
|
date: task.createdAt ? new Date(task.createdAt).toLocaleDateString('zh-CN') : '未知日期'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载任务列表失败:', error)
|
||||||
|
ElMessage.error('加载任务列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 页面初始化时加载任务列表
|
||||||
|
loadTasks()
|
||||||
|
})
|
||||||
|
</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;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 280px !important;
|
||||||
|
background: #000000 !important;
|
||||||
|
padding: 24px 0 !important;
|
||||||
|
border-right: 1px solid #1a1a1a !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
z-index: 100 !important;
|
||||||
|
display: block !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 0 24px 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 14px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-size: 15px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #333;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sora-tag {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分镜视频特殊样式 */
|
||||||
|
.storyboard-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyboard-item .sora-tag {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
padding: 2px 8px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3) !important;
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息卡片 */
|
||||||
|
.user-info-card {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid #333;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-prompt {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-profile-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已发布作品区域 */
|
||||||
|
.published-works {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 0;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-thumbnail img,
|
||||||
|
.work-thumbnail video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-image-thumbnail {
|
||||||
|
display: block;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-video-thumbnail {
|
||||||
|
display: block;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标悬停时显示的做同款按钮 */
|
||||||
|
.hover-create-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-thumbnail:hover .hover-create-btn {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-create-btn .el-button {
|
||||||
|
background: rgba(64, 158, 255, 0.9);
|
||||||
|
border: none;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-create-btn .el-button:hover {
|
||||||
|
background: rgba(64, 158, 255, 1);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* work-overlay / overlay-text 样式已移除(不再使用) */
|
||||||
|
|
||||||
|
.work-info {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9ca3af;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-actions {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-item:hover .work-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-director {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-director span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.image-to-video-page {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.works-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模态框样式 */
|
||||||
|
:deep(.detail-dialog .el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333 !important;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__title) {
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__headerbtn) {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-dialog .el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局覆盖Element Plus默认样式 */
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border: 1px solid #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
height: 50vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-left {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player:hover .play-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d1d5db;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3061
demo/frontend/src/views/ImageToVideoCreate.vue
Normal file
635
demo/frontend/src/views/ImageToVideoDetail.vue
Normal file
@@ -0,0 +1,635 @@
|
|||||||
|
<template>
|
||||||
|
<div class="video-detail-page">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/backgrounds/logo.png" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<el-icon class="action-icon"><User /></el-icon>
|
||||||
|
<el-icon class="action-icon"><Setting /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>文件</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>图片</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
<span>视频</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- 视频播放器区域 -->
|
||||||
|
<div class="video-section">
|
||||||
|
<div class="video-player">
|
||||||
|
<video
|
||||||
|
ref="videoRef"
|
||||||
|
:src="videoData.videoUrl"
|
||||||
|
@click="togglePlay"
|
||||||
|
@timeupdate="updateTime"
|
||||||
|
@loadedmetadata="onLoadedMetadata"
|
||||||
|
>
|
||||||
|
您的浏览器不支持视频播放
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<!-- 视频控制栏 -->
|
||||||
|
<div class="video-controls" v-show="showControls">
|
||||||
|
<div class="controls-left">
|
||||||
|
<el-button circle size="small" @click="togglePlay">
|
||||||
|
<el-icon><VideoPlay v-if="!isPlaying" /><VideoPause v-else /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<span class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls-right">
|
||||||
|
<el-button circle size="small" @click="toggleFullscreen">
|
||||||
|
<el-icon><FullScreen /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频操作按钮 -->
|
||||||
|
<div class="video-actions">
|
||||||
|
<el-tooltip content="下载" placement="bottom">
|
||||||
|
<el-button circle size="small" @click="downloadVideo">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除" placement="bottom">
|
||||||
|
<el-button circle size="small" @click="deleteVideo">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧详情区域 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="detail-header">
|
||||||
|
<h3>图片详情</h3>
|
||||||
|
<p class="subtitle">参考生图</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-content">
|
||||||
|
<div class="input-section">
|
||||||
|
<el-input
|
||||||
|
v-model="detailInput"
|
||||||
|
placeholder="输入详情"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="thumbnails">
|
||||||
|
<div class="thumbnail" v-for="(thumb, index) in thumbnails" :key="index">
|
||||||
|
<img :src="thumb" :alt="`缩略图${index + 1}`" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description">
|
||||||
|
<h4>描述</h4>
|
||||||
|
<p>{{ videoData.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">创建时间</span>
|
||||||
|
<span class="value">{{ videoData.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">视频 ID</span>
|
||||||
|
<span class="value">{{ videoData.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">时长</span>
|
||||||
|
<span class="value">{{ videoData.duration }}s</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">清晰度</span>
|
||||||
|
<span class="value">{{ videoData.resolution }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">宽高比</span>
|
||||||
|
<span class="value">{{ videoData.aspectRatio }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-button">
|
||||||
|
<el-button type="primary" size="large" @click="makeSimilar">
|
||||||
|
做同款
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 滚动指示器 -->
|
||||||
|
<div class="scroll-indicators">
|
||||||
|
<el-icon class="scroll-arrow up"><ArrowUp /></el-icon>
|
||||||
|
<el-icon class="scroll-arrow down"><ArrowDown /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { imageToVideoApi } from '@/api/imageToVideo'
|
||||||
|
import {
|
||||||
|
User, Setting, Document, User as Picture, User as VideoPlay, User as VideoPause,
|
||||||
|
User as FullScreen, User as Download, User as Delete, User as ArrowUp, User as ArrowDown
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const videoRef = ref(null)
|
||||||
|
|
||||||
|
// 视频播放状态
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
const currentTime = ref(0)
|
||||||
|
const duration = ref(0)
|
||||||
|
const showControls = ref(true)
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// 详情数据
|
||||||
|
const detailInput = ref('')
|
||||||
|
const videoData = ref({
|
||||||
|
id: '',
|
||||||
|
videoUrl: '',
|
||||||
|
description: '',
|
||||||
|
createTime: '',
|
||||||
|
duration: 5,
|
||||||
|
resolution: '1080p',
|
||||||
|
aspectRatio: '16:9',
|
||||||
|
status: 'PROCESSING',
|
||||||
|
progress: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const thumbnails = ref([
|
||||||
|
'/images/backgrounds/welcome.jpg',
|
||||||
|
'/images/backgrounds/welcome.jpg'
|
||||||
|
])
|
||||||
|
|
||||||
|
// 视频控制方法
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (!videoRef.value) return
|
||||||
|
|
||||||
|
if (isPlaying.value) {
|
||||||
|
videoRef.value.pause()
|
||||||
|
} else {
|
||||||
|
videoRef.value.play()
|
||||||
|
}
|
||||||
|
isPlaying.value = !isPlaying.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTime = () => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
currentTime.value = videoRef.value.currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLoadedMetadata = () => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
duration.value = videoRef.value.duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time) => {
|
||||||
|
const minutes = Math.floor(time / 60)
|
||||||
|
const seconds = Math.floor(time % 60)
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
if (!videoRef.value) return
|
||||||
|
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
document.exitFullscreen()
|
||||||
|
} else {
|
||||||
|
videoRef.value.requestFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作按钮方法
|
||||||
|
const downloadVideo = () => {
|
||||||
|
ElMessage.success('开始下载视频')
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteVideo = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除这个视频吗?', '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
ElMessage.success('视频已删除')
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeSimilar = () => {
|
||||||
|
ElMessage.info('做同款功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动隐藏控制栏
|
||||||
|
let controlsTimer = null
|
||||||
|
const resetControlsTimer = () => {
|
||||||
|
clearTimeout(controlsTimer)
|
||||||
|
showControls.value = true
|
||||||
|
controlsTimer = setTimeout(() => {
|
||||||
|
showControls.value = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载任务详情
|
||||||
|
const loadTaskDetail = async () => {
|
||||||
|
const taskId = route.params.taskId
|
||||||
|
if (!taskId) {
|
||||||
|
ElMessage.error('任务ID不存在')
|
||||||
|
router.push('/image-to-video')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await imageToVideoApi.getTaskDetail(taskId)
|
||||||
|
|
||||||
|
if (response.data && response.data.success && response.data.data) {
|
||||||
|
const task = response.data.data
|
||||||
|
videoData.value = {
|
||||||
|
id: task.taskId || taskId,
|
||||||
|
videoUrl: task.resultUrl || '',
|
||||||
|
description: task.prompt || '',
|
||||||
|
createTime: task.createdAt || new Date().toISOString(),
|
||||||
|
duration: task.duration || 5,
|
||||||
|
resolution: task.hdMode ? '1080p' : '720p',
|
||||||
|
aspectRatio: task.aspectRatio || '16:9',
|
||||||
|
status: task.status || 'PROCESSING',
|
||||||
|
progress: task.progress || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果任务已完成且有视频URL,设置视频源
|
||||||
|
if (task.status === 'COMPLETED' && task.resultUrl) {
|
||||||
|
videoData.value.videoUrl = task.resultUrl
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data?.message || '获取任务详情失败')
|
||||||
|
router.push('/image-to-video')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载任务详情失败:', error)
|
||||||
|
ElMessage.error('加载任务详情失败,请稍后重试')
|
||||||
|
router.push('/image-to-video')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 加载任务详情
|
||||||
|
loadTaskDetail()
|
||||||
|
|
||||||
|
// 监听鼠标移动来显示/隐藏控制栏
|
||||||
|
document.addEventListener('mousemove', resetControlsTimer)
|
||||||
|
resetControlsTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimeout(controlsTimer)
|
||||||
|
document.removeEventListener('mousemove', resetControlsTimer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-detail-page {
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航栏 */
|
||||||
|
.top-bar {
|
||||||
|
height: 60px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 30px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 60px;
|
||||||
|
width: 200px;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-right: 1px solid #333;
|
||||||
|
padding: 20px 0;
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 20px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .el-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
margin-left: 200px;
|
||||||
|
margin-top: 60px;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频播放器区域 */
|
||||||
|
.video-section {
|
||||||
|
flex: 2;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions .el-button {
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions .el-button:hover {
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
border-color: rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧详情区域 */
|
||||||
|
.detail-section {
|
||||||
|
flex: 1;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-left: 1px solid #333;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnails {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description p {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button .el-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-indicators {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-arrow {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-arrow:hover {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section {
|
||||||
|
flex: none;
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
flex: none;
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
737
demo/frontend/src/views/Login.vue
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/images/backgrounds/logo.svg?v=2" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录卡片 -->
|
||||||
|
<div class="login-card">
|
||||||
|
<!-- 欢迎标题 -->
|
||||||
|
<div class="welcome-title">
|
||||||
|
<span class="welcome-text">欢迎来到</span>
|
||||||
|
<span class="brand-name">Vionow</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录方式切换 -->
|
||||||
|
<div class="login-tabs">
|
||||||
|
<!-- 邮箱登录盒子 -->
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: loginType === 'email' }"
|
||||||
|
@click="loginType = 'email'"
|
||||||
|
>
|
||||||
|
<svg width="105" height="30" viewBox="0 0 105 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.598 6.112V25.638H11.076V24.13H4.316V25.638H1.768V6.112H6.344V2.55H8.996V6.112H13.598ZM4.316 21.712H6.344V16.122H4.316V21.712ZM8.996 21.712H11.076V16.122H8.996V21.712ZM4.316 13.73H6.344V8.556H4.316V13.73ZM8.996 8.556V13.73H11.076V8.556H8.996ZM15.34 3.772H24.232V5.748C23.452 8.4 22.62 10.818 21.736 13.054C23.556 15.654 24.466 17.76 24.492 19.398C24.492 20.958 24.154 22.024 23.504 22.596C22.802 23.22 21.398 23.532 19.318 23.532L18.512 20.802C19.474 20.906 20.28 20.984 20.956 20.984C21.372 20.958 21.658 20.828 21.814 20.62C21.918 20.464 21.97 20.048 21.996 19.398C21.97 17.786 20.982 15.68 19.032 13.054C19.864 11.156 20.67 8.868 21.45 6.164H17.914V26.73H15.34V3.772ZM27.716 12.326H31.616V9.83H34.346V12.326H37.57V14.926H34.346V15.446C35.542 16.538 36.79 17.734 38.038 19.06L36.53 21.348C35.698 20.048 34.97 18.93 34.346 18.046V26.756H31.616V18.202C30.654 20.23 29.432 22.102 27.924 23.818L26.754 20.776C28.756 19.06 30.238 17.11 31.226 14.926H27.716V12.326ZM49.4 10.48V26.73H46.774V25.82H41.106V26.73H38.48V10.48H49.4ZM41.106 23.428H46.774V21.244H41.106V23.428ZM41.106 18.956H46.774V17.058H41.106V18.956ZM41.106 14.77H46.774V12.872H41.106V14.77ZM31.538 6.762C30.966 7.75 30.316 8.634 29.614 9.466L27.248 7.958C28.782 6.294 29.874 4.5 30.498 2.576L33.124 3.148C32.968 3.564 32.838 3.98 32.682 4.37H38.974V6.762H35.568C36.088 7.49 36.504 8.192 36.842 8.842L34.346 9.778C33.878 8.738 33.306 7.724 32.682 6.762H31.538ZM42.64 6.762C42.12 7.828 41.574 8.816 40.95 9.726L38.636 8.244C39.962 6.424 40.898 4.474 41.444 2.446L44.018 3.018C43.862 3.486 43.732 3.928 43.602 4.37H50.83V6.762H46.67C47.19 7.49 47.632 8.192 47.97 8.842L45.578 9.752C45.11 8.712 44.538 7.724 43.862 6.762H42.64ZM57.538 13.522H72.566V20.282H57.538V13.522ZM69.836 17.89V15.888H60.268V17.89H69.836ZM60.45 20.438C61.282 21.296 62.036 22.31 62.712 23.454H67.626C68.354 22.466 68.978 21.426 69.524 20.36L72.046 21.27C71.578 22.05 71.084 22.778 70.564 23.454H76.232V26.028H53.716V23.454H59.826C59.28 22.726 58.63 22.05 57.902 21.4L60.45 20.438ZM56.368 6.112C57.564 6.996 58.63 7.854 59.514 8.686C60.424 7.802 61.152 6.866 61.724 5.878H55.822V3.382H64.792V5.41C64.194 6.918 63.388 8.27 62.374 9.466H68.822C67.262 7.75 66.014 5.904 65.104 3.876L67.366 2.628C67.782 3.59 68.276 4.5 68.848 5.358C69.758 4.63 70.538 3.85 71.188 3.018L73.086 4.708C72.306 5.644 71.37 6.528 70.304 7.308C70.72 7.828 71.188 8.296 71.708 8.764C72.8 7.932 73.71 6.996 74.49 5.982L76.388 7.646C75.608 8.634 74.672 9.544 73.632 10.35C74.776 11.182 76.024 11.962 77.428 12.664L75.634 14.744C73.606 13.6 71.838 12.352 70.33 10.974V11.936H60.788V11.104C59.124 12.56 57.07 13.782 54.626 14.796L52.962 12.664C54.782 11.962 56.316 11.156 57.616 10.246C56.758 9.466 55.744 8.66 54.548 7.828L56.368 6.112ZM82.368 3.408H97.864V11.806H102.258V14.276H98.228L100.282 15.966C98.93 17.63 97.396 18.956 95.68 19.892C97.604 21.296 99.84 22.518 102.388 23.61L101.01 26.002C97.37 24.286 94.432 22.232 92.196 19.814V23.974C92.196 25.794 91.39 26.73 89.804 26.73H86.762L86.164 24.182C87.1 24.286 88.01 24.364 88.894 24.364C89.284 24.364 89.492 24 89.492 23.324V19.918C86.944 22.154 83.928 24.156 80.47 25.95L79.378 23.428C83.278 21.66 86.632 19.58 89.492 17.136V14.276H79.768V11.806H95.108V10.012H83.252V7.672H95.108V5.852H82.368V3.408ZM83.018 14.666C84.526 15.706 85.8 16.746 86.84 17.786L85.072 19.554C84.162 18.566 82.888 17.526 81.224 16.382L83.018 14.666ZM98.176 14.276H92.196V16.824C92.69 17.37 93.236 17.89 93.834 18.41C95.498 17.422 96.954 16.044 98.176 14.276Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分隔线 -->
|
||||||
|
<div class="tab-divider"></div>
|
||||||
|
|
||||||
|
<!-- 账号登录盒子 -->
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: loginType === 'password' }"
|
||||||
|
@click="loginType = 'password'"
|
||||||
|
>
|
||||||
|
<svg width="105" height="30" viewBox="0 0 105 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.362 3.46V20.126H7.996V5.956H3.212V20.126H0.768V3.46H10.362ZM4.434 7.49H6.722V16.018C6.644 18.93 6.202 21.27 5.37 23.012C4.564 24.65 3.264 25.898 1.47 26.782L0.014 24.494C1.704 23.636 2.848 22.57 3.472 21.322C4.044 19.944 4.356 18.176 4.434 16.018V7.49ZM7.684 20.724C8.984 22.05 10.076 23.35 10.986 24.624L9.036 26.574C8.308 25.274 7.294 23.896 5.942 22.414L7.684 20.724ZM17.044 15.732H15.328V23.298C16.342 22.882 17.33 22.362 18.318 21.738L18.786 24.156C17.122 25.196 15.224 26.028 13.092 26.704L11.974 24.286C12.442 24.052 12.676 23.688 12.676 23.22V15.732H11.064V13.132H12.676V2.68H15.328V13.132H23.622V15.732H19.384C20.528 19.45 22.14 22.336 24.168 24.364L22.322 26.47C20.008 24.026 18.24 20.438 17.044 15.732ZM21.23 3.746L23.31 5.41C21.516 8.322 19.436 10.506 17.044 11.91L15.588 9.83C17.772 8.478 19.67 6.45 21.23 3.746ZM29.966 3.356H46.034V11.026H29.966V3.356ZM43.278 8.608V5.8H32.722V8.608H43.278ZM31.76 15.16H26.118V12.534H49.856V15.16H34.516L33.814 17.422H46.372C46.164 21.894 45.748 24.546 45.072 25.378C44.396 26.184 43.122 26.6 41.198 26.6C39.924 26.6 38.78 26.522 37.766 26.392L36.882 23.922C38.286 24.052 39.508 24.13 40.6 24.13C41.874 24.13 42.654 23.87 42.966 23.402C43.252 22.908 43.46 21.738 43.59 19.866H30.564L31.76 15.16ZM56.538 13.522H71.566V20.282H56.538V13.522ZM68.836 17.89V15.888H59.268V17.89H68.836ZM59.45 20.438C60.282 21.296 61.036 22.31 61.712 23.454H66.626C67.354 22.466 67.978 21.426 68.524 20.36L71.046 21.27C70.578 22.05 70.084 22.778 69.564 23.454H75.232V26.028H52.716V23.454H58.826C58.28 22.726 57.63 22.05 56.902 21.4L59.45 20.438ZM55.368 6.112C56.564 6.996 57.63 7.854 58.514 8.686C59.424 7.802 60.152 6.866 60.724 5.878H54.822V3.382H63.792V5.41C63.194 6.918 62.388 8.27 61.374 9.466H67.822C66.262 7.75 65.014 5.904 64.104 3.876L66.366 2.628C66.782 3.59 67.276 4.5 67.848 5.358C68.758 4.63 69.538 3.85 70.188 3.018L72.086 4.708C71.306 5.644 70.37 6.528 69.304 7.308C69.72 7.828 70.188 8.296 70.708 8.764C71.8 7.932 72.71 6.996 73.49 5.982L75.388 7.646C74.608 8.634 73.672 9.544 72.632 10.35C73.776 11.182 75.024 11.962 76.428 12.664L74.634 14.744C72.606 13.6 70.838 12.352 69.33 10.974V11.936H59.788V11.104C58.124 12.56 56.07 13.782 53.626 14.796L51.962 12.664C53.782 11.962 55.316 11.156 56.616 10.246C55.758 9.466 54.744 8.66 53.548 7.828L55.368 6.112ZM81.368 3.408H96.864V11.806H101.258V14.276H97.228L99.282 15.966C97.93 17.63 96.396 18.956 94.68 19.892C96.604 21.296 98.84 22.518 101.388 23.61L100.01 26.002C96.37 24.286 93.432 22.232 91.196 19.814V23.974C91.196 25.794 90.39 26.73 88.804 26.73H85.762L85.164 24.182C86.1 24.286 87.01 24.364 87.894 24.364C88.284 24.364 88.492 24 88.492 23.324V19.918C85.944 22.154 82.928 24.156 79.47 25.95L78.378 23.428C82.278 21.66 85.632 19.58 88.492 17.136V14.276H78.768V11.806H94.108V10.012H82.252V7.672H94.108V5.852H81.368V3.408ZM82.018 14.666C83.526 15.706 84.8 16.746 85.84 17.786L84.072 19.554C83.162 18.566 81.888 17.526 80.224 16.382L82.018 14.666ZM97.176 14.276H91.196V16.824C91.69 17.37 92.236 17.89 92.834 18.41C94.498 17.422 95.954 16.044 97.176 14.276Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<div class="login-form">
|
||||||
|
<!-- 邮箱登录 / 密码登录 表单 -->
|
||||||
|
<div class="email-login">
|
||||||
|
<!-- 邮箱输入 -->
|
||||||
|
<div class="email-input-group">
|
||||||
|
<el-input
|
||||||
|
ref="emailInput"
|
||||||
|
v-model="loginForm.email"
|
||||||
|
placeholder="请输入邮箱地址"
|
||||||
|
class="email-input"
|
||||||
|
type="email"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
<div class="input-error" v-if="errors.email">{{ errors.email }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 验证码输入(仅验证码登录显示) -->
|
||||||
|
<div class="code-input-wrapper" v-if="loginType === 'email'">
|
||||||
|
<el-input
|
||||||
|
ref="codeInput"
|
||||||
|
v-model="loginForm.code"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
class="code-input"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
@input="filterCodeSpaces"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<span
|
||||||
|
class="get-code-text"
|
||||||
|
:class="{ disabled: countdown > 0 || !isEmailValid }"
|
||||||
|
@click="getEmailCode"
|
||||||
|
>
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<div class="input-error" v-if="errors.code">{{ errors.code }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密码输入(仅密码登录显示) -->
|
||||||
|
<div v-if="loginType === 'password'" class="password-input-group">
|
||||||
|
<el-input
|
||||||
|
ref="passwordInput"
|
||||||
|
v-model="loginForm.password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
class="password-input"
|
||||||
|
show-password
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
<div class="input-error" v-if="errors.password">{{ errors.password }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="login-button"
|
||||||
|
:loading="userStore.loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
{{ userStore.loading ? '登录中...' : '登录/注册' }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<!-- 协议文字 -->
|
||||||
|
<p class="agreement-text">
|
||||||
|
登录即表示您同意遵守<router-link to="/user-agreement" class="agreement-link">用户协议</router-link>和<router-link to="/privacy-policy" class="agreement-link">隐私政策</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { loginWithEmail, login, sendEmailCode, setDevEmailCode, getCurrentUser } from '@/api/auth'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const countdown = ref(0)
|
||||||
|
let countdownTimer = null
|
||||||
|
|
||||||
|
const loginType = ref('email') // 'email' or 'password'
|
||||||
|
|
||||||
|
const loginForm = reactive({
|
||||||
|
email: '',
|
||||||
|
code: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// inline errors for fields and server
|
||||||
|
const errors = reactive({
|
||||||
|
email: '',
|
||||||
|
code: '',
|
||||||
|
password: '',
|
||||||
|
server: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// input refs for focusing
|
||||||
|
const emailInput = ref(null)
|
||||||
|
const codeInput = ref(null)
|
||||||
|
const passwordInput = ref(null)
|
||||||
|
|
||||||
|
const isEmailValid = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(loginForm.email))
|
||||||
|
const isCodeValid = computed(() => /^\d{6}$/.test(loginForm.code))
|
||||||
|
const isPasswordValid = computed(() => loginForm.password && loginForm.password.length >= 6)
|
||||||
|
|
||||||
|
const isFormValid = computed(() => {
|
||||||
|
if (loginType.value === 'email') {
|
||||||
|
return isEmailValid.value && isCodeValid.value
|
||||||
|
}
|
||||||
|
return isEmailValid.value && isPasswordValid.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
const clearForm = async () => {
|
||||||
|
loginForm.email = ''
|
||||||
|
loginForm.code = ''
|
||||||
|
loginForm.password = ''
|
||||||
|
errors.email = errors.code = errors.password = errors.server = ''
|
||||||
|
// 重置倒计时
|
||||||
|
if (countdownTimer) {
|
||||||
|
clearInterval(countdownTimer)
|
||||||
|
countdownTimer = null
|
||||||
|
}
|
||||||
|
countdown.value = 0
|
||||||
|
// focus email input after clearing
|
||||||
|
await nextTick()
|
||||||
|
emailInput.value && emailInput.value.focus && emailInput.value.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤验证码中的空格
|
||||||
|
const filterCodeSpaces = () => {
|
||||||
|
loginForm.code = loginForm.code.replace(/\s/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时从URL参数读取邮箱
|
||||||
|
onMounted(() => {
|
||||||
|
// 从URL参数中读取邮箱
|
||||||
|
if (route.query.email) {
|
||||||
|
loginForm.email = route.query.email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 获取邮箱验证码
|
||||||
|
const getEmailCode = async () => {
|
||||||
|
errors.email = ''
|
||||||
|
errors.code = ''
|
||||||
|
errors.password = ''
|
||||||
|
errors.server = ''
|
||||||
|
|
||||||
|
if (!loginForm.email) {
|
||||||
|
errors.email = '请输入邮箱地址'
|
||||||
|
emailInput.value && emailInput.value.focus && emailInput.value.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmailValid.value) {
|
||||||
|
errors.email = '请输入正确的邮箱地址'
|
||||||
|
emailInput.value && emailInput.value.focus && emailInput.value.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用后端API发送邮箱验证码
|
||||||
|
const response = await sendEmailCode(loginForm.email)
|
||||||
|
|
||||||
|
if (response.data && response.data.success) {
|
||||||
|
ElMessage.success('验证码已发送到您的邮箱')
|
||||||
|
// 开始倒计时
|
||||||
|
startCountdown()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data?.message || '发送失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送验证码失败:', error)
|
||||||
|
// 开发环境:显示真实验证码
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// 生成6位随机验证码(与后端逻辑一致)
|
||||||
|
const randomCode = Array.from({length: 6}, () => Math.floor(Math.random() * 10)).join('')
|
||||||
|
|
||||||
|
// 开发模式:将验证码同步到后端
|
||||||
|
try {
|
||||||
|
await setDevEmailCode(loginForm.email, randomCode)
|
||||||
|
} catch (syncError) {
|
||||||
|
console.warn('同步验证码到后端失败:', syncError)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📨 验证码已发送到: ${loginForm.email}`)
|
||||||
|
ElMessage.success(`验证码已发送到您的邮箱`)
|
||||||
|
startCountdown()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(error.response?.data?.message || '网络错误,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdown.value = 60
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(countdownTimer)
|
||||||
|
countdownTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
// 基本邮箱校验
|
||||||
|
if (!loginForm.email) {
|
||||||
|
ElMessage.warning('请输入邮箱地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(loginForm.email)) {
|
||||||
|
ElMessage.warning('请输入正确的邮箱地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录前清除旧的token,避免过期token影响新的登录请求
|
||||||
|
const oldToken = localStorage.getItem('token')
|
||||||
|
console.log('登录前旧token:', oldToken ? oldToken.substring(0, 50) + '...' : 'null')
|
||||||
|
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
userStore.token = null
|
||||||
|
userStore.user = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('开始登录... 登录方式:', loginType)
|
||||||
|
|
||||||
|
let response = null
|
||||||
|
|
||||||
|
if (loginType.value === 'email') {
|
||||||
|
// 验证码登录
|
||||||
|
if (!loginForm.code) {
|
||||||
|
errors.code = '请输入验证码'
|
||||||
|
codeInput.value && codeInput.value.focus && codeInput.value.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isCodeValid.value) {
|
||||||
|
errors.code = '验证码格式不正确,请输入6位数字'
|
||||||
|
codeInput.value && codeInput.value.focus && codeInput.value.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await loginWithEmail({ email: loginForm.email, code: loginForm.code })
|
||||||
|
} else {
|
||||||
|
// 密码登录
|
||||||
|
if (!loginForm.password) {
|
||||||
|
errors.password = '请输入密码'
|
||||||
|
passwordInput.value && passwordInput.value.focus && passwordInput.value.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isPasswordValid.value) {
|
||||||
|
errors.password = '密码至少 6 位'
|
||||||
|
passwordInput.value && passwordInput.value.focus && passwordInput.value.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await login({ email: loginForm.email, password: loginForm.password })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response && response.data && response.data.success) {
|
||||||
|
// 保存用户信息和token
|
||||||
|
const loginUser = response.data.data.user
|
||||||
|
const loginToken = response.data.data.token
|
||||||
|
const needsPasswordChange = response.data.data.needsPasswordChange // 后端直接返回是否需要修改密码
|
||||||
|
|
||||||
|
console.log('登录成功,新token:', loginToken ? loginToken.substring(0, 50) + '...' : 'null')
|
||||||
|
console.log('新旧token是否相同:', oldToken === loginToken)
|
||||||
|
|
||||||
|
localStorage.setItem('token', loginToken)
|
||||||
|
localStorage.setItem('user', JSON.stringify(loginUser))
|
||||||
|
userStore.user = loginUser
|
||||||
|
userStore.token = loginToken
|
||||||
|
|
||||||
|
// 重置初始化状态,确保路由守卫使用新 token
|
||||||
|
userStore.resetInitialized()
|
||||||
|
|
||||||
|
// 验证保存是否成功
|
||||||
|
const savedToken = localStorage.getItem('token')
|
||||||
|
console.log('验证localStorage中的token:', savedToken ? savedToken.substring(0, 50) + '...' : 'null')
|
||||||
|
console.log('token保存成功:', savedToken === loginToken)
|
||||||
|
|
||||||
|
// 根据后端返回的标记设置是否需要修改密码
|
||||||
|
if (needsPasswordChange) {
|
||||||
|
localStorage.setItem('needSetPassword', '1')
|
||||||
|
console.log('新用户首次登录,需要设置密码')
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('needSetPassword')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('登录成功,用户信息:', userStore.user, '需要设置密码:', needsPasswordChange)
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
|
||||||
|
// 等待一下确保状态更新
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200))
|
||||||
|
|
||||||
|
// 如果需要设置密码,跳转到设置密码页面
|
||||||
|
const needSetPassword = localStorage.getItem('needSetPassword') === '1'
|
||||||
|
const redirectPath = needSetPassword ? '/set-password' : (route.query.redirect || '/profile')
|
||||||
|
console.log('准备跳转到:', redirectPath, '需要设置密码:', needSetPassword)
|
||||||
|
|
||||||
|
// 强制刷新页面,确保所有组件使用新token
|
||||||
|
window.location.href = redirectPath
|
||||||
|
return
|
||||||
|
|
||||||
|
console.log('路由跳转完成')
|
||||||
|
} else {
|
||||||
|
const msg = response?.data?.message || '登录失败'
|
||||||
|
errors.server = msg
|
||||||
|
ElMessage.error(msg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
const msg = error.response?.data?.message || '登录失败,请重试'
|
||||||
|
errors.server = msg
|
||||||
|
ElMessage.error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a0e1a url('/images/backgrounds/login_bg.png') center/cover no-repeat;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左上角Logo */
|
||||||
|
.logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
left: 30px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录卡片 */
|
||||||
|
.login-card {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 145px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 773px;
|
||||||
|
height: 796px;
|
||||||
|
max-width: 90vw;
|
||||||
|
background: rgba(121, 121, 121, 0.1);
|
||||||
|
backdrop-filter: blur(50px);
|
||||||
|
-webkit-backdrop-filter: blur(50px);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 80px 82px;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 欢迎标题 */
|
||||||
|
.welcome-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
color: #00D4FF;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录方式切换 */
|
||||||
|
.login-tabs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
user-select: none;
|
||||||
|
color: #9EA9B6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 24px;
|
||||||
|
background: #9EA9B6;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录表单 */
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 邮箱输入组 */
|
||||||
|
.email-input-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 快捷输入标签 */
|
||||||
|
.quick-email-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-tag {
|
||||||
|
background: rgba(64, 158, 255, 0.1);
|
||||||
|
border: 1px solid rgba(64, 158, 255, 0.25);
|
||||||
|
color: #66B1FF;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
user-select: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-tag:hover {
|
||||||
|
background: rgba(64, 158, 255, 0.2);
|
||||||
|
border-color: rgba(64, 158, 255, 0.4);
|
||||||
|
color: #409EFF;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-input :deep(.el-input__wrapper) {
|
||||||
|
background: rgba(217, 217, 217, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 80px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-input :deep(.el-input__wrapper:hover) {
|
||||||
|
background: rgba(217, 217, 217, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-input :deep(.el-input__wrapper.is-focus) {
|
||||||
|
background: rgba(217, 217, 217, 0.3);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-input :deep(.el-input__inner) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
background: transparent;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-input :deep(.el-input__inner::placeholder) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 验证码输入组 */
|
||||||
|
.code-input-wrapper {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input :deep(.el-input__wrapper) {
|
||||||
|
background: rgba(217, 217, 217, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 80px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input :deep(.el-input__wrapper:hover) {
|
||||||
|
background: rgba(217, 217, 217, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input :deep(.el-input__wrapper.is-focus) {
|
||||||
|
background: rgba(217, 217, 217, 0.3);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input :deep(.el-input__inner) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
background: transparent;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input :deep(.el-input__inner::placeholder) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-code-text {
|
||||||
|
color: #00D4FF;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-code-text:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-code-text.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input-group {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__wrapper) {
|
||||||
|
background: rgba(217, 217, 217, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 80px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__wrapper:hover) {
|
||||||
|
background: rgba(217, 217, 217, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__wrapper.is-focus) {
|
||||||
|
background: rgba(217, 217, 217, 0.3);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__inner) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
background: transparent;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input :deep(.el-input__inner::placeholder) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-error {
|
||||||
|
color: #ff7875;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮 */
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
background: #0DC0FF;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 30px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
background: #4DD4FF;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 协议文字 */
|
||||||
|
.agreement-text {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 20px 0 0 0;
|
||||||
|
line-height: 30px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-link {
|
||||||
|
color: #00D4FF;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-link:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.login-card {
|
||||||
|
right: 5%;
|
||||||
|
width: 450px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.login-card {
|
||||||
|
position: relative;
|
||||||
|
top: auto;
|
||||||
|
left: auto;
|
||||||
|
transform: none;
|
||||||
|
margin: 50px auto;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
position: relative;
|
||||||
|
top: auto;
|
||||||
|
left: auto;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-card {
|
||||||
|
padding: 40px 25px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input-group {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1265
demo/frontend/src/views/MemberManagement.vue
Normal file
2886
demo/frontend/src/views/MyWorks.vue
Normal file
381
demo/frontend/src/views/OrderCreate.vue
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-create">
|
||||||
|
<el-page-header @back="$router.go(-1)" content="创建订单">
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
创建订单
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
|
||||||
|
<el-card class="form-card">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="100px"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
|
<el-form-item label="订单类型" prop="orderType">
|
||||||
|
<el-select v-model="form.orderType" placeholder="请选择订单类型">
|
||||||
|
<el-option label="AI服务" value="SERVICE" />
|
||||||
|
<el-option label="AI订阅" value="SUBSCRIPTION" />
|
||||||
|
<el-option label="数字商品" value="DIGITAL" />
|
||||||
|
<el-option label="虚拟商品" value="VIRTUAL" />
|
||||||
|
</el-select>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>选择您要购买的虚拟商品类型:AI服务(如AI绘画、AI写作)、AI订阅(按月/年付费)、数字商品(如软件、电子书)、虚拟商品(如游戏道具、虚拟货币)</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="货币" prop="currency">
|
||||||
|
<el-select v-model="form.currency" placeholder="请选择货币">
|
||||||
|
<el-option label="人民币 (CNY)" value="CNY" />
|
||||||
|
<el-option label="美元 (USD)" value="USD" />
|
||||||
|
<el-option label="欧元 (EUR)" value="EUR" />
|
||||||
|
</el-select>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>选择支付货币类型,系统会根据您选择的货币进行计费和结算</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="订单描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="form.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请详细描述您的订单需求,如:需要AI绘画服务,风格为动漫风格,尺寸为1024x1024像素"
|
||||||
|
/>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>详细描述您的订单需求,包括服务要求、特殊需求等,这将帮助服务提供方更好地理解您的需求</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="联系邮箱" prop="contactEmail">
|
||||||
|
<el-input
|
||||||
|
v-model="form.contactEmail"
|
||||||
|
type="email"
|
||||||
|
placeholder="请输入联系邮箱(用于接收虚拟商品)"
|
||||||
|
/>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>必填项!虚拟商品将通过此邮箱发送给您,请确保邮箱地址正确且可正常接收邮件</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="联系电话" prop="contactPhone">
|
||||||
|
<el-input
|
||||||
|
v-model="form.contactPhone"
|
||||||
|
placeholder="请输入联系电话(可选)"
|
||||||
|
/>
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>可选填写,用于紧急情况联系或重要通知,建议填写以便服务提供方在需要时联系您</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 虚拟商品不需要收货地址 -->
|
||||||
|
<template v-if="isPhysicalOrder">
|
||||||
|
<el-form-item label="收货地址" prop="shippingAddress">
|
||||||
|
<el-input
|
||||||
|
v-model="form.shippingAddress"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入收货地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="账单地址" prop="billingAddress">
|
||||||
|
<el-input
|
||||||
|
v-model="form.billingAddress"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入账单地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 订单项 -->
|
||||||
|
<el-form-item label="虚拟商品">
|
||||||
|
<div class="field-description">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>添加您要购买的虚拟商品,包括商品名称、单价和数量。支持添加多个商品。</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-items">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in form.orderItems"
|
||||||
|
:key="index"
|
||||||
|
class="order-item"
|
||||||
|
>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-input
|
||||||
|
v-model="item.productName"
|
||||||
|
placeholder="商品名称(如:AI绘画服务、AI写作助手、AI翻译服务等)"
|
||||||
|
@input="calculateSubtotal(index)"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-input-number
|
||||||
|
v-model="item.unitPrice"
|
||||||
|
:precision="2"
|
||||||
|
:min="0"
|
||||||
|
placeholder="单价"
|
||||||
|
@change="calculateSubtotal(index)"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-input-number
|
||||||
|
v-model="item.quantity"
|
||||||
|
:min="1"
|
||||||
|
placeholder="数量"
|
||||||
|
@change="calculateSubtotal(index)"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-input
|
||||||
|
v-model="item.subtotal"
|
||||||
|
readonly
|
||||||
|
placeholder="小计"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
circle
|
||||||
|
@click="removeItem(index)"
|
||||||
|
v-if="form.orderItems.length > 1"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="Plus"
|
||||||
|
@click="addItem"
|
||||||
|
class="add-item-btn"
|
||||||
|
>
|
||||||
|
添加虚拟商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<div class="total-amount">
|
||||||
|
<span class="total-label">订单总计:</span>
|
||||||
|
<span class="total-value">{{ form.currency }} {{ totalAmount }}</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useOrderStore } from '@/stores/orders'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Plus, Delete, Check, User } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const orderStore = useOrderStore()
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
orderType: 'SERVICE',
|
||||||
|
currency: 'CNY',
|
||||||
|
description: '',
|
||||||
|
contactEmail: '',
|
||||||
|
contactPhone: '',
|
||||||
|
shippingAddress: '',
|
||||||
|
billingAddress: '',
|
||||||
|
orderItems: [
|
||||||
|
{
|
||||||
|
productName: '',
|
||||||
|
unitPrice: 0,
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
orderType: [
|
||||||
|
{ required: true, message: '请选择订单类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
currency: [
|
||||||
|
{ required: true, message: '请选择货币', trigger: 'change' }
|
||||||
|
],
|
||||||
|
contactEmail: [
|
||||||
|
{ required: true, message: '请输入联系邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为实体商品订单
|
||||||
|
const isPhysicalOrder = computed(() => {
|
||||||
|
return form.orderType === 'PHYSICAL'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算总金额
|
||||||
|
const totalAmount = computed(() => {
|
||||||
|
return form.orderItems.reduce((total, item) => {
|
||||||
|
return total + parseFloat(item.subtotal || 0)
|
||||||
|
}, 0).toFixed(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算小计
|
||||||
|
const calculateSubtotal = (index) => {
|
||||||
|
const item = form.orderItems[index]
|
||||||
|
if (item.unitPrice && item.quantity) {
|
||||||
|
item.subtotal = parseFloat((item.unitPrice * item.quantity).toFixed(2))
|
||||||
|
} else {
|
||||||
|
item.subtotal = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加商品项
|
||||||
|
const addItem = () => {
|
||||||
|
form.orderItems.push({
|
||||||
|
productName: '',
|
||||||
|
unitPrice: 0,
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除商品项
|
||||||
|
const removeItem = (index) => {
|
||||||
|
if (form.orderItems.length > 1) {
|
||||||
|
form.orderItems.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await formRef.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
// 验证订单项
|
||||||
|
const validItems = form.orderItems.filter(item =>
|
||||||
|
item.productName && item.unitPrice > 0 && item.quantity > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if (validItems.length === 0) {
|
||||||
|
ElMessage.error('请至少添加一个有效虚拟商品')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 准备提交数据
|
||||||
|
const orderData = {
|
||||||
|
orderType: form.orderType,
|
||||||
|
currency: form.currency,
|
||||||
|
description: form.description,
|
||||||
|
contactEmail: form.contactEmail,
|
||||||
|
contactPhone: form.contactPhone,
|
||||||
|
shippingAddress: form.shippingAddress,
|
||||||
|
billingAddress: form.billingAddress,
|
||||||
|
totalAmount: parseFloat(totalAmount.value),
|
||||||
|
status: 'PENDING', // 新创建的订单状态为待支付
|
||||||
|
orderItems: validItems.map(item => ({
|
||||||
|
productName: item.productName,
|
||||||
|
unitPrice: parseFloat(item.unitPrice),
|
||||||
|
quantity: parseInt(item.quantity),
|
||||||
|
subtotal: parseFloat(item.subtotal)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await orderStore.createNewOrder(orderData)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
ElMessage.success('虚拟商品订单创建成功!商品将发送到您的邮箱')
|
||||||
|
router.push('/admin/orders')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '创建订单失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create order error:', error)
|
||||||
|
ElMessage.error('创建订单失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-create {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-items {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-btn {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-amount {
|
||||||
|
text-align: right;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-label {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
border: 1px solid #b3d8ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #409eff;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-description .el-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-top: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-description span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
220
demo/frontend/src/views/OrderDetail.vue
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-detail">
|
||||||
|
<el-page-header @back="$router.go(-1)" content="订单详情">
|
||||||
|
<template #extra>
|
||||||
|
<el-button-group>
|
||||||
|
<el-button v-if="order?.canPay()" type="success" @click="handlePayment">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
立即支付
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="order?.canCancel()" type="danger" @click="handleCancel">
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
取消订单
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
|
||||||
|
<el-card v-if="order" class="order-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="order-header">
|
||||||
|
<h3>订单信息</h3>
|
||||||
|
<el-tag :type="getStatusType(order.status)">
|
||||||
|
{{ getStatusText(order.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="订单号">{{ order.orderNumber }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单类型">{{ getOrderTypeText(order.orderType) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单金额">
|
||||||
|
<span class="amount">{{ order.currency }} {{ order.totalAmount }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(order.createdAt) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系邮箱" v-if="order.contactEmail">{{ order.contactEmail }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系电话" v-if="order.contactPhone">{{ order.contactPhone }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div v-if="order.description" class="order-description">
|
||||||
|
<h4>订单描述</h4>
|
||||||
|
<p>{{ order.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="order.orderItems && order.orderItems.length > 0" class="order-items">
|
||||||
|
<h4>订单商品</h4>
|
||||||
|
<el-table :data="order.orderItems" border>
|
||||||
|
<el-table-column prop="productName" label="商品名称" />
|
||||||
|
<el-table-column prop="unitPrice" label="单价" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ order.currency }} {{ row.unitPrice }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="quantity" label="数量" width="80" />
|
||||||
|
<el-table-column prop="subtotal" label="小计" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ order.currency }} {{ row.subtotal }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-empty v-else description="订单不存在" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useOrderStore } from '@/stores/orders'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
User as ArrowLeft,
|
||||||
|
User as ArrowRight,
|
||||||
|
Check,
|
||||||
|
Close,
|
||||||
|
User as Warning,
|
||||||
|
User as Info,
|
||||||
|
Money,
|
||||||
|
CreditCard,
|
||||||
|
Wallet,
|
||||||
|
User as Truck,
|
||||||
|
User as Package,
|
||||||
|
User,
|
||||||
|
User as Calendar,
|
||||||
|
User as Clock
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const orderStore = useOrderStore()
|
||||||
|
|
||||||
|
const order = ref(null)
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': 'warning',
|
||||||
|
'CONFIRMED': 'info',
|
||||||
|
'PAID': 'primary',
|
||||||
|
'PROCESSING': '',
|
||||||
|
'SHIPPED': 'success',
|
||||||
|
'DELIVERED': 'success',
|
||||||
|
'COMPLETED': 'success',
|
||||||
|
'CANCELLED': 'danger',
|
||||||
|
'REFUNDED': 'info'
|
||||||
|
}
|
||||||
|
return statusMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': '待支付',
|
||||||
|
'CONFIRMED': '已确认',
|
||||||
|
'PAID': '已支付',
|
||||||
|
'PROCESSING': '处理中',
|
||||||
|
'SHIPPED': '已发货',
|
||||||
|
'DELIVERED': '已送达',
|
||||||
|
'COMPLETED': '已完成',
|
||||||
|
'CANCELLED': '已取消',
|
||||||
|
'REFUNDED': '已退款'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单类型文本
|
||||||
|
const getOrderTypeText = (orderType) => {
|
||||||
|
const typeMap = {
|
||||||
|
'PRODUCT': '商品订单',
|
||||||
|
'SERVICE': '服务订单',
|
||||||
|
'SUBSCRIPTION': '订阅订单',
|
||||||
|
'DIGITAL': '数字商品',
|
||||||
|
'PHYSICAL': '实体商品'
|
||||||
|
}
|
||||||
|
return typeMap[orderType] || orderType
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理支付
|
||||||
|
const handlePayment = () => {
|
||||||
|
ElMessage.info('支付功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
ElMessage.info('取消订单功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const orderId = route.params.id
|
||||||
|
if (orderId) {
|
||||||
|
const response = await orderStore.fetchOrderById(orderId)
|
||||||
|
if (response.success) {
|
||||||
|
order.value = orderStore.currentOrder
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '获取订单详情失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-detail {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #E6A23C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-description,
|
||||||
|
.order-items {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-description h4,
|
||||||
|
.order-items h4 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
233
demo/frontend/src/views/PaymentCreate.vue
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<div class="payment-create">
|
||||||
|
<el-page-header @back="$router.go(-1)" content="创建支付">
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
创建支付
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
|
||||||
|
<el-card class="form-card">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="100px"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
|
<el-form-item label="订单号" prop="orderId">
|
||||||
|
<el-input
|
||||||
|
v-model="form.orderId"
|
||||||
|
placeholder="请输入订单号"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="支付金额" prop="amount">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.amount"
|
||||||
|
:precision="2"
|
||||||
|
:min="0.01"
|
||||||
|
placeholder="请输入支付金额"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="货币" prop="currency">
|
||||||
|
<el-select v-model="form.currency" placeholder="请选择货币">
|
||||||
|
<el-option label="人民币 (CNY)" value="CNY" />
|
||||||
|
<el-option label="美元 (USD)" value="USD" />
|
||||||
|
<el-option label="欧元 (EUR)" value="EUR" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="支付方式" prop="paymentMethod">
|
||||||
|
<el-radio-group v-model="form.paymentMethod">
|
||||||
|
<el-radio value="ALIPAY">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
支付宝
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="支付描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="form.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入支付描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="回调URL" prop="callbackUrl">
|
||||||
|
<el-input
|
||||||
|
v-model="form.callbackUrl"
|
||||||
|
placeholder="请输入回调URL(可选)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="返回URL" prop="returnUrl">
|
||||||
|
<el-input
|
||||||
|
v-model="form.returnUrl"
|
||||||
|
placeholder="请输入返回URL(可选)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 支付方式说明 -->
|
||||||
|
<el-card class="info-card">
|
||||||
|
<template #header>
|
||||||
|
<h4>支付方式说明</h4>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<div class="payment-method-info">
|
||||||
|
<el-icon size="32" color="#1677FF"><CreditCard /></el-icon>
|
||||||
|
<h5>支付宝</h5>
|
||||||
|
<p>支持支付宝扫码支付和网页支付</p>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Money,
|
||||||
|
CreditCard,
|
||||||
|
Wallet,
|
||||||
|
User as Plus,
|
||||||
|
Check,
|
||||||
|
Close,
|
||||||
|
User as ArrowLeft,
|
||||||
|
User as ArrowRight,
|
||||||
|
User as Upload,
|
||||||
|
User as Download
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
orderId: '',
|
||||||
|
amount: 0,
|
||||||
|
currency: 'CNY',
|
||||||
|
paymentMethod: 'ALIPAY',
|
||||||
|
description: '',
|
||||||
|
callbackUrl: '',
|
||||||
|
returnUrl: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
orderId: [
|
||||||
|
{ required: true, message: '请输入订单号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
amount: [
|
||||||
|
{ required: true, message: '请输入支付金额', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 0.01, message: '支付金额必须大于0', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
currency: [
|
||||||
|
{ required: true, message: '请选择货币', trigger: 'change' }
|
||||||
|
],
|
||||||
|
paymentMethod: [
|
||||||
|
{ required: true, message: '请选择支付方式', trigger: 'change' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await formRef.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 调用真实支付API
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
ElMessage.success('支付创建成功')
|
||||||
|
router.push('/payments')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create payment error:', error)
|
||||||
|
ElMessage.error('创建支付失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-create {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-info {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-info:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-info h5 {
|
||||||
|
margin: 12px 0 8px 0;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-info p {
|
||||||
|
margin: 0;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.payment-method-info {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
747
demo/frontend/src/views/Payments.vue
Normal file
@@ -0,0 +1,747 @@
|
|||||||
|
<template>
|
||||||
|
<div class="payments">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
支付记录
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选和搜索 -->
|
||||||
|
<el-card class="filter-card">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-select
|
||||||
|
v-model="filters.status"
|
||||||
|
placeholder="选择支付状态"
|
||||||
|
clearable
|
||||||
|
@change="handleFilterChange"
|
||||||
|
>
|
||||||
|
<el-option label="全部状态" value="" />
|
||||||
|
<el-option label="待支付" value="PENDING" />
|
||||||
|
<el-option label="支付成功" value="SUCCESS" />
|
||||||
|
<el-option label="支付失败" value="FAILED" />
|
||||||
|
<el-option label="已取消" value="CANCELLED" />
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.search"
|
||||||
|
placeholder="搜索订单号"
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="8">
|
||||||
|
<el-button @click="resetFilters">重置筛选</el-button>
|
||||||
|
<el-button type="success" @click="showSubscriptionDialog('standard')">标准版订阅</el-button>
|
||||||
|
<el-button type="warning" @click="showSubscriptionDialog('professional')">专业版订阅</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 支付记录列表 -->
|
||||||
|
<el-card class="payments-card">
|
||||||
|
<el-table
|
||||||
|
:data="payments"
|
||||||
|
v-loading="loading"
|
||||||
|
empty-text="暂无支付记录"
|
||||||
|
>
|
||||||
|
<el-table-column prop="orderId" label="订单号" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<router-link :to="`/orders/${row.orderId}`" class="order-link">
|
||||||
|
{{ row.orderId }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="amount" label="金额" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="amount">{{ row.currency }} {{ row.amount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="paymentMethod" label="支付方式" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getPaymentMethodType(row.paymentMethod)">
|
||||||
|
{{ getPaymentMethodText(row.paymentMethod) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="description" label="描述" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="description">{{ row.description }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createdAt" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.createdAt) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="paidAt" label="支付时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.paidAt ? formatDate(row.paidAt) : '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
@click="viewPaymentDetail(row)"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'PENDING'"
|
||||||
|
size="small"
|
||||||
|
type="success"
|
||||||
|
@click="testPaymentComplete(row)"
|
||||||
|
>
|
||||||
|
测试完成
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="handleDeletePayment(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 支付详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
title="支付详情"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<div v-if="currentPayment">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="订单号">{{ currentPayment.orderId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付方式">
|
||||||
|
<el-tag :type="getPaymentMethodType(currentPayment.paymentMethod)">
|
||||||
|
{{ getPaymentMethodText(currentPayment.paymentMethod) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付金额">
|
||||||
|
<span class="amount">{{ currentPayment.currency }} {{ currentPayment.amount }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付状态">
|
||||||
|
<el-tag :type="getStatusType(currentPayment.status)">
|
||||||
|
{{ getStatusText(currentPayment.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="外部交易ID" v-if="currentPayment.externalTransactionId">
|
||||||
|
{{ currentPayment.externalTransactionId }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(currentPayment.createdAt) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付时间" v-if="currentPayment.paidAt">
|
||||||
|
{{ formatDate(currentPayment.paidAt) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ formatDate(currentPayment.updatedAt) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div v-if="currentPayment.description" class="payment-description">
|
||||||
|
<h4>支付描述</h4>
|
||||||
|
<p>{{ currentPayment.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 订阅对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="subscriptionDialogVisible"
|
||||||
|
:title="subscriptionDialogTitle"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<div class="subscription-info">
|
||||||
|
<h3>{{ subscriptionInfo.title }}</h3>
|
||||||
|
<p class="price">${{ subscriptionInfo.price }}</p>
|
||||||
|
<p class="description">{{ subscriptionInfo.description }}</p>
|
||||||
|
<div class="benefits">
|
||||||
|
<h4>包含功能:</h4>
|
||||||
|
<ul>
|
||||||
|
<li v-for="benefit in subscriptionInfo.benefits" :key="benefit">
|
||||||
|
{{ benefit }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="points-info">
|
||||||
|
<el-tag type="success">支付完成后可获得 {{ subscriptionInfo.points }} 积分</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="payment-method">
|
||||||
|
<h4>选择支付方式:</h4>
|
||||||
|
<el-radio-group v-model="selectedPaymentMethod" @change="updatePrice">
|
||||||
|
<el-radio label="ALIPAY">支付宝</el-radio>
|
||||||
|
<el-radio label="PAYPAL">PayPal</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<div class="converted-price" v-if="convertedPrice">
|
||||||
|
<p>支付金额:<span class="price-display">{{ convertedPrice }}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="subscriptionDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="createSubscription" :loading="subscriptionLoading">
|
||||||
|
立即订阅
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Money,
|
||||||
|
CreditCard,
|
||||||
|
Wallet,
|
||||||
|
User as Search,
|
||||||
|
User as Filter,
|
||||||
|
User as Plus,
|
||||||
|
User as View,
|
||||||
|
User as Refresh,
|
||||||
|
User as Download,
|
||||||
|
User as Upload,
|
||||||
|
Setting,
|
||||||
|
Check,
|
||||||
|
Close,
|
||||||
|
User as Warning
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { getPayments, testPaymentComplete as testPaymentCompleteApi, createTestPayment, deletePayment } from '@/api/payments'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const loading = ref(false)
|
||||||
|
const payments = ref([])
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const filters = reactive({
|
||||||
|
status: '',
|
||||||
|
search: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 支付详情对话框
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentPayment = ref(null)
|
||||||
|
|
||||||
|
|
||||||
|
// 订阅对话框
|
||||||
|
const subscriptionDialogVisible = ref(false)
|
||||||
|
const subscriptionLoading = ref(false)
|
||||||
|
const subscriptionType = ref('')
|
||||||
|
const selectedPaymentMethod = ref('ALIPAY')
|
||||||
|
const convertedPrice = ref('')
|
||||||
|
const exchangeRate = ref(7.2) // 美元对人民币汇率,可以根据实际情况调整
|
||||||
|
const subscriptionInfo = reactive({
|
||||||
|
title: '',
|
||||||
|
price: 0,
|
||||||
|
description: '',
|
||||||
|
benefits: [],
|
||||||
|
points: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const subscriptionDialogTitle = computed(() => {
|
||||||
|
return subscriptionType.value === 'standard' ? '标准版订阅' : '专业版订阅'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取支付方式类型
|
||||||
|
const getPaymentMethodType = (method) => {
|
||||||
|
const methodMap = {
|
||||||
|
'ALIPAY': 'primary',
|
||||||
|
'PAYPAL': 'success',
|
||||||
|
'WECHAT': 'success',
|
||||||
|
'UNIONPAY': 'warning'
|
||||||
|
}
|
||||||
|
return methodMap[method] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取支付方式文本
|
||||||
|
const getPaymentMethodText = (method) => {
|
||||||
|
const methodMap = {
|
||||||
|
'ALIPAY': '支付宝',
|
||||||
|
'PAYPAL': 'PayPal',
|
||||||
|
'WECHAT': '微信支付',
|
||||||
|
'UNIONPAY': '银联支付'
|
||||||
|
}
|
||||||
|
return methodMap[method] || method
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': 'warning',
|
||||||
|
'SUCCESS': 'success',
|
||||||
|
'FAILED': 'danger',
|
||||||
|
'CANCELLED': 'info'
|
||||||
|
}
|
||||||
|
return statusMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': '待支付',
|
||||||
|
'SUCCESS': '支付成功',
|
||||||
|
'FAILED': '支付失败',
|
||||||
|
'CANCELLED': '已取消'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取支付记录列表
|
||||||
|
const fetchPayments = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const response = await getPayments({
|
||||||
|
page: pagination.page - 1,
|
||||||
|
size: pagination.size,
|
||||||
|
status: filters.status,
|
||||||
|
search: filters.search
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
payments.value = response.data
|
||||||
|
pagination.total = response.total || response.data.length
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '获取支付记录失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch payments error:', error)
|
||||||
|
ElMessage.error('获取支付记录失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选变化
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
const resetFilters = () => {
|
||||||
|
filters.status = ''
|
||||||
|
filters.search = ''
|
||||||
|
pagination.page = 1
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size
|
||||||
|
pagination.page = 1
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
fetchPayments()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看支付详情
|
||||||
|
const viewPaymentDetail = (payment) => {
|
||||||
|
currentPayment.value = payment
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新价格显示
|
||||||
|
const updatePrice = () => {
|
||||||
|
if (selectedPaymentMethod.value === 'ALIPAY') {
|
||||||
|
// 支付宝使用人民币
|
||||||
|
const cnyPrice = (subscriptionInfo.price * exchangeRate.value).toFixed(2)
|
||||||
|
convertedPrice.value = `¥${cnyPrice}`
|
||||||
|
} else if (selectedPaymentMethod.value === 'PAYPAL') {
|
||||||
|
// PayPal使用美元
|
||||||
|
convertedPrice.value = `$${subscriptionInfo.price}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示订阅对话框
|
||||||
|
const showSubscriptionDialog = (type) => {
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
ElMessage.warning('请先登录后再订阅')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionType.value = type
|
||||||
|
|
||||||
|
if (type === 'standard') {
|
||||||
|
subscriptionInfo.title = '标准版订阅'
|
||||||
|
subscriptionInfo.price = 59
|
||||||
|
subscriptionInfo.description = '适合个人用户的基础功能订阅'
|
||||||
|
subscriptionInfo.benefits = [
|
||||||
|
'基础AI功能使用',
|
||||||
|
'每月100次API调用',
|
||||||
|
'邮件技术支持',
|
||||||
|
'基础模板库访问'
|
||||||
|
]
|
||||||
|
subscriptionInfo.points = 200
|
||||||
|
} else if (type === 'professional') {
|
||||||
|
subscriptionInfo.title = '专业版订阅'
|
||||||
|
subscriptionInfo.price = 259
|
||||||
|
subscriptionInfo.description = '适合企业用户的高级功能订阅'
|
||||||
|
subscriptionInfo.benefits = [
|
||||||
|
'高级AI功能使用',
|
||||||
|
'每月1000次API调用',
|
||||||
|
'优先技术支持',
|
||||||
|
'完整模板库访问',
|
||||||
|
'API接口集成',
|
||||||
|
'数据分析报告'
|
||||||
|
]
|
||||||
|
subscriptionInfo.points = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionDialogVisible.value = true
|
||||||
|
// 初始化价格显示
|
||||||
|
updatePrice()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订阅支付
|
||||||
|
const createSubscription = async () => {
|
||||||
|
try {
|
||||||
|
subscriptionLoading.value = true
|
||||||
|
|
||||||
|
// 根据支付方式确定实际支付金额
|
||||||
|
let actualAmount
|
||||||
|
if (selectedPaymentMethod.value === 'ALIPAY') {
|
||||||
|
// 支付宝使用人民币
|
||||||
|
actualAmount = (subscriptionInfo.price * exchangeRate.value).toFixed(2)
|
||||||
|
} else {
|
||||||
|
// PayPal使用美元
|
||||||
|
actualAmount = subscriptionInfo.price.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await createTestPayment({
|
||||||
|
amount: actualAmount,
|
||||||
|
method: selectedPaymentMethod.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
ElMessage.success(`${subscriptionInfo.title}支付记录创建成功`)
|
||||||
|
|
||||||
|
// 根据支付方式调用相应的支付接口
|
||||||
|
if (selectedPaymentMethod.value === 'ALIPAY') {
|
||||||
|
try {
|
||||||
|
const alipayResponse = await createAlipayPayment({
|
||||||
|
paymentId: response.data.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (alipayResponse.success) {
|
||||||
|
// 跳转到支付宝支付页面
|
||||||
|
window.open(alipayResponse.data.paymentUrl, '_blank')
|
||||||
|
ElMessage.success('正在跳转到支付宝支付页面')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(alipayResponse.message || '创建支付宝支付失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建支付宝支付失败:', error)
|
||||||
|
ElMessage.error('创建支付宝支付失败')
|
||||||
|
}
|
||||||
|
} else if (selectedPaymentMethod.value === 'PAYPAL') {
|
||||||
|
try {
|
||||||
|
const paypalResponse = await createPayPalPayment({
|
||||||
|
paymentId: response.data.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (paypalResponse.success) {
|
||||||
|
// 跳转到PayPal支付页面
|
||||||
|
window.open(paypalResponse.data.paymentUrl, '_blank')
|
||||||
|
ElMessage.success('正在跳转到PayPal支付页面')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(paypalResponse.message || '创建PayPal支付失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建PayPal支付失败:', error)
|
||||||
|
ElMessage.error('创建PayPal支付失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionDialogVisible.value = false
|
||||||
|
// 刷新支付记录列表
|
||||||
|
fetchPayments()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '创建订阅支付记录失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create subscription error:', error)
|
||||||
|
ElMessage.error('创建订阅支付记录失败')
|
||||||
|
} finally {
|
||||||
|
subscriptionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 测试支付完成
|
||||||
|
const testPaymentComplete = async (payment) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要测试完成支付 ${payment.orderId} 吗?这将自动创建订单。`,
|
||||||
|
'确认测试',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await testPaymentCompleteApi(payment.id)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
ElMessage.success('支付完成测试成功,订单已自动创建')
|
||||||
|
// 刷新支付记录列表
|
||||||
|
fetchPayments()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '测试支付完成失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('Test payment complete error:', error)
|
||||||
|
ElMessage.error('测试支付完成失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除支付记录
|
||||||
|
const handleDeletePayment = async (payment) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除支付记录 ${payment.orderId} 吗?`,
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await deletePayment(payment.id)
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
// 刷新支付记录列表
|
||||||
|
fetchPayments()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data?.message || '删除失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('Delete payment error:', error)
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchPayments()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payments {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payments-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link {
|
||||||
|
color: #409EFF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #E6A23C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-description {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-description h4 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-description p {
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info h3 {
|
||||||
|
color: #409eff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .price {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #f56c6c;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits {
|
||||||
|
text-align: left;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits h4 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits li {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .benefits li:before {
|
||||||
|
content: "✓ ";
|
||||||
|
color: #67c23a;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .points-info {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .payment-method {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .payment-method h4 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .converted-price {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #b3d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info .price-display {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
578
demo/frontend/src/views/PrivacyPolicy.vue
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
<template>
|
||||||
|
<div class="privacy-page">
|
||||||
|
<div class="privacy-container">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<div class="back-button" @click="goBack">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>返回</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中文版本 -->
|
||||||
|
<div class="privacy-content">
|
||||||
|
<h1>隐私政策</h1>
|
||||||
|
|
||||||
|
<p class="update-date">最后更新:2025年11月1日</p>
|
||||||
|
|
||||||
|
<p class="intro">
|
||||||
|
本隐私政策阐述了当您(下称"您"或"用户")通过网站 (https://vionow.com/) 访问或使用我们的服务时,Vionow(下称"公司"、"我们"或"我们的")关于收集、使用和披露个人信息的政策和程序。本政策还解释了您的隐私权以及适用法律如何保护您。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="intro">
|
||||||
|
使用本服务即表示您同意我们根据本隐私政策收集和使用信息。本文件的编写已考虑到最佳实践和相关的法律标准。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>解释与定义</h2>
|
||||||
|
|
||||||
|
<h3>解释</h3>
|
||||||
|
<p>首字母大写的词语具有下文定义的含义,无论其以单数还是复数形式出现,其含义均相同。</p>
|
||||||
|
|
||||||
|
<h3>定义</h3>
|
||||||
|
<p>为本隐私政策之目的:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>账户:</strong>指您为访问我们的服务而创建的唯一个人资料。</li>
|
||||||
|
<li><strong>关联公司:</strong>指控制我们、被我们控制或与我们共同受控的任何实体。</li>
|
||||||
|
<li><strong>Cookies:</strong>指网站放置在您设备上的小型数据文件。</li>
|
||||||
|
<li><strong>公司:</strong>指在香港适用法律下运营的 Vionow。</li>
|
||||||
|
<li><strong>国家:</strong>指中国香港。</li>
|
||||||
|
<li><strong>设备:</strong>指能够访问本服务的任何技术设备。</li>
|
||||||
|
<li><strong>个人数据:</strong>指与已识别或可识别的个人相关的任何信息。</li>
|
||||||
|
<li><strong>服务:</strong>指公司提供的平台及相关服务。</li>
|
||||||
|
<li><strong>服务提供商:</strong>指与我们签约以处理数据或提供服务的第三方。</li>
|
||||||
|
<li><strong>使用数据:</strong>指在使用服务过程中自动收集的数据。</li>
|
||||||
|
<li><strong>网站:</strong>指位于 https://vionow.com/ 的在线平台。</li>
|
||||||
|
<li><strong>您:</strong>指访问或使用本服务的任何个人或实体。</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>数据收集</h2>
|
||||||
|
|
||||||
|
<h3>收集的数据类型</h3>
|
||||||
|
|
||||||
|
<h4>个人数据</h4>
|
||||||
|
<p>我们可能会要求您提供个人数据,包括但不限于:</p>
|
||||||
|
<ul>
|
||||||
|
<li>电子邮箱地址</li>
|
||||||
|
<li>姓名</li>
|
||||||
|
<li>电话号码</li>
|
||||||
|
<li>您自愿提供的其他身份标识信息</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>使用数据</h4>
|
||||||
|
<p>自动收集的数据可能包括:</p>
|
||||||
|
<ul>
|
||||||
|
<li>IP 地址</li>
|
||||||
|
<li>浏览器和设备信息</li>
|
||||||
|
<li>访问时间与访问页面</li>
|
||||||
|
<li>诊断和性能数据</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>追踪技术</h2>
|
||||||
|
<p>我们使用Cookies 及类似工具来增强功能和进行分析:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>必要性 Cookies:</strong>用于启用核心功能和保障安全。</li>
|
||||||
|
<li><strong>偏好性 Cookies:</strong>用于存储用户设置。</li>
|
||||||
|
<li><strong>分析性 Cookies:</strong>用于衡量性能和使用情况。</li>
|
||||||
|
</ul>
|
||||||
|
<p>您可以通过浏览器修改Cookie 设置。禁用 Cookies 可能会影响某些功能的正常使用。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>数据的使用</h2>
|
||||||
|
<p>我们可能将您的信息用于以下目的:</p>
|
||||||
|
<ul>
|
||||||
|
<li>提供、改进和维护服务</li>
|
||||||
|
<li>管理您的账户</li>
|
||||||
|
<li>履行合同和法律义务</li>
|
||||||
|
<li>就服务更新、支持或营销事宜与您沟通(取决于您的偏好)</li>
|
||||||
|
<li>分析使用模式以改善用户体验</li>
|
||||||
|
<li>用于内部研发</li>
|
||||||
|
<li>遵守法规和法律要求</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>用户生成内容与上传的图片</h2>
|
||||||
|
<p>
|
||||||
|
当您向本服务上传内容(例如图片、渲染图)时,您保留该内容的完全所有权。但是,通过我们的平台提交此类内容,即表示您授予公司一项非独占、全球性、免版税、不可撤销且永久的许可,授权我们仅为推广、营销或展示本服务功能之有限目的,使用、复制、修改和公开展示该等内容,包括但不限于在网站、营销材料或社交媒体上使用,前提是该内容不包含个人数据、可识别的个人或第三方的机密或专有信息。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
如果您希望选择退出此许可,您可以随时通过<a href="mailto:contact@vionow.com">contact@vionow.com</a> 与我们联系。收到有效请求后,我们将停止所有相关的推广使用,并尽合理努力从未来的材料中移除相关内容。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>数据保留</h2>
|
||||||
|
<p>
|
||||||
|
我们仅在为实现本政策所述目的或遵守适用法律所必需的时间内保留个人数据。使用数据可能为分析、安全或法律合规目的而保留。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>数据传输</h2>
|
||||||
|
<p>
|
||||||
|
您的数据可能会在您所在司法管辖区之外进行处理。我们将采取合理的保障措施,确保根据适用标准提供适当的保护。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>您的权利</h2>
|
||||||
|
<p>您有权:</p>
|
||||||
|
<ul>
|
||||||
|
<li>访问、更正或删除您的个人数据</li>
|
||||||
|
<li>反对或限制某些处理活动</li>
|
||||||
|
<li>撤回同意(如适用)</li>
|
||||||
|
<li>向监管机构投诉</li>
|
||||||
|
</ul>
|
||||||
|
<p>您可以通过<a href="mailto:contact@vionow.com">contact@vionow.com</a> 联系我们提交请求。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>数据披露</h2>
|
||||||
|
<p>我们可能在以下情况下披露您的个人数据:</p>
|
||||||
|
<ul>
|
||||||
|
<li>向根据合同义务行事的服务提供商披露</li>
|
||||||
|
<li>与公司重组或出售相关的披露</li>
|
||||||
|
<li>为遵守法律义务或捍卫我们的合法权利</li>
|
||||||
|
<li>为保护用户或公众的安全或权利</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>儿童隐私</h2>
|
||||||
|
<p>
|
||||||
|
我们的服务不面向13岁以下的个人。我们不会有意收集未成年人的个人数据。如果您认为有未成年人提交了个人数据,请联系我们以请求删除。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>第三方网站</h2>
|
||||||
|
<p>
|
||||||
|
我们的网站可能包含指向外部网站的链接。我们对其内容或隐私惯例不承担任何责任。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>本政策的变更</h2>
|
||||||
|
<p>
|
||||||
|
我们保留随时修订本隐私政策的权利。重大变更将通过电子邮件或网站上的醒目通知进行传达。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>联系我们</h2>
|
||||||
|
<p>
|
||||||
|
如果您对本隐私政策有任何疑问或希望行使您的权利,请通过以下方式联系我们:<br>
|
||||||
|
电子邮箱:<a href="mailto:contact@vionow.com">contact@vionow.com</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 分隔线 -->
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- English Version -->
|
||||||
|
<h1 class="english-title">Privacy Policy</h1>
|
||||||
|
|
||||||
|
<p class="update-date">Last updated: June 12, 2025</p>
|
||||||
|
|
||||||
|
<p class="intro">
|
||||||
|
This Privacy Policy outlines the policies and procedures of Vionow ("the Company", "We", "Us", or "Our") regarding the collection, use, and disclosure of personal information when You ("You" or "User") access or use Our services via the Website (https://vionow.com/). It also explains Your privacy rights and how applicable laws protect You.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="intro">
|
||||||
|
By using the Service, You consent to the collection and use of information in accordance with this Privacy Policy. This document has been prepared with consideration for best practices and relevant legal standards.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Interpretation and Definitions</h2>
|
||||||
|
|
||||||
|
<h3>Interpretation</h3>
|
||||||
|
<p>Capitalized words have meanings defined below, which apply equally whether singular or plural.</p>
|
||||||
|
|
||||||
|
<h3>Definitions</h3>
|
||||||
|
<p>For purposes of this Privacy Policy:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Account:</strong> A unique profile created by You to access Our Service.</li>
|
||||||
|
<li><strong>Affiliate:</strong> Any entity that controls, is controlled by, or is under common control with Us.</li>
|
||||||
|
<li><strong>Cookies:</strong> Small data files placed on Your device by the Website.</li>
|
||||||
|
<li><strong>Company:</strong> Vionow, operating under applicable laws in Hong Kong.</li>
|
||||||
|
<li><strong>Country:</strong> Hong Kong.</li>
|
||||||
|
<li><strong>Device:</strong> Any technology capable of accessing the Service.</li>
|
||||||
|
<li><strong>Personal Data:</strong> Information identifying or reasonably identifiable to an individual.</li>
|
||||||
|
<li><strong>Service:</strong> The platform and related services provided by the Company.</li>
|
||||||
|
<li><strong>Service Provider:</strong> Third parties contracted to process data or deliver services.</li>
|
||||||
|
<li><strong>Usage Data:</strong> Automatically collected data about Service usage.</li>
|
||||||
|
<li><strong>Website:</strong> The online platform located at https://vionow.com/.</li>
|
||||||
|
<li><strong>You:</strong> Any individual or entity accessing or using the Service.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Collection of Data</h2>
|
||||||
|
|
||||||
|
<h3>Types of Data Collected</h3>
|
||||||
|
|
||||||
|
<h4>Personal Data</h4>
|
||||||
|
<p>We may request Personal Data including but not limited to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Email address</li>
|
||||||
|
<li>First and last name</li>
|
||||||
|
<li>Phone number</li>
|
||||||
|
<li>Other voluntarily provided identifiers</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Usage Data</h4>
|
||||||
|
<p>Collected automatically and may include:</p>
|
||||||
|
<ul>
|
||||||
|
<li>IP address</li>
|
||||||
|
<li>Browser and device information</li>
|
||||||
|
<li>Access times and visited pages</li>
|
||||||
|
<li>Diagnostic and performance data</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Tracking Technologies</h2>
|
||||||
|
<p>We utilize Cookies and similar tools to enhance functionality and analytics:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Essential Cookies:</strong> Enable core features and security.</li>
|
||||||
|
<li><strong>Preference Cookies:</strong> Store user settings.</li>
|
||||||
|
<li><strong>Analytics Cookies:</strong> Measure performance and usage.</li>
|
||||||
|
</ul>
|
||||||
|
<p>You may modify cookie settings via Your browser. Declining cookies may impair certain functionalities.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Use of Data</h2>
|
||||||
|
<p>We may use Your information for the following purposes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>To deliver, improve, and maintain the Service</li>
|
||||||
|
<li>To administer Your Account</li>
|
||||||
|
<li>To fulfill contractual and legal obligations</li>
|
||||||
|
<li>To communicate with You regarding service updates, support, or marketing (subject to Your preferences)</li>
|
||||||
|
<li>To analyze usage patterns and improve the user experience</li>
|
||||||
|
<li>For internal research and development</li>
|
||||||
|
<li>To comply with regulatory and legal requirements</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>User-Generated Content and Uploaded Images</h2>
|
||||||
|
<p>
|
||||||
|
When You upload content (e.g., images, renders) to the Service, You retain full ownership of that content. However, by submitting such content through Our platform, You hereby grant the Company a non-exclusive, worldwide, royalty-free, irrevocable, and perpetual license to use, reproduce, modify, and publicly display such content strictly for the limited purpose of promoting, marketing, or demonstrating the functionality of the Service, including but not limited to use on the Website, in marketing materials, or on social media, provided that such content does not contain personal data, identifiable individuals, or confidential or proprietary information of third parties.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If You wish to opt out of this license, You may do so at any time by contacting Us at <a href="mailto:contact@vionow.com">contact@vionow.com</a>. Upon receipt of a valid request, We will cease all promotional use and make reasonable efforts to remove the relevant content from future materials.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Retention of Data</h2>
|
||||||
|
<p>
|
||||||
|
We retain Personal Data only as long as necessary to achieve the purposes described herein or as required by applicable law. Usage Data may be retained for analytics, security, or legal compliance.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Data Transfers</h2>
|
||||||
|
<p>
|
||||||
|
Your data may be processed outside of Your jurisdiction. We implement reasonable safeguards to ensure appropriate protection in accordance with applicable standards.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Your Rights</h2>
|
||||||
|
<p>You have the right to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Access, rectify, or delete Your Personal Data</li>
|
||||||
|
<li>Object to or restrict certain processing activities</li>
|
||||||
|
<li>Withdraw consent (where applicable)</li>
|
||||||
|
<li>File a complaint with a supervisory authority</li>
|
||||||
|
</ul>
|
||||||
|
<p>Requests can be submitted by contacting Us at <a href="mailto:contact@vionow.com">contact@vionow.com</a>.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Disclosure of Data</h2>
|
||||||
|
<p>We may disclose Your Personal Data:</p>
|
||||||
|
<ul>
|
||||||
|
<li>To service providers acting under contractual obligations</li>
|
||||||
|
<li>In connection with corporate restructuring or sale</li>
|
||||||
|
<li>To comply with legal obligations or defend Our legal rights</li>
|
||||||
|
<li>To protect the safety or rights of Users or the public</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Children's Privacy</h2>
|
||||||
|
<p>
|
||||||
|
Our Service is not directed to individuals under the age of 13. We do not knowingly collect Personal Data from minors. If You believe that a minor has submitted Personal Data, please contact Us to request removal.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Third-Party Websites</h2>
|
||||||
|
<p>
|
||||||
|
Our Website may contain links to external websites. We are not responsible for their content or privacy practices.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Changes to This Policy</h2>
|
||||||
|
<p>
|
||||||
|
We reserve the right to amend this Privacy Policy at any time. Material changes will be communicated via email or a prominent notice on the Website.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Contact Us</h2>
|
||||||
|
<p>
|
||||||
|
If You have any questions about this Privacy Policy or wish to exercise Your rights, contact us at:<br>
|
||||||
|
Email: <a href="mailto:contact@vionow.com">contact@vionow.com</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.privacy-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||||
|
padding: 40px 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-container {
|
||||||
|
max-width: 900px;
|
||||||
|
max-height: calc(100vh - 80px);
|
||||||
|
margin: 0 auto;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 60px 80px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条样式 */
|
||||||
|
.privacy-container::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-container::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-container::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox 滚动条样式 */
|
||||||
|
.privacy-container {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 212, 255, 0.3) rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #00D4FF;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
transform: translateX(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-content {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00D4FF;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.english-title {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-date {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li::before {
|
||||||
|
content: "·";
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
color: #00D4FF;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #00D4FF;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
margin: 60px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.privacy-container {
|
||||||
|
padding: 40px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, ul li {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.privacy-page {
|
||||||
|
padding: 20px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-container {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||