Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-27 16:23:48 +07:00
co-authored by Claude Opus 5.5
commit 8cdbfee08f
345 changed files with 20773 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
import type { SiteContext } from "./context";
import { escape } from "./render";
export function feedbackPayload(data:FormData,screenshots:string[]=[]) {
const value=(key:string)=>String(data.get(key)||'').trim();
const description=value('content');
if(description.length<10||description.length>500)throw new Error('请填写 10–500 字的问题描述');
const labels:Record<string,string>={category:'问题类型',frequency:'发生频率',province:'所在地区',city:'城市',carrier:'运营商',network:'网络类型',device:'设备类型',model:'设备型号',version:'App 版本'};
const details=Object.entries(labels).filter(([key])=>value(key)).map(([key,label])=>`${label}:${value(key)}`).join('\n');
const category=value('category');
const type=category==='登录异常'?4:category==='其他问题'?5:2;
const remaining=500-description.length;
const context=remaining>2&&details?'\n\n'+details.slice(0,remaining-2):'';
return {type,content:description+context,contact:value('contact'),screenshots};
}
export function renderFeedbackPage(app:SiteContext,preserveMarkup=false) {
const root=document.getElementById('info-content')!;if(!preserveMarkup)root.innerHTML=feedbackMarkup();
const form=root.querySelector<HTMLFormElement>('#feedback-form')!,signal=app.signal;
const input=form.querySelector<HTMLInputElement>('#feedback-files')!,list=form.querySelector('#feedback-file-list')!,error=form.querySelector('#feedback-file-error')!,status=form.querySelector('#feedback-status')!;
let files:File[]=[],previews:string[]=[];
const uploaded=new Map<File,string>();
const clearPreviews=()=>{previews.forEach(url=>URL.revokeObjectURL(url));previews=[];};
const renderFiles=()=>{
clearPreviews();
list.innerHTML=files.map((file,index)=>{const url=URL.createObjectURL(file);previews.push(url);return `<div class="feedback-attachment"><img src="${url}" alt="${escape(file.name || "反馈图片")}预览"><span>${escape(file.name)}</span><button type="button" data-remove-feedback-file="${index}" aria-label="移除${escape(file.name)}">×</button></div>`;}).join('');
};
signal.addEventListener('abort',clearPreviews,{once:true});
input.addEventListener('change',()=>{
if(form.dataset.submitting)return;
const selected=Array.from(input.files||[]);
const invalid=selected.length>3?'最多上传 3 张图片':selected.some(file=>file.size>5*1024*1024||!['image/jpeg','image/png','image/webp'].includes(file.type))?'请上传 5MB 以内的 JPG、PNG 或 WebP 图片':'';
error.textContent=invalid;
if(invalid){input.value='';return;}
files=selected;renderFiles();
},{signal});
list.addEventListener('click',event=>{
const button=(event.target as Element).closest<HTMLElement>('[data-remove-feedback-file]');if(!button||form.dataset.submitting)return;
files.splice(Number(button.dataset.removeFeedbackFile),1);input.value='';renderFiles();
},{signal});
const content=form.querySelector<HTMLTextAreaElement>('[name="content"]')!;
content.addEventListener('input',()=>{form.querySelector('#feedback-count')!.textContent=content.value.length+' / 500';},{signal});
form.addEventListener('submit',event=>{
event.preventDefault();if(form.dataset.submitting||!form.reportValidity())return;
const data=new FormData(form),attachments=[...files];
try{feedbackPayload(data);}catch(reason){status.textContent=reason instanceof Error?reason.message:'请检查反馈内容';return;}
const button=form.querySelector<HTMLButtonElement>('[type="submit"]')!;
button.disabled=true;input.disabled=true;form.dataset.submitting='1';status.textContent='提交中…';
void (async()=>{
const screenshots:string[]=[];
for(const file of attachments){
if(signal.aborted)return;
let path=uploaded.get(file);
if(!path){const body=new FormData();body.append('file',file);body.append('watermark','0');const result=await app.api.post<{sourceNameFs?:string}>('upload/image',body);if(!result.sourceNameFs)throw new Error('图片上传失败,请重试');path=result.sourceNameFs;uploaded.set(file,path);}
screenshots.push(path);
}
if(signal.aborted)return;
await app.api.post('feedback/add',feedbackPayload(data,screenshots));
if(signal.aborted)return;
form.reset();files=[];uploaded.clear();renderFiles();error.textContent='';form.querySelector('#feedback-count')!.textContent='0 / 500';status.textContent='反馈已提交,感谢您的反馈!';
})().catch(reason=>{if(!signal.aborted)status.textContent=reason instanceof Error?reason.message:'反馈提交失败,请重试';}).finally(()=>{button.disabled=false;input.disabled=false;delete form.dataset.submitting;});
},{signal});
}
export const feedbackMarkup=()=>`<form id="feedback-form" class="feedback-form feedback-reference">
<fieldset class="feedback-part">
<legend>问题类型 <em>*</em>
</legend>
<div class="feedback-options">
<label class="feedback-option">
<input type="radio" name="category" value="播放卡顿 / 缓冲" required>
<span>播放卡顿 / 缓冲</span>
</label>
<label class="feedback-option">
<input type="radio" name="category" value="无法播放" required>
<span>无法播放</span>
</label>
<label class="feedback-option">
<input type="radio" name="category" value="画质模糊" required>
<span>画质模糊</span>
</label>
<label class="feedback-option">
<input type="radio" name="category" value="音画不同步" required>
<span>音画不同步</span>
</label>
<label class="feedback-option">
<input type="radio" name="category" value="加载失败" required>
<span>加载失败</span>
</label>
<label class="feedback-option">
<input type="radio" name="category" value="闪退 / 崩溃" required>
<span>闪退 / 崩溃</span>
</label>
<label class="feedback-option">
<input type="radio" name="category" value="登录异常" required>
<span>登录异常</span>
</label>
<label class="feedback-option">
<input type="radio" name="category" value="其他问题" required>
<span>其他问题</span>
</label>
</div>
</fieldset>
<fieldset class="feedback-part">
<legend>问题发生频率 <em>*</em>
</legend>
<div class="feedback-options">
<label class="feedback-option">
<input type="radio" name="frequency" value="每次都会" required>
<span>每次都会</span>
</label>
<label class="feedback-option">
<input type="radio" name="frequency" value="经常出现" required>
<span>经常出现</span>
</label>
<label class="feedback-option">
<input type="radio" name="frequency" value="偶尔出现" required>
<span>偶尔出现</span>
</label>
<label class="feedback-option">
<input type="radio" name="frequency" value="仅出现一次" required>
<span>仅出现一次</span>
</label>
</div>
</fieldset>
<fieldset class="feedback-part">
<legend>网络环境信息 <em>*</em>
</legend>
<div class="feedback-panel">
<label>所在地址</label>
<div class="feedback-address">
<select name="province" aria-label="所在省份" required>
<option value="">所在省份</option>
<option>北京</option>
<option>天津</option>
<option>河北</option>
<option>山西</option>
<option>内蒙古</option>
<option>辽宁</option>
<option>吉林</option>
<option>黑龙江</option>
<option>上海</option>
<option>江苏</option>
<option>浙江</option>
<option>安徽</option>
<option>福建</option>
<option>江西</option>
<option>山东</option>
<option>河南</option>
<option>湖北</option>
<option>湖南</option>
<option>广东</option>
<option>广西</option>
<option>海南</option>
<option>重庆</option>
<option>四川</option>
<option>贵州</option>
<option>云南</option>
<option>西藏</option>
<option>陕西</option>
<option>甘肃</option>
<option>青海</option>
<option>宁夏</option>
<option>新疆</option>
<option>香港</option>
<option>澳门</option>
<option>台湾</option>
<option>海外</option>
</select>
<input name="city" maxlength="100" aria-label="城市(选填)" placeholder="城市(选填)">
</div>
<label>运营商<select name="carrier" required>
<option value="">选择运营商</option>
<option>中国移动</option>
<option>中国联通</option>
<option>中国电信</option>
<option>中国广电</option>
<option>其他</option>
</select>
</label>
<span class="feedback-field-label">网络类型</span>
<div class="feedback-options">
<label class="feedback-option">
<input type="radio" name="network" value="Wi-Fi" required>
<span>Wi-Fi</span>
</label>
<label class="feedback-option">
<input type="radio" name="network" value="5G" required>
<span>5G</span>
</label>
<label class="feedback-option">
<input type="radio" name="network" value="4G" required>
<span>4G</span>
</label>
<label class="feedback-option">
<input type="radio" name="network" value="其他" required>
<span>其他</span>
</label>
</div>
</div>
</fieldset>
<fieldset class="feedback-part">
<legend>设备信息 <em>*</em>
</legend>
<div class="feedback-panel">
<span class="feedback-field-label">设备类型</span>
<div class="feedback-options">
<label class="feedback-option">
<input type="radio" name="device" value="iPhone" required>
<span>iPhone</span>
</label>
<label class="feedback-option">
<input type="radio" name="device" value="Android" required>
<span>Android</span>
</label>
<label class="feedback-option">
<input type="radio" name="device" value="iPad" required>
<span>iPad</span>
</label>
<label class="feedback-option">
<input type="radio" name="device" value="电脑 / 其他" required>
<span>电脑 / 其他</span>
</label>
</div>
<div class="feedback-address">
<label>设备型号(选填)<input name="model" maxlength="100" placeholder="例如:iPhone 15、小米 14">
</label>
<label>App 版本号(选填)<input name="version" maxlength="100" placeholder="例如:v1.2.1">
</label>
</div>
</div>
</fieldset>
<fieldset class="feedback-part">
<legend>问题描述 <em>*</em>
</legend>
<div class="feedback-description-wrap">
<textarea name="content" aria-label="问题描述" minlength="10" maxlength="500" required placeholder="请详细描述您遇到的问题,包括观看的内容、具体表现和发生时间(至少 10 字)">
</textarea>
<span id="feedback-count">0 / 500</span>
</div>
</fieldset>
<fieldset class="feedback-part">
<legend>上传图片</legend>
<div class="feedback-uploads">
<label class="feedback-upload-box">
<input type="file" id="feedback-files" accept="image/jpeg,image/png,image/webp" multiple>
<span class="feedback-upload-plus" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M12 5v14M5 12h14"/>
</svg>
</span>
<strong>上传图片</strong>
<small>支持 JPG / PNG / WebP,最多 3 张</small>
</label>
<div id="feedback-file-list">
</div>
</div>
<p class="feedback-file-note">每张图片不超过 5MB</p>
<p id="feedback-file-error" role="alert">
</p>
<input type="hidden" name="attachments" id="feedback-attachments">
</fieldset>
<fieldset class="feedback-part">
<legend>联系方式 <em>*</em>
</legend>
<input name="contact" aria-label="联系方式" required maxlength="120" placeholder="请输入 Email / Telegram / X 联系方式">
</fieldset>
<div class="feedback-submit-area">
<button type="submit" class="feedback-submit-button">提交反馈</button>
<p>提交后请留意您填写的联系方式。</p>
<span class="feedback-status" id="feedback-status" role="status">
</span>
</div>
</form>`;