首页/ mobileweb/ 到家无忧 · 一站式上门生活服务平台 H5(上门家政 / 保洁 / 月嫂 / 维修 / 搬家)/ 代码预览

💻 代码预览 · 到家无忧 · 一站式上门生活服务平台 H5(上门家政 / 保洁 / 月嫂 / 维修 / 搬家)

mw_service · 上传者 驰旭教育
← 返回详情
app.js · 约 44586 字符
1/* 到家无忧 · 应用层 */
2(function(){
3  var $ = jQuery;
4  var BRAND = '#3b6ef5', BRAND2 = '#06b6d4';
5
6  function load(k,d){ try{var v=localStorage.getItem(k);return v?JSON.parse(v):d;}catch(e){return d;} }
7  function save(k,v){ try{localStorage.setItem(k,JSON.stringify(v));}catch(e){} }
8
9  var K = {orders:'svc_orders', addr:'svc_addr', fav:'svc_fav', coupons:'svc_coupons', member:'svc_member'};
10  var S = {
11    orders: load(K.orders,null), addr: load(K.addr,null), fav: load(K.fav,[]),
12    coupons: load(K.coupons,null), member: load(K.member,null)
13  };
14  if(!S.orders) S.orders = SEED_ORDERS;
15  if(!S.addr)   S.addr   = SEED_ADDR;
16  if(!S.coupons)S.coupons= COUPON_POOL.map(function(c){return Object.assign({},c);});
17  if(!S.member) S.member = {level:'普通会员', balance:0, card:null, points:120};
18  function persist(){ save(K.orders,S.orders); save(K.addr,S.addr); save(K.fav,S.fav); save(K.coupons,S.coupons); save(K.member,S.member); }
19
20  function svcById(id){ return SERVICES.filter(function(s){return s.id===id;})[0]; }
21  function wkById(id){ return WORKERS.filter(function(w){return w.id===id;})[0]; }
22  function catById(id){ return CATEGORIES.filter(function(c){return c.id===id;})[0]; }
23  function money(n){ return '¥' + (Math.round(n*100)/100); }
24  function star(r){ var s=''; for(var i=1;i<=5;i++) s+= i<=r?'★':'☆'; return s; }
25  function esc(s){ return (s||'').replace(/[&<>]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;'}[c];}); }
26
27  var draft = {};   // 预约草稿
28
29  // ---------------- top bar / tab ----------------
30  function setTop(title, back){
31    var h = '<div class="top-inner">';
32    if(back) h += '<button class="top-back" data-act="back">‹</button>';
33    h += '<div class="top-title">'+esc(title)+'</div>';
34    if(back) h += '<span class="top-right"></span>';
35    h += '</div>';
36    $('#topbar').html(h);
37  }
38  var TABS = [['home','🏠','首页'],['category','📑','分类'],['orders','📋','订单'],['coupons','🎫','优惠券'],['mine','👤','我的']];
39  function renderTab(){
40    var cur = (location.hash.replace(/^#\/?/,'').split('/')[0])||'home';
41    var h = TABS.map(function(t){
42      var on = (t[0]===cur)?' on':'';
43      return '<div class="tab'+on+'" data-act="tab" data-route="'+t[0]+'"><div class="tab-ic">'+t[1]+'</div><div class="tab-tx">'+t[2]+'</div></div>';
44    }).join('');
45    $('#tabbar').html(h);
46  }
47
48  // ---------------- router ----------------
49  function parseHash(){
50    var h = location.hash.replace(/^#\/?/,'');
51    var qi = h.indexOf('?'); var q='';
52    if(qi>=0){ q=h.slice(qi+1); h=h.slice(0,qi); }
53    var parts = h.split('/');
54    var qs={}; q.split('&').forEach(function(p){ if(!p)return; var kv=p.split('='); qs[decodeURIComponent(kv[0])]=decodeURIComponent(kv[1]||''); });
55    return {name:parts[0]||'home', param:parts[1]||'', qs:qs};
56  }
57  var ROUTES = {
58    home:renderHome, category:renderCategory, search:renderSearch,
59    service:renderService, worker:renderWorker, booking:renderBooking,
60    orders:renderOrders, order:renderOrder, review:renderReview,
61    coupons:renderCoupons, mine:renderMine, address:renderAddress,
62    addressEdit:renderAddressEdit, favorite:renderFavorite, help:renderHelp, member:renderMember
63  };
64  function render(){
65    var r = parseHash();
66    var fn = ROUTES[r.name] || renderHome;
67    $('#view').html(fn(r.param, r.qs));
68    window.scrollTo(0,0);
69    setTop.apply(null, TOP_TITLE[r.name] || ['到家无忧','home'===r.name]);
70    renderTab();
71    bindView(r.name, r);
72  }
73  var TOP_TITLE = {
74    home:['到家无忧',0], category:['全部分类',0], search:['搜索服务',1],
75    service:['服务详情',1], worker:['服务人员',1], booking:['填写预约',1],
76    orders:['我的订单',1], order:['订单详情',1], review:['服务评价',1],
77    coupons:['优惠券',1], mine:['个人中心',0], address:['我的地址',1],
78    addressEdit:['编辑地址',1], favorite:['我的收藏',1], help:['帮助中心',1], member:['会员中心',1]
79  };
80
81  function go(hash){ location.hash = hash; }
82  function back(){ if(history.length>1) history.back(); else go('#/home'); }
83
84  // ---------------- toast ----------------
85  var toastTimer;
86  function toast(msg){
87    var t=$('#toast'); t.text(msg).addClass('show');
88    clearTimeout(toastTimer); toastTimer=setTimeout(function(){ t.removeClass('show'); },1600);
89  }
90
91  // =====================================================================
92  // 视图:首页
93  // =====================================================================
94  function renderHome(){
95    var cats = CATEGORIES.map(function(c){
96      return '<div class="cat-item" data-act="cat" data-id="'+c.id+'"><div class="cat-emoji">'+c.emoji+'</div><div class="cat-name">'+c.name+'</div></div>';
97    }).join('');
98    var hot = SERVICES.slice(0,6).map(function(s){
99      return '<div class="hcard" data-act="service" data-id="'+s.id+'">'
100        +'<div class="hcard-em">'+s.emoji+'</div>'
101        +'<div class="hcard-bd"><div class="hcard-name">'+s.name+'</div>'
102        +'<div class="hcard-meta"><span class="rate">'+star(s.rating)+' '+s.rating+'</span><span class="sold">已售'+s.sales+'</span></div>'
103        +'<div class="hcard-price">'+money(s.price)+'<span class="unit">/'+s.unit+'</span></div></div></div>';
104    }).join('');
105    var nearby = WORKERS.slice(0,5).map(function(w){
106      return '<div class="wchip" data-act="worker" data-id="'+w.id+'">'
107        +'<div class="w-av">'+w.avatar+'</div><div class="w-info"><div class="w-name">'+w.name+' <span class="w-y">'+w.years+'年</span></div>'
108        +'<div class="w-rate">'+star(w.rating)+' '+w.rating+' · 单'+w.orders+'</div></div></div>';
109    }).join('');
110    var revWall = Object.keys(REVIEWS).slice(0,3).map(function(k){
111      var rv = REVIEWS[k][0];
112      return '<div class="revw"><div class="revw-u">'+rv.u+' <span class="revw-r">'+star(rv.r)+'</span></div><div class="revw-t">'+esc(rv.d)+'</div></div>';
113    }).join('');
114    return ''
115    +'<div class="locbar"><span class="loc">📍 杭州市·西湖区</span><div class="searchbox" data-act="search">🔍 搜索保洁/月嫂/维修…</div></div>'
116    +'<div class="banner"><div class="bn-t1">新人首单立减 30 元</div><div class="bn-t2">实名认证 · 保险保障 · 100% 赔付</div><div class="bn-btn" data-act="coupons">立即领券 ›</div></div>'
117    +'<div class="sec-title">服务分类</div>'
118    +'<div class="cat-grid">'+cats+'</div>'
119    +'<div class="guarantee"><div>✅ 实名认证</div><div>🛡️ 保险保障</div><div>💯 100%赔付</div><div>⚡ 极速响应</div></div>'
120    +'<div class="coupon-roll">'
121      +'<div class="cr-t">限时领券</div>'
122      + COUPON_POOL.filter(function(c){return c.status==='can';}).map(function(c){return '<div class="cr-it" data-act="coupons">'+c.cond+'</div>';}).join('')
123    +'</div>'
124    +'<div class="sec-title">热门服务</div><div class="hlist">'+hot+'</div>'
125    +'<div class="sec-title">附近师傅 / 阿姨</div><div class="wroll">'+nearby+'</div>'
126    +'<div class="sec-title">真实评价</div><div class="revwall">'+revWall+'</div>'
127    +'<div class="bottom-pad"></div>';
128  }
129
130  // =====================================================================
131  // 视图:分类
132  // =====================================================================
133  function renderCategory(){
134    var left = CATEGORIES.map(function(c,i){
135      return '<div class="cate-li'+(i===0?' on':'')+'" data-act="catTab" data-id="'+c.id+'">'+c.emoji+' '+c.name+'</div>';
136    }).join('');
137    function rightHtml(catId){
138      var list = SERVICES.filter(function(s){return s.cat===catId;});
139      return list.map(function(s){
140        return '<div class="srow" data-act="service" data-id="'+s.id+'">'
141          +'<div class="srow-em">'+s.emoji+'</div>'
142          +'<div class="srow-bd"><div class="srow-name">'+s.name+'</div>'
143          +'<div class="srow-tags">'+s.tags.map(function(t){return '<span>'+t+'</span>';}).join('')+'</div>'
144          +'<div class="srow-bot"><span class="rate">'+star(s.rating)+' '+s.rating+'</span><span class="srow-price">'+money(s.price)+'<i>/'+s.unit+'</i></span></div></div></div>';
145      }).join('') || '<div class="empty">该分类暂未上架服务</div>';
146    }
147    return '<div class="cate-wrap"><div class="cate-left">'+left+'</div><div class="cate-right" id="cateRight">'+rightHtml(CATEGORIES[0].id)+'</div></div><div class="bottom-pad"></div>';
148  }
149
150  // =====================================================================
151  // 视图:搜索
152  // =====================================================================
153  function renderSearch(){
154    var hots = ['日常保洁','月嫂','油烟机清洗','上门做饭','开锁','搬家'];
155    var chips = CATEGORIES.map(function(c){return '<span class="sch-chip" data-act="cat" data-id="'+c.id+'">'+c.emoji+c.name+'</span>';}).join('');
156    return ''
157    +'<div class="sch-bar"><input id="schInput" class="sch-input" placeholder="搜索服务 / 人员" /><button class="sch-btn" data-act="doSearch">搜索</button></div>'
158    +'<div class="sch-hot">热门:'+hots.map(function(h){return '<span class="sch-h" data-act="hotSearch" data-q="'+h+'">'+h+'</span>';}).join('')+'</div>'
159    +'<div class="sec-title">按分类找</div><div class="sch-chips">'+chips+'</div>'
160    +'<div id="schResult"></div><div class="bottom-pad"></div>';
161  }
162
163  // =====================================================================
164  // 视图:服务详情
165  // =====================================================================
166  function renderService(id){
167    var s = svcById(id); if(!s) return '<div class="empty">服务不存在</div>';
168    var specHtml = (s.specs||[]).map(function(sp,si){
169      var opts = sp.opts.map(function(o,oi){
170        return '<span class="sp" data-act="spec" data-si="'+si+'" data-oi="'+oi+'">'+o.t+(o.add>0?(' +'+money(o.add)):(o.add<0?(' '+money(o.add)):''))+'</span>';
171      }).join('');
172      return '<div class="spec-line"><div class="spec-name">'+sp.name+'</div><div class="spec-opts">'+opts+'</div></div>';
173    }).join('');
174    var wlist = (s.workers||[]).length? s.workers : WORKERS.slice(0,3).map(function(w){return w.id;});
175    var wHtml = wlist.map(function(wid){
176      var w = wkById(wid); if(!w) return '';
177      return '<div class="wmini" data-act="worker" data-id="'+w.id+'"><div class="w-av">'+w.avatar+'</div><div class="wmini-info"><div class="w-name">'+w.name+' <span class="w-y">'+w.years+'年</span></div><div class="w-rate">'+star(w.rating)+' '+w.rating+'</div></div><span class="w-pick" data-act="pickWorker" data-id="'+w.id+'">指定</span></div>';
178    }).join('');
179    var revs = REVIEWS[id]||[];
180    var revHtml = revs.length? revs.map(function(rv){
181      return '<div class="rev"><div class="rev-u">'+rv.u+' <span class="rev-r">'+star(rv.r)+'</span> <span class="rev-d">'+rv.t+'</span></div>'
182        +'<div class="rev-t">'+esc(rv.d)+'</div>'
183        + (rv.img&&rv.img.length? '<div class="rev-imgs">'+rv.img.map(function(i){return '<span class="rev-img">'+i+'</span>';}).join('')+'</div>':'')
184        +'</div>';
185    }).join('') : '<div class="empty">暂无评价</div>';
186    return ''
187    +'<div class="sv-hero">'+s.emoji+'<div class="sv-hero-bg"></div></div>'
188    +'<div class="sv-head"><div class="sv-name">'+s.name+'</div>'
189    +'<div class="sv-meta"><span class="rate">'+star(s.rating)+' '+s.rating+'</span><span class="sold">已售'+s.sales+'</span></div>'
190    +'<div class="sv-price">'+money(s.price)+'<i>/'+s.unit+'</i></div>'
191    +'<div class="sv-tags">'+s.tags.map(function(t){return '<span>'+t+'</span>';}).join('')+'</div></div>'
192    +'<div class="card"><div class="card-t">服务介绍</div><div class="card-c">'+s.intro+'</div></div>'
193    +'<div class="card"><div class="card-t">适用场景</div><div class="card-c">'+s.scenes+'</div></div>'
194    +'<div class="card"><div class="card-t">注意事项</div><div class="card-c">'+s.notice+'</div></div>'
195    +'<div class="card"><div class="card-t">工具 / 耗材</div><div class="card-c">'+s.tools+'</div></div>'
196    + (specHtml? '<div class="card"><div class="card-t">选择规格</div>'+specHtml+'</div>':'')
197    +'<div class="card"><div class="card-t">可指定服务人员</div>'+wHtml+'</div>'
198    +'<div class="card"><div class="card-t">用户评价('+revs.length+')</div>'+revHtml+'</div>'
199    +'<div class="fixed-bar"><button class="fb-fav" data-act="favService" data-id="'+s.id+'">♡ 收藏</button><button class="fb-book" data-act="bookService" data-id="'+s.id+'">立即预约</button></div>'
200    +'<div class="bottom-pad"></div>';
201  }
202
203  // =====================================================================
204  // 视图:人员详情
205  // =====================================================================
206  function renderWorker(id){
207    var w = wkById(id); if(!w) return '<div class="empty">人员不存在</div>';
208    var certs = w.certs.map(function(c){return '<span class="cert">'+c+'</span>';}).join('');
209    var svc = (w.services||[]).map(function(sid){
210      var s = svcById(sid); if(!s) return '';
211      return '<div class="srow" data-act="service" data-id="'+s.id+'"><div class="srow-em">'+s.emoji+'</div><div class="srow-bd"><div class="srow-name">'+s.name+'</div><div class="srow-bot"><span class="rate">'+star(s.rating)+'</span><span class="srow-price">'+money(s.price)+'</span></div></div></div>';
212    }).join('');
213    return ''
214    +'<div class="wk-hero"><div class="wk-av">'+w.avatar+'</div>'
215    +'<div class="wk-top"><div class="wk-name">'+w.name+' <span class="wk-g">'+w.gender+' · '+w.age+'岁</span></div>'
216    +'<div class="wk-rate">'+star(w.rating)+' '+w.rating+' · 接单'+w.orders+' · 从业'+w.years+'年</div>'
217    +'<div class="wk-region">📍 '+w.region+'</div></div></div>'
218    +'<div class="wk-certs">'+certs+'</div>'
219    +'<div class="card"><div class="card-t">个人简介</div><div class="card-c">'+w.intro+'</div></div>'
220    +'<div class="card"><div class="card-t">擅长服务</div><div class="card-c">'+w.goodAt.map(function(g){return '<span class="tag">'+g+'</span>';}).join('')+'</div></div>'
221    +'<div class="card"><div class="card-t">TA 提供的服务</div>'+svc+'</div>'
222    +'<div class="fixed-bar"><button class="fb-fav" data-act="favWorker" data-id="'+w.id+'">♡ 收藏</button><button class="fb-book" data-act="bookWorker" data-id="'+w.id+'">预约 TA</button></div>'
223    +'<div class="bottom-pad"></div>';
224  }
225
226  // =====================================================================
227  // 视图:预约下单
228  // =====================================================================
229  function renderBooking(param, qs){
230    var sid = qs.sid || draft.sid; if(!sid) return '<div class="empty">请先选择服务</div>';
231    var s = svcById(sid);
232    draft.sid = sid;
233    if(!draft.spec) draft.spec = (s.specs||[]).map(function(sp){return 0;});
234    if(!draft.wid && qs.wid) draft.wid = qs.wid;
235    // 日期 7 天
236    var days=[]; var wd=['周日','周一','周二','周三','周四','周五','周六'];
237    for(var i=0;i<7;i++){ var d=new Date(); d.setDate(d.getDate()+i);
238      days.push({label:(i===0?'今天':(i===1?'明天':wd[d.getDay()])), date:(d.getMonth()+1)+'-'+d.getDate()}); }
239    if(!draft.day) draft.day = 0;
240    if(!draft.slot) draft.slot = 0;
241    var slots=['08:00-10:00','10:00-12:00','13:00-15:00','15:00-17:00','18:00-20:00'];
242    var specHtml = (s.specs||[]).map(function(sp,si){
243      var opts = sp.opts.map(function(o,oi){
244        var on = (draft.spec[si]===oi)?' on':'';
245        return '<span class="sp'+on+'" data-act="spec" data-si="'+si+'" data-oi="'+oi+'">'+o.t+(o.add>0?(' +'+money(o.add)):(o.add<0?(' '+money(o.add)):''))+'</span>';
246      }).join('');
247      return '<div class="spec-line"><div class="spec-name">'+sp.name+'</div><div class="spec-opts">'+opts+'</div></div>';
248    }).join('');
249    var dayHtml = days.map(function(d,di){return '<span class="day'+(draft.day===di?' on':'')+'" data-act="day" data-i="'+di+'">'+d.label+'<br>'+d.date+'</span>';}).join('');
250    var slotHtml = slots.map(function(sl,si){return '<span class="slot'+(draft.slot===si?' on':'')+'" data-act="slot" data-i="'+si+'">'+sl+'</span>';}).join('');
251    var addr = S.addr; var defAddr = addr.filter(function(a){return a.def;})[0] || addr[0];
252    var addrHtml = '<div class="addr-sel" data-act="address">'+(defAddr?('📍 '+defAddr.region+' '+defAddr.detail+' <span class="addr-edit">更换 ›</span>'):'+ 添加服务地址')+'</div>';
253    // 优惠券(可用)
254    var usable = S.coupons.filter(function(c){return c.status==='got'||c.status==='can';});
255    var coupHtml = '<div class="coup-sel" data-act="coupons">未选择优惠券 ›</div>';
256    // 计算金额
257    var base = s.price;
258    (s.specs||[]).forEach(function(sp,si){ base += sp.opts[draft.spec[si]].add||0; });
259    var member = MEMBER_LEVELS.filter(function(m){return m.name===S.member.level;})[0] || MEMBER_LEVELS[0];
260    var afterMember = base * member.discount;
261    var total = afterMember;
262    draft.total = total;
263    var wHtml = draft.wid? ('指定:'+wkById(draft.wid).name) : '系统智能匹配';
264    return ''
265    +'<div class="bk-svc"><div class="bk-em">'+s.emoji+'</div><div class="bk-info"><div class="bk-name">'+s.name+'</div><div class="bk-price">'+money(base)+' <i>规格后</i></div></div></div>'
266    + (specHtml? '<div class="card"><div class="card-t">选择规格</div>'+specHtml+'</div>':'')
267    +'<div class="card"><div class="card-t">服务人员</div><div class="bk-w">'+wHtml+'</div></div>'
268    +'<div class="card"><div class="card-t">上门日期</div><div class="day-row">'+dayHtml+'</div></div>'
269    +'<div class="card"><div class="card-t">时间段</div><div class="slot-row">'+slotHtml+'</div></div>'
270    +'<div class="card"><div class="card-t">服务地址</div>'+addrHtml+'</div>'
271    +'<div class="card"><div class="card-t">优惠券</div>'+coupHtml+'</div>'
272    +'<div class="card"><div class="card-t">备注</div><textarea id="bkNote" class="bk-note" placeholder="例如:厨房重油污、少油少盐、宠物请安置…"></textarea></div>'
273    +'<div class="card bill"><div class="bill-row"><span>服务金额</span><span>'+money(base)+'</span></div>'
274    +'<div class="bill-row"><span>'+member.name+'折扣</span><span>'+(member.discount<1?('-'+money(base-afterMember)):'无')+'</span></div>'
275    +'<div class="bill-row total"><span>实付</span><span>'+money(total)+'</span></div></div>'
276    +'<div class="fixed-bar single"><button class="fb-book" data-act="submitBooking">提交订单 '+money(total)+'</button></div>'
277    +'<div class="bottom-pad"></div>';
278  }
279
280  // =====================================================================
281  // 视图:订单列表
282  // =====================================================================
283  function renderOrders(){
284    var tabs=[['all','全部'],['unpaid','待支付'],['wait','待服务'],['serving','服务中'],['done','已完成'],['cancel','已取消']];
285    var cur = (location.hash.split('/')[1])||'all';
286    var th = tabs.map(function(t){return '<span class="ot'+(cur===t[0]?' on':'')+'" data-act="orderTab" data-s="'+t[0]+'">'+t[1]+'</span>';}).join('');
287    function match(o,s){ if(s==='all')return o.status!=='cancel'; if(s==='cancel')return o.status==='cancel'; return o.status===s; }
288    var list = S.orders.filter(function(o){return match(o,cur);});
289    var items = list.map(function(o){
290      var s = svcById(o.sid);
291      return '<div class="ocard" data-act="order" data-id="'+o.id+'">'
292        +'<div class="oc-top"><span class="oc-name">'+(s?s.emoji+' '+s.name:'订单')+'</span><span class="oc-st">'+statusText(o.status)+'</span></div>'
293        +'<div class="oc-mid">'+ (o.spec?o.spec.join(' / '):'') +' · '+o.date+'</div>'
294        +'<div class="oc-bot"><span class="oc-price">'+money(o.paid||0)+'</span><span class="oc-go">查看 ›</span></div></div>';
295    }).join('') || '<div class="empty">这里还没有订单</div>';
296    return '<div class="otabs">'+th+'</div><div class="olist">'+items+'</div><div class="bottom-pad"></div>';
297  }
298  function statusText(s){ return {unpaid:'待支付',wait:'待服务',serving:'服务中',done:'已完成',cancel:'已取消'}[s]||s; }
299
300  // =====================================================================
301  // 视图:订单详情
302  // =====================================================================
303  function renderOrder(id){
304    var o = S.orders.filter(function(x){return x.id===id;})[0]; if(!o) return '<div class="empty">订单不存在</div>';
305    var s = svcById(o.sid); var w = o.wid?wkById(o.wid):null;
306    var steps=['unpaid','dispatched','serving','done','reviewed'];
307    var labels={unpaid:'待支付',dispatched:'已派单',serving:'服务中',done:'已完成',reviewed:'已评价'};
308    var curIdx = Math.max(0, o.flow.indexOf(o.status==='reviewed'?'reviewed':(o.status==='done'?'done':(o.status==='serving'?'serving':(o.status==='wait'?'dispatched':'unpaid')))));
309    var stepHtml = steps.map(function(st,i){
310      var on = i<=curIdx?' on':'';
311      return '<div class="step'+on+'"><div class="dot"></div><div class="stx">'+labels[st]+'</div></div>';
312    }).join('<div class="stline"></div>');
313    var act = '';
314    if(o.status==='unpaid') act='<button class="fb-fav" data-act="payOrder" data-id="'+o.id+'">去支付</button><button class="fb-book" data-act="cancelOrder" data-id="'+o.id+'">取消订单</button>';
315    else if(o.status==='wait') act='<button class="fb-fav" data-act="changeOrder" data-id="'+o.id+'">改期</button><button class="fb-book" data-act="cancelOrder" data-id="'+o.id+'">取消订单</button>';
316    else if(o.status==='serving') act='<button class="fb-book" data-act="complain" data-id="'+o.id+'">联系客服 / 投诉</button>';
317    else if(o.status==='done' && !o.reviewed) act='<button class="fb-book" data-act="reviewOpen" data-id="'+o.id+'">评价服务</button>';
318    else if(o.status==='done' && o.reviewed) act='<div class="rev-done">已评价:'+star(o.review.r)+' '+esc(o.review.txt)+'</div>';
319    var photos = '';
320    if(o.before&&o.before.length) photos+='<div class="ph-block"><div class="ph-t">服务前</div><div class="ph-imgs">'+o.before.map(function(p){return '<span class="ph">'+p+'</span>';}).join('')+'</div></div>';
321    if(o.after&&o.after.length) photos+='<div class="ph-block"><div class="ph-t">服务后</div><div class="ph-imgs">'+o.after.map(function(p){return '<span class="ph">'+p+'</span>';}).join('')+'</div></div>';
322    return ''
323    +'<div class="od-steps">'+stepHtml+'</div>'
324    +'<div class="card"><div class="oc-name2">'+(s?s.emoji+' '+s.name:'订单')+'</div>'
325    +'<div class="od-row"><span>订单编号</span><span>'+o.id+'</span></div>'
326    +'<div class="od-row"><span>服务规格</span><span>'+(o.spec?o.spec.join(' / '):'-')+'</span></div>'
327    +'<div class="od-row"><span>预约时间</span><span>'+o.date+'</span></div>'
328    +'<div class="od-row"><span>服务地址</span><span>'+o.addr+'</span></div>'
329    +'<div class="od-row"><span>备注</span><span>'+(o.note||'无')+'</span></div>'
330    + (w?('<div class="od-row"><span>服务人员</span><span>'+w.avatar+' '+w.name+'('+w.rating+'★)</span></div>'):'')
331    +'</div>'
332    + (photos?('<div class="card">'+photos+'</div>'):'')
333    +'<div class="card bill"><div class="bill-row"><span>实付金额</span><span>'+money(o.paid||0)+'</span></div><div class="bill-row"><span>支付方式</span><span>'+(o.status==='unpaid'?'待支付':'微信支付')+'</span></div></div>'
334    + (act?('<div class="fixed-bar">'+act+'</div>'):'')
335    +'<div class="bottom-pad"></div>';
336  }
337
338  // =====================================================================
339  // 视图:评价
340  // =====================================================================
341  function renderReview(id){
342    var o = S.orders.filter(function(x){return x.id===id;})[0]; if(!o) return '<div class="empty">订单不存在</div>';
343    var s = svcById(o.sid);
344    return ''
345    +'<div class="rev-head">'+(s?s.emoji+' '+s.name:'服务评价')+'</div>'
346    +'<div class="rate3"><div class="r3-line" data-dim="专业度"><span>专业度</span><span class="stars" data-act="rate" data-dim="专业度">★★★★★</span></div>'
347    +'<div class="r3-line" data-dim="服务态度"><span>服务态度</span><span class="stars" data-act="rate" data-dim="服务态度">★★★★★</span></div>'
348    +'<div class="r3-line" data-dim="准时度"><span>准时度</span><span class="stars" data-act="rate" data-dim="准时度">★★★★★</span></div></div>'
349    +'<textarea id="revTxt" class="rev-txt" placeholder="说说这次的服务体验…"></textarea>'
350    +'<div class="rev-imgs-add"><span class="rev-img">📷 添加图片</span></div>'
351    +'<div class="fixed-bar single"><button class="fb-book" data-act="submitReview" data-id="'+o.id+'">提交评价</button></div>'
352    +'<div class="bottom-pad"></div>';
353  }
354
355  // =====================================================================
356  // 视图:优惠券
357  // =====================================================================
358  function renderCoupons(){
359    var tabs=[['can','可领取'],['got','已领取'],['used','已使用'],['exp','已过期']];
360    var cur=(location.hash.split('/')[1])||'can';
361    var th=tabs.map(function(t){return '<span class="ct'+(cur===t[0]?' on':'')+'" data-act="coupTab" data-s="'+t[0]+'">'+t[1]+'</span>';}).join('');
362    function match(c,s){ if(s==='can')return c.status==='can'; if(s==='got')return c.status==='got'; if(s==='used')return c.status==='used'; if(s==='exp')return c.status==='exp'; return true; }
363    var list=S.coupons.filter(function(c){return match(c,cur);});
364    var items=list.map(function(c){
365      var btn = c.status==='can'? '<button class="cp-btn" data-act="claim" data-id="'+c.id+'">领取</button>'
366              : c.status==='got'? '<button class="cp-btn use" data-act="useCoup" data-id="'+c.id+'">去使用</button>'
367              : '<span class="cp-dis">'+ (c.status==='used'?'已使用':'已过期') +'</span>';
368      return '<div class="cp'+(c.status==='exp'?' exp':'')+'"><div class="cp-l"><div class="cp-cond">'+c.cond+'</div><div class="cp-name">'+c.name+'</div><div class="cp-scope">'+c.scope+'</div></div><div class="cp-r">'+btn+'</div></div>';
369    }).join('') || '<div class="empty">暂无优惠券</div>';
370    return '<div class="ctabs">'+th+'</div><div class="clist">'+items+'</div><div class="bottom-pad"></div>';
371  }
372
373  // =====================================================================
374  // 视图:我的
375  // =====================================================================
376  function renderMine(){
377    var counts={unpaid:0,wait:0,serving:0,done:0,cancel:0};
378    S.orders.forEach(function(o){ if(counts[o.status]!==undefined) counts[o.status]++; });
379    var m=S.member;
380    var mdis = MEMBER_LEVELS.filter(function(x){return x.name===m.level;})[0]||MEMBER_LEVELS[0];
381    var menu=[['coupons','🎫','我的优惠券'],['address','📍','我的地址'],['favorite','❤️','我的收藏'],['member','👑','会员中心'],['help','❓','帮助中心'],['service','🎧','在线客服']];
382    var menuHtml=menu.map(function(mm){return '<div class="m-row" data-act="'+mm[0]+'"><span class="m-ic">'+mm[1]+'</span><span class="m-tx">'+mm[2]+'</span><span class="m-ar">›</span></div>';}).join('');
383    return ''
384    +'<div class="mine-card" data-act="member"><div class="mc-level">'+m.level+'</div>'
385    +'<div class="mc-name">微信用户_'+(''+Math.floor(Math.random()*9000+1000))+'</div>'
386    +'<div class="mc-meta"><span>余额 '+money(m.balance)+'</span><span>积分 '+m.points+'</span><span>折扣 '+(mdis.discount*10).toFixed(1)+'折</span></div></div>'
387    +'<div class="mine-orders"><div class="mo-t">我的订单</div><div class="mo-row">'
388      +'<div data-act="orders" data-s="unpaid"><b>'+counts.unpaid+'</b>待支付</div>'
389      +'<div data-act="orders" data-s="wait"><b>'+counts.wait+'</b>待服务</div>'
390      +'<div data-act="orders" data-s="serving"><b>'+counts.serving+'</b>服务中</div>'
391      +'<div data-act="orders" data-s="done"><b>'+counts.done+'</b>已完成</div></div></div>'
392    +'<div class="m-menu">'+menuHtml+'</div>'
393    +'<div class="bottom-pad"></div>';
394  }
395
396  // =====================================================================
397  // 视图:地址管理
398  // =====================================================================
399  function renderAddress(){
400    var items=S.addr.map(function(a){
401      return '<div class="addr-card"><div class="ac-top"><span class="ac-tag">'+a.tag+'</span><span class="ac-name">'+a.name+' '+a.phone+'</span>'+(a.def?'<span class="ac-def">默认</span>':'')+'</div>'
402        +'<div class="ac-detail">'+a.region+' '+a.detail+'</div>'
403        +'<div class="ac-act"><span data-act="addrEdit" data-id="'+a.id+'">编辑</span><span data-act="addrDel" data-id="'+a.id+'">删除</span>'+(a.def?'':'<span data-act="addrDef" data-id="'+a.id+'">设为默认</span>')+'</div></div>';
404    }).join('') || '<div class="empty">还没有地址</div>';
405    return '<div class="addr-list">'+items+'</div><button class="big-btn" data-act="addrAdd">+ 新增地址</button><div class="bottom-pad"></div>';
406  }
407
408  // =====================================================================
409  // 视图:地址编辑
410  // =====================================================================
411  function renderAddressEdit(param, qs){
412    var id=qs.id||param; var a = id? S.addr.filter(function(x){return x.id===id;})[0] : null;
413    return ''
414    +'<div class="form"><label>联系人</label><input id="f_name" value="'+(a?a.name:'')+'" placeholder="姓名"/>'
415    +'<label>手机号</label><input id="f_phone" value="'+(a?a.phone:'')+'" placeholder="11位手机号"/>'
416    +'<label>所在地区</label><input id="f_region" value="'+(a?a.region:'')+'" placeholder="省/市/区"/>'
417    +'<label>详细地址</label><input id="f_detail" value="'+(a?a.detail:'')+'" placeholder="街道/小区/门牌"/>'
418    +'<label>标签</label><div class="tag-row"><span class="ftag'+(!a||a.tag==='家'?' on':'')+'" data-act="ftag" data-v="家">家</span><span class="ftag'+(a&&a.tag==='公司'?' on':'')+'" data-act="ftag" data-v="公司">公司</span><span class="ftag'+(a&&a.tag==='学校'?' on':'')+'" data-act="ftag" data-v="学校">学校</span></div></div>'
419    +'<div class="fixed-bar single"><button class="fb-book" data-act="saveAddr" data-id="'+(id||'')+'">保存</button></div>'
420    +'<div class="bottom-pad"></div>';
421  }
422
423  // =====================================================================
424  // 视图:收藏
425  // =====================================================================
426  function renderFavorite(){
427    var sFav=S.fav.filter(function(f){return f.type==='s';}).map(function(f){var s=svcById(f.id);if(!s)return '';return '<div class="srow" data-act="service" data-id="'+s.id+'"><div class="srow-em">'+s.emoji+'</div><div class="srow-bd"><div class="srow-name">'+s.name+'</div><div class="srow-bot"><span class="srow-price">'+money(s.price)+'</span></div><div class="fav-del" data-act="favDel" data-type="s" data-id="'+s.id+'">移除</div></div></div>';}).join('');
428    var wFav=S.fav.filter(function(f){return f.type==='w';}).map(function(f){var w=wkById(f.id);if(!w)return '';return '<div class="wmini" data-act="worker" data-id="'+w.id+'"><div class="w-av">'+w.avatar+'</div><div class="wmini-info"><div class="w-name">'+w.name+'</div><div class="w-rate">'+star(w.rating)+'</div></div><div class="fav-del" data-act="favDel" data-type="w" data-id="'+w.id+'">移除</div></div>';}).join('');
429    return '<div class="sec-title">收藏的服务</div>'+(sFav||'<div class="empty">还没有收藏服务</div>')
430      +'<div class="sec-title">收藏的人员</div>'+(wFav||'<div class="empty">还没有收藏人员</div>')+'<div class="bottom-pad"></div>';
431  }
432
433  // =====================================================================
434  // 视图:帮助中心
435  // =====================================================================
436  function renderHelp(){
437    var items=FAQ.map(function(f,i){
438      return '<div class="faq"><div class="faq-q" data-act="faq" data-i="'+i+'">'+ (i+1) +'. '+f.q+' <span class="faq-ar">›</span></div><div class="faq-a" id="faq'+i+'" style="display:none">'+f.a+'</div></div>';
439    }).join('');
440    return '<div class="help-banner">🎧 客服热线 400-820-1995(9:00-21:00)</div>'+items+'<div class="bottom-pad"></div>';
441  }
442
443  // =====================================================================
444  // 视图:会员中心
445  // =====================================================================
446  function renderMember(){
447    var m=S.member;
448    var lvHtml=MEMBER_LEVELS.map(function(L){
449      var on=L.name===m.level?' on':'';
450      return '<div class="lv'+on+'"><div class="lv-name">'+L.name+'</div><div class="lv-perk">'+L.perks.join(' · ')+'</div><div class="lv-need">'+(L.need?('消费满'+money(L.need)+'升级'):'默认')+'</div></div>';
451    }).join('');
452    var cards=[['保洁次卡','10次日常保洁','¥1099','省¥291'],['深度保洁季卡','季度4次','¥999','省¥397'],['全能充值卡','余额充值','¥500','送¥50']];
453    var cardHtml=cards.map(function(c){return '<div class="mcard"><div class="mc-t">'+c[0]+'</div><div class="mc-sub">'+c[1]+'</div><div class="mc-price">'+c[2]+' <span class="mc-save">'+c[3]+'</span></div><button class="mc-buy" data-act="buyCard" data-n="'+c[0]+'">开通</button></div>';}).join('');
454    return ''
455    +'<div class="mine-card"><div class="mc-level">'+m.level+'</div><div class="mc-meta"><span>余额 '+money(m.balance)+'</span><span>积分 '+m.points+'</span></div></div>'
456    +'<div class="sec-title">会员等级</div><div class="lv-list">'+lvHtml+'</div>'
457    +'<div class="sec-title">会员卡 / 次卡</div><div class="mcard-list">'+cardHtml+'</div>'
458    +'<div class="card"><div class="card-t">会员权益</div><div class="card-c">· 全品类会员折扣,等级越高折扣越低<br>· 优先派单,热门阿姨/师傅优先锁定<br>· 专属客服与生日礼遇<br>· 钻石会员赠送全年服务保障险</div></div>'
459    +'<div class="bottom-pad"></div>';
460  }
461
462  // =====================================================================
463  // 事件绑定(每次渲染后按需)
464  // =====================================================================
465  function bindView(name, r){
466    // 分类左栏点击
467    if(name==='category'){
468      $('#view').on('click','.cate-li',function(){
469        var id=$(this).data('id');
470        $('.cate-li').removeClass('on'); $(this).addClass('on');
471        var list=SERVICES.filter(function(s){return s.cat===id;});
472        $('#cateRight').html(list.map(function(s){
473          return '<div class="srow" data-act="service" data-id="'+s.id+'"><div class="srow-em">'+s.emoji+'</div><div class="srow-bd"><div class="srow-name">'+s.name+'</div><div class="srow-tags">'+s.tags.map(function(t){return '<span>'+t+'</span>';}).join('')+'</div><div class="srow-bot"><span class="rate">'+star(s.rating)+'</span><span class="srow-price">'+money(s.price)+'<i>/'+s.unit+'</i></span></div></div></div>';
474        }).join('') || '<div class="empty">该分类暂未上架服务</div>');
475      });
476    }
477    if(name==='search'){
478      $('#view').on('click','.sch-h',function(){ $('#schInput').val($(this).data('q')); doSearch(); });
479    }
480  }
481
482  function doSearch(){
483    var q=($('#schInput').val()||'').trim();
484    if(!q) return;
485    var res=SERVICES.filter(function(s){return s.name.indexOf(q)>=0;});
486    var wres=WORKERS.filter(function(w){return w.name.indexOf(q)>=0 || w.goodAt.join('').indexOf(q)>=0;});
487    var h='';
488    if(res.length) h+='<div class="sec-title">服务</div>'+res.map(function(s){return '<div class="srow" data-act="service" data-id="'+s.id+'"><div class="srow-em">'+s.emoji+'</div><div class="srow-bd"><div class="srow-name">'+s.name+'</div><div class="srow-bot"><span class="srow-price">'+money(s.price)+'</span></div></div></div>';}).join('');
489    if(wres.length) h+='<div class="sec-title">人员</div>'+wres.map(function(w){return '<div class="wmini" data-act="worker" data-id="'+w.id+'"><div class="w-av">'+w.avatar+'</div><div class="wmini-info"><div class="w-name">'+w.name+'</div><div class="w-rate">'+star(w.rating)+'</div></div></div>';}).join('');
490    if(!res.length&&!wres.length) h='<div class="empty">未找到「'+q+'」相关结果</div>';
491    $('#schResult').html(h);
492  }
493
494  // =====================================================================
495  // 全局事件委托
496  // =====================================================================
497  $(document).on('click','[data-act]',function(){
498    var $t=$(this); var act=$t.data('act');
499    switch(act){
500      case 'back': back(); break;
501      case 'tab': go('#/'+$t.data('route')); break;
502      case 'search': go('#/search'); break;
503      case 'cat': go('#/category'); setTimeout(function(){ $('.cate-li[data-id="'+$t.data('id')+'"]').trigger('click'); },30); break;
504      case 'service': go('#/service/'+$t.data('id')); break;
505      case 'worker': go('#/worker/'+$t.data('id')); break;
506      case 'catTab': $('.cate-li').removeClass('on'); $t.addClass('on'); var id=$t.data('id'); var list=SERVICES.filter(function(s){return s.cat===id;}); $('#cateRight').html(list.map(function(s){return '<div class="srow" data-act="service" data-id="'+s.id+'"><div class="srow-em">'+s.emoji+'</div><div class="srow-bd"><div class="srow-name">'+s.name+'</div><div class="srow-tags">'+s.tags.map(function(t){return "<span>"+t+"</span>";}).join("")+'</div><div class="srow-bot"><span class="rate">'+star(s.rating)+'</span><span class="srow-price">'+money(s.price)+'<i>/'+s.unit+'</i></span></div></div></div>';}).join('')||'<div class="empty">该分类暂未上架服务</div>'); break;
507      case 'doSearch': doSearch(); break;
508      case 'hotSearch': $('#schInput').val($t.data('q')); doSearch(); break;
509      case 'spec': { var si=$t.data('si'),oi=$t.data('oi'); draft.spec[si]=oi; render(); break; }
510      case 'pickWorker': draft.wid=$t.data('id'); toast('已指定 '+wkById(draft.wid).name); break;
511      case 'day': draft.day=$t.data('i'); render(); break;
512      case 'slot': draft.slot=$t.data('i'); render(); break;
513      case 'bookService': draft={sid:$t.data('id')}; go('#/booking'); break;
514      case 'bookWorker': { var wid=$t.data('id'); var s=WORKERS.filter(function(w){return w.id===wid;})[0]; if(s&&s.services&&s.services[0]){ draft={sid:s.services[0],wid:wid}; go('#/booking?sid='+s.services[0]+'&wid='+wid); } else { toast('该人员暂无可约服务'); } break; }
515      case 'favService': toggleFav('s',$t.data('id')); toast('已收藏服务'); break;
516      case 'favWorker': toggleFav('w',$t.data('id')); toast('已收藏人员'); break;
517      case 'favDel': S.fav=S.fav.filter(function(f){return !(f.type===$t.data('type')&&f.id===$t.data('id'));}); persist(); render(); break;
518      case 'submitBooking': submitBooking(); break;
519      case 'order': go('#/order/'+$t.data('id')); break;
520      case 'orderTab': go('#/orders/'+$t.data('s')); break;
521      case 'reviewOpen': go('#/review/'+$t.data('id')); break;
522      case 'rate': { var dim=$t.data('dim'); var idx=$t.parent().index(); var cur=parseInt($t.data('val')||5); /* simple: set all stars */ var stars=$t.parent().find('.stars'); stars.text('★★★★★'); $t.closest('.rate3').data(dim,5); break; }
523      case 'submitReview': submitReview($t.data('id')); break;
524      case 'coupTab': go('#/coupons/'+$t.data('s')); break;
525      case 'claim': { var c=S.coupons.filter(function(x){return x.id===$t.data('id');})[0]; if(c){c.status='got'; persist(); toast('领取成功'); render();} break; }
526      case 'useCoup': go('#/category'); break;
527      case 'payOrder': { var o=S.orders.filter(function(x){return x.id===$t.data('id');})[0]; if(o){o.status='wait'; o.flow=['paid','dispatched','wait']; o.paid=svcById(o.sid).price; persist(); toast('支付成功'); render();} break; }
528      case 'cancelOrder': { var o2=S.orders.filter(function(x){return x.id===$t.data('id');})[0]; if(o2){o2.status='cancel'; persist(); toast('已取消'); render();} break; }
529      case 'changeOrder': toast('已提交改期申请,客服将联系您'); break;
530      case 'complain': toast('正在为您接通专属客服…'); break;
531      case 'address': go('#/address'); break;
532      case 'addrAdd': go('#/addressEdit'); break;
533      case 'addrEdit': go('#/addressEdit?id='+$t.data('id')); break;
534      case 'addrDel': S.addr=S.addr.filter(function(a){return a.id!==$t.data('id');}); persist(); render(); toast('已删除'); break;
535      case 'addrDef': S.addr.forEach(function(a){a.def=(a.id===$t.data('id'))?1:0;}); persist(); render(); break;
536      case 'ftag': $('.ftag').removeClass('on'); $t.addClass('on'); break;
537      case 'saveAddr': saveAddr($t.data('id')); break;
538      case 'favorite': go('#/favorite'); break;
539      case 'help': go('#/help'); break;
540      case 'member': go('#/member'); break;
541      case 'coupons': go('#/coupons'); break;
542      case 'mine': go('#/mine'); break;
543      case 'orders': go('#/orders/'+($t.data('s')||'all')); break;
544      case 'service_menu': go('#/service'); break;
545      case 'buyCard': { var n=$t.data('n'); S.member.balance+=500; S.member.points+=50; if(S.member.level==='普通会员')S.member.level='银卡会员'; persist(); toast('已开通'+n+',余额+500'); break; }
546      case 'faq': { var i=$t.data('i'); var el=$('#faq'+i); if(el.css('display')==='none') el.show(); else el.hide(); break; }
547    }
548  });
549  $(document).on('change','#schInput',function(){});
550
551  function toggleFav(type,id){
552    var ex=S.fav.filter(function(f){return f.type===type&&f.id===id;})[0];
553    if(ex) S.fav=S.fav.filter(function(f){return !(f.type===type&&f.id===id);});
554    else S.fav.push({type:type,id:id});
555    persist();
556  }
557
558  function submitBooking(){
559    var sid=draft.sid; var s=svcById(sid); if(!s){toast('请选择服务');return;}
560    var specNames=(s.specs||[]).map(function(sp,si){return sp.opts[draft.spec[si]].t;});
561    var days=[]; var wd=['周日','周一','周二','周三','周四','周五','周六'];
562    for(var i=0;i<7;i++){var d=new Date();d.setDate(d.getDate()+i);days.push((i===0?'今天':(i===1?'明天':wd[d.getDay()]))+' '+(d.getMonth()+1)+'-'+d.getDate());}
563    var slots=['08:00-10:00','10:00-12:00','13:00-15:00','15:00-17:00','18:00-20:00'];
564    var addr=S.addr.filter(function(a){return a.def;})[0]||S.addr[0];
565    var oid='D'+Date.now();
566    var total=draft.total||s.price;
567    var o={id:oid, sid:sid, wid:draft.wid||null, status:'wait', paid:total, date:days[draft.day]+' '+slots[draft.slot], spec:specNames, addr:addr?(addr.region+' '+addr.detail):'未填地址', note:($('#bkNote').val()||''), flow:['paid','dispatched','wait'], reviewed:0};
568    S.orders.unshift(o); persist();
569    toast('预约成功,等待派单');
570    draft={};
571    go('#/order/'+oid);
572  }
573
574  function submitReview(id){
575    var o=S.orders.filter(function(x){return x.id===id;})[0]; if(!o) return;
576    var txt=$('#revTxt').val()||'';
577    o.review={r:5,txt:txt};
578    o.reviewed=1; o.status='done'; o.flow=['paid','dispatched','serving','done','reviewed'];
579    persist(); toast('感谢您的评价!'); go('#/order/'+id);
580  }
581
582  function saveAddr(id){
583    var name=$('#f_name').val(), phone=$('#f_phone').val(), region=$('#f_region').val(), detail=$('#f_detail').val();
584    var tag=$('.ftag.on').data('v')||'家';
585    if(!name||!phone||!region||!detail){ toast('请填写完整信息'); return; }
586    if(id){ var a=S.addr.filter(function(x){return x.id===id;})[0]; if(a){a.name=name;a.phone=phone;a.region=region;a.detail=detail;a.tag=tag;} }
587    else { S.addr.push({id:'a'+Date.now(),name:name,phone:phone,region:region,detail:detail,tag:tag,def:0}); }
588    persist(); toast('已保存'); go('#/address');
589  }
590
591  // ---------------- init ----------------
592  $(function(){ window.addEventListener('hashchange',render); if(!location.hash) location.hash='#/home'; render(); });
593})();
594