Files
web_hgcg_h5/src/lib/client/feedback.ts
T
谢宇宁andClaude Opus 5.5 8cdbfee08f Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:23:48 +07:00

275 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>`;