纯 CSS+JS 实战:手搓一张支持自定义的通用金色电子奖状(万字详细拆解)

纯 CSS+JS 实战:手搓一张支持自定义的通用金色电子奖状(附 10 讲详细拆解)

无论是在学校给学生发"进步之星",还是给公司员工发"优秀员工",我们经常需要生成一张极具仪式感、且能随时修改内容的电子奖状。

如果每次换人都要重写 HTML,或者依赖外部图片导致网络不佳时图标裂开,这非常不专业。

今天,我将把这个项目拆成 10 个核心逻辑块,像老师教课一样,手把手带大家实现一个"数据与表现分离的通用电子奖状生成器".

代码特点

  • 零外部依赖:不依赖任何图片文件、外部图标库(钉钉图标纯手绘 SVG,绝不会有裂图风险)。
  • 数据与视图分离:改为"学校、公司、个人"均可使用的通用模板。
  • 完美还原纸质奖状质感:双线金边、官方仿宋体、羊皮纸背景。
  • 响应式适配:在手机钉钉、微信中打开,排版依然完美。

第一讲:HTML 基础骨架 ------ 使用"占位符"而非固定死文字

很多开发者写奖状,直接把文字写死,导致后期替换极其痛苦。正确的做法是,把所有需要动态替换的部分加上 id,让 HTML 变成一个纯框架。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
    <title>通用电子奖状生成器</title>
</head>
<body>
    <div class="certificate-box">
        <!-- 学校/公司名称 -->
        <div class="school-title" id="js-school">[请填写学校/公司名称]</div>
        
        <!-- 奖项名称 -->
        <div class="award-title" id="js-award">[请填写奖项名称]</div>
        
        <!-- 获奖人(分开占位,方便换称呼) -->
        <div class="recipient">
            <span id="js-name">[获奖人姓名]</span> <span id="js-title">同学/先生/女士</span>
        </div>
        
        <!-- 颁奖词 -->
        <div class="content">
            在 <strong id="js-event">[请填写考核/项目名称]</strong> 中,<br>
            表现优异,进步显著。<br>
            特发此状,以资鼓励!
        </div>
        
        <div class="footer-area">
            <div class="dingtalk-wrapper">
                <!-- 待会儿用纯 SVG 手绘一个钉钉图标 -->
                <svg class="dingtalk-svg" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
                    <rect width="64" height="64" rx="16" fill="#0089FF" />
                    <path d="M28.06 14.04 L46.04 30.24 L34.7 32.24 L33.46 33.8 L43.28 44.36 L26.46 36.68 L13.58 48.84 L23.4 37.62 L16.46 34.56 L22.8 27.32 L28.06 14.04 Z" fill="white" />
                </svg>
                <span class="publish-text" id="js-source">来自钉钉</span>
            </div>
            <div class="date-wrap">
                <div class="signature" id="js-sign">[请填写落款单位]</div>
                <div class="date-text" id="js-date">[请填写日期]</div>
            </div>
        </div>
    </div>
</body>
</html>

讲深一层: 这里的 ID(如 js-school、js-name)就是给 JavaScript 看的"锚点"。以后换人、换单位,我们绝对不会触碰这一段 HTML 代码,全部留给 JS 去操作,这体现了前端开发的"高内聚、低耦合"思想。


第二讲:建立数据配置中心 ------ 所有人名、单位全归它管

为了做到"公司、学校、个人都能用",我们把所有需要动态替换的内容,提炼成一个独立的 AWARD_DATA 对象。你不懂 JS 也没关系,你只需要看懂中文变量名即可。

