first commit
This commit is contained in:
commit
6ca0770bf6
1
.env.development
Normal file
1
.env.development
Normal file
@ -0,0 +1 @@
|
|||||||
|
api_url=http://localhost:8080
|
||||||
1
.env.production
Normal file
1
.env.production
Normal file
@ -0,0 +1 @@
|
|||||||
|
api_url=https://ykt.youcan365.com
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.zip
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
72
README.md
Normal file
72
README.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# YKT Friend Bridge H5
|
||||||
|
|
||||||
|
短信好友邀请中转页。发布包名为 `sms.zip`,默认短路径为 `/sms/{recordId}`。
|
||||||
|
|
||||||
|
## Local Dev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
本地默认读取 `.env.development`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
api_url=http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
默认地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:5174/sms/{recordId}
|
||||||
|
```
|
||||||
|
|
||||||
|
本地服务端默认读取:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:8080/sms/friend-link/{recordId}/data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
生产默认读取 `.env.production`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
api_url=https://ykt.youcan365.com
|
||||||
|
```
|
||||||
|
|
||||||
|
产物:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/sms/
|
||||||
|
sms.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
`sms.zip` 内部自带 `sms/` 目录,上传后解压到站点根目录即可:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
unzip -o sms.zip -d /var/www
|
||||||
|
```
|
||||||
|
|
||||||
|
`api_url` 必须配置,页面内部接口只请求:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/sms/friend-link/{recordId}/data
|
||||||
|
```
|
||||||
|
|
||||||
|
如需覆盖服务端域名,打包前传入:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
api_url=https://your-api-domain.com npm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
后端必须配置 H5 域名,生成短信链接时会使用短路径:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CONFIG_SMS_FRIEND_LINK_BASE_URL=https://your-h5-domain.com
|
||||||
|
生成链接:https://your-h5-domain.com/sms/{recordId}
|
||||||
|
```
|
||||||
BIN
assets/background.png
Normal file
BIN
assets/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/default-avatar.png
Normal file
BIN
assets/default-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 560 B |
46
index.html
Normal file
46
index.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
<title>添加好友</title>
|
||||||
|
<link rel="stylesheet" href="/sms/src/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="login-bg">
|
||||||
|
<img class="bg-image" src="/sms/assets/background.png" alt="" />
|
||||||
|
|
||||||
|
<section class="doctor-card-wrap" aria-live="polite">
|
||||||
|
<div class="doctor-card">
|
||||||
|
<div class="doctor-info">
|
||||||
|
<h1 id="mainTitle" class="doctor-name">医生服务好友</h1>
|
||||||
|
<p id="corpName" class="doctor-hospital">患者精益管理服务平台</p>
|
||||||
|
<p id="friendMeta" class="doctor-dept"></p>
|
||||||
|
<strong id="hintText" class="login-tip">点击下方按钮,添加企业微信好友</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="doctor-avatar">
|
||||||
|
<div class="avatar-board" aria-hidden="true">
|
||||||
|
<div class="avatar avatar-large" id="primaryAvatarFallback">
|
||||||
|
<img id="primaryAvatar" alt="" />
|
||||||
|
<span id="primaryInitial">医</span>
|
||||||
|
</div>
|
||||||
|
<div class="avatar avatar-small" id="secondaryAvatarFallback">
|
||||||
|
<img id="secondaryAvatar" alt="" />
|
||||||
|
<span>患</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="login-btn-wrap">
|
||||||
|
<button id="openButton" class="login-btn" type="button" disabled>加载中</button>
|
||||||
|
</section>
|
||||||
|
<p id="statusText" class="status-text">正在准备好友链接</p>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/sms/config.js"></script>
|
||||||
|
<script src="/sms/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
package.json
Normal file
15
package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "ykt-sms",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "YKT friend invite H5 bridge page",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/build.js",
|
||||||
|
"build:dev": "node scripts/build.js development",
|
||||||
|
"build:prod": "node scripts/build.js production",
|
||||||
|
"package": "node scripts/build.js production && powershell -NoProfile -Command \"Compress-Archive -Path dist\\sms -DestinationPath sms.zip -Force\"",
|
||||||
|
"dev": "node scripts/dev-server.js",
|
||||||
|
"start": "node scripts/dev-server.js",
|
||||||
|
"check": "node --check scripts/dev-server.js && node --check src/main.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
102
scripts/build.js
Normal file
102
scripts/build.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, "..");
|
||||||
|
const dist = path.join(root, "dist");
|
||||||
|
const appDist = path.join(dist, "sms");
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return typeof value === "string" ? value.trim() : value ? String(value).trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = normalizeText(process.argv[2] || process.env.NODE_ENV || "production") || "production";
|
||||||
|
|
||||||
|
function assertNoLocalhost(name, value) {
|
||||||
|
if (!value) return;
|
||||||
|
if (/^(https?:\/\/)?(localhost|127\.0\.0\.1)(?::|\/|$)/i.test(value)) {
|
||||||
|
throw new Error(`${name} must not be localhost when building release package`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertRequired(name, value) {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`${name} is required`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDir(src, dest) {
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||||
|
const srcPath = path.join(src, entry.name);
|
||||||
|
const destPath = path.join(dest, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
copyDir(srcPath, destPath);
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
fs.copyFileSync(srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(filePath, content) {
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnvValue(value) {
|
||||||
|
const text = normalizeText(value);
|
||||||
|
if (
|
||||||
|
(text.startsWith('"') && text.endsWith('"')) ||
|
||||||
|
(text.startsWith("'") && text.endsWith("'"))
|
||||||
|
) {
|
||||||
|
return text.slice(1, -1);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEnvFile(fileName) {
|
||||||
|
const filePath = path.join(root, fileName);
|
||||||
|
if (!fs.existsSync(filePath)) return {};
|
||||||
|
const env = {};
|
||||||
|
const content = fs.readFileSync(filePath, "utf8");
|
||||||
|
for (const line of content.split(/\r?\n/)) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||||
|
const index = trimmed.indexOf("=");
|
||||||
|
if (index <= 0) continue;
|
||||||
|
const key = trimmed.slice(0, index).trim();
|
||||||
|
const value = parseEnvValue(trimmed.slice(index + 1));
|
||||||
|
env[key] = value;
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileEnv = {
|
||||||
|
...loadEnvFile(".env"),
|
||||||
|
...loadEnvFile(`.env.${mode}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiBase = normalizeText(
|
||||||
|
process.env.api_url ||
|
||||||
|
fileEnv.api_url ||
|
||||||
|
"",
|
||||||
|
).replace(/\/+$/, "");
|
||||||
|
|
||||||
|
assertRequired("api_url", apiBase);
|
||||||
|
if (mode === "production") {
|
||||||
|
assertNoLocalhost("api_url", apiBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(dist, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(appDist, { recursive: true });
|
||||||
|
fs.copyFileSync(path.join(root, "index.html"), path.join(appDist, "index.html"));
|
||||||
|
copyDir(path.join(root, "src"), path.join(appDist, "src"));
|
||||||
|
copyDir(path.join(root, "assets"), path.join(appDist, "assets"));
|
||||||
|
writeFile(
|
||||||
|
path.join(appDist, "config.js"),
|
||||||
|
`window.api_url=${JSON.stringify(apiBase)};\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Built ykt-sms to ${appDist}`);
|
||||||
|
console.log(`Route: /sms/{id}`);
|
||||||
|
console.log(`Mode: ${mode}`);
|
||||||
|
console.log(`API base: ${apiBase}`);
|
||||||
103
scripts/dev-server.js
Normal file
103
scripts/dev-server.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const http = require("http");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, "..");
|
||||||
|
const port = Number(process.env.PORT || process.env.CONFIG_SMS_FRIEND_BRIDGE_PORT || 5174);
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return typeof value === "string" ? value.trim() : value ? String(value).trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnvValue(value) {
|
||||||
|
const text = normalizeText(value);
|
||||||
|
if (
|
||||||
|
(text.startsWith('"') && text.endsWith('"')) ||
|
||||||
|
(text.startsWith("'") && text.endsWith("'"))
|
||||||
|
) {
|
||||||
|
return text.slice(1, -1);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEnvFile(fileName) {
|
||||||
|
const filePath = path.join(root, fileName);
|
||||||
|
if (!fs.existsSync(filePath)) return {};
|
||||||
|
const env = {};
|
||||||
|
const content = fs.readFileSync(filePath, "utf8");
|
||||||
|
for (const line of content.split(/\r?\n/)) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||||
|
const index = trimmed.indexOf("=");
|
||||||
|
if (index <= 0) continue;
|
||||||
|
env[trimmed.slice(0, index).trim()] = parseEnvValue(trimmed.slice(index + 1));
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileEnv = {
|
||||||
|
...loadEnvFile(".env"),
|
||||||
|
...loadEnvFile(".env.development"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mimeTypes = {
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".js": "application/javascript; charset=utf-8",
|
||||||
|
".json": "application/json; charset=utf-8",
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
};
|
||||||
|
|
||||||
|
function sendText(res, text, contentType) {
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
});
|
||||||
|
res.end(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendFile(res, filePath) {
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
||||||
|
});
|
||||||
|
fs.createReadStream(filePath).pipe(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAssetPath(urlPath) {
|
||||||
|
const cleanPath = decodeURIComponent(urlPath.split("?")[0]);
|
||||||
|
const assetPath = cleanPath.replace(/^\/sms\//, "/");
|
||||||
|
const candidate = path.resolve(root, assetPath.replace(/^\/+/, ""));
|
||||||
|
if (!candidate.startsWith(root)) return "";
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const urlPath = (req.url || "/").split("?")[0];
|
||||||
|
if (urlPath === "/config.js" || urlPath === "/sms/config.js") {
|
||||||
|
const apiBase =
|
||||||
|
process.env.api_url ||
|
||||||
|
fileEnv.api_url ||
|
||||||
|
"";
|
||||||
|
sendText(
|
||||||
|
res,
|
||||||
|
`window.api_url=${JSON.stringify(apiBase)};\n`,
|
||||||
|
"application/javascript; charset=utf-8",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetPath = resolveAssetPath(req.url || "/");
|
||||||
|
if (assetPath && fs.existsSync(assetPath) && fs.statSync(assetPath).isFile()) {
|
||||||
|
sendFile(res, assetPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendFile(res, path.join(root, "index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(`Friend bridge H5 running at http://localhost:${port}/sms/{id}`);
|
||||||
|
});
|
||||||
130
src/main.js
Normal file
130
src/main.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
(function () {
|
||||||
|
const elements = {
|
||||||
|
title: document.getElementById("mainTitle"),
|
||||||
|
corpName: document.getElementById("corpName"),
|
||||||
|
hintText: document.getElementById("hintText"),
|
||||||
|
openButton: document.getElementById("openButton"),
|
||||||
|
statusText: document.getElementById("statusText"),
|
||||||
|
primaryInitial: document.getElementById("primaryInitial"),
|
||||||
|
primaryAvatar: document.getElementById("primaryAvatar"),
|
||||||
|
primaryAvatarFallback: document.getElementById("primaryAvatarFallback"),
|
||||||
|
secondaryAvatar: document.getElementById("secondaryAvatar"),
|
||||||
|
secondaryAvatarFallback: document.getElementById("secondaryAvatarFallback"),
|
||||||
|
friendMeta: document.getElementById("friendMeta"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let scheme = "";
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return typeof value === "string" ? value.trim() : value ? String(value).trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecordId() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const queryId = normalizeText(params.get("id"));
|
||||||
|
if (queryId) return queryId;
|
||||||
|
|
||||||
|
const match = window.location.pathname.match(/\/sms\/([^/?#]+)/);
|
||||||
|
return match ? decodeURIComponent(match[1]) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApiBase() {
|
||||||
|
const apiUrl = normalizeText(window.api_url);
|
||||||
|
return apiUrl ? apiUrl.replace(/\/+$/, "") : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(text) {
|
||||||
|
elements.statusText.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setError(message) {
|
||||||
|
elements.title.textContent = "链接暂不可用";
|
||||||
|
elements.hintText.textContent = message || "请联系工作人员重新获取好友链接";
|
||||||
|
elements.friendMeta.textContent = "";
|
||||||
|
elements.openButton.textContent = "无法打开";
|
||||||
|
elements.openButton.disabled = true;
|
||||||
|
setStatus("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAvatar(src, friendName, imageEl, fallbackEl) {
|
||||||
|
const avatar = normalizeText(src);
|
||||||
|
if (fallbackEl === elements.primaryAvatarFallback) {
|
||||||
|
elements.primaryInitial.textContent = friendName.slice(0, 1) || "医";
|
||||||
|
}
|
||||||
|
fallbackEl.classList.remove("has-image");
|
||||||
|
imageEl.removeAttribute("src");
|
||||||
|
|
||||||
|
if (!avatar) return;
|
||||||
|
imageEl.onload = function () {
|
||||||
|
fallbackEl.classList.add("has-image");
|
||||||
|
};
|
||||||
|
imageEl.onerror = function () {
|
||||||
|
fallbackEl.classList.remove("has-image");
|
||||||
|
imageEl.removeAttribute("src");
|
||||||
|
};
|
||||||
|
imageEl.src = avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillPage(data) {
|
||||||
|
const friendName = normalizeText(data.friendName) || "医生";
|
||||||
|
const corpName = normalizeText(data.corpName) || "患者精益管理服务平台";
|
||||||
|
const friendJob = normalizeText(data.friendJob || data.job);
|
||||||
|
const friendIntro = normalizeText(data.friendIntro || data.memberTroduce);
|
||||||
|
const friendAvatar = normalizeText(data.friendAvatar || data.avatar);
|
||||||
|
const avatarList = Array.isArray(data.avatarList) ? data.avatarList.map(normalizeText).filter(Boolean) : [];
|
||||||
|
const secondaryAvatar = normalizeText(data.patientAvatar || data.customerAvatar || avatarList[1]);
|
||||||
|
const title = friendName.endsWith("医生") ? `${friendName}服务好友` : `${friendName}医生服务好友`;
|
||||||
|
|
||||||
|
document.title = normalizeText(data.title) || "添加好友";
|
||||||
|
elements.title.textContent = title;
|
||||||
|
elements.corpName.textContent = corpName;
|
||||||
|
elements.friendMeta.textContent = friendJob || friendIntro || "";
|
||||||
|
elements.friendMeta.hidden = !elements.friendMeta.textContent;
|
||||||
|
elements.hintText.textContent = normalizeText(data.hintText) || "点击下方按钮,添加企业微信好友";
|
||||||
|
elements.openButton.textContent = normalizeText(data.actionText) || "去 加 好 友";
|
||||||
|
setAvatar(friendAvatar || avatarList[0], friendName, elements.primaryAvatar, elements.primaryAvatarFallback);
|
||||||
|
setAvatar(secondaryAvatar, "患", elements.secondaryAvatar, elements.secondaryAvatarFallback);
|
||||||
|
elements.openButton.disabled = false;
|
||||||
|
scheme = normalizeText(data.scheme);
|
||||||
|
setStatus("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLink() {
|
||||||
|
const recordId = getRecordId();
|
||||||
|
if (!recordId) {
|
||||||
|
setError("好友链接参数缺失");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
if (!apiBase) {
|
||||||
|
setError("接口地址未配置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await fetch(`${apiBase}/sms/friend-link/${encodeURIComponent(recordId)}/data`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const contentType = response.headers.get("content-type") || "";
|
||||||
|
if (!contentType.includes("application/json")) {
|
||||||
|
setError("好友链接不存在或已失效");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
if (!response.ok || !result.success || !result.data?.scheme) {
|
||||||
|
setError(result.message || "好友链接不存在或已失效");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fillPage(result.data);
|
||||||
|
} catch (error) {
|
||||||
|
setError("好友链接加载失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.openButton.addEventListener("click", function () {
|
||||||
|
if (!scheme) return;
|
||||||
|
window.location.href = scheme;
|
||||||
|
});
|
||||||
|
|
||||||
|
loadLink();
|
||||||
|
})();
|
||||||
281
src/styles.css
Normal file
281
src/styles.css
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--blue: #1677ff;
|
||||||
|
--blue-deep: #1265d8;
|
||||||
|
--text: #171923;
|
||||||
|
--muted: #7a8292;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--shadow: 0 18px 44px rgba(34, 88, 164, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-bg {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
padding-bottom: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 250px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-card-wrap {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: calc(100vw - 48px);
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 160px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
min-height: 210px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 50px 12px 30px;
|
||||||
|
background: linear-gradient(180deg, #dbe8ff 0%, #fff 50.25%);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 16px rgba(59, 124, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-avatar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border: 3px solid #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f2f2f2;
|
||||||
|
box-shadow: 0 1px 4px rgba(59, 124, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-board {
|
||||||
|
position: relative;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
position: absolute;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
background: linear-gradient(160deg, #b9d8ff, #1d79ed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 14%;
|
||||||
|
width: 34%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: -15%;
|
||||||
|
width: 68%;
|
||||||
|
aspect-ratio: 1.1;
|
||||||
|
border-radius: 48% 48% 0 0;
|
||||||
|
background: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar span {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: 28px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar img {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar.has-image img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar.has-image span,
|
||||||
|
.avatar.has-image::before,
|
||||||
|
.avatar.has-image::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-large {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-small {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: linear-gradient(160deg, #d5e8ff, #48a0ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-name {
|
||||||
|
margin: 20px 0 0;
|
||||||
|
color: #1d2129;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-hospital {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #78808f;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.45;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-dept {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 9px 0 0;
|
||||||
|
padding: 0 5px;
|
||||||
|
border: 1px solid rgba(26, 62, 132, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #213e80;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-dept:empty,
|
||||||
|
.doctor-dept[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tip {
|
||||||
|
display: block;
|
||||||
|
margin: 20px 0 0;
|
||||||
|
color: #000;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.45;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn-wrap {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
width: 100vw;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: calc(100vw - 56px);
|
||||||
|
max-width: 300px;
|
||||||
|
height: 48px;
|
||||||
|
line-height: 48px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 24px;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(270deg, #1b5cc8 2.26%, #0877f1 94.33%);
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 124, 255, 0.08);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-indent: 0.18em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
background: #c7d3e4;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
min-height: 20px;
|
||||||
|
margin: 16px 0 0;
|
||||||
|
color: #8b96a8;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.doctor-card-wrap {
|
||||||
|
width: calc(100vw - 36px);
|
||||||
|
margin-top: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-card {
|
||||||
|
min-height: 198px;
|
||||||
|
padding-right: 12px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doctor-name {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tip {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn-wrap {
|
||||||
|
margin-top: 58px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user