feat:initial commit
|
|
@ -0,0 +1,23 @@
|
|||
# 依赖包
|
||||
node_modules/
|
||||
|
||||
# 构建产物
|
||||
dist/
|
||||
dist-ssr/
|
||||
*.local
|
||||
|
||||
# 系统自动生成的无用文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# 日志文件
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# 编辑器配置
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
|
@ -0,0 +1 @@
|
|||
@batiao:registry=https://maven.batiao8.com/repository/npm-releases/
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>装样大师</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
[2026-07-22 11:40:20,637] [INFO] [D:\AAA-Code\模拟器pc端\frontend\service\main.cjs:13] server start at http://127.0.0.1:9270
|
||||
[2026-07-22 11:40:52,948] [INFO] [D:\AAA-Code\模拟器pc端\frontend\service\main.cjs:13] server start at http://127.0.0.1:9270
|
||||
[2026-07-22 11:40:53,151] [INFO] [unknow:0] GET 200 /
|
||||
[2026-07-22 11:40:53,179] [INFO] [unknow:0] GET 200 /.well-known/appspecific/com.chrome.devtools.json
|
||||
[2026-07-22 11:40:53,230] [INFO] [unknow:0] GET 200 /favicon.ico
|
||||
[2026-07-22 11:41:09,737] [INFO] [unknow:0] GET 200 /
|
||||
[2026-07-22 11:41:09,788] [INFO] [unknow:0] GET 200 /.well-known/appspecific/com.chrome.devtools.json
|
||||
[2026-07-22 11:41:09,850] [INFO] [unknow:0] GET 200 /favicon.ico
|
||||
[2026-07-22 11:44:37,840] [INFO] [D:\AAA-Code\模拟器pc端\frontend\service\main.cjs:13] server start at http://127.0.0.1:9270
|
||||
[2026-07-22 11:44:39,868] [INFO] [unknow:0] GET 200 /
|
||||
[2026-07-22 11:44:39,906] [INFO] [unknow:0] GET 200 /.well-known/appspecific/com.chrome.devtools.json
|
||||
[2026-07-22 11:44:40,032] [INFO] [unknow:0] GET 200 /src/public/header/chat-icon.png
|
||||
[2026-07-22 11:44:40,033] [INFO] [unknow:0] GET 200 /src/public/header/money-icon.png
|
||||
[2026-07-22 11:44:40,036] [INFO] [unknow:0] GET 200 /src/public/header/zhengjianmoni.png
|
||||
[2026-07-22 11:44:40,038] [INFO] [unknow:0] GET 200 /src/public/header/feijipiao.png
|
||||
[2026-07-22 11:44:40,056] [INFO] [unknow:0] GET 200 /src/public/header/huochepiao.png
|
||||
[2026-07-22 11:44:40,058] [INFO] [unknow:0] GET 200 /src/public/header/gouwuxiangqing.png
|
||||
[2026-07-22 11:44:40,059] [INFO] [unknow:0] GET 200 /src/public/header/qinglvzheng.png
|
||||
[2026-07-22 11:44:40,060] [INFO] [unknow:0] GET 200 /src/public/header/shipinliaotian.png
|
||||
[2026-07-22 11:44:40,093] [INFO] [unknow:0] GET 200 /src/public/index/liaotianmoni.png
|
||||
[2026-07-22 11:44:40,094] [INFO] [unknow:0] GET 200 /src/public/index/yuemoni.png
|
||||
[2026-07-22 11:44:40,095] [INFO] [unknow:0] GET 200 /src/public/index/gongzidan.png
|
||||
[2026-07-22 11:44:40,095] [INFO] [unknow:0] GET 200 /src/public/index/huochepiao.png
|
||||
[2026-07-22 11:44:40,097] [INFO] [unknow:0] GET 200 /src/public/index/feijipiao.png
|
||||
[2026-07-22 11:44:40,099] [INFO] [unknow:0] GET 200 /src/public/index/zhengjian.png
|
||||
[2026-07-22 11:44:40,106] [INFO] [unknow:0] GET 200 /src/public/index/gouwumoni.png
|
||||
[2026-07-22 11:44:40,110] [INFO] [unknow:0] GET 200 /src/public/index/service2.png
|
||||
[2026-07-22 11:44:40,111] [INFO] [unknow:0] GET 200 /src/public/index/service1.png
|
||||
[2026-07-22 11:44:40,112] [INFO] [unknow:0] GET 200 /src/public/index/service4.png
|
||||
[2026-07-22 11:44:40,113] [INFO] [unknow:0] GET 200 /src/public/index/service3.png
|
||||
[2026-07-22 11:44:40,148] [INFO] [unknow:0] GET 200 /favicon.ico
|
||||
[2026-07-22 11:44:40,249] [INFO] [D:\AAA-Code\模拟器pc端\frontend\service\core\router\index.cjs:115] GET 400 /api/user/config?nonce=0a7749351dc3464e80f434725e2f62f7&signature=34558c5c41e567ed8c8f698a80493987×tamp=1784691880
|
||||
[2026-07-22 11:45:45,061] [INFO] [D:\AAA-Code\模拟器pc端\frontend\service\main.cjs:13] server start at http://127.0.0.1:9270
|
||||
[2026-07-22 11:45:49,496] [INFO] [unknow:0] GET 200 /
|
||||
[2026-07-22 11:45:49,550] [INFO] [unknow:0] GET 200 /.well-known/appspecific/com.chrome.devtools.json
|
||||
[2026-07-22 11:45:49,654] [INFO] [unknow:0] GET 200 /src/public/header/money-icon.png
|
||||
[2026-07-22 11:45:49,656] [INFO] [unknow:0] GET 200 /src/public/header/chat-icon.png
|
||||
[2026-07-22 11:45:49,658] [INFO] [unknow:0] GET 200 /src/public/header/zhengjianmoni.png
|
||||
[2026-07-22 11:45:49,661] [INFO] [unknow:0] GET 200 /src/public/header/feijipiao.png
|
||||
[2026-07-22 11:45:49,663] [INFO] [unknow:0] GET 200 /src/public/header/huochepiao.png
|
||||
[2026-07-22 11:45:49,669] [INFO] [unknow:0] GET 200 /src/public/header/gouwuxiangqing.png
|
||||
[2026-07-22 11:45:49,695] [INFO] [unknow:0] GET 200 /src/public/header/qinglvzheng.png
|
||||
[2026-07-22 11:45:49,696] [INFO] [unknow:0] GET 200 /src/public/header/shipinliaotian.png
|
||||
[2026-07-22 11:45:49,735] [INFO] [unknow:0] GET 200 /src/public/index/huochepiao.png
|
||||
[2026-07-22 11:45:49,736] [INFO] [unknow:0] GET 200 /src/public/index/yuemoni.png
|
||||
[2026-07-22 11:45:49,741] [INFO] [unknow:0] GET 200 /src/public/index/gongzidan.png
|
||||
[2026-07-22 11:45:49,743] [INFO] [unknow:0] GET 200 /src/public/index/liaotianmoni.png
|
||||
[2026-07-22 11:45:49,745] [INFO] [unknow:0] GET 200 /src/public/index/feijipiao.png
|
||||
[2026-07-22 11:45:49,755] [INFO] [unknow:0] GET 200 /src/public/index/gouwumoni.png
|
||||
[2026-07-22 11:45:49,757] [INFO] [unknow:0] GET 200 /src/public/index/zhengjian.png
|
||||
[2026-07-22 11:45:49,759] [INFO] [unknow:0] GET 200 /src/public/index/service1.png
|
||||
[2026-07-22 11:45:49,762] [INFO] [unknow:0] GET 200 /src/public/index/service2.png
|
||||
[2026-07-22 11:45:49,765] [INFO] [unknow:0] GET 200 /src/public/index/service3.png
|
||||
[2026-07-22 11:45:49,768] [INFO] [unknow:0] GET 200 /src/public/index/service4.png
|
||||
[2026-07-22 11:45:49,780] [INFO] [unknow:0] GET 200 /favicon.ico
|
||||
[2026-07-22 11:45:49,926] [INFO] [D:\AAA-Code\模拟器pc端\frontend\service\core\router\index.cjs:28] GET 200 /api/user/config?nonce=a64235de6ea7ffebad418f9c2811cf65&signature=4c0d5aea657628fea8eb614c30bbc072×tamp=1784691949
|
||||
[2026-07-22 11:53:07,083] [INFO] [unknow:0] GET 200 /
|
||||
[2026-07-22 11:53:07,194] [INFO] [unknow:0] GET 200 /src/public/header/money-icon.png
|
||||
[2026-07-22 11:53:07,195] [INFO] [unknow:0] GET 200 /src/public/header/chat-icon.png
|
||||
[2026-07-22 11:53:07,195] [INFO] [unknow:0] GET 200 /src/public/header/gouwuxiangqing.png
|
||||
[2026-07-22 11:53:07,196] [INFO] [unknow:0] GET 200 /src/public/header/zhengjianmoni.png
|
||||
[2026-07-22 11:53:07,197] [INFO] [unknow:0] GET 200 /src/public/header/feijipiao.png
|
||||
[2026-07-22 11:53:07,197] [INFO] [unknow:0] GET 200 /src/public/header/huochepiao.png
|
||||
[2026-07-22 11:53:07,215] [INFO] [unknow:0] GET 200 /src/public/header/qinglvzheng.png
|
||||
[2026-07-22 11:53:07,216] [INFO] [unknow:0] GET 200 /src/public/header/shipinliaotian.png
|
||||
[2026-07-22 11:53:07,226] [INFO] [unknow:0] GET 200 /favicon.ico
|
||||
[2026-07-22 11:53:07,318] [INFO] [D:\AAA-Code\模拟器pc端\frontend\service\core\router\index.cjs:28] GET 200 /api/user?nonce=fddaedee3575963db77e91737a2348ef&signature=8088d7e0f2529b98895a16838cf5a5be×tamp=1784692387
|
||||
[2026-07-22 11:53:07,321] [INFO] [D:\AAA-Code\模拟器pc端\frontend\service\core\router\index.cjs:28] GET 200 /api/user/config?nonce=08ef64bc62ffad421fe79bee1cbba2c5&signature=d13b6ce366e861bbce4924bd500e68c3×tamp=1784692387
|
||||
[2026-07-22 11:53:11,132] [INFO] [unknow:0] GET 200 /.well-known/appspecific/com.chrome.devtools.json
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "wechat-pc-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "node --stack-size=12800 --stack-trace-limit=20 ./service/main.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@batiao/batiao-sdk-vue-vite": "^1.0.357",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.6.8",
|
||||
"body-parser": "^2.3.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"element-plus": "^2.6.1",
|
||||
"express": "^5.2.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"mime": "^4.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"request": "^2.88.2",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"winston": "^3.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"sass": "^1.71.1",
|
||||
"vite": "^5.2.0"
|
||||
},
|
||||
"auth": {
|
||||
"sign": "xn08hYoizXhZ1zHP8DVqfCm2yHxPmhil",
|
||||
"decode": "e4rOtnF8tJjtHO7ecZeJHN1rapED5ImB"
|
||||
},
|
||||
"authExpire": 5,
|
||||
"debug": false,
|
||||
"debug_build": false,
|
||||
"dat_only_main": true
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
const logger = require('../utils/log.cjs')
|
||||
const routes = require('./routes.cjs')
|
||||
const httpProxy = require('http-proxy')
|
||||
const proxy = httpProxy.createProxyServer({})
|
||||
const fs = require('fs')
|
||||
const dist = 'dist'
|
||||
let mime
|
||||
import('mime').then((res) => {
|
||||
mime = res.default
|
||||
})
|
||||
let proxys = require('../../../src/proxy.cjs')
|
||||
const { Exception } = require("sass");
|
||||
const httpOk = 200
|
||||
const httpBadRequest = 400
|
||||
const matchProxy = (request, response) => {
|
||||
const match = request.url.split('/')[1]
|
||||
const proxyInfo = proxys['/' + match]
|
||||
if (proxyInfo) {
|
||||
const host = request.headers.host;
|
||||
delete request.headers.host
|
||||
if (typeof proxyInfo.rewrite === 'function') {
|
||||
request.url = proxyInfo.rewrite(request.url)
|
||||
}
|
||||
proxy.web(request, response, {
|
||||
target: proxyInfo.target || ("http://" + host),
|
||||
changeOrigin: proxyInfo.changeOrigin
|
||||
})
|
||||
logger.info(`${request.method} ${httpOk} ${request.url}`)
|
||||
return true
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
const matchRouter = async (request, response) => {
|
||||
let [url] = request.url.split('?')
|
||||
let route = routes
|
||||
let url_sp = url.split('/')
|
||||
for (let p of url_sp) {
|
||||
if (!p) {
|
||||
continue
|
||||
}
|
||||
if (route[p]) {
|
||||
route = route[p]
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (typeof route === 'function') {
|
||||
await route(request, response)
|
||||
// response.writeHead(httpOk, { 'Content-Type': 'application/json' })
|
||||
// response.end(JSON.stringify(res))
|
||||
logger.info(`${request.method} ${httpOk} ${request.url}`)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const matchFile = async (request, response) => {
|
||||
return new Promise((resolve) => {
|
||||
const path = request.url.split('?')[0]
|
||||
fs.readFile(dist + decodeURIComponent(path), (err, file) => {
|
||||
if (!err) {
|
||||
const mimetype = mime.getType(request.url)
|
||||
const responseHdeaders = { 'Content-Type': mimetype }
|
||||
if (path.endsWith("-sw.js")) {
|
||||
responseHdeaders["Service-Worker-Allowed"] = '/'
|
||||
}
|
||||
response.writeHead(httpOk, responseHdeaders)
|
||||
response.end(file)
|
||||
// logger.info(`${request.method} ${httpOk} ${request.url}`)
|
||||
resolve(true)
|
||||
} else {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const endIndex = async (request, response) => {
|
||||
console.log(dist + '/index.html')
|
||||
fs.readFile(dist + '/index.html', (err, html) => {
|
||||
if (!err) {
|
||||
response.writeHead(httpOk, { 'Content-type': 'text/html;charset=utf-8' })
|
||||
response.end(html)
|
||||
logger.info(`${request.method} ${httpOk} ${request.url}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const matchType = async (request, response) => {
|
||||
const isRouter = await matchRouter(request, response)
|
||||
if (isRouter) {
|
||||
return
|
||||
}
|
||||
let isProxy = matchProxy(request, response)
|
||||
if (isProxy) {
|
||||
return
|
||||
}
|
||||
let isFile = await matchFile(request, response)
|
||||
if (isFile) {
|
||||
return
|
||||
}
|
||||
await endIndex(request, response)
|
||||
}
|
||||
|
||||
const httpService = async (request, response) => {
|
||||
try {
|
||||
await matchType(request, response)
|
||||
} catch (e) {
|
||||
response.writeHead(httpBadRequest, { 'Content-Type': 'application/json' })
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
code: -1,
|
||||
message: String(e)
|
||||
})
|
||||
)
|
||||
logger.info(`${request.method} ${httpBadRequest} ${request.url}`)
|
||||
}
|
||||
}
|
||||
module.exports = {
|
||||
httpService,
|
||||
matchType,
|
||||
matchRouter,
|
||||
matchFile,
|
||||
endIndex,
|
||||
matchProxy,
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// const logger = require("../utils/log.cjs")
|
||||
const config = require("../utils/config.cjs");
|
||||
const request = require("request");
|
||||
const CryptoJS = require("crypto-js");
|
||||
|
||||
module.exports = {
|
||||
config: async () => {
|
||||
return config;
|
||||
},
|
||||
getEnvByKey:async (req,res)=>{
|
||||
const query = req.query;
|
||||
const key = query.key;
|
||||
res.writeHead(200, {"Content-Type": "text/plain"});
|
||||
res.end(process.env[key])
|
||||
},
|
||||
downloadWithFileName: async (req, res) => {
|
||||
const query = req.query;
|
||||
const response = request(decodeURIComponent(query.url), {
|
||||
headers: query.headers,
|
||||
});
|
||||
response.on("response", (resp) => {
|
||||
["content-length", "content-type"].forEach(
|
||||
(key) => {
|
||||
if (resp.headers[key]) {
|
||||
res.setHeader(key, resp.headers[key]);
|
||||
}
|
||||
}
|
||||
);
|
||||
let disposition;
|
||||
if(query.filename){
|
||||
const filename = encodeURIComponent(query.filename);
|
||||
disposition = `attachment; filename="${filename}"`;
|
||||
}else {
|
||||
disposition = resp.headers["content-dispositions"] || ""
|
||||
}
|
||||
res.setHeader("content-disposition", disposition);
|
||||
res.setHeader("access-control-allow-origin", "*")
|
||||
});
|
||||
|
||||
response.on("error", (e) => {
|
||||
res.status(500).send(String(e));
|
||||
});
|
||||
response.on("end", () => {
|
||||
res.end();
|
||||
});
|
||||
response.on("data", (chunk) => {
|
||||
res.write(chunk);
|
||||
});
|
||||
},
|
||||
download: async (req, res) => {
|
||||
const query = req.query;
|
||||
query.url = CryptoJS.enc.Utf8.stringify(
|
||||
CryptoJS.enc.Base64.parse(query.url)
|
||||
).toString();
|
||||
query.headers = {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
|
||||
Accept: "*/*",
|
||||
"Accept-Language":
|
||||
"zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
|
||||
Connection: "keep-alive",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "cross-site",
|
||||
...JSON.parse(
|
||||
CryptoJS.enc.Utf8.stringify(
|
||||
CryptoJS.enc.Base64.parse(query.headers)
|
||||
).toString()
|
||||
),
|
||||
};
|
||||
const response = request(query.url, {
|
||||
headers: query.headers,
|
||||
});
|
||||
response.on("response", (resp) => {
|
||||
["content-length", "content-type", "content-disposition"].forEach(
|
||||
(key) => {
|
||||
if (resp.headers[key]) {
|
||||
res.setHeader(key, resp.headers[key]);
|
||||
}
|
||||
}
|
||||
);
|
||||
res.setHeader("access-control-allow-origin", "*")
|
||||
});
|
||||
|
||||
response.on("error", (e) => {
|
||||
res.status(500).send(String(e));
|
||||
});
|
||||
response.on("end", () => {
|
||||
res.end();
|
||||
});
|
||||
response.on("data", (chunk) => {
|
||||
res.write(chunk);
|
||||
});
|
||||
// response.pipe(res);
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
const config = require("../../../src/config.json")
|
||||
module.exports = config
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
const winston = require('winston');
|
||||
function thisLine() {
|
||||
try {
|
||||
const e = new Error();
|
||||
const regex = /\((.*):(\d+):(\d+)\)$/
|
||||
const stacks = e.stack.split("\n")
|
||||
const temp = []
|
||||
stacks.forEach(stack => {
|
||||
if (!stack.includes("node_modules")) {
|
||||
temp.push(stack)
|
||||
}
|
||||
})
|
||||
const match = regex.exec(temp[3]);
|
||||
return {
|
||||
filepath: match[1],
|
||||
line: match[2],
|
||||
column: match[3]
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
filepath: "unknow",
|
||||
line: "0",
|
||||
column: "0"
|
||||
};
|
||||
}
|
||||
}
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({
|
||||
dirname: 'log', filename: 'server.log'
|
||||
}),
|
||||
],
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss,SSS" }),
|
||||
winston.format.align(),
|
||||
winston.format.splat(),
|
||||
winston.format.printf(
|
||||
(info) => {
|
||||
const { timestamp, level, message } = info;
|
||||
const { filepath, line } = thisLine()
|
||||
return `[${timestamp}] [${level.toUpperCase()}] [${filepath}:${line}] ${message}`; // 包括文件名和行号
|
||||
}
|
||||
)
|
||||
),
|
||||
maxSize: '500m', // 每个日志文件最大尺寸
|
||||
maxFiles: '1d' // 保留的日志文件天数
|
||||
});
|
||||
|
||||
module.exports = logger
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/* eslint-disable no-undef */
|
||||
// 这里是生产环境的反向代理逻辑
|
||||
const initService = () => {
|
||||
const express = require("express");
|
||||
const router = require("./core/router/index.cjs");
|
||||
// const logger = console
|
||||
const config = require("./core/utils/config.cjs");
|
||||
const logger = require("./core/utils/log.cjs");
|
||||
const server = express();
|
||||
const PORT = config.port;
|
||||
server.use(router.httpService);
|
||||
server.listen(PORT, function () {
|
||||
logger.info(`server start at http://127.0.0.1:${PORT}`);
|
||||
});
|
||||
};
|
||||
try {
|
||||
initService();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 这里是开发环境的反向代理逻辑
|
||||
const routes = require("./core/router/routes.cjs");
|
||||
const router = require("./core/router/index.cjs");
|
||||
|
||||
module.exports = async function mock(req, res, next) {
|
||||
if (await router.matchRouter(req,res)) {
|
||||
return;
|
||||
}
|
||||
if (router.matchProxy(req, res)){
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div id="app-wrapper">
|
||||
<AppHeader class="header" />
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
<AppFooter class="footer" v-if="false" />
|
||||
<!-- Payment Dialog -->
|
||||
<PaymentDialog ref="paymentDialog" @success="onPaymentSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, provide, onMounted } from 'vue'
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import AppFooter from '@/components/AppFooter.vue'
|
||||
import PaymentDialog from '@/components/PaymentDialog.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import api from '@/api'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const paymentDialog = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (localStorage.getItem('token') && !userStore.tokenChecked) {
|
||||
// 用静默验证:401时不跳转登录页(避免注册/登录页被拦截)
|
||||
// api.auth.profileSilent().then(res => {
|
||||
// userStore.username = res.username
|
||||
// userStore.isVip = res.is_vip
|
||||
// userStore.avatarUrl = res.avatar_url
|
||||
// userStore.membershipExpireAt = res.membership_expire_at
|
||||
// userStore.tokenChecked = true
|
||||
// userStore.tokenValid = true
|
||||
// }).catch(() => {
|
||||
// userStore.tokenChecked = true
|
||||
// userStore.tokenValid = false
|
||||
// })
|
||||
userStore.fetchProfile().catch(() => { })
|
||||
}
|
||||
})
|
||||
|
||||
provide('showPayment', () => {
|
||||
paymentDialog.value?.open()
|
||||
})
|
||||
|
||||
function onPaymentSuccess() {
|
||||
userStore.fetchProfile()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app-wrapper {
|
||||
min-height: 100vh;
|
||||
max-width: 2000px;
|
||||
min-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { get, postJson } from "@/utils/requestService";
|
||||
import appConfig from "@/utils/appConfig";
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 0. 获取系统与用户初始化配置 (user-config)
|
||||
* 文档参考: https://gs.batiao8.com/doc/#api-User-user-config
|
||||
*/
|
||||
async getUserConfig(data = {}) {
|
||||
return await get(appConfig.baseURL, "/user/config", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 1. 发送手机验证码 (send-user-code)
|
||||
* 文档参考: https://gs.batiao8.com/doc/#api-User-send-user-code
|
||||
* @param {Object} data - { phone: '13800138000' }
|
||||
*/
|
||||
async sendCode(data) {
|
||||
return await postJson(appConfig.baseURL, "/user/code", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 2. 手机验证码登录 (user-login)
|
||||
* 文档参考: https://gs.batiao8.com/doc/#api-User-user-login
|
||||
* @param {Object} data - { login_type: 'phone', phone: { phone, code, timestamp } }
|
||||
*/
|
||||
async login(data) {
|
||||
return await postJson(appConfig.baseURL, "/user/login", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 3. 获取微信扫码登录二维码
|
||||
*/
|
||||
async getLoginCode(data = {}) {
|
||||
return await postJson(appConfig.baseURL, "/weixin/qrcode", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 4. 轮询微信扫码状态
|
||||
* @param {Object} data - { key: 'xxx' }
|
||||
*/
|
||||
async askLoginCode(data) {
|
||||
return await get(appConfig.baseURL, "/weixin/qrcode", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 5. 获取当前登录用户信息
|
||||
*/
|
||||
async getUserInfo(data = {}) {
|
||||
return await get(appConfig.baseURL, "/user", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
async getGoods(data = { type: "member" }) {
|
||||
return await get(appConfig.baseURL, "/order/goods", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
async createOrder(data = {}) {
|
||||
return await postJson(appConfig.baseURL, "/order", data);
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from '@/router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
http.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
return config
|
||||
})
|
||||
|
||||
http.interceptors.response.use(
|
||||
res => res.data,
|
||||
err => {
|
||||
if (err.response?.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
const userStore = useUserStore()
|
||||
userStore.reset()
|
||||
// _noRedirect 标记:静默验证时不跳转(避免注册/登录页被重定向)
|
||||
if (!err.config?._noRedirect) {
|
||||
router.push('/login')
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
// 支持静默模式:config._silent=true 时不弹出错误提示
|
||||
if (!err.config?._silent) {
|
||||
const msg = err.response?.data?.error || '请求失败'
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
// Auth
|
||||
export const auth = {
|
||||
register: data => http.post('/auth/register', data),
|
||||
login: data => http.post('/auth/login', data),
|
||||
logout: () => http.post('/auth/logout'),
|
||||
profile: () => http.get('/auth/profile'),
|
||||
profileSilent: () => http.get('/auth/profile', { _silent: true, _noRedirect: true }),
|
||||
captcha: () => http.get('/auth/captcha'),
|
||||
wechatLoginUrl: () => http.get('/auth/wechat/login'),
|
||||
}
|
||||
|
||||
// Works
|
||||
export const works = {
|
||||
list: params => http.get('/works', { params }),
|
||||
create: data => http.post('/works', data),
|
||||
get: id => http.get(`/works/${id}`),
|
||||
update: (id, data) => http.put(`/works/${id}`, data),
|
||||
delete: id => http.delete(`/works/${id}`),
|
||||
}
|
||||
|
||||
// Payment
|
||||
export const payment = {
|
||||
createOrder: planType => http.post('/payment/create-order', { plan_type: planType }),
|
||||
getOrder: orderNo => http.get(`/payment/order/${orderNo}`),
|
||||
orders: () => http.get('/payment/my-orders'),
|
||||
checkResult: orderNo => http.get(`/payment/result/${orderNo}`, { _silent: true }),
|
||||
}
|
||||
|
||||
// Product (商品管理)
|
||||
export const product = {
|
||||
list: () => http.get('/product/list'), // 前端公开:启用商品列表
|
||||
buy: (productId, data) => http.post(`/product/${productId}/buy`, data), // 购买商品
|
||||
// 管理端接口(需登录token)
|
||||
adminList: params => http.get('/product', { params }), // 分页列表
|
||||
get: id => http.get(`/product/${id}`),
|
||||
create: data => http.post('/product', data),
|
||||
update: (id, data) => http.put(`/product/${id}`, data),
|
||||
delete: id => http.delete(`/product/${id}`),
|
||||
toggle: id => http.post(`/product/${id}/toggle`),
|
||||
}
|
||||
|
||||
// Public
|
||||
export const publicApi = {
|
||||
banners: () => http.get('/public/banners'),
|
||||
}
|
||||
|
||||
export default { auth, works, payment, product, public: publicApi }
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<div class="footer-box">
|
||||
<div class="top-box">
|
||||
<div class="item left">
|
||||
<div class="title">关于我们</div>
|
||||
<p class="">
|
||||
我们是一个由“整蛊爱好者”和“戏精本精”组成的团队,致力于打造一个开放、包容、充满创意的平台——让每个人都能轻松释放自己的想象力,用图片编辑的方式创作出独一无二的恶搞作品。
|
||||
</p>
|
||||
</div>
|
||||
<div class="item right">
|
||||
<div class="title">联系方式</div>
|
||||
<div class="desc" style="margin-top: 0;">在线客服:</div>
|
||||
<div class="desc">公司:红红火火恍恍惚惚或或或或</div>
|
||||
<div class="desc">地址:红红火火恍恍惚惚或或或或或或</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-text">《用户协议》、《隐私政策》、《退款政策》</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footer-box {
|
||||
background-color: #1A1A1A;
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
|
||||
.top-box {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 200px;
|
||||
|
||||
.item {
|
||||
width: 489px;
|
||||
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
color: #979797;
|
||||
line-height: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #FFFFFF;
|
||||
line-height: 24px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #979797;
|
||||
line-height: 25px;
|
||||
max-width: 489px;
|
||||
}
|
||||
|
||||
.bottom-text {
|
||||
font-size: 14px;
|
||||
color: #979797;
|
||||
line-height: 12px;
|
||||
margin-top: 28px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,529 @@
|
|||
<template>
|
||||
<div style="height: 112px;"></div>
|
||||
<header class="app-header">
|
||||
<div class="group_47637">
|
||||
<div v-if="true" class="notice-banner-wrapper">
|
||||
<img class="banner-bg" src="@/public/header/down-app-banner-bg.png" alt="heightFix">
|
||||
<div class="banner-content">
|
||||
<span class="main-text">【 装样大师 】APP已上线!</span>
|
||||
<span class="sub-text">各大应用市场均可下载</span>
|
||||
<img class="down-app-img" src="@/public/header/down-app-btn.png" alt="">
|
||||
</div>
|
||||
<!-- <img class="close-icon" src="@/public/header/close.png" alt=""> -->
|
||||
</div>
|
||||
<div class="app-header-bar">
|
||||
<div class="left-box">
|
||||
<img class="logo-img" src="@/public/header/logo-img.png" />
|
||||
<img class="logo-name" src="@/public/header/logo-name.png" mode="widthFix" />
|
||||
<div class="line"></div>
|
||||
<div class="menu-box">
|
||||
<template v-for="item in navList" :key="item.key">
|
||||
<div v-if="item.children" class="dropdown-wrapper">
|
||||
<span :class="['item', { active: isItemActive(item) }]"> {{ item.name }} </span>
|
||||
<div class="dropdown-panel" style="padding-top: 22px;">
|
||||
<div class="panel-content">
|
||||
<div class="dropdown-item" v-for="child in item.children" :key="child.key"
|
||||
@click="handleNavClick(child)">
|
||||
<img class="icon" :src="child.icon" alt=""> {{ child.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else :class="['item', { active: isItemActive(item) }]" @click="handleNavClick(item)">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-box">
|
||||
<img v-if="!userStore.isVip && isLoggedIn" @click="handleCommand('vip')" class="vip-logo"
|
||||
src="@/public/header/kaitonghuiyuan.png" mode="heightFix" />
|
||||
<img v-if="!isLoggedIn" @click="openLoginDialog" class="vip-logo" src="@/public/header/login-btn.png"
|
||||
mode="heightFix" />
|
||||
<div v-else class="dropdown-wrapper">
|
||||
<div class="user-info-trigger">
|
||||
<div class="user-avatar">
|
||||
<img class="user-logo-img" :src="userStore.avatarUrl" />
|
||||
</div>
|
||||
<span class="user-name"> {{ userStore.username || '已登录用户' }} </span>
|
||||
</div>
|
||||
<div class="dropdown-panel">
|
||||
<div class="panel-content" style="width: 200px;">
|
||||
<div class="dropdown-item mg-7" @click="handleCommand('profile')">
|
||||
<img class="icon" src="@/public/header/gerenzhongxin.png" alt=""> 个人中心
|
||||
</div>
|
||||
<div class="dropdown-item mg-7" @click="handleCommand('vip')">
|
||||
<img class="icon" src="@/public/header/kaitonghuiyuanmenu.png" alt=""> 开通会员
|
||||
</div>
|
||||
<div class="dropdown-item mg-7" @click="handleCommand('works')">
|
||||
<img class="icon" src="@/public/header/wodezuoping.png" alt=""> 我的作品
|
||||
</div>
|
||||
<div style="border-top: 0.5px solid #D8D8D8;height: 1px;"></div>
|
||||
<div class="dropdown-item mg-7" style="border-radius: 0 0 8px 8px;" @click="handleCommand('logout')">
|
||||
<img class="icon" src="@/public/header/tuichudenglu.png" alt="">退出登录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoginDialog ref="loginDialogRef" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject, ref, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import LoginDialog from '@/components/LoginDialog.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const showPayment = inject('showPayment')
|
||||
const loginDialogRef = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (localStorage.getItem('token')) {
|
||||
userStore.fetchProfile().catch(() => { })
|
||||
}
|
||||
if (route.query.showLogin) {
|
||||
loginDialogRef.value?.open()
|
||||
const query = { ...route.query }
|
||||
delete query.showLogin
|
||||
router.replace({ query })
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => route.query.showLogin, (newVal) => {
|
||||
if (newVal) {
|
||||
loginDialogRef.value?.open()
|
||||
const query = { ...route.query }
|
||||
delete query.showLogin
|
||||
router.replace({ query })
|
||||
}
|
||||
})
|
||||
|
||||
const isLoggedIn = computed(() => userStore.isLoggedIn)
|
||||
|
||||
const navList = [
|
||||
{ name: '首页', key: 'index', path: '/' },
|
||||
{
|
||||
name: '微信模拟',
|
||||
key: 'wechat',
|
||||
children: [
|
||||
{ name: '模拟聊天', key: 'chat', path: '/chat', icon: '/src/public/header/chat-icon.png' },
|
||||
{ name: '余额模拟', key: 'balance', path: '/balance', icon: '/src/public/header/money-icon.png' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '娱乐模拟',
|
||||
key: 'entertainment',
|
||||
children: [
|
||||
{ name: '工资单', key: 'gongzidan', path: '/gongzidan', icon: '/src/public/header/zhengjianmoni.png' },
|
||||
{ name: '飞机票', key: 'flight', path: '/flight', icon: '/src/public/header/feijipiao.png' },
|
||||
{ name: '火车票', key: 'train', path: '/train', icon: '/src/public/header/huochepiao.png' },
|
||||
{ name: '购物详情', key: 'shopping', path: '/shopping', icon: '/src/public/header/gouwuxiangqing.png' },
|
||||
{ name: '情侣证', key: 'couple', path: '/couple', icon: '/src/public/header/qinglvzheng.png' },
|
||||
{ name: '视频聊天', key: 'video', path: '/video', icon: '/src/public/header/shipinliaotian.png' }
|
||||
]
|
||||
},
|
||||
{ name: '作品库', key: 'works', path: '/works' },
|
||||
{ name: '关于我们', key: 'about', path: '/about' }
|
||||
]
|
||||
|
||||
function handleNavClick(item) {
|
||||
if (item.key === 'works' && !isLoggedIn.value) {
|
||||
loginDialogRef.value?.open()
|
||||
return
|
||||
}
|
||||
if (item.path) {
|
||||
router.push(item.path)
|
||||
}
|
||||
}
|
||||
|
||||
function isItemActive(item) {
|
||||
if (item.path === route.path) return true
|
||||
if (item.children) {
|
||||
return item.children.some(child => child.path === route.path)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function handleCommand(cmd) {
|
||||
if (cmd === 'logout') {
|
||||
try { await api.auth.logout() } catch { }
|
||||
userStore.reset()
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user_info')
|
||||
ElMessage.success('已退出登录')
|
||||
router.push('/')
|
||||
} else if (cmd === 'works') {
|
||||
if (!isLoggedIn.value) {
|
||||
loginDialogRef.value?.open()
|
||||
return
|
||||
}
|
||||
router.push('/works')
|
||||
} else if (cmd === 'profile') {
|
||||
router.push('/profile')
|
||||
} else if (cmd === 'vip') {
|
||||
showPayment?.()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function openLoginDialog() {
|
||||
loginDialogRef.value?.open()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
max-width: 2000px;
|
||||
min-width: 1200px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, .08);
|
||||
z-index: 100;
|
||||
|
||||
.header-inner {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #07c160;
|
||||
|
||||
.logo-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
padding: 4px 0;
|
||||
|
||||
&.router-link-active {
|
||||
color: #07c160;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-dropdown-trigger {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.app-header {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.notice-banner-wrapper {
|
||||
position: relative;
|
||||
min-width: 1200px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: linear-gradient(258deg, #C05BFF 0%, #2E95FF 68.44%, #55D5FC 88.34%, #6CFFEA 100%);
|
||||
|
||||
.banner-text {
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 52px;
|
||||
|
||||
.main-text {
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 2px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
line-height: 52px;
|
||||
letter-spacing: 5px;
|
||||
}
|
||||
|
||||
.down-app-img {
|
||||
height: 26px;
|
||||
margin-left: 18px;
|
||||
cursor: pointer;
|
||||
animation: pulseScale 1.5s infinite ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulseScale {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.banner-bg {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 0;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
right: 34px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.app-header-bar {
|
||||
width: 100%;
|
||||
padding: 22px 72px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.left-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.logo-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.logo-name {
|
||||
width: 87px;
|
||||
}
|
||||
|
||||
.line {
|
||||
margin-left: 30px;
|
||||
width: 2px;
|
||||
height: 26px;
|
||||
background: #D8D8D8;
|
||||
}
|
||||
|
||||
.menu-box {
|
||||
display: flex;
|
||||
margin-left: 48px;
|
||||
gap: 50px;
|
||||
margin-right: 100px;
|
||||
|
||||
.item {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
color: #767676;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
position: relative;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 26px;
|
||||
height: 4px;
|
||||
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%);
|
||||
border-radius: 129px 129px 129px 129px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.vip-logo {
|
||||
height: 32px;
|
||||
margin-right: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
margin-right: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #eaeaea;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
.user-logo-img {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
color: #1A1A1A;
|
||||
line-height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
&:hover .dropdown-panel {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-panel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
padding-top: 15px;
|
||||
z-index: 100;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.panel-content {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 1px 0;
|
||||
min-width: 120px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid #fff;
|
||||
filter: drop-shadow(0 -2px 2px rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
padding: 12px 20px;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
color: #1A1A1A;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
margin: 3px 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.mg-7 {
|
||||
margin: 7px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #F7F8FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-info-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div class="base-dialog">
|
||||
|
||||
<el-dialog class="base-dialog" v-model="visible" :width="width" align-center>
|
||||
<img src="@/public/header/close.png" class="close" @click="handleClose" />
|
||||
<slot></slot>
|
||||
<template #footer v-if="$slots.footer">
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
width: { type: [Number, String], default: '600px' },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.base-dialog {
|
||||
position: relative;
|
||||
|
||||
.close {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: -34px;
|
||||
right: -34px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,594 @@
|
|||
<template>
|
||||
<BaseDialog v-model="visible" width="400px">
|
||||
<div class="login-wrapper">
|
||||
<div class="login-title">
|
||||
<h2 style="position: relative;z-index: 1;">登录</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<template v-if="loginType === 'phone'">
|
||||
<div class="input-box" style="margin-top: 98px;">
|
||||
<div class="phone-prefix">
|
||||
<span>+86</span>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<input type="text" v-model="form.phone" placeholder="请输入手机号" class="native-input" />
|
||||
</div>
|
||||
|
||||
<div class="code-box-row">
|
||||
<div class="input-box code-input-box">
|
||||
<input type="text" v-model="form.code" placeholder="请输入验证码" class="native-input" />
|
||||
</div>
|
||||
<button class="send-code-btn" type="button" :disabled="sendingCode || countdown > 0" @click="sendCode">
|
||||
<span class="text">{{ countdown > 0 ? `${countdown}s` : (sendingCode ? '发送中...' : '发送验证码') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="error-text" v-show="showError">请输入正确验证码</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="qrcode-wrapper">
|
||||
<div class="qrcode-img">
|
||||
<img v-if="qrcode" :src="qrcode" style="width: 100%; height: 100%; object-fit: cover;" alt="微信扫码登录" />
|
||||
</div>
|
||||
<!-- <img class="qrcode-img" src="/src/public/login/qrcode.png" alt="微信登录二维码" /> -->
|
||||
<div class="qr-text">
|
||||
请使用微信扫描二维码登录<br>
|
||||
“装样大师” 网页版
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="agreement-row" @click="form.agreed = !form.agreed"
|
||||
:style="{ 'margin-top': loginType === 'phone' ? '54px' : '0px' }">
|
||||
<div class="check-icon" :class="{ 'is-checked': form.agreed }">
|
||||
<img v-if="form.agreed" class="empty-circle" src="/src/public/login/agreement.png">
|
||||
<img v-else class="empty-circle" src="/src/public/login/unAgreement.png">
|
||||
</div>
|
||||
<span class="agreement-text">我已阅读并同意<a @click.stop>《用户协议》</a>和<a @click.stop>《隐私政策》</a></span>
|
||||
</div>
|
||||
|
||||
<button class="submit-btn" :class="{ 'is-loading': loading }" @click="handleLogin">登录</button>
|
||||
|
||||
<div class="other-methods">
|
||||
<div class="line"></div>
|
||||
<div class="text">其他登录方式</div>
|
||||
<div class="line2"></div>
|
||||
</div>
|
||||
|
||||
<div class="wechat-icon" @click="toggleLoginType">
|
||||
<img class="login-style" src="/src/public/login/wechat-login.png" v-if="loginType === 'phone'" />
|
||||
<img class="login-style" src="/src/public/login/phone-login.png" v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import api from '@/api'
|
||||
import authService from '@/api/authService'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import BaseDialog from './BaseDialog.vue'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const userStore = useUserStore()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const sendingCode = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer = null
|
||||
let qrTimer = null
|
||||
const qrcode = ref('')
|
||||
const qrKey = ref('')
|
||||
const showError = ref(false)
|
||||
const loginType = ref('phone')
|
||||
|
||||
const form = reactive({ phone: '', code: '', agreed: false })
|
||||
|
||||
function open() {
|
||||
visible.value = true
|
||||
loginType.value = 'phone'
|
||||
form.phone = localStorage.getItem('remember_phone') || ''
|
||||
form.code = ''
|
||||
form.agreed = false
|
||||
showError.value = false
|
||||
fetchQrCode()
|
||||
}
|
||||
|
||||
async function fetchQrCode() {
|
||||
try {
|
||||
// 确保请求二维码前本地有 Token (若缺失则自动拉取游客 Token)
|
||||
if (!localStorage.getItem('token')) {
|
||||
const configRes = await authService.getUserConfig()
|
||||
if (configRes && configRes.data && configRes.data.token) {
|
||||
localStorage.setItem('token', configRes.data.token)
|
||||
}
|
||||
}
|
||||
const res = await authService.getLoginCode()
|
||||
if (res && res.data) {
|
||||
qrKey.value = res.data.key
|
||||
qrcode.value = res.data.qrcode
|
||||
|
||||
if (qrTimer) clearInterval(qrTimer)
|
||||
qrTimer = setInterval(checkQrLogin, 2000)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取微信登录二维码失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkQrLogin() {
|
||||
if (loginType.value !== 'wechat' || !visible.value) return
|
||||
try {
|
||||
const res = await authService.askLoginCode({ key: qrKey.value })
|
||||
if (res && res.code === 0 && res.data && res.data.token) {
|
||||
localStorage.setItem('token', res.data.token)
|
||||
if (qrTimer) {
|
||||
clearInterval(qrTimer)
|
||||
qrTimer = null
|
||||
}
|
||||
visible.value = false
|
||||
emit('success')
|
||||
ElMessage.success('微信登录成功')
|
||||
window.location.reload()
|
||||
}
|
||||
} catch (err) {
|
||||
// 忽略轮询中的错误
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLoginType() {
|
||||
loginType.value = loginType.value === 'phone' ? 'wechat' : 'phone'
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
countdown.value = 60
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
if (countdown.value > 1) {
|
||||
countdown.value--
|
||||
} else {
|
||||
countdown.value = 0
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const smsTimestamp = ref('')
|
||||
|
||||
async function sendCode() {
|
||||
if (sendingCode.value || countdown.value > 0) return
|
||||
|
||||
const phoneReg = /^1[3-9]\d{9}$/
|
||||
if (!form.phone) {
|
||||
ElMessage.warning('请输入手机号')
|
||||
return
|
||||
}
|
||||
if (!phoneReg.test(form.phone)) {
|
||||
ElMessage.warning('请输入正确的11位手机号')
|
||||
return
|
||||
}
|
||||
|
||||
// 点击发送验证码时清空验证码,并记住手机号
|
||||
form.code = ''
|
||||
showError.value = false
|
||||
if (form.phone) {
|
||||
localStorage.setItem('remember_phone', form.phone)
|
||||
}
|
||||
|
||||
sendingCode.value = true
|
||||
try {
|
||||
const res = await authService.sendCode({ phone: form.phone })
|
||||
if (res && res.data && res.data.timestamp) {
|
||||
smsTimestamp.value = res.data.timestamp
|
||||
}
|
||||
ElMessage.success(res?.message || '验证码发送成功')
|
||||
startCountdown()
|
||||
} catch (err) {
|
||||
console.error('发送验证码失败:', err)
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
if (qrTimer) {
|
||||
clearInterval(qrTimer)
|
||||
qrTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
async function handleLogin() {
|
||||
showError.value = false
|
||||
if (!form.phone || !form.code) {
|
||||
showError.value = true
|
||||
return
|
||||
}
|
||||
if (!form.agreed) {
|
||||
ElMessage.warning('请先阅读并同意用户协议和隐私政策')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const loginPayload = {
|
||||
type: 'phone',
|
||||
data: {
|
||||
phone: form.phone,
|
||||
code: form.code,
|
||||
timestamp: smsTimestamp.value
|
||||
}
|
||||
}
|
||||
const res = await authService.login(loginPayload)
|
||||
if (res && res.data && res.data.token) {
|
||||
localStorage.setItem('token', res.data.token)
|
||||
userStore.setUser(res.data, form.phone)
|
||||
console.log("登录成功", res);
|
||||
ElMessage.success('登录成功')
|
||||
visible.value = false
|
||||
emit('success')
|
||||
// window.location.reload()
|
||||
} else {
|
||||
showError.value = true
|
||||
}
|
||||
} catch (err) {
|
||||
showError.value = true
|
||||
console.error('手机验证码登录失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWechatLogin() {
|
||||
// try {
|
||||
// const res = await api.auth.wechatLoginUrl()
|
||||
// if (res.oauth_url) {
|
||||
// window.location.href = res.oauth_url
|
||||
// // }
|
||||
// } catch (err) {
|
||||
// ElMessage.warning('微信登录暂不可用,请联系管理员')
|
||||
// }
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.image-style-login-dialog {
|
||||
border-radius: 20px !important;
|
||||
background: #ffffff !important;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1) !important;
|
||||
position: relative;
|
||||
overflow: visible !important;
|
||||
|
||||
.el-dialog__header {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: -45px;
|
||||
right: -5px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
z-index: 999;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
padding: 6px 30px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
// margin-bottom: 98px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
height: 33px;
|
||||
background-image: url('@/public/index/decoration1.png');
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -24px;
|
||||
opacity: 0.5;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 700;
|
||||
font-size: 30px;
|
||||
color: #1A1A1A;
|
||||
line-height: 30px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.title-deco {
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 24px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
background: #D8D8D850;
|
||||
border-radius: 12px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
.phone-prefix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #3D3D3D;
|
||||
font-size: 14px;
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 29px;
|
||||
background: #CECBDC;
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.native-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
color: #1A1A1A;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
color: #767676;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-box-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 52px;
|
||||
|
||||
.code-input-box {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.send-code-btn {
|
||||
width: 110px;
|
||||
height: 50px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #D8D8D8;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
outline: none;
|
||||
|
||||
.text {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
background: linear-gradient(124.63845108274714deg, #A679F4 7%, #778CF5 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fcfaff;
|
||||
border-color: #9254ff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
border-color: #e4e4e4;
|
||||
background: #f5f5f5;
|
||||
pointer-events: none;
|
||||
|
||||
.text {
|
||||
background: none;
|
||||
-webkit-text-fill-color: #999;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 12px;
|
||||
color: #FF4D4F;
|
||||
line-height: 10px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: -10px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.agreement-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.check-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.empty-circle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
font-size: 12px;
|
||||
color: #AAAAAA;
|
||||
|
||||
a {
|
||||
color: #767676;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
border-radius: 27px;
|
||||
background: linear-gradient(90deg, #778CF5 0%, #A679F4 100%);
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: #FFFFFF;
|
||||
line-height: 20px;
|
||||
border: none;
|
||||
margin-top: 8px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
.other-methods {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 14px;
|
||||
gap: 12px;
|
||||
|
||||
.line {
|
||||
width: 60px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #FFFFFF 0%, #E5E5E5 100%);
|
||||
}
|
||||
|
||||
.line2 {
|
||||
width: 60px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #E5E5E5 0%, #FFFFFF 100%);
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
color: #767676;
|
||||
line-height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.qrcode-img {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin-bottom: 18px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.qr-text {
|
||||
font-size: 14px;
|
||||
color: #1A1A1A;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #7c88ff 0%, #b77bff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 15px auto 0;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(163, 113, 255, 0.2);
|
||||
|
||||
.login-style {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,739 @@
|
|||
<template>
|
||||
<BaseDialog v-model="visible" width="1000px" class="payment-base-dialog">
|
||||
<div class="recharge-dialog">
|
||||
<!-- 头部用户信息 -->
|
||||
<div class="user-header">
|
||||
<el-avatar :size="45" :src="userInfo.avatar_url || undefined">
|
||||
{{ userInfo.username?.charAt(0)?.toUpperCase() }}
|
||||
</el-avatar>
|
||||
<div class="user-meta">
|
||||
<span class="username">{{ userInfo.username || '用户' }}</span>
|
||||
<span class="vip-status" :class="{ expired: !isVip }">
|
||||
{{ isVip ? `会员 ${formatExpire(userInfo.membership_expire_at)}到期` : '未开通会员' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-body">
|
||||
<!-- 左侧面板 -->
|
||||
<div class="left-panel">
|
||||
<div v-if="!loading && productList.length" class="plans-scroll" ref="scrollContainer" @mousedown="onMouseDown"
|
||||
@mouseleave="onMouseLeave" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||
<div v-for="(item, idx) in productList" :key="item.goods_id" class="plan-card"
|
||||
:class="{ active: selectedProductId === item.goods_id }" @click="handleProductChange(item.goods_id)">
|
||||
<div class="plan-name">{{ item.goods_name }}</div>
|
||||
<div class="plan-original">¥{{ item.origin_price }}</div>
|
||||
<div class="plan-price"><span class="currency">¥</span>{{ item.price }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="loading" class="loading-wrap">
|
||||
<el-icon class="is-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<span>加载商品...</span>
|
||||
</div>
|
||||
|
||||
<!-- 会员权益 -->
|
||||
<div class="privilege-section">
|
||||
<div class="privilege-title">
|
||||
<img style="width: 171px;height: 18px;" src="@/public/rechargeDialog/huiyuanquanyi.png" alt="">
|
||||
</div>
|
||||
<div class="privilege-grid">
|
||||
<div class="scroll-row">
|
||||
<div class="scroll-content right-to-left">
|
||||
<div class="scroll-group">
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon1.png" alt=""> 无水印高清下载</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon2.png" alt=""> 模拟微信</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon3.png" alt=""> 工资单</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon4.png" alt=""> 证件模拟</div>
|
||||
</div>
|
||||
<div class="scroll-group">
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon1.png" alt=""> 无水印高清下载</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon2.png" alt=""> 模拟微信</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon3.png" alt=""> 工资单</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon4.png" alt=""> 证件模拟</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-row">
|
||||
<div class="scroll-content left-to-right">
|
||||
<div class="scroll-group">
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon5.png" alt=""> 模拟飞机票</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon6.png" alt=""> 模拟高铁票</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon7.png" alt=""> 模拟购物订单</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon8.png" alt=""> 我的零钱</div>
|
||||
</div>
|
||||
<div class="scroll-group">
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon5.png" alt=""> 模拟飞机票</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon6.png" alt=""> 模拟高铁票</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon7.png" alt=""> 模拟购物订单</div>
|
||||
<div class="p-item"><img class="icon" src="@/public/rechargeDialog/icon8.png" alt=""> 我的零钱</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧毛玻璃面板 -->
|
||||
<div class="right-panel">
|
||||
<div class="price-display">
|
||||
<span class="currency">¥</span>
|
||||
<span class="price-num">{{ Number(currentPrice).toFixed(2) || '0.00' }}</span>
|
||||
</div>
|
||||
<div class="discount-tag" v-if="currentOriginalPrice">
|
||||
已优惠 ¥{{ (currentOriginalPrice - currentPrice) }}
|
||||
</div>
|
||||
|
||||
<div class="qr-box">
|
||||
<img v-if="qrCode" :src="qrCode" alt="支付二维码" class="qr-img" />
|
||||
<div v-else class="qr-placeholder" @click="handlePay">
|
||||
<el-icon v-if="paying" class="is-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<span v-else>点击获取支付码</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pay-methods">
|
||||
<div class="method-btn" :class="{ active: payMethod === 'weixin' }"
|
||||
@click="handlePayMethodChange('weixin')">
|
||||
<img class="icon" src="@/public/rechargeDialog/wxpay.png" alt="微信支付">
|
||||
微信支付
|
||||
</div>
|
||||
<div class="method-btn alipay" :class="{ active: payMethod === 'alipay' }"
|
||||
@click="handlePayMethodChange('alipay')">
|
||||
<img class="icon" src="@/public/rechargeDialog/alipay.png" alt="支付宝">
|
||||
支付宝支付
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-tips">
|
||||
<p>支付即视为同意《用户协议》</p>
|
||||
<p>如遇到支付问题在线反馈</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import BaseDialog from './BaseDialog.vue'
|
||||
import { Loading, Download, ChatDotRound, Tickets, Postcard, Position, Van, ShoppingBag, Wallet } from '@element-plus/icons-vue'
|
||||
import authService from '@/api/authService.js'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const userStore = useUserStore()
|
||||
|
||||
const visible = ref(false)
|
||||
const selectedProductId = ref(null)
|
||||
const paying = ref(false)
|
||||
const qrCode = ref('')
|
||||
const loading = ref(false)
|
||||
const productList = ref([])
|
||||
const payMethod = ref('weixin')
|
||||
let pollTimer = null
|
||||
|
||||
// 拖拽滚动相关状态
|
||||
const scrollContainer = ref(null)
|
||||
const isDragging = ref(false)
|
||||
const startX = ref(0)
|
||||
const scrollLeft = ref(0)
|
||||
let hasDragged = false
|
||||
|
||||
const userInfo = computed(() => ({
|
||||
username: userStore.username,
|
||||
avatar_url: userStore.avatarUrl || '',
|
||||
membership_expire_at: userStore.membershipExpireAt || null,
|
||||
}))
|
||||
const isVip = computed(() => userStore.isVip)
|
||||
|
||||
const currentPrice = computed(() => {
|
||||
const p = productList.value.find(i => i.goods_id === selectedProductId.value)
|
||||
return p ? p.price : ''
|
||||
})
|
||||
const currentOriginalPrice = computed(() => {
|
||||
const p = productList.value.find(i => i.goods_id === selectedProductId.value)
|
||||
return p ? (p.origin_price || '') : ''
|
||||
})
|
||||
|
||||
function open() {
|
||||
visible.value = true
|
||||
selectedProductId.value = null
|
||||
qrCode.value = ''
|
||||
paying.value = false
|
||||
loadProducts()
|
||||
}
|
||||
|
||||
async function loadProducts() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await authService.getGoods()
|
||||
console.log('商品列表', res)
|
||||
productList.value = Array.isArray(res.data) ? res.data : []
|
||||
|
||||
if (productList.value.length > 0) {
|
||||
// 默认选中第一个
|
||||
selectedProductId.value = productList.value[0].goods_id
|
||||
}
|
||||
if (!productList.value.length) ElMessage.warning('暂无可售商品')
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.response?.data?.error || '加载商品失败')
|
||||
productList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能格式化支付二维码图片地址
|
||||
* 解决微信支付二维码加载不出来的常见原因:
|
||||
* 1. 微信原生支付协议 (weixin://) 无法直接用 <img> 渲染,需要转换为二维码图片 URL;
|
||||
* 2. 纯 Base64 字符串缺失 data:image/png;base64, 前缀;
|
||||
* 3. 兼容 res.data.qrcodeBase64、res.data.qrcode 等不同字段。
|
||||
*/
|
||||
function getQrCodeUrl(resData) {
|
||||
if (!resData) return ''
|
||||
const dataObj = resData.data || resData
|
||||
|
||||
// 提取可能的字段
|
||||
const raw = dataObj.qrcodeBase64 || dataObj.qrcode_base64 || dataObj.qrcode || dataObj.payParam || (typeof dataObj === 'string' ? dataObj : '')
|
||||
|
||||
if (!raw) return ''
|
||||
|
||||
// 1. 已经是完整的 Data URI 或 HTTP(S) 图片链接
|
||||
if (raw.startsWith('data:image/') || raw.startsWith('http://') || raw.startsWith('https://')) {
|
||||
return raw
|
||||
}
|
||||
|
||||
// 2. 纯 Base64 字符串(补齐前缀)
|
||||
if (!raw.startsWith('weixin://') && !raw.startsWith('alipays://') && raw.length > 100) {
|
||||
return `data:image/png;base64,${raw.replace(/\s/g, '')}`
|
||||
}
|
||||
|
||||
// 3. 微信协议链接 (weixin://wxpay/...) 或网页链接,动态转换为可渲染的二维码图片
|
||||
if (raw.startsWith('weixin://') || raw.startsWith('alipays://') || raw.includes('://')) {
|
||||
return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(raw)}`
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
|
||||
async function handlePay() {
|
||||
if (!selectedProductId.value) return
|
||||
paying.value = true
|
||||
const data = {
|
||||
"goods_id": selectedProductId.value, // 从商品列表获取
|
||||
"pay_type": payMethod.value, // 支付方式,weixin/alipay/apple/bank/balance
|
||||
"pay_source": "web", // 默认app,支持app、jsapi(小程序)、web、h5
|
||||
"source": "web", // 下单商品位置,业务自定义
|
||||
"extra": {
|
||||
qrCodeImgUrl: "true",
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await authService.createOrder(data)
|
||||
console.log('创建订单响应结果:', res)
|
||||
const orderData = res.data || res
|
||||
|
||||
if (payMethod.value === 'alipay') {
|
||||
qrCode.value = orderData.payParam || ''
|
||||
} else {
|
||||
// 使用智能格式化获取可供 <img> 渲染的二维码图片地址
|
||||
qrCode.value = getQrCodeUrl(orderData)
|
||||
}
|
||||
|
||||
const orderNo = res.order_no || orderData.order_no || orderData.orderId
|
||||
if (orderNo) {
|
||||
startPolling(orderNo)
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.response?.data?.error || '创建订单失败')
|
||||
} finally {
|
||||
paying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePayMethodChange(method) {
|
||||
if (payMethod.value === method) return
|
||||
payMethod.value = method
|
||||
if (qrCode.value) {
|
||||
handlePay()
|
||||
}
|
||||
}
|
||||
|
||||
function handleProductChange(id) {
|
||||
if (hasDragged) return
|
||||
if (selectedProductId.value === id) return
|
||||
selectedProductId.value = id
|
||||
if (qrCode.value) {
|
||||
handlePay()
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽滚动逻辑
|
||||
function onMouseDown(e) {
|
||||
if (!scrollContainer.value) return
|
||||
isDragging.value = true
|
||||
hasDragged = false
|
||||
scrollContainer.value.style.cursor = 'grabbing'
|
||||
startX.value = e.pageX - scrollContainer.value.offsetLeft
|
||||
scrollLeft.value = scrollContainer.value.scrollLeft
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
isDragging.value = false
|
||||
if (scrollContainer.value) scrollContainer.value.style.cursor = 'grab'
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
isDragging.value = false
|
||||
if (scrollContainer.value) scrollContainer.value.style.cursor = 'grab'
|
||||
}
|
||||
|
||||
function onMouseMove(e) {
|
||||
if (!isDragging.value || !scrollContainer.value) return
|
||||
e.preventDefault()
|
||||
const x = e.pageX - scrollContainer.value.offsetLeft
|
||||
const walk = (x - startX.value) * 1.5 // 滚动速度倍率
|
||||
if (Math.abs(walk) > 5) {
|
||||
hasDragged = true
|
||||
}
|
||||
scrollContainer.value.scrollLeft = scrollLeft.value - walk
|
||||
}
|
||||
|
||||
function startPolling(orderNo) {
|
||||
if (pollTimer) clearInterval(pollTimer)
|
||||
let count = 0
|
||||
pollTimer = setInterval(async () => {
|
||||
count++
|
||||
try {
|
||||
const res = await api.payment.getOrder(orderNo)
|
||||
if (res.status === 'paid') {
|
||||
clearInterval(pollTimer)
|
||||
ElMessage.success('支付成功,会员已开通')
|
||||
setTimeout(() => {
|
||||
visible.value = false
|
||||
emit('success')
|
||||
}, 1500)
|
||||
}
|
||||
} catch { }
|
||||
if (count > 120) {
|
||||
clearInterval(pollTimer)
|
||||
ElMessage.warning('轮询超时,请刷新页面查看')
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return '-'
|
||||
return new Date(d).toLocaleString('zh-CN')
|
||||
}
|
||||
function formatExpire(d) {
|
||||
if (!d) return ''
|
||||
return new Date(d).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer) clearInterval(pollTimer)
|
||||
})
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.recharge-dialog {
|
||||
width: 1000px;
|
||||
height: 704px;
|
||||
background-image: url('/src/public/rechargeDialog/bg.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-header {
|
||||
position: absolute;
|
||||
top: 124px;
|
||||
left: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: #fff;
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
color: #FFFFFF;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.vip-status {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.vip-status {
|
||||
margin-top: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #FFFFFF;
|
||||
line-height: 14px;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
margin-top: 187px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
flex-shrink: 0;
|
||||
width: 677px;
|
||||
height: 493px;
|
||||
padding: 44px 46px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.plans-scroll {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
flex: 0 0 160px;
|
||||
height: 210px;
|
||||
background: #F5F5FF;
|
||||
border-radius: 16px 16px 16px 16px;
|
||||
border: 2px solid #BFC8F8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding-top: 30px;
|
||||
transition: all 0.3s;
|
||||
|
||||
.plan-name {
|
||||
font-size: 24px;
|
||||
color: #26106B;
|
||||
line-height: 21px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.plan-original {
|
||||
width: 100%;
|
||||
padding-left: 33px;
|
||||
font-size: 20px;
|
||||
color: #26106B;
|
||||
line-height: 20px;
|
||||
text-decoration: line-through;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.plan-price {
|
||||
font-size: 55px;
|
||||
color: #26106B;
|
||||
line-height: 52px;
|
||||
font-weight: 500;
|
||||
|
||||
.currency {
|
||||
font-size: 26px;
|
||||
color: #26106B;
|
||||
line-height: 26px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: url('/src/public/rechargeDialog/active-product-bg.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
background-clip: padding-box;
|
||||
border: none;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
left: -2px;
|
||||
z-index: -1;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #B17DFF, #50E5FF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
height: 220px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.privilege-section {
|
||||
margin-top: 38px;
|
||||
|
||||
.privilege-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 34px;
|
||||
|
||||
.text {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #8B70FF;
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 40px;
|
||||
height: 1px;
|
||||
background: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
.privilege-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
.scroll-row {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.scroll-content {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
|
||||
&.right-to-left {
|
||||
animation: scroll-rl 15s linear infinite;
|
||||
}
|
||||
|
||||
&.left-to-right {
|
||||
transform: translateX(-50%);
|
||||
animation: scroll-lr 15s linear infinite;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.p-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #1A1A1A;
|
||||
line-height: 14px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
width: 145px;
|
||||
|
||||
.icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
color: #8B70FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scroll-rl {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scroll-lr {
|
||||
0% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
width: 251px;
|
||||
height: 493px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 53px 16px 22px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.price-display {
|
||||
color: #fff;
|
||||
|
||||
.currency {
|
||||
font-size: 32px;
|
||||
color: #FFFFFF;
|
||||
line-height: 48px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.price-num {
|
||||
font-size: 52px;
|
||||
color: #FFFFFF;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.discount-tag {
|
||||
padding-inline: 20px;
|
||||
min-width: 120px;
|
||||
height: 44px;
|
||||
background: url('/src/public/rechargeDialog/discount-tag-bg.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
color: #fff;
|
||||
padding: 0 12px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 28px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
line-height: 48px;
|
||||
color: #FFFFFF;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qr-box {
|
||||
width: 158px;
|
||||
height: 158px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.qr-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.qr-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
background: #f5f5f5;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.pay-methods {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: auto;
|
||||
|
||||
.method-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #FFFFFF;
|
||||
line-height: 14px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.active {
|
||||
background: #FFFFFF40;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-tips {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #FFFFFFAA;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-base-dialog {
|
||||
|
||||
|
||||
:deep(.el-dialog) {
|
||||
padding: 0 !important;
|
||||
background-color: transparent;
|
||||
box-shadow: 0 0 0 0 #00000000;
|
||||
}
|
||||
|
||||
:deep(.base-dialog .close) {
|
||||
top: 80px !important;
|
||||
right: -32px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
<template>
|
||||
<div v-if="type === 'large'" class="feature-card" @click="$emit('click')">
|
||||
<div class="row-top">
|
||||
<img class="feature-img" :src="icon" />
|
||||
<div class="text-col">
|
||||
<span class="feature-title"> {{ title }} </span>
|
||||
<span class="feature-desc"> {{ desc }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-box">
|
||||
<span class="btn"> 开始模拟 </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="tool-card" @click="$emit('click')">
|
||||
<img class="tool-img" :src="icon" />
|
||||
<div class="tool-text">
|
||||
<span class="tool-title"> {{ title }} </span>
|
||||
<span class="tool-desc"> {{ desc }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
desc: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'small'
|
||||
}
|
||||
})
|
||||
defineEmits(['click'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* Large Card */
|
||||
.feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 72px 35px 28px 44px;
|
||||
position: relative;
|
||||
width: 550px;
|
||||
height: 300px;
|
||||
border-radius: 19px;
|
||||
background-color: #ffffff;
|
||||
transition: box-shadow 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.row-top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feature-img {
|
||||
width: 154px;
|
||||
height: 154px;
|
||||
border-radius: 20px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.text-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-weight: 700;
|
||||
font-size: 30px;
|
||||
color: #1A1A1A;
|
||||
line-height: 30px;
|
||||
margin-bottom: 30px;
|
||||
font-family: Source Han Sans, Source Han Sans;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-family: Source Han Sans, Source Han Sans;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
color: #767676;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.btn-box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: auto;
|
||||
|
||||
.btn {
|
||||
width: 120px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%);
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Small Card */
|
||||
.tool-card {
|
||||
position: relative;
|
||||
width: 370px;
|
||||
height: 200px;
|
||||
border-radius: 15px;
|
||||
background-color: #f7f8ff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
isolation: isolate;
|
||||
margin-top: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 0px 20px 0px #E7E8F3;
|
||||
}
|
||||
|
||||
.tool-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 15px;
|
||||
object-fit: cover;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
margin-left: 35px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.tool-text {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 198px;
|
||||
height: 200px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
z-index: 0;
|
||||
margin-left: 12px;
|
||||
margin-top: 0;
|
||||
|
||||
.tool-title {
|
||||
width: 127px;
|
||||
height: 36px;
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
white-space: pre;
|
||||
margin-top: 63px;
|
||||
margin-bottom: -4.5px;
|
||||
margin-left: calc(50% - 99px);
|
||||
margin-right: auto;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.tool-desc {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
white-space: pre;
|
||||
margin-top: 23px;
|
||||
margin-bottom: -4px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-size: 20px;
|
||||
color: #767676;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div class="warning-footer">
|
||||
<img src="@/public/warning.png" class="warning-icon" />
|
||||
<span class="warning-text">本工具生成的截图仅供娱乐参考,禁止用于任何非法用途</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.warning-footer {
|
||||
margin: 0 auto;
|
||||
max-width: 2000px;
|
||||
min-width: 1200px;
|
||||
height: 54px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
|
||||
.warning-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: #767676;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
<template>
|
||||
<PhoneMockup :theme="settings.theme" :class="{ 'ios-font': settings.phoneModel === 'ios' }">
|
||||
<!-- Status Bar -->
|
||||
<StatusBar :time="settings.statusTime" :battery="settings.battery" :signal="settings.signal" :wifi="settings.wifi"
|
||||
:is-charging="settings.isCharging" :theme="settings.theme" />
|
||||
|
||||
<!-- Nav Bar -->
|
||||
<div class="nav-bar">
|
||||
<img class="back-icon" :src="`/src/public/common/back-${settings.theme}.png`" alt="">
|
||||
<span class="right-title">{{ settings.pageTitle || '零钱明细' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="page-content">
|
||||
<div class="user-info">
|
||||
<img class="money-icon" src="@/public/balance/money.png" alt="">
|
||||
<p class="nick-name">{{ settings.nickName || '我的零钱' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="amount-display wx-font-regular">
|
||||
<span class="amount-symbol">¥</span>
|
||||
<span class="amount-num">{{ (settings.amount ?? 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="settings.showLimit" class="limit-warrp">
|
||||
<div class="limit-text">
|
||||
<img class="icon" src="@/public/balance/warning.png" alt="">
|
||||
<span>支付额度剩余<span class="wx-font-regular">¥{{ Number(settings.limitAmount || 0).toFixed(2)
|
||||
}}</span>,了解解决办法</span>
|
||||
<el-icon size="14">
|
||||
<ArrowRight color="#E75E63" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="settings.showLiqitong" class="liqitong-card">
|
||||
<img src="@/public/balance/lingtong-logo.png" class="lt-icon" />
|
||||
<div class="lt-info">
|
||||
<p class="lt-desc">{{ settings.liqitongDesc || '转入零钱通,能赚又能花' }}</p>
|
||||
<p class="lt-rate">零钱通 {{ settings.liqitongRate || '七日年化1.17%' }}</p>
|
||||
</div>
|
||||
<el-icon size="16">
|
||||
<ArrowRight color="#AEAEAE" />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div v-else class="lingtong-text">
|
||||
<span>转入零钱通,能赚又能花</span>
|
||||
<el-icon size="16">
|
||||
<ArrowRight color="#FA9D3B" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="settings.showFAQ" class="faq-section">
|
||||
|
||||
<div class="btn-group">
|
||||
<button v-if="settings.showChargeBtn" class="action-btn primary">充值</button>
|
||||
<button v-if="settings.showWithdrawBtn" class="action-btn secondary">提现</button>
|
||||
</div>
|
||||
<p class="faq-title">常见问题</p>
|
||||
<p class="faq-desc">{{ settings.serviceText || '本服务由财付通提供' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Home Bar -->
|
||||
<HomeBar />
|
||||
</PhoneMockup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PhoneMockup from '@/components/simulator/PhoneMockup.vue'
|
||||
import StatusBar from '@/components/simulator/StatusBar.vue'
|
||||
import HomeBar from '@/components/simulator/homeBar.vue'
|
||||
|
||||
defineProps({ settings: { type: Object, default: () => ({}) } })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$w: 393px;
|
||||
|
||||
.ios-font {
|
||||
font-family: -apple-system, "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nav-bar {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px 0 12px;
|
||||
background: var(--wx-bg-primary);
|
||||
color: var(--wx-text-primary);
|
||||
|
||||
.back-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.right-title {
|
||||
font-size: 16px;
|
||||
color: var(--wx-text-primary);
|
||||
line-height: 23px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 55px 20px;
|
||||
background: var(--wx-bg-primary);
|
||||
color: var(--wx-text-primary);
|
||||
|
||||
.user-info {
|
||||
text-align: center;
|
||||
margin-bottom: 21px;
|
||||
|
||||
.money-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.nick-name {
|
||||
margin-top: 32px;
|
||||
font-size: 16px;
|
||||
color: var(--wx-text-primary);
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-display {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
.amount-symbol {
|
||||
font-weight: 700;
|
||||
font-size: 30px;
|
||||
color: var(--wx-text-primary);
|
||||
line-height: 40px;
|
||||
// margin-top: 3px;
|
||||
}
|
||||
|
||||
.amount-num {
|
||||
font-weight: 500;
|
||||
font-size: 46px;
|
||||
color: var(--wx-text-primary);
|
||||
line-height: 40px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.limit-warrp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.limit-text {
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: var(--wx-limit-text-red);
|
||||
line-height: 24px;
|
||||
background: var(--wx-limit-bg-red);
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
padding: 9px 4px 9px 8px;
|
||||
margin: 24px auto 0;
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.liqitong-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
margin-top: 34px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--wx-line-color);
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--wx-line-color);
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
|
||||
.lt-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.lt-info {
|
||||
flex: 1;
|
||||
|
||||
.lt-desc {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: var(--wx-text-primary);
|
||||
}
|
||||
|
||||
.lt-rate {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
color: var(--wx-text-primary);
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.lt-arrow {
|
||||
font-size: 20px;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.lingtong-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
margin-top: 18px;
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
color: #FA9D3B;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 65px;
|
||||
|
||||
.action-btn {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
padding: 13px;
|
||||
width: 175px;
|
||||
height: 46px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
|
||||
&.primary {
|
||||
background: var(--wx-btn-primary);
|
||||
font-size: 16px;
|
||||
color: #FFFFFF;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--wx-btn-secondary);
|
||||
font-size: 16px;
|
||||
color: var(--wx-text-primary);
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.faq-section {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 53px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.faq-title {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
color: var(--wx-desc-text-blue);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.faq-desc {
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
color: var(--wx-desc-text-grey);
|
||||
line-height: 20px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,931 @@
|
|||
<template>
|
||||
<PhoneMockup :theme="settings.theme" :class="{ 'ios-font': settings.phoneModel === 'ios' }">
|
||||
<!-- Status Bar -->
|
||||
<StatusBar :time="settings.statusTime" :battery="settings.battery" :signal="settings.signal" :wifi="settings.wifi"
|
||||
:is-charging="settings.isCharging" :theme="settings.theme"
|
||||
:bgColor="settings.theme == 'light' ? '#ECECEC' : '#111111'" />
|
||||
|
||||
<!-- Nav Bar -->
|
||||
<div class="nav-bar">
|
||||
<div class="between-box left-box">
|
||||
<img class="back-icon" :src="`/src/public/common/back-${settings.theme}.png`" alt="">
|
||||
<span v-if="settings.unread > 0" class="unread">{{ settings.unread }}</span>
|
||||
</div>
|
||||
<div class="center-box" :style="{ 'margin-right': !settings.earpiece && !settings.notDisturb ? '0' : '14px' }">
|
||||
<div class="title-wrapper">
|
||||
<div class="text-primary">{{ settings.theirName }}</div>
|
||||
<div class="icons-wrapper">
|
||||
<img v-if="settings.earpiece" class="icon tingtong" src="@/public/wx-chat/tingtong.png" alt="">
|
||||
<img v-if="settings.notDisturb" class="icon miandarao" src="@/public/wx-chat/miandarao.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="between-box right-box">
|
||||
<img class="back-icon" :src="`/src/public/common/more-${settings.theme}.png`" alt="">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Chat Area -->
|
||||
<div class="chat-area" :style="chatAreaStyle">
|
||||
<!-- Messages -->
|
||||
<div v-for="(msg, idx) in messages" :key="msg.id" :class="['msg-row', msg.type, msg.sender]">
|
||||
|
||||
<!-- Time divider -->
|
||||
<div v-if="msg.type === 'time'" class="flex-row flex-1" style="justify-content: center;display: flex;">
|
||||
<div class="time-divider" :class="{ 'has-bg': settings.wallpaper, 'dark': settings.theme == 'dark' }">
|
||||
{{ formatTimeDisplay(msg.timeText) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System message -->
|
||||
<div v-else-if="msg.type === 'system'" class="flex-row flex-1" style="justify-content: center;display: flex;">
|
||||
<div class="system-msg" :class="{ 'has-bg': settings.wallpaper, 'dark': settings.theme == 'dark' }"
|
||||
v-html="msg.content"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Text message -->
|
||||
<div v-else-if="msg.type === 'text'" class="bubble-wrap" :class="msg.sender">
|
||||
<img :src="getAvatar(msg.sender)" class="avatar" alt="" />
|
||||
<div class="bubble text-bubble" :class="{ 'failed': msg.status === 'failed' && msg.sender === 'me' }">
|
||||
<span v-html="msg.content"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Red packet -->
|
||||
<div v-else-if="msg.type === 'redpacket'" class="bubble-wrap" :class="msg.sender">
|
||||
<img :src="getAvatar(msg.sender)" class="avatar" alt="" />
|
||||
<div class="bubble redpacket-bubble"
|
||||
:class="{ 'failed': msg.status === 'failed' && msg.sender === 'me', 'rp-received': msg.status != 'send' }">
|
||||
<div class="rp-content flex-row flex-1">
|
||||
<div class="rp-icon">
|
||||
<img style="width: 100%;height: 100%;"
|
||||
:src="`/src/public/wx-chat/${msg.status == 'received' ? 'open-red-bag' : 'red-bag'}${settings.theme === 'dark' ? '-dark' : ''}.png`"
|
||||
alt="">
|
||||
</div>
|
||||
<div class="flex column">
|
||||
<span class="rp-text">{{ truncateText(msg.title || '恭喜发财,大吉大利', 144, 16) }}</span>
|
||||
<span v-if="msg.status != 'send'" class="rp-desc-text">{{ msg.status == 'received' ? '已领取' : '已过期'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rp-label">微信红包</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transfer -->
|
||||
<div v-else-if="msg.type === 'transfer'" class="bubble-wrap" :class="msg.sender">
|
||||
<img :src="getAvatar(msg.sender)" class="avatar" alt="" />
|
||||
<div class="bubble transfer-bubble" :class="{
|
||||
'failed': msg.status === 'failed' && msg.sender === 'me',
|
||||
'tr-received': msg.status === 'received' || msg.status === 'returned',
|
||||
'tr-expired': msg.status === 'expired'
|
||||
}">
|
||||
<div class="rp-content tr-content-box flex-row flex-1">
|
||||
<div class="tr-icon">
|
||||
<img v-if="msg.status == 'expired'" style="width: 100%;height: 100%;"
|
||||
:src="`/src/public/wx-chat/zhuanzhang-yiguoqi${settings.theme === 'dark' ? '-dark' : ''}.png`" alt="">
|
||||
<img v-else-if="msg.status == 'received'" style="width: 100%;height: 100%;"
|
||||
:src="`/src/public/wx-chat/zhuanzhang-yijieshou${settings.theme === 'dark' ? '-dark' : ''}.png`"
|
||||
alt="">
|
||||
<img v-else-if="msg.status == 'returned'" style="width: 100%;height: 100%;"
|
||||
:src="`/src/public/wx-chat/zhuanzhang-yituihuan${settings.theme === 'dark' ? '-dark' : ''}.png`"
|
||||
alt="">
|
||||
<img v-else style="width: 100%;height: 100%;"
|
||||
:src="`/src/public/wx-chat/zhuanzhang-weijieshou${settings.theme === 'dark' ? '-dark' : ''}.png`"
|
||||
alt="">
|
||||
</div>
|
||||
<div class="tr-content">
|
||||
<div class="tr-amount wx-font-regular"> <span class="symbol">¥</span>{{ (msg.amount || 0).toFixed(2) }}
|
||||
</div>
|
||||
<div class="tr-remark">{{ truncateText(msg.remark || '你发起了一笔转账', 155, 12) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tr-label">微信转账</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Call Message (Video/Audio) -->
|
||||
<div v-else-if="msg.type === 'call'" class="bubble-wrap" :class="msg.sender">
|
||||
<img :src="getAvatar(msg.sender)" class="avatar" alt="" />
|
||||
<div class="bubble text-bubble" style="display: flex; align-items: center; gap: 6px; padding: 10px 14px;">
|
||||
<template v-if="msg.sender === 'me'">
|
||||
<span style="font-size: 15px;">{{ getCallText(msg) }}</span>
|
||||
<img v-if="msg.callType === 'video'" class="call-icon" src="@/public/wx-chat/video-light.png" alt="">
|
||||
<img v-else class="call-icon" src="@/public/wx-chat/voice-light.png" alt="">
|
||||
</template>
|
||||
<template v-else>
|
||||
<img v-if="msg.callType === 'video'" class="call-icon other"
|
||||
:src="`/src/public/wx-chat/video-${settings.theme === 'dark' ? 'dark' : 'light'}.png`" alt="">
|
||||
<img v-else class="call-icon other"
|
||||
:src="`/src/public/wx-chat/voice-${settings.theme === 'dark' ? 'dark' : 'light'}.png`" alt="">
|
||||
<span style="font-size: 15px;">{{ getCallText(msg) }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voice Message (Hold-to-talk) -->
|
||||
<div v-else-if="msg.type === 'voice'" class="bubble-wrap" :class="msg.sender">
|
||||
<img :src="getAvatar(msg.sender)" class="avatar" alt="" />
|
||||
<div class="bubble text-bubble voice-msg"
|
||||
:class="{ 'failed': msg.status === 'failed' && msg.sender === 'me' }"
|
||||
:style="`display: flex; align-items: center; gap: 2px; padding: 10px 14px; width: ${getVoiceWidth(msg.duration)}px;`">
|
||||
<template v-if="msg.sender === 'me'">
|
||||
<span style="font-size: 15px; flex: 1;text-align: right;">{{ msg.duration || 1 }}"</span>
|
||||
<img class="voice-msg-icon" src="@/public/wx-chat/voice-msg-light.png" alt="">
|
||||
</template>
|
||||
<template v-else>
|
||||
<img class="voice-msg-icon other"
|
||||
:src="`/src/public/wx-chat/voice-msg-${settings.theme === 'dark' ? 'dark' : 'light'}.png`" alt="">
|
||||
<span style="font-size: 15px; flex: 1; text-align: left;">{{ msg.duration || 1 }}"</span>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Unread red dot -->
|
||||
<div v-if="msg.sender === 'other' && msg.status === 'unread'"
|
||||
style="width: 8px; height: 8px; background-color: #F35651; border-radius: 50%; margin-left: 11px; margin-top: 16px;">
|
||||
</div>
|
||||
<div v-if="msg.sender === 'other' && msg.id === firstUnreadVoiceId" class="voice-text"
|
||||
:class="settings.theme">
|
||||
<span>转文字</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Bar -->
|
||||
<div class="input-bar" v-if="settings.showInput !== false">
|
||||
<img class="icon" :src="`/src/public/wx-chat/keyboard-${settings.theme}.png`" alt="">
|
||||
<div class="input-field">
|
||||
<span v-if="settings.voiceMode" class="text">按住 说话</span>
|
||||
<img v-if="settings.showMicIcon" class="mic" src="@/public/wx-chat/mic.png" alt="">
|
||||
</div>
|
||||
<img class="icon" :src="`/src/public/wx-chat/sticker-${settings.theme}.png`" alt="">
|
||||
<img class="icon" :src="`/src/public/wx-chat/add-${settings.theme}.png`" alt="">
|
||||
</div>
|
||||
|
||||
<!-- Home Bar -->
|
||||
<HomeBar v-if="settings.showBottomBar !== false" />
|
||||
</PhoneMockup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import PhoneMockup from '@/components/simulator/PhoneMockup.vue'
|
||||
import StatusBar from '@/components/simulator/StatusBar.vue'
|
||||
import HomeBar from '@/components/simulator/homeBar.vue'
|
||||
|
||||
const props = defineProps({
|
||||
messages: { type: Array, default: () => [] },
|
||||
settings: { type: Object, default: () => ({}) },
|
||||
})
|
||||
|
||||
const displayTitle = computed(() => {
|
||||
return props.settings.title || props.settings.theirName || '微信'
|
||||
})
|
||||
|
||||
const firstUnreadVoiceId = computed(() => {
|
||||
const msg = props.messages.find(m => m.type === 'voice' && m.sender === 'other' && m.status === 'unread')
|
||||
return msg ? msg.id : null
|
||||
})
|
||||
|
||||
function getAvatar(sender) {
|
||||
if (sender === 'me') return props.settings.myAvatarUrl || '/src/public/header/logo-img.png'
|
||||
return props.settings.theirAvatarUrl || '/src/public/common/default-avatar.png'
|
||||
}
|
||||
|
||||
function getVoiceWidth(dur) {
|
||||
let d = parseInt(dur, 10) || 1
|
||||
if (d > 60) d = 60
|
||||
if (d <= 1) return 70
|
||||
|
||||
let w = 70
|
||||
if (d <= 10) {
|
||||
w += (d - 1) * 8
|
||||
} else {
|
||||
w += 9 * 8 // 1-10秒的总增量
|
||||
w += (d - 10) * 8 / 10
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
function getCallText(msg) {
|
||||
const isMe = msg.sender === 'me'
|
||||
|
||||
if (msg.status === 'connected') {
|
||||
let dur = msg.duration || '00:00'
|
||||
// 如果只输入了纯数字(例如:8 或 12),则自动转为 mm:ss
|
||||
if (/^\d+$/.test(dur)) {
|
||||
const d = parseInt(dur, 10)
|
||||
const m = Math.floor(d / 60).toString().padStart(2, '0')
|
||||
const s = (d % 60).toString().padStart(2, '0')
|
||||
dur = `${m}:${s}`
|
||||
}
|
||||
return `通话时长 ${dur}`
|
||||
}
|
||||
|
||||
if (msg.status === 'refused') return isMe ? '对方已拒绝' : '已拒绝'
|
||||
if (msg.status === 'missed') return isMe ? '对方无应答' : '未应答'
|
||||
if (msg.status === 'cancelled') return isMe ? '已取消' : '对方已取消'
|
||||
|
||||
if (msg.status === 'unread' || msg.status === 'read') return `${msg.duration || 1}''`
|
||||
|
||||
return '中断通话'
|
||||
}
|
||||
|
||||
function truncateText(text, maxWidth, fontSize) {
|
||||
if (!text) return ''
|
||||
let totalWidth = 0
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const charCode = text.charCodeAt(i)
|
||||
totalWidth += (charCode > 255) ? fontSize : (fontSize * 0.55)
|
||||
}
|
||||
if (totalWidth <= maxWidth) return text
|
||||
|
||||
let width = 0
|
||||
let result = ''
|
||||
// 估算 '...' 的宽度,大约为 fontSize
|
||||
const limit = maxWidth - fontSize
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const charCode = text.charCodeAt(i)
|
||||
// 全角字符/中文字符算作 fontSize 宽度,半角算作 fontSize * 0.55
|
||||
const charWidth = (charCode > 255) ? fontSize : (fontSize * 0.55)
|
||||
if (width + charWidth > limit) {
|
||||
return result + '...'
|
||||
}
|
||||
width += charWidth
|
||||
result += text[i]
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
function formatTimeDisplay(timeText) {
|
||||
if (!timeText) return ''
|
||||
|
||||
let dateStr = timeText
|
||||
if (typeof dateStr === 'string') {
|
||||
dateStr = dateStr.replace(/年|月/g, '/').replace(/日/g, '')
|
||||
dateStr = dateStr.replace(/-/g, '/')
|
||||
}
|
||||
|
||||
let date = new Date(dateStr)
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
if (/^\s*\d{1,2}:\d{1,2}(:\d{1,2})?\s*$/.test(timeText)) {
|
||||
const timeStr = timeText.trim()
|
||||
const today = new Date()
|
||||
const yyyy = today.getFullYear()
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(today.getDate()).padStart(2, '0')
|
||||
date = new Date(`${yyyy}/${mm}/${dd} ${timeStr}`)
|
||||
}
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return timeText
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
|
||||
const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
|
||||
const diffTime = todayDate.getTime() - targetDate.getTime()
|
||||
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
const hhmm = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0')
|
||||
|
||||
if (diffDays === 0) {
|
||||
return hhmm
|
||||
} else if (diffDays === 1) {
|
||||
return `昨天 ${hhmm}`
|
||||
} else if (diffDays > 1 && diffDays < 7) {
|
||||
const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
return `${days[date.getDay()]} ${hhmm}`
|
||||
} else if (date.getFullYear() === now.getFullYear()) {
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日 ${hhmm}`
|
||||
} else {
|
||||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${hhmm}`
|
||||
}
|
||||
}
|
||||
|
||||
const chatAreaStyle = computed(() => {
|
||||
if (!props.settings.wallpaper) return undefined
|
||||
return {
|
||||
backgroundImage: `url(${props.settings.wallpaper})`,
|
||||
backgroundSize: 'cover',
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ios-font {
|
||||
font-family: -apple-system, "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
position: relative;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
background: var(--wx-bg-secondary);
|
||||
color: var(--wx-text-primary);
|
||||
|
||||
.left-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.unread {
|
||||
padding: 2px 6px;
|
||||
font-size: 13px;
|
||||
color: var(--wx-text-primary);
|
||||
line-height: 16px;
|
||||
background-color: var(--wx-chat-unread-bg);
|
||||
border-radius: 508px;
|
||||
height: 20px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--wx-chat-line-color);
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.right-title {
|
||||
font-size: 16px;
|
||||
color: var(--wx-text-primary);
|
||||
line-height: 23px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.right-box {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.between-box {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.center-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 6px;
|
||||
|
||||
.title-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: calc(100%);
|
||||
/* 给绝对定位的图标留出空间,防止重叠右侧按钮 */
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--wx-text-primary);
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icons-wrapper {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--wx-bg-secondary);
|
||||
padding: 12px;
|
||||
min-height: 0;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.time-divider {
|
||||
text-align: center;
|
||||
color: var(--wx-chat-time);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
padding: 0 3px;
|
||||
border-radius: 4px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.system-msg {
|
||||
text-align: center;
|
||||
color: #767676;
|
||||
font-size: 13px;
|
||||
padding: 0px 10px;
|
||||
line-height: 20px;
|
||||
border-radius: 4px;
|
||||
margin: 0 8px;
|
||||
|
||||
&.dark {
|
||||
color: #AAAAAA;
|
||||
}
|
||||
}
|
||||
|
||||
.has-bg {
|
||||
background-color: var(--wx-chat-time-bg);
|
||||
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.msg-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.bubble-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&.me {
|
||||
margin-left: 50px;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.avatar {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.other {
|
||||
margin-right: 50px;
|
||||
|
||||
.avatar {
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.voice-text {
|
||||
padding: 3px 7px;
|
||||
background-color: #E1E1E1;
|
||||
border-radius: 243px 243px 243px 243px;
|
||||
font-size: 11px;
|
||||
color: #A1A1A1;
|
||||
line-height: 12px;
|
||||
margin-top: 11px;
|
||||
margin-left: 8px;
|
||||
|
||||
&.dark {
|
||||
background-color: #1D1D1D;
|
||||
color: #767676;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 8px 10px;
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.text-bubble {
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.call-icon {
|
||||
width: 26px;
|
||||
height: 16px;
|
||||
margin-left: 2px;
|
||||
|
||||
&.other {
|
||||
margin-right: 2px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.voice-msg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&.other {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.voice-msg {
|
||||
display: flex;
|
||||
justify-content: flex-end !important;
|
||||
min-width: 80px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bubble-wrap.other .text-bubble {
|
||||
background: var(--wx-chat-bubble-other);
|
||||
color: var(--wx-text-primary);
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: -4px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-top: 3px solid transparent;
|
||||
border-right: 3px solid var(--wx-chat-bubble-other);
|
||||
border-bottom: 6px solid var(--wx-chat-bubble-other);
|
||||
transform: rotate(135deg);
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.bubble-wrap.me .text-bubble {
|
||||
background: var(--wx-chat-bubble-me);
|
||||
color: #1A1A1A;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: -4px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid var(--wx-chat-bubble-me);
|
||||
border-top: 3px solid var(--wx-chat-bubble-me);
|
||||
border-right: 3px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
transform: rotate(135deg);
|
||||
border-top-left-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.bubble-wrap.me .bubble {
|
||||
&.failed {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: url('/src/public/wx-chat/send-filled.png');
|
||||
background-size: 100% 100%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
left: -26px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.redpacket-bubble,
|
||||
.transfer-bubble {
|
||||
height: 82px;
|
||||
width: 230px;
|
||||
background: var(--wx-redpacket-bg);
|
||||
color: var(--wx-redpacket-text);
|
||||
padding: 10px 13px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
.rp-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 10px;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.tr-content-box {
|
||||
gap: 8px;
|
||||
padding-bottom: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rp-icon,
|
||||
.tr-icon {
|
||||
width: 36px;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tr-icon {
|
||||
width: 43px;
|
||||
height: 43px;
|
||||
}
|
||||
|
||||
.rp-text {
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.rp-desc-text {
|
||||
font-size: 11px;
|
||||
margin-top: 5px;
|
||||
color: var(--wx-redpacket-redpacket-text);
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.tr-remark {
|
||||
font-size: 13px;
|
||||
color: var(--wx-redpacket-text);
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 160px;
|
||||
/* Use width instead of max-width for html2canvas */
|
||||
}
|
||||
|
||||
.rp-label,
|
||||
.tr-label {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 13px;
|
||||
font-size: 11px;
|
||||
color: var(--wx-redpacket-desc-text);
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.redpacket-bubble.rp-received {
|
||||
background-color: var(--wx-redpacket-received-bg);
|
||||
color: var(--wx-redpacket-redpacket-text);
|
||||
|
||||
.rp-label {
|
||||
color: var(--wx-redpacket-redpacket-text);
|
||||
}
|
||||
}
|
||||
|
||||
.transfer-bubble {
|
||||
padding: 12px 11px;
|
||||
|
||||
.tr-amount {
|
||||
font-size: 16px;
|
||||
color: var(--wx-redpacket-text);
|
||||
line-height: 21px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
.symbol {
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 21px;
|
||||
text-align: center;
|
||||
line-height: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
.tr-remark {
|
||||
line-height: 16px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
&.tr-received {
|
||||
background-color: var(--wx-transfer-received-bg);
|
||||
|
||||
.tr-amount {
|
||||
color: var(--wx-transfer-received-text);
|
||||
}
|
||||
|
||||
.tr-remark {
|
||||
color: var(--wx-transfer-received-text);
|
||||
}
|
||||
|
||||
.tr-label {
|
||||
color: var(--wx-transfer-received-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.tr-expired {
|
||||
background-color: var(--wx-transfer-expired-bg);
|
||||
|
||||
.tr-amount {
|
||||
color: var(--wx-transfer-expired-text);
|
||||
}
|
||||
|
||||
.tr-remark {
|
||||
color: var(--wx-transfer-expired-text);
|
||||
}
|
||||
|
||||
.tr-label {
|
||||
color: var(--wx-transfer-expired-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bubble-wrap.other .redpacket-bubble,
|
||||
.bubble-wrap.other .transfer-bubble {
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: -4px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-top: 3px solid transparent;
|
||||
border-right: 3px solid var(--wx-redpacket-bg);
|
||||
border-bottom: 6px solid var(--wx-redpacket-bg);
|
||||
transform: rotate(135deg);
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
&.rp-received::after {
|
||||
border-left: 6px solid transparent;
|
||||
border-top: 3px solid transparent;
|
||||
border-right: 3px solid var(--wx-redpacket-received-bg);
|
||||
border-bottom: 6px solid var(--wx-redpacket-received-bg);
|
||||
}
|
||||
|
||||
&.tr-received::after {
|
||||
border-left: 6px solid transparent;
|
||||
border-top: 3px solid transparent;
|
||||
border-right: 3px solid var(--wx-transfer-received-bg);
|
||||
border-bottom: 6px solid var(--wx-transfer-received-bg);
|
||||
}
|
||||
|
||||
&.tr-expired::after {
|
||||
border-left: 6px solid transparent;
|
||||
border-top: 3px solid transparent;
|
||||
border-right: 3px solid var(--wx-transfer-expired-bg);
|
||||
border-bottom: 6px solid var(--wx-transfer-expired-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.bubble-wrap.me .redpacket-bubble,
|
||||
.bubble-wrap.me .transfer-bubble {
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: -4px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid var(--wx-redpacket-bg);
|
||||
border-top: 3px solid var(--wx-redpacket-bg);
|
||||
border-right: 3px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
transform: rotate(135deg);
|
||||
border-top-left-radius: 2px;
|
||||
}
|
||||
|
||||
&.rp-received::after {
|
||||
border-left: 6px solid var(--wx-redpacket-received-bg);
|
||||
border-top: 3px solid var(--wx-redpacket-received-bg);
|
||||
border-right: 3px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
}
|
||||
|
||||
&.tr-received::after {
|
||||
border-left: 6px solid var(--wx-transfer-received-bg);
|
||||
border-top: 3px solid var(--wx-transfer-received-bg);
|
||||
border-right: 3px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
}
|
||||
|
||||
&.tr-expired::after {
|
||||
border-left: 6px solid var(--wx-transfer-expired-bg);
|
||||
border-top: 3px solid var(--wx-transfer-expired-bg);
|
||||
border-right: 3px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-bar {
|
||||
position: relative;
|
||||
background: var(--wx-chat-input-bg-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
gap: 8px;
|
||||
padding-bottom: 42px;
|
||||
padding-top: 8px;
|
||||
|
||||
.icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--wx-chat-line-color);
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
font-size: 26px;
|
||||
cursor: pointer;
|
||||
color: var(--wx-chat-line-color);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
height: 38px;
|
||||
background: var(--wx-chat-bubble-other);
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
margin-right: -3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.text {
|
||||
font-size: 16px;
|
||||
color: #1A1A1A;
|
||||
line-height: 21px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mic {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--wx-bg-primary);
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.flex.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div class="phone-box">
|
||||
<img class="phone-bg" src="@/public/simulator/ios-phone.png" alt="">
|
||||
<div class="iphone-frame" :class="theme" id="capture-frame">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="watermark-overlay"></div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'light',
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$w: 375px;
|
||||
|
||||
.phone-box {
|
||||
width: 395px;
|
||||
height: 824px;
|
||||
position: relative;
|
||||
transform-origin: top center;
|
||||
transform: scale(calc(285/395));
|
||||
margin-bottom: calc(824px * (285 / 395) - 824px);
|
||||
|
||||
.iphone-frame {
|
||||
position: relative;
|
||||
width: 375px;
|
||||
height: 812.98px;
|
||||
border-radius: 40px;
|
||||
background-color: var(--wx-bg-primary);
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.phone-bg {
|
||||
position: absolute;
|
||||
width: 395px;
|
||||
height: 824px;
|
||||
top: -6px;
|
||||
left: -10px;
|
||||
}
|
||||
|
||||
.watermark-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 375px;
|
||||
height: 812.98px;
|
||||
border-radius: 40px;
|
||||
background-image: url('@/public/common/watermaker.png');
|
||||
background-size: 125px 125px;
|
||||
background-repeat: repeat;
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
<template>
|
||||
<div class="editor-page">
|
||||
<div class="workspace">
|
||||
<!-- 左侧预览区 -->
|
||||
<div class="preview-area col" ref="previewAreaRef">
|
||||
<div class="preview-scroll">
|
||||
<slot name="preview">
|
||||
<!-- <PhoneMockup>
|
||||
<StatusBar />
|
||||
</PhoneMockup> -->
|
||||
<BalancePreview> </BalancePreview>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧配置区 -->
|
||||
<div class="panel-area col">
|
||||
<div class="panel-header">
|
||||
<div class="tabs-container">
|
||||
<div v-for="tab in tabs" :key="tab.value" class="tab-item" :class="{ active: activeTab === tab.value }"
|
||||
@click="handleTabClick(tab)">
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="actions">
|
||||
<div class="btn-box">
|
||||
<img src="@/public/simulator/save-btn.png" alt="" class="save-btn btn" @click="$emit('save')">
|
||||
<img src="@/public/simulator/export-btn.png" alt="" class="export-btn btn" @click="$emit('export')">
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-scroll">
|
||||
<div class="setting-list">
|
||||
<slot name="config"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WarningFooter />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { FolderAdd, Download } from '@element-plus/icons-vue'
|
||||
import WarningFooter from '@/components/WarningFooter.vue'
|
||||
import PhoneMockup from '@/components/simulator/PhoneMockup.vue'
|
||||
import StatusBar from '@/components/simulator/StatusBar.vue'
|
||||
|
||||
defineProps({
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [{ label: '编辑设置', value: 'edit' }]
|
||||
},
|
||||
activeTab: {
|
||||
type: String,
|
||||
default: 'edit'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['save', 'export', 'update:activeTab', 'tab-click'])
|
||||
|
||||
function handleTabClick(tab) {
|
||||
emit('update:activeTab', tab.value)
|
||||
emit('tab-click', tab)
|
||||
}
|
||||
|
||||
const previewAreaRef = ref()
|
||||
// 将 ref 暴露出去,供外部水印或导出截图逻辑使用
|
||||
defineExpose({ previewAreaRef })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editor-page {
|
||||
height: calc(100vh - 166px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #FAFAFB;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
padding: 40px 10px 80px;
|
||||
min-width: 1400px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-area {
|
||||
flex: 0 0 350px;
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.preview-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 列统一样式 ===== */
|
||||
.col {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
padding: 0 18px;
|
||||
padding-left: 56px;
|
||||
height: 49px;
|
||||
background: rgba(255, 255, 255, .85);
|
||||
border-bottom: 1px solid #D8D8D8;
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
gap: 30px;
|
||||
|
||||
.tab-item {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: #1A1A1A;
|
||||
line-height: 20px;
|
||||
padding: 0 14px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background-color: #5AC878;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-box {
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 580px;
|
||||
padding: 18px 68px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 编辑设置面板 - 浅紫主题 */
|
||||
.panel-area {
|
||||
flex: 1;
|
||||
width: 1000px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 18px !important;
|
||||
/* 浅紫 */
|
||||
|
||||
/* 穿透样式让插槽内部也能复用相同的配置项样式 */
|
||||
:deep(.setting-list) {
|
||||
.section-label {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 0;
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: rgba(94, 53, 177, .12);
|
||||
margin: 18px 0 14px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 14px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #767676;
|
||||
line-height: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-input__wrapper,
|
||||
.el-radio-group,
|
||||
.el-input-number,
|
||||
.el-select__wrapper,
|
||||
.rich-editor {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
// box-shadow: 0 1px 3px #D8D8D880, 0 0 0 1px #D8D8D8;
|
||||
transition: box-shadow .2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 6px rgba(94, 53, 177, .12), 0 0 0 1px rgba(94, 53, 177, .1) inset;
|
||||
}
|
||||
|
||||
&.is-focus,
|
||||
&.is-focused,
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px rgba(103, 58, 183, .4) inset;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.is-checked {
|
||||
.el-switch__core {
|
||||
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%);
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-time-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-input-number {
|
||||
.el-input__inner {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.el-checkbox__label {
|
||||
color: #555;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.el-slider {
|
||||
.el-slider__bar {
|
||||
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%);
|
||||
}
|
||||
|
||||
.el-slider__button {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: 400;
|
||||
font-size: 14px !important;
|
||||
color: #1A1A1A !important;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.switch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<div class="status-bar" :class="{ 'dark-mode': theme === 'dark' }" :style="{ 'background-color': bgColor }">
|
||||
<span class="time">{{ time }}</span>
|
||||
<span class="status-icons">
|
||||
<img class="signal" :src="`/src/public/status/signal-${theme === 'dark' ? 'dark-' + signal : signal}.png`"
|
||||
alt="" />
|
||||
<img class="wifi" :src="`/src/public/status/wifi-${theme === 'dark' ? 'dark-' + wifi : wifi}.png`" alt="" />
|
||||
<div class="battery">
|
||||
<div class="power" :class="{ 'full-power': battery >= 100 }"
|
||||
:style="{ 'background-color': isCharging ? '#2DC752' : 'var(--battery-bg)', width: battery + '%' }">
|
||||
</div>
|
||||
<div class="percent">
|
||||
<span class="battery-text" :style="{ 'color': isCharging ? '#ffffff' : 'var(--battery-color)' }">
|
||||
{{ battery }}</span>
|
||||
<img v-if="isCharging" class="charging" src="/src/public/status/charging.png" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<img style="width: 1px;height: 4px;margin-left: 1px;" src="/src/public/status/battery.png" alt="" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
time: {
|
||||
type: String,
|
||||
default: '12:00'
|
||||
},
|
||||
battery: {
|
||||
type: [String, Number],
|
||||
default: 60
|
||||
},
|
||||
signal: {
|
||||
type: [String, Number],
|
||||
default: 4
|
||||
},
|
||||
wifi: {
|
||||
type: [String, Number],
|
||||
default: 3
|
||||
},
|
||||
isCharging: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'light'
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'transparent'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.status-bar {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
// align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 20px 31px 16px 44px;
|
||||
color: var(--wx-icon-color);
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
|
||||
.time {
|
||||
font-weight: 500;
|
||||
font-size: 17px;
|
||||
color: var(--wx-text-primary);
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--wx-icon-color);
|
||||
|
||||
.signal {
|
||||
width: 20px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.wifi {
|
||||
width: 17px;
|
||||
height: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.battery {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 13px;
|
||||
background: #A7A7A7;
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
color: #FFFFFF;
|
||||
line-height: 11px;
|
||||
margin-left: 6px;
|
||||
text-align: center;
|
||||
|
||||
.power {
|
||||
height: 100%;
|
||||
display: block;
|
||||
border-radius: 4px 0 0 4px;
|
||||
|
||||
&.full-power {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.percent {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
|
||||
.battery-text {
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
color: #FFFFFF;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.charging {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 9px;
|
||||
// margin-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<div class="home-bar">
|
||||
<div class="nav"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.home-bar {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.nav {
|
||||
height: 5px;
|
||||
width: 136px;
|
||||
margin-top: 23px;
|
||||
background-color: var(--ipone-bottom-bar);
|
||||
border-radius: 143px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"title": "快存图",
|
||||
"baseUrl": "/kct",
|
||||
"messageDuration": 3000,
|
||||
"requestTimeout": 60000,
|
||||
"successCode": [
|
||||
200,
|
||||
0
|
||||
],
|
||||
"invalidCode": [
|
||||
1001106,
|
||||
1001003
|
||||
],
|
||||
"port": 9270 ,
|
||||
"rbBuldInfo": {
|
||||
"api": "http://rb2.batiao8.com/console/custom/deploy/f5b5219cce4a7d47a60416ef4d3fa891",
|
||||
"key": "bNJXlzwc"
|
||||
},
|
||||
|
||||
"policyUrl": "kct/static/policy",
|
||||
"policyUrl2": "kct/static/policy2_web",
|
||||
|
||||
"goodsType":"member",
|
||||
"ignoreCodeErrorList": ["GET&/api/weixin/qrcode","GET&/api/app/code", "GET&/api/order"],
|
||||
"hideConsole": true,
|
||||
"downloadSuccessCode":[200,201,202,203,204,205,206],
|
||||
"configIgnoreParams": ["inputvalue"]
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import './styles/global.scss'
|
||||
|
||||
import authService from '@/api/authService'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// Register all Element Plus icons
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(ElementPlus, { locale: zhCn })
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
|
||||
// 后台异步获取配置与游客 Token,不阻塞首屏渲染
|
||||
authService.getUserConfig().then(res => {
|
||||
if (res && res.data) {
|
||||
console.log("用户配置", res.data)
|
||||
// 只有在本地没有 token 时,才写入游客 token
|
||||
if (res.data.token && !localStorage.getItem('token')) {
|
||||
localStorage.setItem('token', res.data.token)
|
||||
}
|
||||
if (res.data.config) {
|
||||
window.__USER_CONFIG__ = res.data.config
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取用户配置失败:', err)
|
||||
})
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
module.exports = {
|
||||
'/api': {
|
||||
target: 'https://flaunt.batiao8.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path
|
||||
},
|
||||
'/static': {
|
||||
target: 'http://wechat.wenyitu.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 266 B |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 549 B |
|
After Width: | Height: | Size: 356 B |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 411 B |
|
After Width: | Height: | Size: 371 B |
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 616 B |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 844 B |
|
After Width: | Height: | Size: 645 B |
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 530 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 696 B |
|
After Width: | Height: | Size: 853 B |
|
After Width: | Height: | Size: 656 B |
|
After Width: | Height: | Size: 559 B |
|
After Width: | Height: | Size: 860 B |
|
After Width: | Height: | Size: 642 B |
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>关于我们</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 997 B |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 1003 B |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 465 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 842 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 807 B |
|
After Width: | Height: | Size: 575 KiB |
|
After Width: | Height: | Size: 4.1 KiB |