javascript 复制代码
// ============================================================
// 核心配置区(就在这里修改,奖状内容立刻变成你的!)
// 不管是给员工、学生,还是给导师发奖,改完这儿就行。
// ============================================================
const AWARD_DATA = {
    schoolName:"",       // 改成你的单位名称
    awardTitle: "",          // 改成你要发的奖项
    recipientName: "",             // 改成获奖人姓名
    recipientTitle: "",              // 改成"先生"、"女士"、"老师"等
    eventName: "", // 改成具体的事件
    publishSource: "",           // 发奖平台(钉钉/微信/企业微信)
    signature: "",         // 落款单位
    issueDate: ""               // 落款日期
};

讲深一层: 这种"数据与视图分离"的做法,是开发大型项目的基石。哪怕是一个小小的奖状生成器,运用了这个思维,以后你要给 100 个人生成奖状,就只需要复制 100 次这段配置代码,改改姓名和日期即可,完全不用去抠 CSS 或 HTML 排版。


第三讲:渲染引擎 ------ 把数据精准注入到 HTML 框架中

光有数据没用,我们需要一个 JavaScript 的渲染函数。它就像快递员一样,把前面配置好的 AWARD_DATA 精准地塞进 HTML 的 id 占位符里。

javascript 复制代码
// 渲染引擎:把数据加载到页面相应的位置
function renderCertificate(data) {
    document.getElementById('js-school').innerText = data.schoolName;
    document.getElementById('js-award').innerText = data.awardTitle;
    document.getElementById('js-name').innerText = data.recipientName;
    document.getElementById('js-title').innerText = data.recipientTitle;
    document.getElementById('js-event').innerText = data.eventName;
    document.getElementById('js-source').innerText = data.publishSource;
    document.getElementById('js-sign').innerText = data.signature;
    document.getElementById('js-date').innerText = data.issueDate;
}

// 页面加载完成后自动触发渲染
window.onload = function() {
    renderCertificate(AWARD_DATA);
};

讲深一层: 我们使用了最基础的 document.getElementById().innerText。在"前端框架(如 Vue/React)"大行其道的今天,原生 JS 依然能极好地胜任这种静态模板填充工作,且没有任何加载依赖包的开销,不到 10 行代码解决了"动态换人"的问题。


第四讲:全局背景环境 ------ 奠定宣纸质感,凸显主体

从这一讲开始,我们正式进入 CSS 美工阶段。纯白背景会让奖状的白边"隐形"且没有质感。

css 复制代码
body { 
    background: #f0e6d2; /* 浅牛皮纸色,模拟老式奖状底纹 */
    margin: 0; 
    padding: 30px; 
    font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif; 
    color: #4a3b1f; /* 深褐色文字,比纯黑更柔和,看着更高级 */
    display: flex; 
    justify-content: center; 
    align-items: center; 
    min-height: 100vh; 
    box-sizing: border-box; 
}

讲深一层: display: flex; justify-content: center; align-items: center; 是 CSS 最强的居中大法,无论屏幕多大,这个奖状永远像挂在墙上一样,完美居中。我们在四周留出了 30px 的呼吸空间,不会让边框顶到屏幕边缘。


第五讲:核心金边容器 ------ 拒绝图片,纯 CSS 画双线金边

很多框架实现双线金边需要用两张图堆叠,这在电子奖状里极度糟糕。我们只用一行 CSS 属性搞定。

css 复制代码
.certificate-box {
    width: 100%; 
    max-width: 680px; 
    background: #ffffff;
    /* 灵魂代码:6px 宽度 + double 双线模式 + 金色 */
    border: 6px double #d4af37; 
    border-radius: 6px; 
    padding: 50px 40px 30px 40px;
    text-align: center; 
    box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}

讲深一层: border: 6px double #d4af37; 中的 double 是 CSS 原生支持的属性值。它能在 6px 的线段内自动生成 "外细线-空白-内细线" 的精密结构,和传统印刷的烫金工艺如出一辙。加上淡淡的 box-shadow,一张纸的厚度感跃然纸上。


效果图↓

第六讲:官方字体与排版 ------ 不使用卡通体

学校及企事业单位的奖状,通常使用"仿宋"或"楷体"。我们通过字体栈来优化不同电脑的显示效果。

