@@ -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>`;
|
||||
Reference in New Issue
Block a user