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