css 复制代码
.school-title {
    font-family: "华文仿宋", "STFangsong", "仿宋", serif;
    font-size: 26px; font-weight: bold; color: #b8860b; /* 深金色 */
    letter-spacing: 4px; 
    display: inline-block; /* 控制下划线长度 */
    border-bottom: 2px solid #e8d5a3;
    padding-bottom: 10px;
}

.award-title {
    font-family: "华文仿宋", "STFangsong", "仿宋", serif;
    font-size: 42px; font-weight: 700; color: #d4af37;
    letter-spacing: 6px;
    text-shadow: 1px 1px 0px rgba(0,0,0,0.03);
}

讲深一层: 注意 .school-title 里的 display: inline-block;。如果没有这行代码,底部的金线会横跨整个奖状,非常突兀;加上这个属性,金线就会老老实实地缩在文字的正下方,这是排版印刷非常讲究的细节。


第七讲:姓名专属金线 ------ 凸显获奖人的荣誉感

奖状的名字底端,必须有一条细致的"修饰线"。我们利用 HTML 的 精准定位。

css 复制代码
.recipient { font-size: 24px; font-weight: 700; letter-spacing: 2px; }
.recipient span { 
    border-bottom: 2px solid #d4af37; 
    padding: 0 4px; 
}

讲深一层: 使用 padding: 0 4px; 能巧妙地让金线左右两端都比文字长出 4 像素,避免了金线紧贴文字的死板感。这些细微的缝隙正是纸质印刷品的魅力所在。


第八讲:无图化发布标志 ------ 纯 SVG 绘制钉钉图标

很多人用 标签引入钉钉图标,一旦目标机器断网,或者用浏览器打开本地文件,就会出现"图片裂开"。为了让奖状在任何网络环境下都能 100% 显示,我们手写一个钉钉图标的 SVG 代码。

css 复制代码
.dingtalk-svg { width: 36px; height: 36px; }

(SVG 路径已在第一讲的 HTML 骨架中预埋。它由蓝色的圆角矩形和白色的翅膀路径组成,无需任何外部资源)。

讲深一层: SVG 是矢量图,字体小,放大 1000 倍也不会模糊,而且它是纯文本代码。把这个图标内嵌在 HTML 中,是一个奖状保持"科技感"和"普适性"的终极解决方案。


第九讲:移动端自适应 ------ 钉钉/微信里绝不乱码

很多奖状发到手机钉钉群里就变形了,我们必须用 @media 专门给手机做适配。

css 复制代码
@media (max-width: 600px) {
    body { padding: 15px; }
    .certificate-box { padding: 30px 20px; }
    .footer-area { flex-direction: column; align-items: center; gap: 20px; }
}

讲深一层: 当屏幕宽度小于 600px(手机标准)时,底部原来左右并排的"发布平台"和"落款",会通过 flex-direction: column; 变为垂直排列,并且全部居中。这样无论手机多窄,都不会出现内容被挤出屏幕的情况。


第十讲:实战应用演示 ------ 如何给公司、学校和个人生成奖状?

这一讲教你核心用法。我前面写的所有代码,你都不用管,你只需要进入 AWARD_DATA 配置区修改参数。

场景 1:给公司的优秀员工发奖

只需修改配置区:

javascript 复制代码
const AWARD_DATA = {
    schoolName: "XX科技股份有限公司",
    awardTitle: "最佳员工奖",
    recipientName: "王小明",
    recipientTitle: "先生",
    eventName: "2026年度Q3业绩冲刺",
    publishSource: "来自企业微信",
    signature: "XX科技股份有限公司",
    issueDate: "2026年9月"
};

场景 2:给辅导班老师发感谢状

只需修改配置区:

