Repo .git was damaged (refs/ missing, index truncated, object store near-empty). Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak, then full worktree committed as a rollback baseline.
1529 lines
60 KiB
HTML
1529 lines
60 KiB
HTML
{{template "base/header" .}}
|
||
|
||
<!-- 二维码预览依赖 jQuery(同步加载,保证下方 easy.qrcode 初始化就绪);jQuery 统一使用 3.7.1 -->
|
||
<script src="https://cdnjs.snrat.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||
<script src='https://r2.337845818.best/tamabox/cdn/js/remake/easy.qrcode.min.js' type="text/javascript" charset="utf-8"></script>
|
||
|
||
<!-- Pickr 颜色选择器 -->
|
||
<link rel="stylesheet" href="https://r2.337845818.best/tamabox/cdn/lib/pickr/1.10.1/nano.min.css"/>
|
||
<script src="https://r2.337845818.best/tamabox/cdn/lib/pickr/1.10.1/pickr.min.js"></script>
|
||
|
||
<!-- Cropper.js 头像/背景裁剪(迁 cdnjs.snrat.com,sha256 与官方一致;R2 原存非 min 版,snrat 为 min 版语义等价) -->
|
||
<link rel="stylesheet" href="https://cdnjs.snrat.com/ajax/libs/cropperjs/1.6.2/cropper.min.css"/>
|
||
<script src="https://cdnjs.snrat.com/ajax/libs/cropperjs/1.6.2/cropper.min.js"></script>
|
||
|
||
<style>
|
||
.range-input-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.range-input-group input[type="range"] {
|
||
flex: 1;
|
||
}
|
||
.range-input-group input[type="number"] {
|
||
width: 70px;
|
||
}
|
||
.range-input-group .range-value {
|
||
min-width: 40px;
|
||
text-align: right;
|
||
}
|
||
.color-picker {
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
.preview-card {
|
||
background: #fff;
|
||
padding: 20px;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
}
|
||
.preview-card .qrcode-wrapper {
|
||
width: 100%;
|
||
max-width: 220px;
|
||
margin: 0 auto;
|
||
}
|
||
.theme-btn {
|
||
margin: 2px;
|
||
font-size: 0.8rem;
|
||
}
|
||
.social-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.social-item label.checkbox-label {
|
||
margin: 0;
|
||
white-space: nowrap;
|
||
}
|
||
.social-item input[type="text"] {
|
||
flex: 1;
|
||
}
|
||
.social-item input[type="text"]:invalid {
|
||
border-color: #f0506e;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.preview-card {
|
||
margin-top: 20px;
|
||
}
|
||
}
|
||
|
||
/* 面板切换:与提问箱主页一致的胶囊样式(用户设置 / 提问 / 私聊 / 投票) */
|
||
.profile-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 4px;
|
||
margin-bottom: 18px;
|
||
border: 1px solid var(--tb-border, #e5e5e5);
|
||
border-radius: 9px;
|
||
background: var(--tb-surface-2, #fafafa);
|
||
}
|
||
.profile-tab {
|
||
flex: 1;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
padding: 9px 8px;
|
||
background: transparent;
|
||
color: var(--tb-text-muted, #666);
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
white-space: nowrap;
|
||
transition: background 0.2s ease, color 0.2s ease;
|
||
}
|
||
.profile-tab:hover { color: var(--tb-primary, #1e87f0); }
|
||
.profile-tab.is-active {
|
||
background: var(--tb-surface, #fff);
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10);
|
||
color: var(--tb-primary, #1e87f0);
|
||
font-weight: 600;
|
||
}
|
||
.profile-panel[hidden] { display: none !important; }
|
||
html[data-theme="dark"] .profile-tabs {
|
||
border-color: var(--tb-border);
|
||
background: var(--tb-surface-2);
|
||
}
|
||
html[data-theme="dark"] .profile-tab { color: var(--tb-text-muted); }
|
||
html[data-theme="dark"] .profile-tab.is-active { background: var(--tb-surface); color: var(--tb-primary); }
|
||
|
||
/* 功能控制卡片(部分模板 user/feature-control-form 使用) */
|
||
.feat-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
.feat-card-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
.feat-card-head h3 { font-size: 1.05rem; }
|
||
.feat-status {
|
||
flex: 0 0 auto;
|
||
font-size: 0.72rem;
|
||
line-height: 1.6;
|
||
padding: 0 8px;
|
||
border-radius: 2px;
|
||
}
|
||
.feat-status.is-open { background: #32d296; color: #fff; }
|
||
.feat-status.is-closed { background: #f0506e; color: #fff; }
|
||
.feat-hint {
|
||
display: block;
|
||
font-size: 0.78rem;
|
||
line-height: 1.5;
|
||
color: #999;
|
||
}
|
||
.feat-field { display: block; }
|
||
.feat-field > label > span {
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
font-size: 0.875rem;
|
||
}
|
||
.feat-field-label {
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
font-size: 0.875rem;
|
||
}
|
||
/* 收集模式三选一(长期开放 / 定时收集 / 循环定时),胶囊样式复用 .feat-day */
|
||
.feat-modes {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.is-hidden { display: none !important; }
|
||
.feat-field input[type="datetime-local"],
|
||
.feat-field input[type="number"] { width: 100%; }
|
||
/* 循环定时(每周开放日 + 每日时间段) */
|
||
.feat-schedule { display: flex; flex-direction: column; gap: 8px; }
|
||
.feat-schedule-days {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
.feat-day {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
padding: 3px 8px;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 999px;
|
||
font-size: 0.78rem;
|
||
color: #666;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.feat-day input { margin: 0; }
|
||
.feat-day.is-on {
|
||
border-color: #1e87f0;
|
||
background: rgba(30, 135, 240, 0.08);
|
||
color: #1e87f0;
|
||
}
|
||
.feat-schedule-times {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
.feat-schedule-times label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 0.82rem;
|
||
color: #666;
|
||
}
|
||
.feat-schedule-times input[type="time"] {
|
||
width: 130px;
|
||
padding: 4px 8px;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 4px;
|
||
}
|
||
html[data-theme="dark"] .feat-day {
|
||
border-color: var(--tb-border);
|
||
color: var(--tb-text-muted);
|
||
}
|
||
html[data-theme="dark"] .feat-day.is-on {
|
||
border-color: #1e87f0;
|
||
background: rgba(30, 135, 240, 0.14);
|
||
color: #1e87f0;
|
||
}
|
||
html[data-theme="dark"] .feat-schedule-times input[type="time"] {
|
||
border-color: var(--tb-border);
|
||
background: var(--tb-surface);
|
||
color: var(--tb-text);
|
||
}
|
||
.feat-quota-line {
|
||
font-size: 0.82rem;
|
||
color: #666;
|
||
text-align: center;
|
||
}
|
||
.feat-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
align-items: center;
|
||
margin-top: auto;
|
||
}
|
||
.feat-actions form { margin: 0; }
|
||
html[data-theme="dark"] .feat-card {
|
||
border-color: var(--tb-border);
|
||
background: var(--tb-surface);
|
||
}
|
||
html[data-theme="dark"] .feat-card-head h3 { color: var(--tb-text); }
|
||
html[data-theme="dark"] .feat-hint,
|
||
html[data-theme="dark"] .feat-quota-line { color: var(--tb-text-muted); }
|
||
|
||
/* 私聊面板聚合卡:chat 专属(功能开关/收集窗口 + 消息上限设置)同卡呈现,
|
||
feat 卡去掉自身边框避免嵌套;第二区 legend 降级为小节标题 */
|
||
.chat-zone .feat-card {
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
}
|
||
.chat-zone hr { margin: 14px 0; }
|
||
.chat-zone .uk-legend {
|
||
font-size: 1.05rem;
|
||
margin: 0 0 10px;
|
||
}
|
||
|
||
/* 标签块输入:回车或逗号成词,每个词可独立删除;原 input 隐藏后作为提交载体 */
|
||
.tb-tag-box {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 8px;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 4px;
|
||
background: #fff;
|
||
cursor: text;
|
||
}
|
||
.tb-tag-box:focus-within { border-color: #1e87f0; }
|
||
.tb-tag-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 2px 8px;
|
||
border-radius: 3px;
|
||
background: #1e87f0;
|
||
color: #fff;
|
||
font-size: 0.875rem;
|
||
line-height: 1.8;
|
||
}
|
||
.tb-tag-chip button {
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
color: inherit;
|
||
font-size: 1rem;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
opacity: 0.75;
|
||
}
|
||
.tb-tag-chip button:hover { opacity: 1; }
|
||
.tb-tag-entry {
|
||
flex: 1;
|
||
min-width: 120px;
|
||
border: 0;
|
||
outline: 0;
|
||
background: transparent;
|
||
color: inherit;
|
||
font-size: 0.875rem;
|
||
}
|
||
.tb-tag-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-top: 4px;
|
||
font-size: 0.75rem;
|
||
color: #999;
|
||
}
|
||
.tb-tag-error { color: #f0506e; }
|
||
html[data-theme="dark"] .tb-tag-box {
|
||
border-color: var(--tb-border);
|
||
background: var(--tb-surface);
|
||
color: var(--tb-text);
|
||
}
|
||
html[data-theme="dark"] .tb-tag-entry { color: var(--tb-text); }
|
||
html[data-theme="dark"] .tb-tag-meta { color: var(--tb-text-muted); }
|
||
|
||
/* ===== profile account 面板卡片化(复用 UIkit 变量体系,暗色自动跟随) ===== */
|
||
.tb-pcard {
|
||
background: #fff;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 10px;
|
||
padding: 13px 14px 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.tb-pcard-title {
|
||
margin: 0 0 10px;
|
||
font-size: 1.02rem;
|
||
font-weight: 600;
|
||
}
|
||
/* 全页纵向压缩:面板内表单字段间距由 UIkit 默认 20px 收紧到 12px */
|
||
.profile-panel .uk-margin { margin-bottom: 12px; }
|
||
.tb-pcard-title .tb-pcard-extra {
|
||
float: right;
|
||
font-size: 0.76rem;
|
||
font-weight: 400;
|
||
color: #999;
|
||
line-height: 1.6;
|
||
}
|
||
/* 头像与背景预览:两项同行、整体居中;头像框与背景框“显示高度”一致(150px) */
|
||
.tb-media-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
align-items: flex-start;
|
||
gap: 14px 32px;
|
||
}
|
||
.tb-media-item {
|
||
/* 头像列与背景列等宽(280px),内部各自居中 → 两个“更换 xx”组件观感一致 */
|
||
flex: 0 0 280px;
|
||
max-width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
.tb-media-frame {
|
||
display: block;
|
||
flex: none;
|
||
border: 1px solid #e5e5e5;
|
||
background: #f7f7f7;
|
||
overflow: hidden;
|
||
}
|
||
.tb-media-frame.tb-frame-avatar {
|
||
width: 150px;
|
||
height: 150px;
|
||
border-radius: 50%;
|
||
}
|
||
.tb-media-frame.tb-frame-bg {
|
||
/* 16:9 且高 150 ≈ 头像框直径 150:两框视觉等高 */
|
||
width: 267px;
|
||
max-width: 100%;
|
||
aspect-ratio: 16 / 9;
|
||
border-radius: 8px;
|
||
}
|
||
.tb-media-frame img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
.tb-ratio-tag {
|
||
display: inline-block;
|
||
margin: 8px 0 0;
|
||
font-size: 0.72rem;
|
||
line-height: 1;
|
||
color: #666;
|
||
background: #f4f4f4;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 999px;
|
||
padding: 4px 9px;
|
||
}
|
||
.tb-file-hint {
|
||
display: block;
|
||
font-size: 0.78rem;
|
||
line-height: 1.5;
|
||
color: #999;
|
||
margin-top: 6px;
|
||
}
|
||
.tb-media-actions { margin-top: 10px; }
|
||
/* 更换头像 / 更换背景两个按钮同宽 */
|
||
.tb-media-actions .uk-button { min-width: 150px; }
|
||
/* 卡片级保存按钮行(居中):头像背景、基础资料、二维码、社交、改密各块共用 */
|
||
.tb-card-save,
|
||
.tb-media-save {
|
||
margin-top: 6px;
|
||
text-align: center;
|
||
}
|
||
/* 账号操作:导出两按钮同行、退出登录与停用同行;按钮组居中、说明行小字居中 */
|
||
.tb-account-actions { margin-top: 0; }
|
||
.tb-account-pair {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 10px 14px;
|
||
padding: 8px 0;
|
||
}
|
||
.tb-account-pair form { margin: 0; }
|
||
.tb-account-note {
|
||
display: block;
|
||
margin: 0 0 10px;
|
||
text-align: center;
|
||
font-size: 0.78rem;
|
||
line-height: 1.6;
|
||
color: #999;
|
||
}
|
||
.tb-account-note:last-child { margin-bottom: 0; }
|
||
/* 裁剪弹窗:图片区域限制在视口内,移动端可用 */
|
||
.tb-crop-stage {
|
||
max-width: 100%;
|
||
text-align: center;
|
||
}
|
||
.tb-crop-stage img {
|
||
display: inline-block;
|
||
max-width: 100%;
|
||
max-height: 58vh;
|
||
}
|
||
#media-crop-modal .uk-modal-dialog { width: 640px; }
|
||
.tb-crop-hint { margin-top: 12px; }
|
||
html[data-theme="dark"] .tb-pcard {
|
||
border-color: var(--tb-border);
|
||
background: var(--tb-surface);
|
||
}
|
||
html[data-theme="dark"] .tb-pcard-title { color: var(--tb-text); }
|
||
html[data-theme="dark"] .tb-pcard-extra,
|
||
html[data-theme="dark"] .tb-file-hint { color: var(--tb-text-muted); }
|
||
html[data-theme="dark"] .tb-media-frame {
|
||
border-color: var(--tb-border);
|
||
background: var(--tb-surface-2);
|
||
}
|
||
html[data-theme="dark"] .tb-ratio-tag {
|
||
color: var(--tb-text-muted);
|
||
border-color: var(--tb-border);
|
||
background: var(--tb-surface-2);
|
||
}
|
||
html[data-theme="dark"] .tb-account-note { color: var(--tb-text-muted); }
|
||
html[data-theme="dark"] .tb-pcard .uk-accordion-title {
|
||
color: var(--tb-text);
|
||
}
|
||
html[data-theme="dark"] .tb-pcard .uk-accordion-title:hover {
|
||
color: var(--tb-primary);
|
||
}
|
||
/* 移动端:更紧凑的留白与全宽操作区 */
|
||
@media (max-width: 640px) {
|
||
.tb-pcard { padding: 13px 12px 8px; }
|
||
.tb-media-row { gap: 14px; }
|
||
.tb-media-item { flex-basis: 100%; width: auto; max-width: none; }
|
||
.tb-media-frame.tb-frame-avatar { width: 120px; height: 120px; }
|
||
.tb-media-frame.tb-frame-bg { width: 100%; max-width: none; }
|
||
.tb-media-actions .uk-button,
|
||
.tb-media-save .uk-button,
|
||
.tb-card-save .uk-button { width: 100%; }
|
||
}
|
||
</style>
|
||
|
||
<!-- 提示条置于面板之外:任何面板保存后的 flash 都可见(面板会被 hidden 隐藏) -->
|
||
{{template "base/alert" .}}
|
||
|
||
<!-- 头像/背景裁剪弹窗(Cropper.js):确认后把裁剪结果写回对应的文件域,随主表单一并提交 -->
|
||
<div id="media-crop-modal" uk-modal>
|
||
<div class="uk-modal-dialog">
|
||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||
<div class="uk-modal-header">
|
||
<h2 class="uk-modal-title">{{t .LangCode "profile.cropTitle"}}</h2>
|
||
</div>
|
||
<div class="uk-modal-body">
|
||
<div class="tb-crop-stage">
|
||
<img id="media-crop-image" alt="">
|
||
</div>
|
||
<p class="uk-text-meta tb-crop-hint" id="media-crop-hint"></p>
|
||
</div>
|
||
<div class="uk-modal-footer uk-text-right">
|
||
<button type="button" class="uk-button uk-button-default uk-modal-close">{{t .LangCode "common.cancel"}}</button>
|
||
<button type="button" class="uk-button uk-button-primary" id="media-crop-confirm">{{t .LangCode "profile.cropConfirm"}}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 面板切换:用户设置 与 提问/私聊/投票 的功能管理分开 -->
|
||
<div class="profile-tabs" role="tablist">
|
||
<button type="button" class="profile-tab{{ if eq .ActiveProfileTab "account" }} is-active{{ end }}" data-profile-tab="account" role="tab" aria-selected="{{ if eq .ActiveProfileTab "account" }}true{{ else }}false{{ end }}">{{t .LangCode "admin.tabAccount"}}</button>
|
||
<button type="button" class="profile-tab{{ if eq .ActiveProfileTab "question" }} is-active{{ end }}" data-profile-tab="question" role="tab" aria-selected="{{ if eq .ActiveProfileTab "question" }}true{{ else }}false{{ end }}">{{t .LangCode "explore.questionsTab"}}</button>
|
||
<button type="button" class="profile-tab{{ if eq .ActiveProfileTab "chat" }} is-active{{ end }}" data-profile-tab="chat" role="tab" aria-selected="{{ if eq .ActiveProfileTab "chat" }}true{{ else }}false{{ end }}">{{t .LangCode "qlist.chatsTab"}}</button>
|
||
<button type="button" class="profile-tab{{ if eq .ActiveProfileTab "vote" }} is-active{{ end }}" data-profile-tab="vote" role="tab" aria-selected="{{ if eq .ActiveProfileTab "vote" }}true{{ else }}false{{ end }}">{{t .LangCode "explore.votesTab"}}</button>
|
||
</div>
|
||
|
||
<!-- ===================== 用户设置面板 ===================== -->
|
||
<div class="profile-panel" data-profile-panel="account"{{ if ne .ActiveProfileTab "account" }} hidden{{ end }}>
|
||
<form id="profile-form" method="post" enctype="multipart/form-data" action="/user/profile/update">
|
||
{{ .CSRFTokenHTML }}
|
||
|
||
<!-- 1 基础资料:账号与展示基础信息 -->
|
||
<section class="tb-pcard">
|
||
<h3 class="tb-pcard-title">{{t .LangCode "profile.card.basic"}}</h3>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.email"}}</label>
|
||
<input class="uk-input" type="text" disabled value="{{.LoggedUser.Email}}">
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.name"}}</label>
|
||
<input name="name" class="uk-input" type="text" value="{{.LoggedUser.Name}}">
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.nameColor"}}</label>
|
||
<div class="color-picker" data-default="{{.LoggedUser.Usernamecolor}}"></div>
|
||
<input type="hidden" name="usernamecolor" value="{{.LoggedUser.Usernamecolor}}">
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.intro"}}</label>
|
||
<input name="intro" class="uk-input" type="text" value="{{.LoggedUser.Intro}}">
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.introColor"}}</label>
|
||
<div class="color-picker" data-default="{{.LoggedUser.Introcolor}}"></div>
|
||
<input type="hidden" name="introcolor" value="{{.LoggedUser.Introcolor}}">
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.lang"}}</label>
|
||
<select name="language" class="uk-select">
|
||
{{ range .SupportedLangs }}
|
||
<option value="{{.Code}}" {{ if eq $.LoggedUser.Language .Code }}selected{{ end }}>{{.Name}}</option>
|
||
{{ end }}
|
||
</select>
|
||
<span class="uk-text-small uk-text-muted">{{t .LangCode "admin.langHint"}}</span>
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.notify"}}</label>
|
||
<label>
|
||
<input name="notify_email" class="uk-checkbox" type="checkbox"
|
||
{{ if eq .LoggedUser.Notify "email"}}checked{{end}}>
|
||
<span class="uk-text-small"> {{t .LangCode "admin.notifyEmail"}}</span>
|
||
</label>
|
||
</div>
|
||
<div class="tb-card-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "profile.saveBasic"}}</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 2 头像与背景:选择文件即弹裁剪框,预览实时更新;头像 1:1、背景 16:9 -->
|
||
<section class="tb-pcard">
|
||
<h3 class="tb-pcard-title">{{t .LangCode "profile.card.avatarBg"}}</h3>
|
||
<div class="tb-media-row">
|
||
<div class="tb-media-item">
|
||
<span class="tb-media-frame tb-frame-avatar">
|
||
<img id="avatar-prev" alt="" src="{{ if .LoggedUser.Avatar }}{{.LoggedUser.Avatar}}{{ else }}{{.DefaultAvatar}}{{ end }}">
|
||
</span>
|
||
<span class="tb-ratio-tag">1:1</span>
|
||
<div class="tb-media-actions">
|
||
<input type="file" name="avatar" id="avatar-input" accept="image/*" class="uk-hidden">
|
||
<button type="button" class="uk-button uk-button-default uk-button-small" data-pick-image="avatar">{{t .LangCode "profile.pickAvatar"}}</button>
|
||
</div>
|
||
<span class="tb-file-hint">{{t .LangCode "profile.avatarCropHint"}}</span>
|
||
</div>
|
||
<div class="tb-media-item">
|
||
<span class="tb-media-frame tb-frame-bg">
|
||
<img id="bg-prev" alt=""{{ if .LoggedUser.Background }} src="{{.LoggedUser.Background}}"{{ end }}>
|
||
</span>
|
||
<span class="tb-ratio-tag">16:9</span>
|
||
<div class="tb-media-actions">
|
||
<input type="file" name="background" id="background-input" accept="image/*" class="uk-hidden">
|
||
<button type="button" class="uk-button uk-button-default uk-button-small" data-pick-image="background">{{t .LangCode "profile.pickBackground"}}</button>
|
||
</div>
|
||
<span class="tb-file-hint">{{t .LangCode "profile.backgroundCropHint"}}</span>
|
||
</div>
|
||
</div>
|
||
<!-- 图片更换后在此即时保存(与主表单共用提交,裁剪结果随文件域一并上传) -->
|
||
<div class="tb-media-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "profile.saveMedia"}}</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 3 二维码样式:默认展开(可折叠),独立保存 -->
|
||
<section class="tb-pcard">
|
||
<ul class="uk-accordion" uk-accordion="multiple: true">
|
||
<li class="uk-open">
|
||
<a class="uk-accordion-title" href="#">{{t .LangCode "profile.card.qr"}}</a>
|
||
<div class="uk-accordion-content">
|
||
<div class="uk-grid" uk-grid>
|
||
<div class="uk-width-2-3@m uk-width-1-1">
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.qrBgColor"}}</label>
|
||
<div class="color-picker" data-default="{{.LoggedUser.Qrcodecolor}}"></div>
|
||
<input type="hidden" name="qrcodecolor" value="{{.LoggedUser.Qrcodecolor}}">
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.qrPdColor"}}</label>
|
||
<div class="color-picker" data-default="{{.LoggedUser.Qrcodepdpcolor}}"></div>
|
||
<input type="hidden" name="qrcodepdpcolor" value="{{.LoggedUser.Qrcodepdpcolor}}">
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.qrFgColor"}}</label>
|
||
<div class="uk-margin-small">
|
||
<label>
|
||
<input type="checkbox" id="use-custom-fg" name="use_custom_qr_color"
|
||
{{ if .LoggedUser.UseCustomQRColor }}checked{{ end }}>
|
||
{{t .LangCode "admin.useCustomFg"}}
|
||
</label>
|
||
</div>
|
||
<div class="color-picker" data-default="{{.LoggedUser.Qrcodebackcolor}}"></div>
|
||
<input type="hidden" name="qrcodebackcolor" value="{{.LoggedUser.Qrcodebackcolor}}">
|
||
<span class="uk-text-small uk-text-muted">{{t .LangCode "admin.customFgHint"}}</span>
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.qrDotSize"}}</label>
|
||
<div class="range-input-group">
|
||
<input name="dotscale" class="uk-range" type="range" step="0.01" min="0" max="1" value="{{.LoggedUser.Dotscale}}">
|
||
<span class="range-value" id="dotscale-val">{{.LoggedUser.Dotscale}}</span>
|
||
<input class="uk-input uk-form-width-xsmall" type="number" step="0.01" min="0" max="1" id="dotscale-num" value="{{.LoggedUser.Dotscale}}">
|
||
</div>
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.qrBgAlpha"}}</label>
|
||
<div class="range-input-group">
|
||
<input name="backgroundimagealpha" class="uk-range" type="range" step="0.01" min="0" max="1" value="{{.LoggedUser.Backgroundimagealpha}}">
|
||
<span class="range-value" id="bgalpha-val">{{.LoggedUser.Backgroundimagealpha}}</span>
|
||
<input class="uk-input uk-form-width-xsmall" type="number" step="0.01" min="0" max="1" id="bgalpha-num" value="{{.LoggedUser.Backgroundimagealpha}}">
|
||
</div>
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.qrBackground"}}</label>
|
||
<div uk-form-custom="target: true">
|
||
<input type="file" name="backgroundimage" accept="image/*" id="backgroundimage-file">
|
||
<input class="uk-input uk-form-width-large" type="text" placeholder="{{t .LangCode "admin.qrBackgroundPlaceholder"}}" disabled>
|
||
</div>
|
||
<span class="uk-text-small uk-text-muted">{{t .LangCode "admin.qrPreviewHint"}}</span>
|
||
</div>
|
||
</div>
|
||
<div class="uk-width-1-3@m uk-width-1-1">
|
||
<div class="preview-card">
|
||
<h4 class="uk-text-center">{{t .LangCode "admin.preview"}}</h4>
|
||
<div class="qrcode-wrapper" id="qrcode-preview"></div>
|
||
<hr class="uk-margin-small">
|
||
<div class="uk-text-small">
|
||
<strong>{{t .LangCode "admin.themePreset"}}</strong>
|
||
<div class="uk-flex uk-flex-wrap uk-flex-center">
|
||
<button type="button" class="uk-button uk-button-default uk-button-small theme-btn" data-theme="default">{{t .LangCode "admin.themeDefault"}}</button>
|
||
<button type="button" class="uk-button uk-button-default uk-button-small theme-btn" data-theme="dark">{{t .LangCode "admin.themeDark"}}</button>
|
||
<button type="button" class="uk-button uk-button-default uk-button-small theme-btn" data-theme="ocean">{{t .LangCode "admin.themeOcean"}}</button>
|
||
<button type="button" class="uk-button uk-button-default uk-button-small theme-btn" data-theme="sunset">{{t .LangCode "admin.themeSunset"}}</button>
|
||
</div>
|
||
</div>
|
||
<div class="uk-margin-small-top">
|
||
<button type="button" id="reset-qr-defaults" class="uk-button uk-button-default uk-button-small uk-width-1-1">{{t .LangCode "admin.resetDefaults"}}</button>
|
||
</div>
|
||
<p class="uk-text-meta uk-margin-small-top">{{t .LangCode "admin.previewHint"}}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
<div class="tb-card-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "profile.saveQR"}}</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 4 社交主页:默认展开(可折叠),独立保存 -->
|
||
<section class="tb-pcard">
|
||
<ul class="uk-accordion" uk-accordion="multiple: true">
|
||
<li class="uk-open">
|
||
<a class="uk-accordion-title" href="#">{{t .LangCode "admin.social"}}</a>
|
||
<div class="uk-accordion-content">
|
||
<span class="uk-text-muted">{{t .LangCode "admin.socialHint"}}</span>
|
||
<div class="uk-margin social-item">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" class="social-enable" data-target="acfunlink"> {{t .LangCode "admin.enableAcfun"}}
|
||
</label>
|
||
<input name="acfunlink" class="uk-input social-input" type="text" placeholder="acfun.cn/xxx" value="{{.LoggedUser.Acfunlink}}">
|
||
</div>
|
||
<div class="uk-margin social-item">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" class="social-enable" data-target="bililink"> {{t .LangCode "admin.enableBili"}}
|
||
</label>
|
||
<input name="bililink" class="uk-input social-input" type="text" placeholder="bilibili.com/xxx" value="{{.LoggedUser.Bililink}}">
|
||
</div>
|
||
<div class="uk-margin social-item">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" class="social-enable" data-target="weibolink"> {{t .LangCode "admin.enableWeibo"}}
|
||
</label>
|
||
<input name="weibolink" class="uk-input social-input" type="text" placeholder="weibo.com/xxx" value="{{.LoggedUser.Weibolink}}">
|
||
</div>
|
||
<div class="uk-margin social-item">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" class="social-enable" data-target="xiaohongshulink"> {{t .LangCode "admin.enableXiaohongshu"}}
|
||
</label>
|
||
<input name="xiaohongshulink" class="uk-input social-input" type="text" placeholder="xiaohongshu.com/xxx" value="{{.LoggedUser.Xiaohongshulink}}">
|
||
</div>
|
||
<div class="uk-margin social-item">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" class="social-enable" data-target="bloglink"> {{t .LangCode "admin.enableBlog"}}
|
||
</label>
|
||
<input name="bloglink" class="uk-input social-input" type="text" placeholder="example.com" value="{{.LoggedUser.Bloglink}}">
|
||
</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
<div class="tb-card-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "profile.saveSocial"}}</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 5 修改密码:保存成功后强制登出,需重新登录 -->
|
||
<section class="tb-pcard">
|
||
<h3 class="tb-pcard-title">{{t .LangCode "profile.card.password"}}</h3>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.oldPassword"}}</label>
|
||
<input name="old_password" class="uk-input" type="password" placeholder="{{t .LangCode "admin.oldPasswordHint"}}" autocomplete="current-password">
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.newPassword"}}</label>
|
||
<input name="new_password" class="uk-input" type="password" placeholder="{{t .LangCode "admin.newPasswordHint"}}" autocomplete="new-password">
|
||
</div>
|
||
<p class="uk-text-small uk-text-muted">{{t .LangCode "profile.pwdNote"}}</p>
|
||
<hr class="uk-margin-small">
|
||
<div class="tb-card-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "profile.savePassword"}}</button>
|
||
</div>
|
||
</section>
|
||
</form>
|
||
|
||
<!-- 6 标签与展示:独立表单,保存即生效 -->
|
||
<section class="tb-pcard">
|
||
<h3 class="tb-pcard-title">{{t .LangCode "admin.tags"}}</h3>
|
||
<form method="post" action="/user/tags/update">
|
||
{{ .CSRFTokenHTML }}
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.tagsHint"}}</label>
|
||
<input name="tags" class="uk-input" type="text" value="{{.LoggedUser.Tags}}" placeholder="{{t .LangCode "admin.tagsPlaceholder"}}"
|
||
data-tb-tag-input
|
||
{{ with $.TagLimits }}data-tb-max="{{ .MaxTags }}" data-tb-max-len="{{ .MaxTagLen }}"{{ end }}
|
||
data-tb-msg-too-many="{{t .LangCode "admin.flash.tagsTooMany"}}"
|
||
data-tb-msg-too-long="{{t .LangCode "admin.flash.tagTooLong"}}"
|
||
data-tb-msg-duplicate="{{t .LangCode "admin.tagInputDuplicate"}}"
|
||
data-tb-msg-hint="{{t .LangCode "admin.tagInputHint"}}">
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label>
|
||
<input name="show_in_home" class="uk-checkbox" type="checkbox" {{ if .LoggedUser.ShowInHome }}checked{{end}}>
|
||
<span class="uk-text-small"> {{t .LangCode "admin.showInHome"}}</span>
|
||
</label>
|
||
</div>
|
||
<div class="tb-card-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "admin.updateTags"}}</button>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
|
||
<!-- 7 页面访问密码:独立表单 -->
|
||
<section class="tb-pcard">
|
||
<h3 class="tb-pcard-title">{{t .LangCode "admin.pagePassword"}}</h3>
|
||
<form method="post" action="/user/profile/update-page-password">
|
||
{{ .CSRFTokenHTML }}
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t .LangCode "admin.pagePasswordHint"}}</label>
|
||
<input name="password" class="uk-input" type="password" placeholder="{{t .LangCode "admin.pagePasswordPlaceholder"}}">
|
||
</div>
|
||
<div class="tb-card-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t .LangCode "admin.updatePagePassword"}}</button>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
|
||
<!-- 8 账号操作:导出两按钮同行;退出登录与停用我的账号同行 -->
|
||
<section class="tb-pcard">
|
||
<h3 class="tb-pcard-title">{{t .LangCode "admin.account"}}</h3>
|
||
<div class="tb-account-actions">
|
||
<div class="tb-account-pair">
|
||
<form action="/user/profile/export" method="post" target="_blank" data-tb-skip-submit-feedback="1">
|
||
{{ .CSRFTokenHTML }}
|
||
<button class="uk-button uk-button-default">{{t .LangCode "admin.export"}}</button>
|
||
</form>
|
||
<form action="/user/profile/export-json" method="post" target="_blank" data-tb-skip-submit-feedback="1">
|
||
{{ .CSRFTokenHTML }}
|
||
<button class="uk-button uk-button-default">{{t .LangCode "admin.exportJSON"}}</button>
|
||
</form>
|
||
</div>
|
||
<span class="tb-account-note">{{t .LangCode "admin.exportHint" (t .LangCode "site.title")}} {{t .LangCode "admin.exportJSONHint"}}</span>
|
||
<div class="tb-account-pair">
|
||
<!-- Logout 使用独立 POST 表单(携带 CSRF token),GET 链接会留下 CSRF 风险 -->
|
||
<form method="post" action="/user/logout">
|
||
{{ .CSRFTokenHTML }}
|
||
<button type="submit" class="uk-button uk-button-danger">{{t .LangCode "admin.logout"}}</button>
|
||
</form>
|
||
<a class="uk-button uk-button-danger" href="/user/profile/deactivate">{{t .LangCode "admin.deactivate"}}</a>
|
||
</div>
|
||
<span class="tb-account-note">{{t .LangCode "admin.deactivateHint" | safe}}</span>
|
||
</div>
|
||
</section>
|
||
</div><!-- /用户设置面板 -->
|
||
|
||
|
||
<!-- ===================== 提问功能管理面板 ===================== -->
|
||
<div class="profile-panel" data-profile-panel="question"{{ if ne .ActiveProfileTab "question" }} hidden{{ end }}>
|
||
{{ with .FeatureQuestion }}{{template "user/feature-control-form" .}}{{ end }}
|
||
|
||
<hr>
|
||
<div class="uk-margin">
|
||
<form method="post" action="/user/harassment/update?tab=question">
|
||
{{ $.CSRFTokenHTML }}
|
||
<legend class="uk-legend">{{t $.LangCode "admin.harassment"}}</legend>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t $.LangCode "admin.askLimit"}}</label><br>
|
||
<label>
|
||
<input name="register_only" class="uk-checkbox" type="checkbox"
|
||
{{ if eq $.LoggedUser.HarassmentSetting "register_only"}}checked{{end}} >
|
||
<span class="uk-text-small"> {{t $.LangCode "admin.registerOnly"}}</span>
|
||
</label>
|
||
</div>
|
||
<div class="tb-card-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t $.LangCode "admin.updateHarassment"}}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 全局屏蔽词:三个功能面板共用同一份(见 user/global-blockwords 部分模板) -->
|
||
{{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=question") }}
|
||
|
||
<hr>
|
||
<div class="uk-margin">
|
||
<form method="post" action="/user/profile/update-ask-password">
|
||
{{ $.CSRFTokenHTML }}
|
||
<legend class="uk-legend">{{t $.LangCode "admin.askPassword"}}</legend>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t $.LangCode "admin.askPasswordHint"}}</label>
|
||
<input name="password" class="uk-input" type="password" placeholder="{{t $.LangCode "admin.askPasswordPlaceholder"}}">
|
||
</div>
|
||
<div class="tb-card-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t $.LangCode "admin.updateAskPassword"}}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div><!-- /提问功能管理面板 -->
|
||
|
||
<!-- ===================== 私聊功能管理面板 ===================== -->
|
||
<div class="profile-panel" data-profile-panel="chat"{{ if ne .ActiveProfileTab "chat" }} hidden{{ end }}>
|
||
<!-- 私聊专属聚合卡:功能开关/收集窗口 + 私聊设置(消息上限)同属私聊,归入一张卡片 -->
|
||
<section class="tb-pcard chat-zone">
|
||
{{ with .FeatureChat }}{{template "user/feature-control-form" .}}{{ end }}
|
||
|
||
<hr>
|
||
<form method="post" action="/user/profile/update-chat-settings">
|
||
{{ $.CSRFTokenHTML }}
|
||
<legend class="uk-legend">{{t $.LangCode "admin.chat"}}</legend>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t $.LangCode "admin.chatMaxMessages"}}</label>
|
||
{{ with $.ChatLimits }}
|
||
<input name="chat_max_messages" class="uk-input uk-width-small" type="number" min="1" max="{{ .MaxChatMessages }}" value="{{ $.LoggedUser.ChatMaxMessages }}">
|
||
{{ end }}
|
||
</div>
|
||
<div class="uk-margin">
|
||
<label class="uk-form-label">{{t $.LangCode "admin.chatDailyLimit"}}</label>
|
||
{{ with $.ChatLimits }}
|
||
<input name="chat_daily_limit" class="uk-input uk-width-small" type="number" min="0" max="{{ .DailyMessageLimitMax }}" value="{{ $.LoggedUser.DailyMessageLimit }}">
|
||
{{ end }}
|
||
<span class="uk-text-muted uk-text-small">{{t $.LangCode "admin.dailyLimitHint"}}</span>
|
||
</div>
|
||
<div class="tb-card-save">
|
||
<button type="submit" class="uk-button uk-button-primary">{{t $.LangCode "admin.updateChat"}}</button>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
|
||
<hr>
|
||
<!-- 全局屏蔽词:提问/私聊/投票三面板共用同一份(见 user/global-blockwords 部分模板) -->
|
||
{{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=chat") }}
|
||
</div><!-- /私聊功能管理面板 -->
|
||
|
||
<!-- ===================== 投票功能管理面板 ===================== -->
|
||
<div class="profile-panel" data-profile-panel="vote"{{ if ne .ActiveProfileTab "vote" }} hidden{{ end }}>
|
||
{{ with .FeatureVote }}{{template "user/feature-control-form" .}}{{ end }}
|
||
|
||
<hr>
|
||
<!-- 全局屏蔽词:三个功能面板共用同一份(见 user/global-blockwords 部分模板) -->
|
||
{{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=vote") }}
|
||
</div><!-- /投票功能管理面板 -->
|
||
|
||
<script>
|
||
(function() {
|
||
// ========== 社交信息复选框交互 + 非空验证 ==========
|
||
function initSocialCheckboxes() {
|
||
document.querySelectorAll('.social-item').forEach(function(item) {
|
||
var checkbox = item.querySelector('.social-enable');
|
||
var input = item.querySelector('.social-input');
|
||
if (!checkbox || !input) return;
|
||
|
||
// 初始化状态
|
||
var currentVal = input.value.trim();
|
||
if (currentVal === 'none' || currentVal === '') {
|
||
checkbox.checked = false;
|
||
input.value = 'none';
|
||
input.disabled = true;
|
||
input.removeAttribute('required');
|
||
} else {
|
||
checkbox.checked = true;
|
||
input.disabled = false;
|
||
input.setAttribute('required', 'required');
|
||
}
|
||
|
||
// 复选框切换
|
||
checkbox.addEventListener('change', function() {
|
||
if (checkbox.checked) {
|
||
input.disabled = false;
|
||
if (input.value === 'none') input.value = '';
|
||
input.setAttribute('required', 'required');
|
||
input.focus();
|
||
} else {
|
||
input.value = 'none';
|
||
input.disabled = true;
|
||
input.removeAttribute('required');
|
||
// 清除可能出现的自定义验证消息
|
||
input.setCustomValidity('');
|
||
}
|
||
});
|
||
|
||
// 输入时清除自定义验证消息
|
||
input.addEventListener('input', function() {
|
||
if (input.validity.customError) {
|
||
input.setCustomValidity('');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 主表单提交验证
|
||
var profileForm = document.getElementById('profile-form');
|
||
profileForm.addEventListener('submit', function(e) {
|
||
var valid = true;
|
||
document.querySelectorAll('.social-item').forEach(function(item) {
|
||
var checkbox = item.querySelector('.social-enable');
|
||
var input = item.querySelector('.social-input');
|
||
if (checkbox.checked && input.value.trim() === '') {
|
||
input.setCustomValidity('{{t .LangCode "admin.socialRequired"}}');
|
||
valid = false;
|
||
} else {
|
||
input.setCustomValidity('');
|
||
}
|
||
});
|
||
|
||
if (!valid) {
|
||
e.preventDefault();
|
||
// 触发第一个无效输入框的报告
|
||
profileForm.reportValidity();
|
||
} else {
|
||
// 提交前确保未启用的项值为 "none"
|
||
document.querySelectorAll('.social-item').forEach(function(item) {
|
||
var checkbox = item.querySelector('.social-enable');
|
||
var input = item.querySelector('.social-input');
|
||
if (!checkbox.checked) {
|
||
input.value = 'none';
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
initSocialCheckboxes();
|
||
|
||
// ========== 其余脚本(颜色选择器、预览等) ==========
|
||
function debounce(func, wait) {
|
||
var timeout;
|
||
return function() {
|
||
var context = this, args = arguments;
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(function() { func.apply(context, args); }, wait);
|
||
};
|
||
}
|
||
|
||
// Pickr 初始化
|
||
document.querySelectorAll('.color-picker').forEach(function(el) {
|
||
var hiddenInput = el.nextElementSibling;
|
||
if (!hiddenInput || hiddenInput.type !== 'hidden') return;
|
||
var pickr = Pickr.create({
|
||
el: el,
|
||
theme: 'nano',
|
||
default: hiddenInput.value || '#000000',
|
||
defaultRepresentation: 'HEX',
|
||
components: {
|
||
preview: true,
|
||
opacity: false,
|
||
hue: true,
|
||
interaction: {
|
||
hex: true,
|
||
input: true,
|
||
clear: true,
|
||
save: true
|
||
}
|
||
}
|
||
});
|
||
pickr.on('save', function(color) {
|
||
var hex = color.toHEXA().toString();
|
||
hiddenInput.value = hex;
|
||
hiddenInput.dispatchEvent(new Event('input'));
|
||
});
|
||
});
|
||
|
||
// 滑块联动
|
||
function bindRangeNumber(range, num, span) {
|
||
if (!range || !num) return;
|
||
var update = function() {
|
||
num.value = range.value;
|
||
if (span) span.textContent = parseFloat(range.value).toFixed(2);
|
||
debouncedUpdatePreview();
|
||
};
|
||
range.addEventListener('input', update);
|
||
num.addEventListener('change', function() {
|
||
var val = parseFloat(num.value);
|
||
if (isNaN(val)) return;
|
||
range.value = val;
|
||
if (span) span.textContent = val.toFixed(2);
|
||
debouncedUpdatePreview();
|
||
});
|
||
if (span) span.textContent = parseFloat(range.value).toFixed(2);
|
||
}
|
||
|
||
var dotScaleRange = document.querySelector('input[name="dotscale"]');
|
||
var dotScaleNum = document.getElementById('dotscale-num');
|
||
var dotScaleSpan = document.getElementById('dotscale-val');
|
||
var bgAlphaRange = document.querySelector('input[name="backgroundimagealpha"]');
|
||
var bgAlphaNum = document.getElementById('bgalpha-num');
|
||
var bgAlphaSpan = document.getElementById('bgalpha-val');
|
||
bindRangeNumber(dotScaleRange, dotScaleNum, dotScaleSpan);
|
||
bindRangeNumber(bgAlphaRange, bgAlphaNum, bgAlphaSpan);
|
||
|
||
// 背景图即时预览
|
||
var bgImageFile = document.getElementById('backgroundimage-file');
|
||
window._bgImageDataURL = null;
|
||
if (bgImageFile) {
|
||
bgImageFile.addEventListener('change', function(e) {
|
||
var file = e.target.files[0];
|
||
if (!file) return;
|
||
var reader = new FileReader();
|
||
reader.onload = function(ev) {
|
||
window._bgImageDataURL = ev.target.result;
|
||
debouncedUpdatePreview();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
// 二维码预览核心
|
||
var boxURL = window.location.origin + '/_/' + '{{.LoggedUser.Domain}}';
|
||
var previewEl = document.getElementById('qrcode-preview');
|
||
var useCustomFg = document.getElementById('use-custom-fg');
|
||
updatePreview();
|
||
|
||
function getInputVal(name, fallback) {
|
||
var input = document.querySelector('input[name="' + name + '"]');
|
||
var v = input ? (input.value || '').trim() : '';
|
||
return v || (fallback || '#000000');
|
||
}
|
||
|
||
function updatePreview() {
|
||
var useFg = useCustomFg.checked;
|
||
var containerWidth = previewEl.clientWidth || 220;
|
||
var size = Math.min(containerWidth, 220);
|
||
// 关键修复:禁止 autoColor。新用户未设颜色时 easy.qrcode 的 autoColor 会自动取一个
|
||
// ≈背景色的前景,导致整张二维码变成背景色方块、无法扫描。改为始终传入明确的
|
||
// colorDark / colorLight 兜底值(空字段时回退到标准黑白)。
|
||
var dotScaleVal = parseFloat(dotScaleRange.value) || 0.4;
|
||
var bgAlphaVal = parseFloat(bgAlphaRange.value) || 0.8;
|
||
var options = {
|
||
text: boxURL,
|
||
width: size,
|
||
height: size,
|
||
colorDark: useFg ? getInputVal('qrcodebackcolor', '#000000') : '#000000',
|
||
colorLight: getInputVal('qrcodecolor', '#FFFFFF'),
|
||
dotScale: dotScaleVal,
|
||
dotScaleTiming: dotScaleVal,
|
||
dotScaleA: dotScaleVal,
|
||
correctLevel: QRCode.CorrectLevel.H,
|
||
logo: (function () {
|
||
// ?v=2:击穿曾被无 ACAO 响应污染的浏览器缓存条目;仅对 http(s) 绝对地址追加
|
||
var a = "{{.LoggedUser.Avatar}}" || "{{.DefaultAvatar}}";
|
||
return a ? (a.indexOf("http") === 0 ? a + "?v=2" : a) : undefined;
|
||
})(),
|
||
logoWidth: Math.floor(size * 0.25),
|
||
logoHeight: Math.floor(size * 0.25),
|
||
logoBackgroundTransparent: true,
|
||
// 直连 R2 等跨域图片须以 CORS 模式加载,画布才不会被污染、可正常导出;
|
||
// 前提是图片所在桶/域名已配 Access-Control-Allow-Origin(如 R2 CORS Policy)。
|
||
crossOrigin: 'anonymous',
|
||
// 默认不注入任何二维码背景图:仅在用户自己上传/设置了 BackgroundImage 时才使用,
|
||
// 或本次会话通过本地文件选择器临时预览(window._bgImageDataURL)。
|
||
// 没有背景图时(autoColor:false + 显式 colorDark/colorLight)才能稳定输出可扫描的黑白二维码,
|
||
// 否则在「无背景图却勾选自定义前景色」时容易糊成一坨。
|
||
backgroundImage: window._bgImageDataURL || ("{{.LoggedUser.BackgroundImage}}" ? "{{.LoggedUser.BackgroundImage}}" : undefined),
|
||
backgroundImageAlpha: bgAlphaVal,
|
||
autoColor: false,
|
||
PO: getInputVal('qrcodepdpcolor', '#000000'),
|
||
PI: getInputVal('qrcodepdpcolor', '#000000'),
|
||
AO: '',
|
||
AI: '',
|
||
timing: getInputVal('qrcodepdpcolor', '#000000')
|
||
};
|
||
// 降级渲染(P0-②):头像 logo / 背景图慢或失败时先出基础码,探针成功自动升级带 logo;
|
||
// 结果按 URL 同会话记忆,拖动滑块高频重绘时 logo 已知可用 → 一次成型、不闪降级。
|
||
window.tbRenderQR(previewEl, function () { return options; });
|
||
}
|
||
|
||
var debouncedUpdatePreview = debounce(updatePreview, 150);
|
||
// 二维码卡默认展开后:若初始不在 account 面板(?tab=question 等),容器曾 hidden 导致
|
||
// 预览按回退 220px 绘制;面板切回时由 tab 脚本调用本函数按真实宽度重绘。
|
||
window.__tbRefreshAccountPreview = debouncedUpdatePreview;
|
||
|
||
var colorInputs = document.querySelectorAll('input[type="hidden"][name="qrcodebackcolor"], input[type="hidden"][name="qrcodecolor"], input[type="hidden"][name="qrcodepdpcolor"]');
|
||
colorInputs.forEach(function(input) {
|
||
input.addEventListener('input', debouncedUpdatePreview);
|
||
});
|
||
useCustomFg.addEventListener('change', debouncedUpdatePreview);
|
||
window.addEventListener('resize', debouncedUpdatePreview);
|
||
// uk-accordion 展开/收起时按真实宽度重绘(初始默认展开;从其它面板切回见下方 tab 钩子)
|
||
document.addEventListener('shown', function(e) {
|
||
if (e.target && e.target.closest && e.target.closest('.uk-accordion')) {
|
||
debouncedUpdatePreview();
|
||
}
|
||
});
|
||
updatePreview();
|
||
|
||
// 预设主题
|
||
var themes = {
|
||
'default': {
|
||
qrcodecolor: '#FFFFFF',
|
||
qrcodebackcolor: '#000000',
|
||
qrcodepdpcolor: '#000000',
|
||
dotscale: 0.35,
|
||
bgalpha: 0.8
|
||
},
|
||
'dark': {
|
||
qrcodecolor: '#1a1a1a',
|
||
qrcodebackcolor: '#FFFFFF',
|
||
qrcodepdpcolor: '#FFFFFF',
|
||
dotscale: 0.5,
|
||
bgalpha: 0.9
|
||
},
|
||
'ocean': {
|
||
qrcodecolor: '#E0F7FA',
|
||
qrcodebackcolor: '#006064',
|
||
qrcodepdpcolor: '#004D40',
|
||
dotscale: 0.4,
|
||
bgalpha: 0.7
|
||
},
|
||
'sunset': {
|
||
qrcodecolor: '#FFF3E0',
|
||
qrcodebackcolor: '#BF360C',
|
||
qrcodepdpcolor: '#870000',
|
||
dotscale: 0.45,
|
||
bgalpha: 0.75
|
||
}
|
||
};
|
||
|
||
function applyTheme(theme) {
|
||
var t = themes[theme];
|
||
if (!t) return;
|
||
document.querySelector('input[name="qrcodecolor"]').value = t.qrcodecolor;
|
||
document.querySelector('input[name="qrcodebackcolor"]').value = t.qrcodebackcolor;
|
||
document.querySelector('input[name="qrcodepdpcolor"]').value = t.qrcodepdpcolor;
|
||
dotScaleRange.value = t.dotscale;
|
||
dotScaleNum.value = t.dotscale;
|
||
dotScaleSpan.textContent = parseFloat(t.dotscale).toFixed(2);
|
||
bgAlphaRange.value = t.bgalpha;
|
||
bgAlphaNum.value = t.bgalpha;
|
||
bgAlphaSpan.textContent = parseFloat(t.bgalpha).toFixed(2);
|
||
|
||
document.querySelector('input[name="qrcodecolor"]').dispatchEvent(new Event('input'));
|
||
document.querySelector('input[name="qrcodebackcolor"]').dispatchEvent(new Event('input'));
|
||
document.querySelector('input[name="qrcodepdpcolor"]').dispatchEvent(new Event('input'));
|
||
debouncedUpdatePreview();
|
||
}
|
||
|
||
document.querySelectorAll('.theme-btn').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
applyTheme(this.getAttribute('data-theme'));
|
||
});
|
||
});
|
||
|
||
document.getElementById('reset-qr-defaults').addEventListener('click', function() {
|
||
applyTheme('default');
|
||
useCustomFg.checked = false;
|
||
debouncedUpdatePreview();
|
||
});
|
||
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
// 面板切换:用户设置 / 提问 / 私聊 / 投票。
|
||
// 服务端已按 ?tab= 渲染初始面板,此处仅在前端切换,并用 hash 保持可分享。
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
var tabs = document.querySelectorAll('[data-profile-tab]');
|
||
var panels = document.querySelectorAll('[data-profile-panel]');
|
||
function showPanel(name, pushToURL) {
|
||
tabs.forEach(function(tab) {
|
||
var active = tab.dataset.profileTab === name;
|
||
tab.classList.toggle('is-active', active);
|
||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||
});
|
||
panels.forEach(function(panel) {
|
||
panel.hidden = panel.dataset.profilePanel !== name;
|
||
});
|
||
// 切回用户设置面板时按真实宽度重绘二维码预览(展开状态不会再触发 accordion 'shown')
|
||
if (name === 'account' && window.__tbRefreshAccountPreview) {
|
||
window.__tbRefreshAccountPreview();
|
||
}
|
||
if (pushToURL && window.history && window.history.replaceState) {
|
||
window.history.replaceState({}, document.title, '/user/profile?tab=' + name);
|
||
}
|
||
}
|
||
tabs.forEach(function(tab) {
|
||
tab.addEventListener('click', function() { showPanel(tab.dataset.profileTab, true); });
|
||
});
|
||
var hash = (window.location.hash || '').replace('#', '');
|
||
if (hash && document.querySelector('[data-profile-tab="' + hash + '"]')) {
|
||
showPanel(hash, false);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
// 标签 / 屏蔽词的标签块输入:把原本的逗号分隔文本框升级为"回车或逗号成词 + 逐条删除"。
|
||
// 渐进增强:接管前原 input 就是可用的普通文本框;接管后原 input 转为隐藏的提交载体,
|
||
// 提交前把标签块拼回逗号串写回它,后端解析逻辑完全不变(仍按逗号分隔)。
|
||
(function () {
|
||
// 后端兼容中文逗号,前端切分口径保持一致
|
||
function parseList(value) {
|
||
return String(value || '').split(/[,,]/).map(function (s) {
|
||
return s.trim();
|
||
}).filter(function (s) { return s !== ''; });
|
||
}
|
||
|
||
function initTagInput(input) {
|
||
var max = parseInt(input.getAttribute('data-tb-max'), 10);
|
||
var maxLen = parseInt(input.getAttribute('data-tb-max-len'), 10);
|
||
// data 属性缺失(如错误分支重渲染)时回落到与 db 常量一致的宽松值,后端仍是唯一权威
|
||
if (!max || max < 1) { max = 10; }
|
||
if (!maxLen || maxLen < 1) { maxLen = 10; }
|
||
var msgTooMany = input.getAttribute('data-tb-msg-too-many') || '';
|
||
var msgTooLong = input.getAttribute('data-tb-msg-too-long') || '';
|
||
var msgDuplicate = input.getAttribute('data-tb-msg-duplicate') || '';
|
||
var hint = input.getAttribute('data-tb-msg-hint') || '';
|
||
|
||
var items = parseList(input.value);
|
||
|
||
var box = document.createElement('div');
|
||
box.className = 'tb-tag-box';
|
||
var entry = document.createElement('input');
|
||
entry.type = 'text';
|
||
entry.className = 'tb-tag-entry';
|
||
box.appendChild(entry);
|
||
|
||
var meta = document.createElement('div');
|
||
meta.className = 'tb-tag-meta';
|
||
var count = document.createElement('span');
|
||
var error = document.createElement('span');
|
||
error.className = 'tb-tag-error';
|
||
meta.appendChild(count);
|
||
meta.appendChild(error);
|
||
|
||
var wrap = document.createElement('div');
|
||
wrap.appendChild(box);
|
||
wrap.appendChild(meta);
|
||
|
||
input.style.display = 'none';
|
||
if (input.parentNode) { input.parentNode.insertBefore(wrap, input.nextSibling); }
|
||
|
||
function sync() { input.value = items.join(','); }
|
||
function showError(msg) { error.textContent = msg || ''; }
|
||
|
||
function commit(raw) {
|
||
var v = String(raw || '').trim().replace(/[,,]/g, '');
|
||
if (v === '') { return; }
|
||
if (items.length >= max) { showError(msgTooMany); return; }
|
||
if (Array.from(v).length > maxLen) { showError(msgTooLong.replace('%s', v)); return; }
|
||
if (items.indexOf(v) >= 0) { showError(msgDuplicate); return; }
|
||
showError('');
|
||
items.push(v);
|
||
render();
|
||
}
|
||
|
||
function render() {
|
||
Array.prototype.slice.call(box.childNodes).forEach(function (node) {
|
||
if (node !== entry) { box.removeChild(node); }
|
||
});
|
||
items.forEach(function (text, i) {
|
||
var chip = document.createElement('span');
|
||
chip.className = 'tb-tag-chip';
|
||
chip.appendChild(document.createTextNode(text));
|
||
var del = document.createElement('button');
|
||
del.type = 'button';
|
||
del.textContent = '×';
|
||
del.title = text;
|
||
del.addEventListener('click', function () {
|
||
items.splice(i, 1);
|
||
showError('');
|
||
render();
|
||
});
|
||
chip.appendChild(del);
|
||
box.insertBefore(chip, entry);
|
||
});
|
||
entry.disabled = items.length >= max;
|
||
entry.placeholder = entry.disabled ? '' : hint;
|
||
count.textContent = items.length + ' / ' + max;
|
||
sync();
|
||
}
|
||
|
||
box.addEventListener('click', function (e) {
|
||
if (e.target === box) { entry.focus(); }
|
||
});
|
||
entry.addEventListener('input', function () {
|
||
if (entry.value.indexOf(',') >= 0 || entry.value.indexOf(',') >= 0) {
|
||
commit(entry.value);
|
||
entry.value = '';
|
||
}
|
||
});
|
||
entry.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
commit(entry.value);
|
||
entry.value = '';
|
||
} else if (e.key === 'Backspace' && entry.value === '' && items.length > 0) {
|
||
items.pop();
|
||
showError('');
|
||
render();
|
||
}
|
||
});
|
||
// 失焦时收下残留输入,避免"打了字没回车就提交"导致内容丢失
|
||
entry.addEventListener('blur', function () {
|
||
if (entry.value.trim() !== '') {
|
||
commit(entry.value);
|
||
entry.value = '';
|
||
}
|
||
});
|
||
|
||
if (input.form) {
|
||
input.form.addEventListener('submit', function () {
|
||
commit(entry.value);
|
||
entry.value = '';
|
||
sync();
|
||
});
|
||
}
|
||
|
||
render();
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
document.querySelectorAll('[data-tb-tag-input]').forEach(initTagInput);
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<script data-cfasync="false">
|
||
// ========== 头像 / 背景:选择即弹裁剪框(1:1 / 16:9),确认后写回 file input 随主表单提交 ==========
|
||
(function () {
|
||
var MSG_AVATAR_TOO_LARGE = '{{t .LangCode "profile.err.avatarTooLarge"}}';
|
||
var MSG_BG_TOO_LARGE = '{{t .LangCode "profile.err.backgroundTooLarge"}}';
|
||
var MSG_NO_CROPPER = '{{t .LangCode "profile.err.cropLibUnavailable"}}';
|
||
var HINT_AVATAR = '{{t .LangCode "profile.avatarCropHint"}}';
|
||
var HINT_BG = '{{t .LangCode "profile.backgroundCropHint"}}';
|
||
|
||
var MAX_FILE = 2 * 1024 * 1024; // 与 storage.MaxAvatarSize / MaxBackgroundSize 一致
|
||
|
||
var media = {
|
||
avatar: {
|
||
input: document.getElementById('avatar-input'),
|
||
prev: document.getElementById('avatar-prev'),
|
||
ratio: 1,
|
||
hint: HINT_AVATAR,
|
||
maxSizeMsg: MSG_AVATAR_TOO_LARGE,
|
||
// 仅限制输出上限,避免小图被强行放大;canvas 宽高比由裁剪框锁定保证
|
||
canvas: { maxWidth: 512 },
|
||
fileType: 'image/png', fileExt: 'png', quality: null
|
||
},
|
||
background: {
|
||
input: document.getElementById('background-input'),
|
||
prev: document.getElementById('bg-prev'),
|
||
ratio: 16 / 9,
|
||
hint: HINT_BG,
|
||
maxSizeMsg: MSG_BG_TOO_LARGE,
|
||
canvas: { maxWidth: 1920 },
|
||
fileType: 'image/jpeg', fileExt: 'jpg', quality: 0.92
|
||
}
|
||
};
|
||
|
||
var modalEl = document.getElementById('media-crop-modal');
|
||
var stageImg = document.getElementById('media-crop-image');
|
||
var hintEl = document.getElementById('media-crop-hint');
|
||
var confirmBtn = document.getElementById('media-crop-confirm');
|
||
var cropper = null;
|
||
var pendingKey = null; // avatar | background
|
||
var pendingURL = null; // 原始选中文件的 objectURL
|
||
var appliedURL = {}; // 已应用裁剪结果的 objectURL(替换新文件时回收)
|
||
// 不在解析期创建 UIkit 组件:Cloudflare Rocket Loader / defer 环境下 UIkit 可能尚未执行,
|
||
// 顶层写 UIkit.modal(...) 会抛 ReferenceError 中断整个脚本,导致下方按钮监听全部失效。
|
||
// 改为在使用前懒创建(见 startCrop),并统一经由 whenReady 等待 UIkit / Cropper 就绪。
|
||
var modalObj = null;
|
||
|
||
function notify(msg) {
|
||
// UIkit 不可用时退回原生 alert,保证任何环境下用户都能看到原因
|
||
if (window.UIkit) { UIkit.modal.alert(msg); } else { window.alert(msg); }
|
||
}
|
||
|
||
// UIkit(header defer)与 Cropper(R2 CDN)就绪后回调;Rocket Loader 会延迟第三方脚本执行,
|
||
// 但最多容忍 3s —— 超时说明资源确实缺失(如 Cropper 尚未上传),给出可理解提示而非静默失败。
|
||
function whenReady(cb) {
|
||
if (window.UIkit && window.Cropper) { cb(); return; }
|
||
var waited = 0;
|
||
var timer = setInterval(function () {
|
||
waited += 100;
|
||
if (window.UIkit && window.Cropper) {
|
||
clearInterval(timer);
|
||
cb();
|
||
} else if (waited >= 3000) {
|
||
clearInterval(timer);
|
||
notify(MSG_NO_CROPPER);
|
||
}
|
||
}, 100);
|
||
}
|
||
|
||
function revoke(store, key) {
|
||
if (store[key]) { URL.revokeObjectURL(store[key]); store[key] = null; }
|
||
}
|
||
|
||
function startCrop(key) {
|
||
var cfg = media[key];
|
||
if (!cfg) return;
|
||
// 文件大小即时校验(不依赖第三方库,立即反馈)
|
||
var file = cfg.input && cfg.input.files && cfg.input.files[0];
|
||
if (!file) return;
|
||
if (file.size > MAX_FILE) { notify(cfg.maxSizeMsg); cfg.input.value = ''; return; }
|
||
|
||
whenReady(function () {
|
||
// 等待期间用户可能重新选择了文件,重新读取
|
||
var f = cfg.input.files && cfg.input.files[0];
|
||
if (!f) return;
|
||
if (f.size > MAX_FILE) { notify(cfg.maxSizeMsg); cfg.input.value = ''; return; }
|
||
if (!modalObj) { modalObj = UIkit.modal(modalEl); }
|
||
|
||
revoke(appliedURL, key);
|
||
pendingKey = key;
|
||
if (pendingURL) { URL.revokeObjectURL(pendingURL); pendingURL = null; }
|
||
pendingURL = URL.createObjectURL(f);
|
||
|
||
hintEl.textContent = cfg.hint;
|
||
stageImg.onload = function () { initCropper(); };
|
||
stageImg.src = pendingURL;
|
||
modalObj.show();
|
||
});
|
||
}
|
||
|
||
function initCropper() {
|
||
if (cropper) { cropper.destroy(); cropper = null; }
|
||
if (!pendingKey) return;
|
||
var cfg = media[pendingKey];
|
||
// 容器宽度优先:modal 显示后再创建,保证测量准确(移动端可用手势拖拽/缩放)
|
||
var box = stageImg.parentElement;
|
||
cropper = new Cropper(stageImg, {
|
||
viewMode: 1,
|
||
dragMode: 'move',
|
||
aspectRatio: cfg.ratio,
|
||
autoCropArea: 1,
|
||
responsive: true,
|
||
background: false,
|
||
rotatable: true,
|
||
zoomable: true,
|
||
wheelZoomRatio: 0.08
|
||
});
|
||
}
|
||
|
||
// 收起时销毁实例,回收资源
|
||
modalEl.addEventListener('hidden', function () {
|
||
if (cropper) { cropper.destroy(); cropper = null; }
|
||
if (pendingURL) { URL.revokeObjectURL(pendingURL); pendingURL = null; }
|
||
stageImg.removeAttribute('src');
|
||
pendingKey = null;
|
||
});
|
||
|
||
confirmBtn.addEventListener('click', function () {
|
||
if (!cropper || !pendingKey || !modalObj) return;
|
||
var cfg = media[pendingKey];
|
||
var canvas = cropper.getCroppedCanvas(cfg.canvas);
|
||
if (!canvas) return;
|
||
canvas.toBlob(function (blob) {
|
||
if (!blob) return;
|
||
var dt = new DataTransfer();
|
||
dt.items.add(new File([blob], 'cropped-' + pendingKey + '.' + cfg.fileExt, { type: cfg.fileType }));
|
||
cfg.input.files = dt.files;
|
||
appliedURL[pendingKey] = URL.createObjectURL(blob);
|
||
if (cfg.prev) { cfg.prev.src = appliedURL[pendingKey]; cfg.prev.style.display = 'block'; }
|
||
modalObj.hide();
|
||
}, cfg.fileType, cfg.quality || undefined);
|
||
});
|
||
|
||
// 入口按钮触发文件选择
|
||
document.querySelectorAll('[data-pick-image]').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
var key = btn.getAttribute('data-pick-image');
|
||
var input = media[key] && media[key].input;
|
||
if (input) input.click();
|
||
});
|
||
});
|
||
|
||
// 选择文件后进入裁剪
|
||
Object.keys(media).forEach(function (key) {
|
||
var input = media[key].input;
|
||
if (!input) return;
|
||
input.addEventListener('change', function () { startCrop(key); });
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
{{template "base/footer" .}}
|