javascript 复制代码
const AWARD_DATA = {
    schoolName: "某教育培训机构",
    awardTitle: "优秀教师奖",
    recipientName: "李红梅",
    recipientTitle: "老师",
    eventName: "2026年秋季教研成果展示",
    publishSource: "来自钉钉",
    signature: "某教育培训机构",
    issueDate: "2026年8月"
};

配置好后,保存 HTML 文件在浏览器刷新,奖状上的文字就会瞬间变为你填的内容。这就是"配置驱动"带来的极高灵活性。


最终完整版开源代码(一键复制即可使用)

如果你不需要自己琢磨原理,只想在电脑里存一个随时能改的电子奖状,请直接复制下方完整的代码,保存为 award.html。双击打开,在顶部的 AWARD_DATA 里修改文字,你的奖状就做出来了。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
    <title>商务通用 · 电子荣誉证书</title>
    <style>
        /* 背景:高质感米白底,模拟羊皮纸 */
        body { 
            background: #e8e2d5; 
            margin: 0; 
            padding: 30px; 
            font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif; 
            color: #3a2e1b; 
            display: flex; 
            justify-content: center; 
            align-items: center; 
            min-height: 100vh; 
            box-sizing: border-box; 
        }

        /* ============================================================
           极简入场动画:只有0.8s的纸张展开感
           ============================================================ */
        @keyframes unfoldPaper {
            0% { opacity: 0; transform: scale(0.96) translateY(15px); }
            100% { opacity: 1; transform: scale(1) translateY(0); }
        }

        /* ============================================================
           证书外框(外层金边 + 内层金边)
           ============================================================ */
        .certificate-box {
            width: 100%;
            max-width: 780px;
            background: #ffffff;
            position: relative;
            /* 外层双线金边 */
            border: 5px double #d4af37; 
            border-radius: 10px;
            padding: 0; 
            animation: unfoldPaper 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
            box-shadow: 0 10px 35px rgba(0,0,0,0.08);
            overflow: visible; /* 允许红丝带伸出 */
        }

        /* 内层细金线框(增强印刷定制感) */
        .certificate-inner {
            margin: 15px;
            padding: 45px 50px 30px 50px;
            border: 1px solid #d4af37;
            border-radius: 5px;
            position: relative;
            background: #ffffff;
            min-height: 400px;
        }

        /* ============================================================
           顶部大红花结(纯 SVG 手绘)
           ============================================================ */
        .ribbon-container {
            position: absolute;
            top: -32px;
            left: 50%;
            transform: translateX(-50%);
            width: 130px;
            height: 70px;
            z-index: 10;
        }

        .ribbon-container svg {
            width: 100%;
            height: 100%;
            filter: drop-shadow(0 2px 6px rgba(0,0,0,0.15));
        }

        /* ============================================================
           大印章(纯CSS绘制,加粗边框)
           ============================================================ */
        .official-seal {
            position: absolute;
            bottom: 30px;
            right: 35px;
            width: 105px;
            height: 105px;
            border: 5px solid #b32d26;
            border-radius: 50%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: #b32d26;
            background: rgba(255, 255, 255, 0.85); /* 印泥渗透感 */
            z-index: 5;
            transform: rotate(-12deg);
            box-shadow: 0 0 15px rgba(179, 45, 38, 0.08);
            opacity: 0.85;
        }
        .official-seal .star {
            font-size: 28px;
            line-height: 1;
            margin-bottom: 2px;
            font-weight: bold;
        }
        .official-seal .seal-text {
            font-family: "华文楷体", "STKaiti", "楷体", serif;
            font-size: 13px;
            font-weight: bold;
            line-height: 1.3;
            text-align: center;
            letter-spacing: 1px;
            padding: 0 5px;
        }

        /* ============================================================
           高级排版(全套楷体/仿宋,加粗金线)
           ============================================================ */
        .org-title {
            font-family: "华文楷体", "STKaiti", "楷体", serif;
            font-size: 28px;
            font-weight: bold;
            color: #8b6508;
            letter-spacing: 4px;
            margin-bottom: 5px;
            text-align: center;
        }

        .award-title {
            font-family: "华文楷体", "STKaiti", "楷体", serif;
            font-size: 42px;
            font-weight: 700;
            color: #b32d26; /* 证书专用深红色 */
            letter-spacing: 8px;
            margin: 10px 0 20px 0;
            text-align: center;
            text-shadow: 0.5px 0.5px 1px rgba(0,0,0,0.03);
        }

        .recipient-area {
            font-size: 26px;
            font-weight: bold;
            color: #3a2e1b;
            margin: 15px 0 25px 0;
            text-align: center;
            letter-spacing: 2px;
        }
        .recipient-area span {
            border-bottom: 2px solid #d4af37;
            padding: 0 6px;
            font-family: "华文楷体", "STKaiti", "楷体", serif;
        }

        .content-area {
            font-family: "华文楷体", "STKaiti", "楷体", serif;
            font-size: 20px;
            line-height: 2.2;
            color: #3a2e1b;
            margin: 0 0 40px 0;
            text-align: center;
            letter-spacing: 1.5px;
            padding: 0 20px;
        }
        .content-area strong {
            font-weight: bold;
        }

        /* 底部双栏布局(去掉钉钉后留白更显官方) */
        .footer-area {
            display: flex;
            justify-content: space-between;
            align-items: flex-end;
            margin-top: 10px;
            padding-top: 15px;
            border-top: 1px solid #d4af37;
        }

        .footer-left {
            flex: 1;
            font-size: 14px;
            color: #888;
            text-align: left;
            letter-spacing: 1px;
        }

        .date-wrap {
            text-align: right;
            padding-right: 10px;
        }
        .signature {
            font-family: "华文楷体", "STKaiti", "楷体", serif;
            font-size: 20px;
            color: #3a2e1b;
            font-weight: 600;
            margin-bottom: 4px;
            letter-spacing: 4px;
        }
        .date-text {
            font-size: 16px;
            color: #555;
            font-weight: 500;
            letter-spacing: 1px;
        }

        /* 移动端适配 */
        @media (max-width: 600px) {
            body { padding: 15px; }
            .certificate-inner { padding: 40px 20px 20px 20px; }
            .org-title { font-size: 22px; letter-spacing: 3px; }
            .award-title { font-size: 32px; letter-spacing: 6px; }
            .recipient-area { font-size: 22px; }
            .content-area { font-size: 18px; padding: 0; }
            .official-seal { width: 80px; height: 80px; bottom: 15px; right: 15px; }
            .official-seal .seal-text { font-size: 11px; }
            .official-seal .star { font-size: 22px; }
            .ribbon-container { width: 100px; height: 50px; top: -25px; }
            .footer-area { flex-direction: column; align-items: flex-end; gap: 10px; }
            .footer-left { text-align: right; width: 100%; }
        }
    </style>
</head>
<body>
    
    <div class="certificate-box">
        <div class="certificate-inner">
            
            <!-- 顶部红丝带大花 -->
            <div class="ribbon-container">
                <svg viewBox="0 0 130 70" xmlns="http://www.w3.org/2000/svg">
                    <!-- 左右下飘带 -->
                    <path d="M20 30 C10 50, 15 65, 25 70 L50 50 C40 40, 25 35, 20 30 Z" fill="#b32d26" />
                    <path d="M110 30 C120 50, 115 65, 105 70 L80 50 C90 40, 105 35, 110 30 Z" fill="#b32d26" />
                    <!-- 上方主花瓣 -->
                    <path d="M30 25 C35 5, 95 5, 100 25 C105 45, 90 55, 65 60 C40 55, 25 45, 30 25 Z" fill="#d43d34" />
                    <!-- 下方辅花瓣 -->
                    <path d="M35 40 C40 65, 90 65, 95 40 C100 30, 80 20, 65 20 C50 20, 30 30, 35 40 Z" fill="#c22c24" />
                    <!-- 中心亮色花蕊 -->
                    <circle cx="65" cy="35" r="15" fill="#f17a74" />
                    <circle cx="65" cy="35" r="6" fill="#b32d26" />
                </svg>
            </div>

            <!-- 单位/公司抬头 -->
            <div class="org-title" id="js-org">XX单位</div>
            
            <!-- 奖项名称 -->
            <div class="award-title" id="js-award">荣誉证书</div>
            
            <!-- 获奖人 -->
            <div class="recipient-area">
                <span id="js-name">获奖人</span> <span id="js-title">先生/女士</span>
            </div>
            
            <!-- 正文内容 -->
            <div class="content-area">
                在 <strong id="js-event">XX年度评优活动</strong> 中,<br>
                表现优异,成绩突出。<br>
                特发此证,以资鼓励!
            </div>

            <!-- 底部(去掉钉钉,完全官方留白) -->
            <div class="footer-area">
                <div class="footer-left">
                    <!-- 可以放一句官方声明,也可以空着,显得更干净 -->
                </div>
                <div class="date-wrap">
                    <div class="signature" id="js-sign">发证单位</div>
                    <div class="date-text" id="js-date">XXXX年XX月</div>
                </div>
            </div>

            <!-- 右下角官方大印章 -->
            <div class="official-seal">
                <div class="star">★</div>
                <div class="seal-text" id="js-seal">发证单位</div>
            </div>

        </div>
    </div>

    <script>
        /* ============================================================
           数据配置区(支持学校、公司、乙方表彰,改这里即可)
           ============================================================ */
        const AWARD_DATA = {
            orgName: "XX有限公司/学校",     // 抬头机构
            awardTitle: "优秀员工/进步奖",    // 奖状名称
            recipientName: "王小明",          // 获奖人姓名
            recipientTitle: "先生/女士/同学", // 称呼
            awardEvent: "2026年Q3项目考核",   // 具体事由
            signature: "XX单位",              // 落款机构
            seal: "XX单位\n颁发",             // 印章内容(支持 \n 换行)
            issueDate: "2026年8月"            // 日期
        };

        /* ============================================================
           数据渲染引擎
           ============================================================ */
        function renderCertificate(data) {
            document.getElementById('js-org').innerText = data.orgName;
            document.getElementById('js-award').innerText = data.awardTitle;
            document.getElementById('js-name').innerText = data.recipientName;
            document.getElementById('js-title').innerText = data.recipientTitle;
            document.getElementById('js-event').innerText = data.awardEvent;
            document.getElementById('js-sign').innerText = data.signature;
            document.getElementById('js-seal').innerHTML = data.seal.replace('\n', '<br>');
            document.getElementById('js-date').innerText = data.issueDate;
        }

        window.onload = function() {
            renderCertificate(AWARD_DATA);
        };
    </script>

</body>
相关推荐
用户847181054192 小时前
DeepAgents.js 教程 06—— 跨会话长期记忆(Memory与AGENTS.md)
javascript·agent
阿黎梨梨2 小时前
React 表单处理与性能优化:从入门到进阶
前端·react.js
Whbbit19992 小时前
发布自己的 shadcn-vue 扩展组件,并支持 CLI 安装
前端·开源
Tian_Hang2 小时前
Vue3 父子组件通讯
前端·javascript·vue.js
breeze jiang2 小时前
React memo、useCallback 与 useMemo:父组件多状态时如何减少无效渲染
前端·javascript·react.js
小黑技术栈2 小时前
Java前端基础到入门——16day
java·开发语言·前端
朱涛的自习室3 小时前
从 Prompt 到 Graph:AI 工程的进化史
android·前端·人工智能
敲代码的玉米C4 小时前
让两个模型一写一审
前端·人工智能·架构
代码不加糖4 小时前
common.js和es6中模块引l入的区别?
前端·javascript·es6