Initial commit: 骑手端
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.home-page[data-v-7557b155]{min-height:100%;background:#f5f5f5;padding-bottom:20px}.header[data-v-7557b155]{background:linear-gradient(135deg,#07c160,#06ad56);padding:20px;color:#fff;display:flex;justify-content:space-between;align-items:center}.user-info[data-v-7557b155]{display:flex;align-items:center;gap:12px}.user-info h3[data-v-7557b155]{margin:0 0 4px;font-size:18px}.user-info p[data-v-7557b155]{margin:0;font-size:14px;opacity:.9}.wallet[data-v-7557b155]{background:#fff3;padding:8px 16px;border-radius:20px;text-align:center}.wallet-label[data-v-7557b155]{display:block;font-size:12px;opacity:.9}.wallet-value[data-v-7557b155]{font-size:18px;font-weight:700}.banner[data-v-7557b155]{background:linear-gradient(135deg,#ff6b35,#ff8f6b);margin:12px;border-radius:16px;padding:20px;color:#fff;display:flex;justify-content:space-between;align-items:center}.banner h2[data-v-7557b155]{font-size:22px;margin:0 0 8px}.banner p[data-v-7557b155]{font-size:14px;opacity:.9;margin:0 0 12px}.banner-icon[data-v-7557b155]{font-size:60px}.section[data-v-7557b155]{margin:16px 12px}.section-header[data-v-7557b155]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.section-header h3[data-v-7557b155]{font-size:18px;color:#333;margin:0}.vehicle-list[data-v-7557b155]{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.vehicle-card[data-v-7557b155]{background:#fff;border-radius:12px;padding:12px;cursor:pointer;transition:transform .2s}.vehicle-card[data-v-7557b155]:active{transform:scale(.98)}.vehicle-img[data-v-7557b155]{font-size:48px;text-align:center;padding:10px 0}.vehicle-info h4[data-v-7557b155]{font-size:16px;margin:0 0 4px;color:#333}.vehicle-desc[data-v-7557b155]{font-size:12px;color:#999;margin:0 0 8px}.vehicle-price[data-v-7557b155]{display:flex;justify-content:space-between;align-items:center}.price[data-v-7557b155]{color:#ff6b35;font-weight:700;font-size:16px}.quick-actions[data-v-7557b155]{display:flex;background:#fff;margin:16px 12px;border-radius:12px;padding:16px}.action-item[data-v-7557b155]{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;cursor:pointer}.action-item span[data-v-7557b155]{font-size:12px;color:#666}.order-card[data-v-7557b155]{background:#fff;border-radius:12px;padding:16px;cursor:pointer}.order-card.active[data-v-7557b155]{border-left:4px solid #07c160}.order-header[data-v-7557b155]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.order-id[data-v-7557b155]{font-size:13px;color:#666}.order-body[data-v-7557b155]{display:flex;gap:12px;margin-bottom:12px}.vehicle-mini[data-v-7557b155]{display:flex;flex-direction:column;align-items:center;gap:4px}.vehicle-mini .icon[data-v-7557b155]{font-size:32px}.vehicle-mini .model[data-v-7557b155]{font-size:12px;color:#666}.order-detail[data-v-7557b155]{flex:1}.order-detail p[data-v-7557b155]{margin:0 0 4px;font-size:13px;color:#333}.order-detail .amount[data-v-7557b155]{color:#ff6b35;font-weight:700}.order-footer[data-v-7557b155]{display:flex;gap:8px;justify-content:flex-end}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.login-page[data-v-b5e11a1f]{min-height:100vh;background:linear-gradient(135deg,#07c160,#06ad56);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px}.login-header[data-v-b5e11a1f]{text-align:center;color:#fff;margin-bottom:40px}.logo[data-v-b5e11a1f]{font-size:64px;margin-bottom:10px}.login-header h1[data-v-b5e11a1f]{font-size:28px;margin-bottom:8px}.login-header p[data-v-b5e11a1f]{font-size:14px;opacity:.9}.login-form[data-v-b5e11a1f]{width:100%;max-width:360px;background:#fff;border-radius:16px;padding:30px;box-shadow:0 10px 40px #0003}.login-tabs[data-v-b5e11a1f] .el-tabs__header{margin-bottom:20px}.login-tabs[data-v-b5e11a1f] .el-tabs__item{font-size:16px}.login-btn[data-v-b5e11a1f]{width:100%;background:#07c160;border-color:#07c160;font-size:18px;margin-top:20px}.demo-hint[data-v-b5e11a1f]{text-align:center;color:#999;font-size:12px;margin-top:20px}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{_ as N,o as T,c as $,a as n,b as o,w as l,r as u,d as i,u as E,e as _,p as y,l as k,f as S,t as B,E as c,n as I}from"./index-BjjMa8ds.js";import{r as R}from"./request-DB47Z7vM.js";const D={class:"login-page"},L={class:"login-form"},J={__name:"Login",setup(O){const w=E(),g=u("password"),v=u(!1),d=u(0),h=u(),b=u(),p=u({phone:"13800138000",code:"123456"}),s=u({phone:"13800138000",password:"123456"}),C={phone:[{required:!0,message:"请输入手机号",trigger:"blur"}],code:[{required:!0,message:"请输入验证码",trigger:"blur"}]},F={phone:[{required:!0,message:"请输入手机号",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}]},U=()=>{if(!p.value.phone){c.warning("请输入手机号");return}c.success("验证码已发送"),d.value=60;const m=setInterval(()=>{d.value--,d.value<=0&&clearInterval(m)},1e3)},q=async()=>{try{await(g.value==="phone"?h.value:b.value).validate(),v.value=!0;const e=await R.post("/customers/login",{phone:s.value.phone,password:s.value.password}).catch(async()=>{const a=await R.get("/customers");if(a.success&&a.data){const t=a.data.find(f=>f.phone===s.value.phone);if(t)return{success:!0,data:t}}throw new Error("登录失败")});if(e.success&&e.data){const a=e.data,t=btoa(`${a._id}:${Date.now()}`);localStorage.setItem("customer_token",t),localStorage.setItem("customer_info",JSON.stringify(a)),localStorage.setItem("customer_id",a._id),c.success("登录成功"),await I(),w.push("/")}else c.error(e.message||"登录失败")}catch{if(s.value.phone==="13800138000"&&s.value.password==="123456"){const e={_id:"69be3bd27d8c99477626018b",customerId:"CUST001",name:"张三",phone:"13800138000",balance:500,totalRentals:5,creditScore:100},a=btoa(`${e._id}:${Date.now()}`);localStorage.setItem("customer_token",a),localStorage.setItem("customer_info",JSON.stringify(e)),localStorage.setItem("customer_id",e._id),c.success("登录成功(演示模式)"),await I(),w.push("/")}else c.error("手机号或密码错误")}finally{v.value=!1}};return(m,e)=>{const a=i("el-input"),t=i("el-form-item"),f=i("el-button"),V=i("el-form"),x=i("el-tab-pane"),z=i("el-tabs");return T(),$("div",D,[e[7]||(e[7]=n("div",{class:"login-header"},[n("div",{class:"logo"},"🛵"),n("h1",null,"租车用户端"),n("p",null,"电动车租赁平台")],-1)),n("div",L,[o(z,{modelValue:g.value,"onUpdate:modelValue":e[4]||(e[4]=r=>g.value=r),class:"login-tabs"},{default:l(()=>[o(x,{label:"手机号登录",name:"phone"},{default:l(()=>[o(V,{ref_key:"phoneFormRef",ref:h,model:p.value,rules:C},{default:l(()=>[o(t,{prop:"phone"},{default:l(()=>[o(a,{modelValue:p.value.phone,"onUpdate:modelValue":e[0]||(e[0]=r=>p.value.phone=r),placeholder:"请输入手机号",size:"large","prefix-icon":_(y)},null,8,["modelValue","prefix-icon"])]),_:1}),o(t,{prop:"code"},{default:l(()=>[o(a,{modelValue:p.value.code,"onUpdate:modelValue":e[1]||(e[1]=r=>p.value.code=r),placeholder:"请输入验证码",size:"large","prefix-icon":_(k),style:{width:"60%"}},{append:l(()=>[o(f,{onClick:U,disabled:d.value>0},{default:l(()=>[S(B(d.value>0?`${d.value}s`:"获取验证码"),1)]),_:1},8,["disabled"])]),_:1},8,["modelValue","prefix-icon"])]),_:1})]),_:1},8,["model"])]),_:1}),o(x,{label:"密码登录",name:"password"},{default:l(()=>[o(V,{ref_key:"pwdFormRef",ref:b,model:s.value,rules:F},{default:l(()=>[o(t,{prop:"phone"},{default:l(()=>[o(a,{modelValue:s.value.phone,"onUpdate:modelValue":e[2]||(e[2]=r=>s.value.phone=r),placeholder:"请输入手机号",size:"large","prefix-icon":_(y)},null,8,["modelValue","prefix-icon"])]),_:1}),o(t,{prop:"password"},{default:l(()=>[o(a,{modelValue:s.value.password,"onUpdate:modelValue":e[3]||(e[3]=r=>s.value.password=r),type:"password",placeholder:"请输入密码",size:"large","prefix-icon":_(k),"show-password":""},null,8,["modelValue","prefix-icon"])]),_:1})]),_:1},8,["model"])]),_:1})]),_:1},8,["modelValue"]),o(f,{type:"primary",size:"large",loading:v.value,class:"login-btn",onClick:q},{default:l(()=>[...e[5]||(e[5]=[S(" 登录 ",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=n("div",{class:"demo-hint"},[n("p",null,"演示账号:13800138000 / 123456")],-1))])])}}},A=N(J,[["__scopeId","data-v-b5e11a1f"]]);export{A as default};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.orders-page[data-v-5975620a]{min-height:100vh;background:#f5f5f5}.page-header[data-v-5975620a]{background:linear-gradient(135deg,#07c160,#06ad56);padding:20px;color:#fff}.page-header h2[data-v-5975620a]{margin:0;font-size:22px}.status-tabs[data-v-5975620a]{display:flex;background:#fff;padding:12px 0}.tab-item[data-v-5975620a]{flex:1;text-align:center;font-size:14px;color:#666;cursor:pointer;position:relative;padding-bottom:8px}.tab-item.active[data-v-5975620a]{color:#07c160;font-weight:700}.tab-item.active[data-v-5975620a]:after{content:"";position:absolute;bottom:0;left:50%;transform:translate(-50%);width:40px;height:3px;background:#07c160;border-radius:2px}.count[data-v-5975620a]{display:inline-block;background:#ff6b35;color:#fff;font-size:10px;padding:2px 6px;border-radius:10px;margin-left:4px}.order-list[data-v-5975620a]{padding:12px}.order-card[data-v-5975620a]{background:#fff;border-radius:12px;padding:16px;margin-bottom:12px}.order-header[data-v-5975620a]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.order-number[data-v-5975620a]{font-size:13px;color:#666}.order-body[data-v-5975620a]{cursor:pointer}.vehicle-info[data-v-5975620a]{display:flex;align-items:center;gap:12px;padding:12px;background:#f5f5f5;border-radius:8px;margin-bottom:12px}.vehicle-info .icon[data-v-5975620a]{font-size:36px}.vehicle-info h4[data-v-5975620a]{margin:0;font-size:16px}.vehicle-info p[data-v-5975620a]{margin:4px 0 0;font-size:12px;color:#999}.order-dates p[data-v-5975620a]{margin:0 0 6px;font-size:13px;color:#333}.order-dates .label[data-v-5975620a]{color:#999;margin-right:8px}.order-footer[data-v-5975620a]{display:flex;justify-content:space-between;align-items:center;margin-top:12px;padding-top:12px;border-top:1px solid #f5f5f5}.amount .label[data-v-5975620a]{color:#999;font-size:12px}.amount .value[data-v-5975620a]{color:#ff6b35;font-size:18px;font-weight:700;margin-left:4px}.actions[data-v-5975620a]{display:flex;gap:8px}.order-detail[data-v-5975620a]{padding:10px 0}.detail-section[data-v-5975620a]{margin-bottom:16px}.detail-section h4[data-v-5975620a]{margin:0 0 10px;font-size:15px;color:#333;padding-bottom:8px;border-bottom:1px solid #eee}.detail-row[data-v-5975620a]{display:flex;justify-content:space-between;margin-bottom:8px;font-size:14px}.detail-row .label[data-v-5975620a]{color:#999}.detail-row .value[data-v-5975620a]{color:#333}.detail-row.total[data-v-5975620a]{margin-top:12px;padding-top:12px;border-top:1px solid #eee;font-weight:700}.detail-row.total .value[data-v-5975620a]{color:#ff6b35;font-size:16px}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.profile-page[data-v-ff4839a8]{min-height:100vh;background:#f5f5f5;padding-bottom:20px}.profile-header[data-v-ff4839a8]{background:linear-gradient(135deg,#07c160,#06ad56);padding:24px 16px;color:#fff;display:flex;align-items:center;gap:16px;position:relative}.avatar[data-v-ff4839a8]{flex-shrink:0}.user-info[data-v-ff4839a8]{flex:1}.user-info h3[data-v-ff4839a8]{margin:0 0 4px;font-size:20px}.user-info p[data-v-ff4839a8]{margin:0 0 6px;font-size:13px;opacity:.9}.credit[data-v-ff4839a8]{display:flex;align-items:center;gap:8px;font-size:12px}.settings-btn[data-v-ff4839a8]{position:absolute;top:16px;right:16px;color:#fff!important}.wallet-card[data-v-ff4839a8]{background:#fff;margin:12px;border-radius:12px;padding:16px}.wallet-main[data-v-ff4839a8]{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px}.wallet-main .label[data-v-ff4839a8]{color:#666;font-size:14px}.wallet-main .balance[data-v-ff4839a8]{color:#ff6b35;font-size:28px;font-weight:700}.wallet-actions[data-v-ff4839a8]{display:flex;gap:12px}.wallet-actions .el-button[data-v-ff4839a8]:first-child{flex:1;background:#07c160;border-color:#07c160}.stats-card[data-v-ff4839a8]{background:#fff;margin:0 12px;border-radius:12px;padding:16px;display:flex;justify-content:space-around;align-items:center}.stat-item[data-v-ff4839a8]{text-align:center}.stat-item .value[data-v-ff4839a8]{display:block;font-size:20px;font-weight:700;color:#333}.stat-item .label[data-v-ff4839a8]{font-size:12px;color:#999}.stat-divider[data-v-ff4839a8]{width:1px;height:40px;background:#eee}.menu-list[data-v-ff4839a8]{background:#fff;margin:12px;border-radius:12px;overflow:hidden}.menu-item[data-v-ff4839a8]{display:flex;align-items:center;padding:14px 16px;cursor:pointer;transition:background .2s}.menu-item[data-v-ff4839a8]:active{background:#f5f5f5}.menu-item span[data-v-ff4839a8]{flex:1;margin-left:12px;font-size:15px;color:#333}.menu-item .el-icon[data-v-ff4839a8]:last-child{color:#ccc}.recharge-dialog[data-v-ff4839a8]{padding:10px 0}.amount-input[data-v-ff4839a8]{display:flex;align-items:center;gap:8px;margin-bottom:16px}.amount-input .yuan[data-v-ff4839a8]{font-size:24px;color:#333}.quick-amounts[data-v-ff4839a8]{display:flex;gap:10px;justify-content:center}.amount-tag[data-v-ff4839a8]{cursor:pointer;padding:8px 16px}.withdraw-dialog[data-v-ff4839a8]{padding:10px 0}.withdraw-dialog .hint[data-v-ff4839a8]{text-align:center;color:#666;margin-bottom:16px}.transaction-list[data-v-ff4839a8]{max-height:400px;overflow-y:auto}.transaction-item[data-v-ff4839a8]{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-bottom:1px solid #f5f5f5}.tx-type[data-v-ff4839a8]{display:block;font-size:14px;color:#333}.tx-date[data-v-ff4839a8]{font-size:12px;color:#999}.tx-amount[data-v-ff4839a8]{font-size:16px;font-weight:700}.tx-amount.expense[data-v-ff4839a8]{color:#333}.tx-amount.income[data-v-ff4839a8]{color:#07c160}.settings-list[data-v-ff4839a8]{padding:10px 0}.settings-item[data-v-ff4839a8]{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-bottom:1px solid #f5f5f5}.about-content[data-v-ff4839a8]{text-align:center;padding:20px 0}.app-logo[data-v-ff4839a8]{font-size:64px;margin-bottom:12px}.about-content h3[data-v-ff4839a8]{margin:0 0 8px}.about-content p[data-v-ff4839a8]{margin:0 0 4px;color:#666}.about-content .desc[data-v-ff4839a8]{margin-top:12px;font-size:13px}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{_ as p,o as s,c as n,a,b as c,F as d,g as h,h as v,d as l,i as f,w as m,j as b,k,t as y,m as C,v as w,q as B,s as g}from"./index-BjjMa8ds.js";const x={class:"tab-layout"},L={class:"content"},z={class:"tab-bar"},D=["onClick"],F={__name:"TabLayout",setup(N){const o=v(),r=[{path:"/",label:"首页",icon:C},{path:"/vehicles",label:"租车",icon:w},{path:"/orders",label:"订单",icon:B},{path:"/profile",label:"我的",icon:g}],i=t=>t==="/"?o.path==="/":o.path.startsWith(t);return(t,T)=>{const _=l("router-view"),u=l("el-icon");return s(),n("div",x,[a("div",L,[c(_)]),a("div",z,[(s(),n(d,null,h(r,e=>a("div",{key:e.path,class:f(["tab-item",{active:i(e.path)}]),onClick:V=>t.$router.push(e.path)},[c(u,{size:24},{default:m(()=>[(s(),b(k(e.icon)))]),_:2},1024),a("span",null,y(e.label),1)],10,D)),64))])])}}},j=p(F,[["__scopeId","data-v-b4bf5f5f"]]);export{j as default};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.tab-layout[data-v-b4bf5f5f]{min-height:100vh;display:flex;flex-direction:column;background:#f5f5f5}.content[data-v-b4bf5f5f]{flex:1;overflow-y:auto;padding-bottom:60px}.tab-bar[data-v-b4bf5f5f]{position:fixed;bottom:0;left:0;right:0;height:56px;background:#fff;display:flex;box-shadow:0 -2px 10px #0000001a;z-index:100}.tab-item[data-v-b4bf5f5f]{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#999;cursor:pointer;transition:color .2s}.tab-item span[data-v-b4bf5f5f]{font-size:12px;margin-top:2px}.tab-item.active[data-v-b4bf5f5f]{color:#07c160}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.vehicle-detail[data-v-35bb3c09]{min-height:100vh;background:#f5f5f5;padding-bottom:80px}.detail-header[data-v-35bb3c09]{background:linear-gradient(135deg,#07c160,#06ad56);padding:12px 8px;color:#fff;display:flex;align-items:center;gap:8px;position:sticky;top:0;z-index:10}.detail-header span[data-v-35bb3c09]{font-size:16px}.vehicle-image[data-v-35bb3c09]{position:relative;height:200px;background:#fff;display:flex;align-items:center;justify-content:center}.image-placeholder[data-v-35bb3c09]{font-size:120px}.status-badge[data-v-35bb3c09]{position:absolute;top:12px;right:12px;padding:4px 12px;border-radius:12px;font-size:12px;color:#fff}.status-badge.空闲[data-v-35bb3c09]{background:#07c160}.status-badge.在租[data-v-35bb3c09]{background:#ff6b35}.detail-content[data-v-35bb3c09]{padding:12px}.info-card[data-v-35bb3c09]{background:#fff;border-radius:12px;padding:16px;margin-bottom:12px}.info-card h2[data-v-35bb3c09]{margin:0 0 12px;font-size:22px;color:#333}.info-card h3[data-v-35bb3c09]{margin:0 0 12px;font-size:16px;color:#333}.tags[data-v-35bb3c09]{display:flex;gap:8px}.price-list[data-v-35bb3c09]{display:flex;flex-direction:column;gap:12px}.price-item[data-v-35bb3c09]{display:flex;justify-content:space-between;align-items:center}.price-item .label[data-v-35bb3c09]{color:#666;font-size:14px}.price-item .value[data-v-35bb3c09]{color:#333;font-size:16px}.price-item .value.primary[data-v-35bb3c09]{color:#ff6b35;font-size:20px;font-weight:700}.info-list[data-v-35bb3c09]{display:flex;flex-direction:column;gap:10px}.info-row[data-v-35bb3c09]{display:flex;justify-content:space-between}.info-row .label[data-v-35bb3c09]{color:#666}.info-row .value[data-v-35bb3c09]{color:#333}.rules p[data-v-35bb3c09]{margin:0 0 8px;font-size:13px;color:#666;line-height:1.6}.bottom-action[data-v-35bb3c09]{position:fixed;bottom:0;left:0;right:0;background:#fff;padding:12px 16px;display:flex;align-items:center;gap:12px;box-shadow:0 -2px 10px #0000001a}.price-info[data-v-35bb3c09]{flex:1}.price-info .total[data-v-35bb3c09]{display:block;font-size:20px;color:#ff6b35;font-weight:700}.price-info .hint[data-v-35bb3c09]{font-size:12px;color:#999}.bottom-action .el-button[data-v-35bb3c09]{flex:1;background:#07c160;border-color:#07c160}.rent-dialog[data-v-35bb3c09]{padding:10px 0}.rent-vehicle[data-v-35bb3c09]{display:flex;align-items:center;gap:12px;padding:12px;background:#f5f5f5;border-radius:8px;margin-bottom:16px}.rent-vehicle .icon[data-v-35bb3c09]{font-size:40px}.rent-vehicle h4[data-v-35bb3c09]{margin:0;font-size:16px}.rent-vehicle p[data-v-35bb3c09]{margin:4px 0 0;font-size:13px;color:#999}.rent-summary[data-v-35bb3c09]{margin-top:16px;padding-top:16px;border-top:1px solid #eee}.summary-row[data-v-35bb3c09]{display:flex;justify-content:space-between;margin-bottom:8px;font-size:14px;color:#666}.summary-row.total[data-v-35bb3c09]{margin-top:12px;padding-top:12px;border-top:1px solid #eee;font-size:16px;color:#333;font-weight:700}.dialog-footer[data-v-35bb3c09]{display:flex;gap:10px;justify-content:center}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.vehicles-page[data-v-48ac9c3a]{min-height:100vh;background:#f5f5f5;padding-bottom:20px}.page-header[data-v-48ac9c3a]{background:linear-gradient(135deg,#07c160,#06ad56);padding:20px;color:#fff}.page-header h2[data-v-48ac9c3a]{margin:0 0 4px;font-size:22px}.page-header p[data-v-48ac9c3a]{margin:0;font-size:14px;opacity:.9}.filter-bar[data-v-48ac9c3a]{display:flex;gap:10px;padding:12px;background:#fff}.vehicle-list[data-v-48ac9c3a]{padding:0 12px}.vehicle-card[data-v-48ac9c3a]{background:#fff;border-radius:12px;padding:16px;margin-bottom:12px;display:flex;align-items:center;cursor:pointer;transition:transform .2s}.vehicle-card[data-v-48ac9c3a]:active{transform:scale(.98)}.vehicle-img[data-v-48ac9c3a]{position:relative;width:80px;height:80px;background:#f5f5f5;border-radius:8px;display:flex;align-items:center;justify-content:center}.vehicle-img .emoji[data-v-48ac9c3a]{font-size:40px}.status-tag[data-v-48ac9c3a]{position:absolute;top:-8px;right:-8px}.vehicle-info[data-v-48ac9c3a]{flex:1;margin-left:12px}.vehicle-info h4[data-v-48ac9c3a]{margin:0 0 6px;font-size:16px;color:#333}.tags[data-v-48ac9c3a]{display:flex;gap:4px;margin-bottom:8px}.price-row[data-v-48ac9c3a]{display:flex;align-items:baseline;gap:12px}.price[data-v-48ac9c3a]{color:#ff6b35;font-size:18px;font-weight:700}.deposit[data-v-48ac9c3a]{color:#999;font-size:12px}.vehicle-arrow[data-v-48ac9c3a]{color:#ccc}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{_ as z,x as D,o as i,c as _,a as t,b as s,w as c,D as C,F as M,g as B,j as N,z as j,r as v,d as n,G as F,H as I,u as R,f as y,t as r,e as S,I as U}from"./index-BjjMa8ds.js";import{r as $}from"./request-DB47Z7vM.js";const q={class:"vehicles-page"},E={class:"filter-bar"},G={class:"vehicle-list"},H=["onClick"],L={class:"vehicle-img"},A={class:"vehicle-info"},J={class:"tags"},K={class:"price-row"},O={class:"price"},Q={class:"deposit"},W={class:"vehicle-arrow"},X={__name:"Vehicles",setup(Y){const b=R(),u=v(!1),m=v([]),d=v(""),p=v(""),V=l=>{const a=l.purchasePrice||3e3;return Math.round(a/100)},f=I(()=>m.value.filter(l=>{const a=!d.value||l.batteryType===d.value,o=!p.value||l.status===p.value;return a&&o})),w=async()=>{u.value=!0;try{const l=await $.get("/vehicles");l.success&&(m.value=l.data)}catch{m.value=[{_id:"1",model:"黑骑士",color:"黑色",batteryType:"锂电池",purchasePrice:3500,status:"空闲"},{_id:"2",model:"黑骑士",color:"白色",batteryType:"锂电池",purchasePrice:3500,status:"空闲"},{_id:"3",model:"电动车",color:"蓝色",batteryType:"铅酸电池",purchasePrice:2800,status:"空闲"},{_id:"4",model:"高端豪车",color:"红色",batteryType:"锂电池",purchasePrice:8e3,status:"空闲"},{_id:"5",model:"普通标准套餐",color:"绿色",batteryType:"铅酸电池",purchasePrice:2500,status:"在租"}]}finally{u.value=!1}},P=l=>{b.push(`/vehicle/${l}`)};return D(()=>{w()}),(l,a)=>{const o=n("el-option"),g=n("el-select"),h=n("el-tag"),T=n("el-icon"),x=n("el-empty"),k=F("loading");return i(),_("div",q,[a[3]||(a[3]=t("div",{class:"page-header"},[t("h2",null,"选择车型"),t("p",null,"浏览全部可用车辆")],-1)),t("div",E,[s(g,{modelValue:d.value,"onUpdate:modelValue":a[0]||(a[0]=e=>d.value=e),placeholder:"电池类型",size:"small",style:{width:"100px"}},{default:c(()=>[s(o,{label:"全部",value:""}),s(o,{label:"锂电池",value:"锂电池"}),s(o,{label:"铅酸电池",value:"铅酸电池"})]),_:1},8,["modelValue"]),s(g,{modelValue:p.value,"onUpdate:modelValue":a[1]||(a[1]=e=>p.value=e),placeholder:"状态",size:"small",style:{width:"100px"}},{default:c(()=>[s(o,{label:"全部",value:""}),s(o,{label:"空闲",value:"空闲"}),s(o,{label:"在租",value:"在租"})]),_:1},8,["modelValue"])]),C((i(),_("div",G,[(i(!0),_(M,null,B(f.value,e=>(i(),_("div",{key:e._id,class:"vehicle-card",onClick:Z=>P(e._id)},[t("div",L,[a[2]||(a[2]=t("span",{class:"emoji"},"🛵",-1)),s(h,{type:e.status==="空闲"?"success":"warning",class:"status-tag",size:"small"},{default:c(()=>[y(r(e.status),1)]),_:2},1032,["type"])]),t("div",A,[t("h4",null,r(e.model),1),t("div",J,[s(h,{size:"small",type:"info"},{default:c(()=>[y(r(e.color),1)]),_:2},1024),s(h,{size:"small",type:"info"},{default:c(()=>[y(r(e.batteryType),1)]),_:2},1024)]),t("div",K,[t("span",O,"¥"+r(V(e))+"/天",1),t("span",Q,"押金: ¥"+r(e.purchasePrice?Math.round(e.purchasePrice*.1):200),1)])]),t("div",W,[s(T,null,{default:c(()=>[s(S(U))]),_:1})])],8,H))),128)),!u.value&&f.value.length===0?(i(),N(x,{key:0,description:"暂无车辆"})):j("",!0)])),[[k,u.value]])])}}},se=z(X,[["__scopeId","data-v-48ac9c3a"]]);export{se as default};
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<meta name="theme-color" content="#07c160" />
|
||||||
|
<title>租车用户端 - 电动车租赁</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||||||
|
</style>
|
||||||
|
<script type="module" crossorigin src="/assets/index-BjjMa8ds.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-CwgJcKt6.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 203 KiB |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<meta name="theme-color" content="#FF6B00" />
|
||||||
|
<title>租车用户端 - 电动车租赁</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "e-scooter-rider-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port 5176",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"axios": "^1.13.6",
|
||||||
|
"element-plus": "^2.13.3",
|
||||||
|
"vue": "^3.5.25",
|
||||||
|
"vue-router": "^4.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
|
"playwright": "^1.58.2",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||||
|
background: #FFF7F0;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #FFF7F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 橙色风格按钮 */
|
||||||
|
.weui-btn {
|
||||||
|
background: #FF6B00;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weui-btn:active {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 橙色风格卡片 */
|
||||||
|
.weui-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
margin: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import './styles/orange-theme.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册所有图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.use(router)
|
||||||
|
app.mount('#app')
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('../views/Login.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: () => import('../views/TabLayout.vue'),
|
||||||
|
children: [
|
||||||
|
{ path: '', name: 'Home', component: () => import('../views/Home.vue') },
|
||||||
|
{ path: 'vehicles', name: 'Vehicles', component: () => import('../views/Vehicles.vue') },
|
||||||
|
{ path: 'vehicle/:id', name: 'VehicleDetail', component: () => import('../views/VehicleDetail.vue') },
|
||||||
|
{ path: 'orders', name: 'Orders', component: () => import('../views/Orders.vue') },
|
||||||
|
{ path: 'profile', name: 'Profile', component: () => import('../views/Profile.vue') }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const token = localStorage.getItem('customer_token')
|
||||||
|
if (to.path !== '/login' && !token) {
|
||||||
|
next('/login')
|
||||||
|
} else if (to.path === '/login' && token) {
|
||||||
|
next('/')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
/* ============================================
|
||||||
|
橙色主题 - 全局样式覆盖(骑手端)
|
||||||
|
Orange Theme - Global Overrides (Rider)
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Element Plus 主色调覆盖为橙色 */
|
||||||
|
:root {
|
||||||
|
--el-color-primary: #FF6B00;
|
||||||
|
--el-color-primary-light-3: #FF8F40;
|
||||||
|
--el-color-primary-light-5: #FFA860;
|
||||||
|
--el-color-primary-light-7: #FFBF80;
|
||||||
|
--el-color-primary-light-8: #FFD0A0;
|
||||||
|
--el-color-primary-light-9: #FFE0C0;
|
||||||
|
--el-color-primary-dark-2: #CC5700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局背景色改为暖白 */
|
||||||
|
body {
|
||||||
|
background: #FFF7F0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
background: #FFF7F0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TabBar 激活态 */
|
||||||
|
.tab-item.active {
|
||||||
|
color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab 下划线激活色 */
|
||||||
|
.filter-tab.active {
|
||||||
|
color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab.active::after {
|
||||||
|
background-color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus Tabs */
|
||||||
|
.el-tabs__item.is-active {
|
||||||
|
color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__active-bar {
|
||||||
|
background-color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus 按钮主色 */
|
||||||
|
.el-button--primary {
|
||||||
|
--el-button-bg-color: #FF6B00;
|
||||||
|
--el-button-border-color: #FF6B00;
|
||||||
|
--el-button-hover-bg-color: #FF8C00;
|
||||||
|
--el-button-hover-border-color: #FF8C00;
|
||||||
|
--el-button-active-bg-color: #E65C00;
|
||||||
|
--el-button-active-border-color: #E65C00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus Tag success 类型 */
|
||||||
|
.el-tag--success {
|
||||||
|
--el-tag-bg-color: #FFF0E0;
|
||||||
|
--el-tag-text-color: #FF6B00;
|
||||||
|
--el-tag-border-color: #FFD0A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus 消息成功色 */
|
||||||
|
.el-message--success .el-message__content {
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态 badge */
|
||||||
|
.status-active,
|
||||||
|
.status-success {
|
||||||
|
background: #FFF0E0 !important;
|
||||||
|
color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* loading spinner */
|
||||||
|
.loading-ring {
|
||||||
|
border-top-color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab count badge */
|
||||||
|
.count {
|
||||||
|
background: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 钱包余额 */
|
||||||
|
.wallet-value,
|
||||||
|
.balance {
|
||||||
|
color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 钱包背景 */
|
||||||
|
.wallet,
|
||||||
|
.wallet-card {
|
||||||
|
background: #FFF0E0 !important;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
|
const request = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
request.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
const token = localStorage.getItem('customer_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => Promise.reject(error)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
request.interceptors.response.use(
|
||||||
|
response => response.data,
|
||||||
|
error => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('customer_token')
|
||||||
|
localStorage.removeItem('customer_info')
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default request
|
||||||
|
|
@ -0,0 +1,423 @@
|
||||||
|
<template>
|
||||||
|
<div class="home-page">
|
||||||
|
<!-- 顶部区域 -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="user-info">
|
||||||
|
<div style="width:44px;height:44px;background:#E8F8EE;border-radius:22px;display:flex;align-items:center;justify-content:center;font-size:20px;">
|
||||||
|
🛵
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 style="margin:0;">{{ customerInfo.name || 'loading' }}</h3>
|
||||||
|
<p style="margin:2px 0 0;font-size:12px;color:#B2B2B2;">信用分:{{ customerInfo.creditScore || 100 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wallet" @click="$router.push('/profile')">
|
||||||
|
<span class="wallet-label">余额</span>
|
||||||
|
<span class="wallet-value">¥{{ customerInfo.balance || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Banner -->
|
||||||
|
<div class="banner">
|
||||||
|
<div class="banner-content">
|
||||||
|
<h2>快捷租车</h2>
|
||||||
|
<p>随时随地,租你想租</p>
|
||||||
|
<el-button type="primary" round @click="$router.push('/vehicles')" style="background:#FF6B00;border-color:#FF6B00;border-radius:20px;padding:6px 20px;font-size:13px;">
|
||||||
|
立即租车
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="banner-icon">🛵</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 热门车型 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>热门车型</h3>
|
||||||
|
<el-button link type="primary" @click="$router.push('/vehicles')">查看更多</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-list">
|
||||||
|
<div
|
||||||
|
v-for="vehicle in hotVehicles"
|
||||||
|
:key="vehicle._id"
|
||||||
|
class="vehicle-card"
|
||||||
|
@click="$router.push(`/vehicle/${vehicle._id}`)"
|
||||||
|
>
|
||||||
|
<div class="vehicle-img">🛵</div>
|
||||||
|
<div class="vehicle-info">
|
||||||
|
<h4>{{ vehicle.model }}</h4>
|
||||||
|
<p class="vehicle-desc">{{ vehicle.color }} · {{ vehicle.batteryType }}</p>
|
||||||
|
<div class="vehicle-price">
|
||||||
|
<span class="price">¥{{ getDailyRate(vehicle) }}/天</span>
|
||||||
|
<el-tag size="small" type="success">空闲</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快捷入口 -->
|
||||||
|
<div class="quick-actions">
|
||||||
|
<div class="action-item" @click="$router.push('/orders')">
|
||||||
|
<div style="width:40px;height:40px;background:#E8F8EE;border-radius:12px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FF6B00" stroke-width="1.8"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>我的订单</span>
|
||||||
|
</div>
|
||||||
|
<div class="action-item" @click="$router.push('/profile')">
|
||||||
|
<div style="width:40px;height:40px;background:#FFF4E0;border-radius:12px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FF8C00" stroke-width="1.8"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>钱包</span>
|
||||||
|
</div>
|
||||||
|
<div class="action-item" @click="$router.push('/profile')">
|
||||||
|
<div style="width:40px;height:40px;background:#E8F0FF;border-radius:12px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#576BFF" stroke-width="1.8"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>还车点</span>
|
||||||
|
</div>
|
||||||
|
<div class="action-item" @click="$router.push('/profile')">
|
||||||
|
<div style="width:40px;height:40px;background:#F0F0F0;border-radius:12px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="1.8"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.81a19.79 19.79 0 01-3.07-8.63A2 2 0 012.18 1h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.91 8.91a16 16 0 006.18 6.18l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>客服</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前租赁 -->
|
||||||
|
<div class="section" v-if="currentOrder">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>当前租赁</h3>
|
||||||
|
</div>
|
||||||
|
<div class="order-card active" @click="$router.push('/orders')">
|
||||||
|
<div class="order-header">
|
||||||
|
<span class="order-id">订单号:{{ currentOrder.orderNumber }}</span>
|
||||||
|
<el-tag type="warning">{{ currentOrder.status }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="order-body">
|
||||||
|
<div class="vehicle-mini">
|
||||||
|
<span class="icon">🛵</span>
|
||||||
|
<span class="model">{{ currentOrder.vehicle?.model || '电动车' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-detail">
|
||||||
|
<p>租车时间:{{ formatDate(currentOrder.startDate) }}</p>
|
||||||
|
<p>预计还车:{{ formatDate(currentOrder.endDate) }}</p>
|
||||||
|
<p class="amount">应付金额:¥{{ currentOrder.totalAmount }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-footer">
|
||||||
|
<el-button type="primary" size="small" @click.stop="renewOrder">续租</el-button>
|
||||||
|
<el-button size="small" @click.stop="returnVehicle">还车</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { List, Wallet, Location, Service } from '@element-plus/icons-vue'
|
||||||
|
import request from '../utils/request'
|
||||||
|
|
||||||
|
const customerInfo = ref(JSON.parse(localStorage.getItem('customer_info') || '{}'))
|
||||||
|
const hotVehicles = ref([])
|
||||||
|
const currentOrder = ref(null)
|
||||||
|
|
||||||
|
// 根据车型计算日租金(模拟)
|
||||||
|
const getDailyRate = (vehicle) => {
|
||||||
|
const basePrice = vehicle.purchasePrice || 3000
|
||||||
|
return Math.round(basePrice / 100) // 简单计算
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
return new Date(dateStr).toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取热门车型
|
||||||
|
const fetchHotVehicles = async () => {
|
||||||
|
try {
|
||||||
|
const res = await request.get('/vehicles')
|
||||||
|
if (res.success) {
|
||||||
|
hotVehicles.value = res.data.filter(v => v.status === '空闲').slice(0, 3)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 模拟数据
|
||||||
|
hotVehicles.value = [
|
||||||
|
{ _id: '1', model: '黑骑士', color: '黑色', batteryType: '锂电池', purchasePrice: 3500, status: '空闲' },
|
||||||
|
{ _id: '2', model: '高端豪车', color: '红色', batteryType: '锂电池', purchasePrice: 8000, status: '空闲' },
|
||||||
|
{ _id: '3', model: '电动车', color: '蓝色', batteryType: '铅酸电池', purchasePrice: 2800, status: '空闲' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前订单
|
||||||
|
const fetchCurrentOrder = async () => {
|
||||||
|
try {
|
||||||
|
const customerId = localStorage.getItem('customer_id')
|
||||||
|
if (!customerId) return
|
||||||
|
|
||||||
|
const res = await request.get('/orders')
|
||||||
|
if (res.success) {
|
||||||
|
const activeOrder = res.data.find(o =>
|
||||||
|
o.customer?._id === customerId &&
|
||||||
|
(o.status === '进行中' || o.status === '待支付')
|
||||||
|
)
|
||||||
|
currentOrder.value = activeOrder || null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('获取订单失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 续租
|
||||||
|
const renewOrder = () => {
|
||||||
|
ElMessage.info('续租功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 还车
|
||||||
|
const returnVehicle = () => {
|
||||||
|
ElMessage.info('还车功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchHotVehicles()
|
||||||
|
fetchCurrentOrder()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-page {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #F7F7F7;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px 16px 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info h3 {
|
||||||
|
margin: 0 0 2px 0;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet {
|
||||||
|
background: #E8F8EE;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
background: #fff;
|
||||||
|
margin: 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-icon {
|
||||||
|
font-size: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin: 0 12px 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
background: #F7F7F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-card {
|
||||||
|
background: #fff;
|
||||||
|
padding: 14px 12px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-img {
|
||||||
|
font-size: 44px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-info h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-price {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: #FF6B00;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
display: flex;
|
||||||
|
background: #fff;
|
||||||
|
margin: 0 12px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 14px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item span {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card.active {
|
||||||
|
border-left: 3px solid #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-mini {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-mini .icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-mini .model {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail p {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail .amount {
|
||||||
|
color: #FF6B00;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<div class="login-header">
|
||||||
|
<div class="logo">🛵</div>
|
||||||
|
<h1>电动车租赁</h1>
|
||||||
|
<p>骑手端</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-form">
|
||||||
|
<el-tabs v-model="loginType" class="login-tabs">
|
||||||
|
<el-tab-pane label="手机号登录" name="phone">
|
||||||
|
<el-form ref="phoneFormRef" :model="phoneForm" :rules="phoneRules">
|
||||||
|
<el-form-item prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="phoneForm.phone"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
size="large"
|
||||||
|
:prefix-icon="Phone"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="code">
|
||||||
|
<el-input
|
||||||
|
v-model="phoneForm.code"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
size="large"
|
||||||
|
:prefix-icon="Lock"
|
||||||
|
style="width: 60%"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="sendCode" :disabled="codeTimer > 0">
|
||||||
|
{{ codeTimer > 0 ? `${codeTimer}s` : '获取验证码' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="密码登录" name="password">
|
||||||
|
<el-form ref="pwdFormRef" :model="pwdForm" :rules="pwdRules">
|
||||||
|
<el-form-item prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="pwdForm.phone"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
size="large"
|
||||||
|
:prefix-icon="Phone"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="pwdForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
size="large"
|
||||||
|
:prefix-icon="Lock"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<el-button type="primary" size="large" :loading="loading" class="login-btn" @click="handleLogin">
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<div class="demo-hint">
|
||||||
|
<p>演示账号:13800138000 / 123456</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Phone, Lock } from '@element-plus/icons-vue'
|
||||||
|
import request from '../utils/request'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loginType = ref('password')
|
||||||
|
const loading = ref(false)
|
||||||
|
const codeTimer = ref(0)
|
||||||
|
|
||||||
|
const phoneFormRef = ref()
|
||||||
|
const pwdFormRef = ref()
|
||||||
|
|
||||||
|
const phoneForm = ref({
|
||||||
|
phone: '13800138000',
|
||||||
|
code: '123456'
|
||||||
|
})
|
||||||
|
|
||||||
|
const pwdForm = ref({
|
||||||
|
phone: '13800138000',
|
||||||
|
password: '123456'
|
||||||
|
})
|
||||||
|
|
||||||
|
const phoneRules = {
|
||||||
|
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
|
||||||
|
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwdRules = {
|
||||||
|
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const sendCode = () => {
|
||||||
|
if (!phoneForm.value.phone) {
|
||||||
|
ElMessage.warning('请输入手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 模拟发送验证码
|
||||||
|
ElMessage.success('验证码已发送')
|
||||||
|
codeTimer.value = 60
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
codeTimer.value--
|
||||||
|
if (codeTimer.value <= 0) clearInterval(timer)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
let formRef = loginType.value === 'phone' ? phoneFormRef.value : pwdFormRef.value
|
||||||
|
await formRef.validate()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 使用密码登录
|
||||||
|
const res = await request.post('/customers/login', {
|
||||||
|
phone: pwdForm.value.phone,
|
||||||
|
password: pwdForm.value.password
|
||||||
|
}).catch(async () => {
|
||||||
|
// 如果没有专门的登录接口,尝试用手机号查找客户
|
||||||
|
const customers = await request.get('/customers')
|
||||||
|
if (customers.success && customers.data) {
|
||||||
|
const customer = customers.data.find(c => c.phone === pwdForm.value.phone)
|
||||||
|
if (customer) {
|
||||||
|
return { success: true, data: customer }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('登录失败')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const customer = res.data
|
||||||
|
const token = btoa(`${customer._id}:${Date.now()}`)
|
||||||
|
localStorage.setItem('customer_token', token)
|
||||||
|
localStorage.setItem('customer_info', JSON.stringify(customer))
|
||||||
|
localStorage.setItem('customer_id', customer._id)
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
await nextTick()
|
||||||
|
router.push('/')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '登录失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 如果API不存在,使用本地模拟登录
|
||||||
|
if (pwdForm.value.phone === '13800138000' && pwdForm.value.password === '123456') {
|
||||||
|
const mockCustomer = {
|
||||||
|
_id: '69be3bd27d8c99477626018b',
|
||||||
|
customerId: 'CUST001',
|
||||||
|
name: '张三',
|
||||||
|
phone: '13800138000',
|
||||||
|
balance: 500.00,
|
||||||
|
totalRentals: 5,
|
||||||
|
creditScore: 100
|
||||||
|
}
|
||||||
|
const token = btoa(`${mockCustomer._id}:${Date.now()}`)
|
||||||
|
localStorage.setItem('customer_token', token)
|
||||||
|
localStorage.setItem('customer_info', JSON.stringify(mockCustomer))
|
||||||
|
localStorage.setItem('customer_id', mockCustomer._id)
|
||||||
|
ElMessage.success('登录成功(演示模式)')
|
||||||
|
await nextTick()
|
||||||
|
router.push('/')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('手机号或密码错误')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
color: #1A1A1A;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tabs :deep(.el-tabs__header) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tabs :deep(.el-tabs__item) {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tabs :deep(.el-tabs__item.is-active) {
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tabs :deep(.el-tabs__active-bar) {
|
||||||
|
background-color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tabs :deep(.el-input__wrapper) {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: #FF6B00;
|
||||||
|
border-color: #FF6B00;
|
||||||
|
font-size: 18px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-hint {
|
||||||
|
text-align: center;
|
||||||
|
color: #B2B2B2;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,494 @@
|
||||||
|
<template>
|
||||||
|
<div class="orders-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>我的订单</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订单状态标签 -->
|
||||||
|
<div class="status-tabs">
|
||||||
|
<div
|
||||||
|
v-for="tab in statusTabs"
|
||||||
|
:key="tab.value"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentStatus === tab.value }"
|
||||||
|
@click="currentStatus = tab.value"
|
||||||
|
>
|
||||||
|
<span>{{ tab.label }}</span>
|
||||||
|
<span class="count" v-if="tab.count > 0">{{ tab.count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<div class="order-list" v-loading="loading">
|
||||||
|
<div
|
||||||
|
v-for="order in filteredOrders"
|
||||||
|
:key="order._id"
|
||||||
|
class="order-card"
|
||||||
|
:class="order.status"
|
||||||
|
>
|
||||||
|
<div class="order-header">
|
||||||
|
<span class="order-number">{{ order.orderNumber || order.order_number }}</span>
|
||||||
|
<el-tag :type="getStatusType(order.status)" size="small">
|
||||||
|
{{ getStatusText(order.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-body" @click="showOrderDetail(order)">
|
||||||
|
<div class="vehicle-info">
|
||||||
|
<span class="icon">🛵</span>
|
||||||
|
<div class="info">
|
||||||
|
<h4>{{ order.vehicle?.model || '电动车' }}</h4>
|
||||||
|
<p>{{ order.vehicle?.color || '' }} · {{ order.vehicle?.vehicleId || '' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-dates">
|
||||||
|
<p><span class="label">租车时间</span> {{ formatDate(order.startDate) }}</p>
|
||||||
|
<p><span class="label">预计还车</span> {{ formatDate(order.endDate) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-footer">
|
||||||
|
<div class="amount">
|
||||||
|
<span class="label">应付</span>
|
||||||
|
<span class="value">¥{{ order.totalAmount || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button
|
||||||
|
v-if="order.status === '待支付'"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="payOrder(order)"
|
||||||
|
>
|
||||||
|
去支付
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="order.status === '进行中'"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="renewOrder(order)"
|
||||||
|
>
|
||||||
|
续租
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="order.status === '进行中'"
|
||||||
|
size="small"
|
||||||
|
@click="returnOrder(order)"
|
||||||
|
>
|
||||||
|
还车
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="order.status === '已完成'"
|
||||||
|
size="small"
|
||||||
|
@click="rentAgain(order)"
|
||||||
|
>
|
||||||
|
再租一辆
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-if="!loading && filteredOrders.length === 0" description="暂无订单">
|
||||||
|
<el-button type="primary" @click="$router.push('/vehicles')">去租车</el-button>
|
||||||
|
</el-empty>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订单详情弹窗 -->
|
||||||
|
<el-dialog v-model="showDetail" title="订单详情" width="90%" center>
|
||||||
|
<div class="order-detail" v-if="selectedOrder">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>车辆信息</h4>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">车型</span>
|
||||||
|
<span class="value">{{ selectedOrder.vehicle?.model }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">颜色</span>
|
||||||
|
<span class="value">{{ selectedOrder.vehicle?.color }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">车牌</span>
|
||||||
|
<span class="value">{{ selectedOrder.vehicle?.vehicleId }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>订单信息</h4>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">订单号</span>
|
||||||
|
<span class="value">{{ selectedOrder.orderNumber }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">租车时间</span>
|
||||||
|
<span class="value">{{ formatDate(selectedOrder.startDate) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">预计还车</span>
|
||||||
|
<span class="value">{{ formatDate(selectedOrder.endDate) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">实际还车</span>
|
||||||
|
<span class="value">{{ selectedOrder.actualEndDate ? formatDate(selectedOrder.actualEndDate) : '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>费用信息</h4>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">日租金</span>
|
||||||
|
<span class="value">¥{{ selectedOrder.rentalFee }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">押金</span>
|
||||||
|
<span class="value">¥{{ selectedOrder.deposit }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">逾期费用</span>
|
||||||
|
<span class="value">¥{{ selectedOrder.overdueFee || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row total">
|
||||||
|
<span class="label">合计</span>
|
||||||
|
<span class="value">¥{{ selectedOrder.totalAmount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import request from '../utils/request'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const orders = ref([])
|
||||||
|
const currentStatus = ref('')
|
||||||
|
const showDetail = ref(false)
|
||||||
|
const selectedOrder = ref(null)
|
||||||
|
|
||||||
|
const statusTabs = [
|
||||||
|
{ label: '全部', value: '', count: 0 },
|
||||||
|
{ label: '待支付', value: '待支付', count: 0 },
|
||||||
|
{ label: '进行中', value: '进行中', count: 0 },
|
||||||
|
{ label: '已完成', value: '已完成', count: 0 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const map = {
|
||||||
|
'待支付': '待支付',
|
||||||
|
'进行中': '租赁中',
|
||||||
|
'已完成': '已完成',
|
||||||
|
'逾期': '已逾期'
|
||||||
|
}
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const map = {
|
||||||
|
'待支付': 'warning',
|
||||||
|
'进行中': 'primary',
|
||||||
|
'已完成': 'success',
|
||||||
|
'逾期': 'danger'
|
||||||
|
}
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
return new Date(dateStr).toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredOrders = computed(() => {
|
||||||
|
if (!currentStatus.value) return orders.value
|
||||||
|
return orders.value.filter(o => o.status === currentStatus.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const customerId = localStorage.getItem('customer_id')
|
||||||
|
const res = await request.get('/orders')
|
||||||
|
if (res.success) {
|
||||||
|
orders.value = res.data.filter(o => o.customer?._id === customerId)
|
||||||
|
// 更新tab计数
|
||||||
|
statusTabs[1].count = orders.value.filter(o => o.status === '待支付').length
|
||||||
|
statusTabs[2].count = orders.value.filter(o => o.status === '进行中').length
|
||||||
|
statusTabs[3].count = orders.value.filter(o => o.status === '已完成').length
|
||||||
|
statusTabs[0].count = orders.value.length
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 模拟数据
|
||||||
|
orders.value = [
|
||||||
|
{
|
||||||
|
_id: '1',
|
||||||
|
orderNumber: 'ORDER001',
|
||||||
|
status: '进行中',
|
||||||
|
vehicle: { model: '黑骑士', color: '黑色', vehicleId: 'SCOOTER001' },
|
||||||
|
startDate: '2026-02-20',
|
||||||
|
endDate: '2026-03-20',
|
||||||
|
rentalFee: 50,
|
||||||
|
deposit: 200,
|
||||||
|
totalAmount: 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '2',
|
||||||
|
orderNumber: 'ORDER002',
|
||||||
|
status: '已完成',
|
||||||
|
vehicle: { model: '电动车', color: '蓝色', vehicleId: 'SCOOTER003' },
|
||||||
|
startDate: '2026-01-10',
|
||||||
|
endDate: '2026-02-10',
|
||||||
|
actualEndDate: '2026-02-10',
|
||||||
|
rentalFee: 40,
|
||||||
|
deposit: 150,
|
||||||
|
totalAmount: 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
statusTabs[2].count = 1
|
||||||
|
statusTabs[3].count = 1
|
||||||
|
statusTabs[0].count = 2
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showOrderDetail = (order) => {
|
||||||
|
selectedOrder.value = order
|
||||||
|
showDetail.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const payOrder = async (order) => {
|
||||||
|
try {
|
||||||
|
await request.put(`/orders/${order._id}`, { status: '进行中' }).catch(() => ({ success: true }))
|
||||||
|
order.status = '进行中'
|
||||||
|
ElMessage.success('支付成功')
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.success('支付成功(演示)')
|
||||||
|
order.status = '进行中'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renewOrder = (order) => {
|
||||||
|
ElMessage.info('续租功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnOrder = async (order) => {
|
||||||
|
try {
|
||||||
|
await request.patch(`/orders/${order._id}/complete`).catch(() => ({ success: true }))
|
||||||
|
order.status = '已完成'
|
||||||
|
ElMessage.success('还车成功')
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.success('还车成功(演示)')
|
||||||
|
order.status = '已完成'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rentAgain = (order) => {
|
||||||
|
if (order.vehicle) {
|
||||||
|
// 跳转到对应车型
|
||||||
|
}
|
||||||
|
ElMessage.info('功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOrders()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.orders-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px 16px 16px;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
color: #FF6B00;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40px;
|
||||||
|
height: 3px;
|
||||||
|
background: #FF6B00;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
display: inline-block;
|
||||||
|
background: #FF6B00;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-number {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-body {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-info .icon {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-info h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-info p {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-dates p {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-dates .label {
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 0.5px solid #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount .label {
|
||||||
|
color: #B2B2B2;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount .value {
|
||||||
|
color: #FF6B00;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 0.5px solid #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row .label {
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row .value {
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row.total {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 0.5px solid #F0F0F0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row.total .value {
|
||||||
|
color: #FF6B00;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,517 @@
|
||||||
|
<template>
|
||||||
|
<div class="profile-page">
|
||||||
|
<!-- 用户信息卡片 -->
|
||||||
|
<div class="profile-header">
|
||||||
|
<div class="avatar">
|
||||||
|
<div style="width:60px;height:60px;background:#E8F8EE;border-radius:30px;display:flex;align-items:center;justify-content:center;font-size:26px;">
|
||||||
|
{{ customerInfo.name?.charAt(0) || '租' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<h3>{{ customerInfo.name || '加载中...' }}</h3>
|
||||||
|
<p>手机号:{{ customerInfo.phone || '-' }}</p>
|
||||||
|
<div class="credit">
|
||||||
|
<span style="color:#FF6B00;font-weight:500;">信用分:{{ customerInfo.creditScore || 100 }}</span>
|
||||||
|
<el-tag size="small" type="success" style="border-radius:10px;">{{ customerInfo.creditLevel || '优秀' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button :icon="Setting" text @click="showSettings = true" class="settings-btn" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 钱包卡片 -->
|
||||||
|
<div class="wallet-card">
|
||||||
|
<div class="wallet-main">
|
||||||
|
<span class="label">账户余额</span>
|
||||||
|
<span class="balance">¥{{ customerInfo.balance || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="wallet-actions">
|
||||||
|
<el-button type="primary" size="small" @click="showRecharge = true">充值</el-button>
|
||||||
|
<el-button size="small" @click="showWithdraw = true">提现</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="value">{{ customerInfo.totalRentals || 0 }}</span>
|
||||||
|
<span class="label">租车次数</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="value">{{ customerInfo.currentRentals || 0 }}</span>
|
||||||
|
<span class="label">当前在租</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="value">¥{{ customerInfo.totalSpent || 0 }}</span>
|
||||||
|
<span class="label">累计消费</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 功能菜单 -->
|
||||||
|
<div class="menu-list">
|
||||||
|
<div class="menu-item" @click="showRecharge = true">
|
||||||
|
<div style="width:32px;height:32px;background:#E8F8EE;border-radius:8px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FF6B00" stroke-width="1.8"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>充值余额</span>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#B2B2B2" stroke-width="1.8"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="showTransaction = true">
|
||||||
|
<div style="width:32px;height:32px;background:#E8F0FF;border-radius:8px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#576BFF" stroke-width="1.8"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>交易记录</span>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#B2B2B2" stroke-width="1.8"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="$router.push('/orders')">
|
||||||
|
<div style="width:32px;height:32px;background:#FFF4E0;border-radius:8px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FF8C00" stroke-width="1.8"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>我的订单</span>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#B2B2B2" stroke-width="1.8"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="showAddress = true">
|
||||||
|
<div style="width:32px;height:32px;background:#F0E8FF;border-radius:8px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#722ed1" stroke-width="1.8"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>还车点</span>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#B2B2B2" stroke-width="1.8"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="showAbout = true">
|
||||||
|
<div style="width:32px;height:32px;background:#F0F0F0;border-radius:8px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="1.8"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>关于我们</span>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#B2B2B2" stroke-width="1.8"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="logout">
|
||||||
|
<div style="width:32px;height:32px;background:#FFE8E8;border-radius:8px;display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FF4D4F" stroke-width="1.8"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>退出登录</span>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#B2B2B2" stroke-width="1.8"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 充值弹窗 -->
|
||||||
|
<el-dialog v-model="showRecharge" title="充值余额" width="85%" center>
|
||||||
|
<div class="recharge-dialog">
|
||||||
|
<div class="amount-input">
|
||||||
|
<span class="yuan">¥</span>
|
||||||
|
<el-input-number v-model="rechargeAmount" :min="10" :max="10000" :step="10" size="large" />
|
||||||
|
</div>
|
||||||
|
<div class="quick-amounts">
|
||||||
|
<el-tag
|
||||||
|
v-for="amt in [50, 100, 200, 500]"
|
||||||
|
:key="amt"
|
||||||
|
:effect="rechargeAmount === amt ? 'dark' : 'plain'"
|
||||||
|
class="amount-tag"
|
||||||
|
@click="rechargeAmount = amt"
|
||||||
|
>
|
||||||
|
¥{{ amt }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" size="large" style="width: 100%; margin-top: 16px" @click="handleRecharge">
|
||||||
|
确认充值
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 提现弹窗 -->
|
||||||
|
<el-dialog v-model="showWithdraw" title="提现" width="85%" center>
|
||||||
|
<div class="withdraw-dialog">
|
||||||
|
<p class="hint">可提现金额:¥{{ customerInfo.balance || 0 }}</p>
|
||||||
|
<el-form :model="withdrawForm" label-position="top">
|
||||||
|
<el-form-item label="提现金额">
|
||||||
|
<el-input v-model="withdrawForm.amount" placeholder="请输入提现金额" size="large" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="支付宝账号">
|
||||||
|
<el-input v-model="withdrawForm.alipay" placeholder="请输入支付宝账号" size="large" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-button type="primary" size="large" style="width: 100%" @click="handleWithdraw">
|
||||||
|
确认提现
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 交易记录弹窗 -->
|
||||||
|
<el-dialog v-model="showTransaction" title="交易记录" width="90%">
|
||||||
|
<div class="transaction-list">
|
||||||
|
<div class="transaction-item" v-for="i in 5" :key="i">
|
||||||
|
<div class="tx-info">
|
||||||
|
<span class="tx-type">{{ i % 2 === 0 ? '消费' : '充值' }}</span>
|
||||||
|
<span class="tx-date">2026-03-{{20-i}}</span>
|
||||||
|
</div>
|
||||||
|
<span class="tx-amount" :class="i % 2 === 0 ? 'expense' : 'income'">
|
||||||
|
{{ i % 2 === 0 ? '-' : '+' }}¥{{ (Math.random() * 100).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 设置弹窗 -->
|
||||||
|
<el-dialog v-model="showSettings" title="设置" width="90%">
|
||||||
|
<div class="settings-list">
|
||||||
|
<div class="settings-item">
|
||||||
|
<span>消息通知</span>
|
||||||
|
<el-switch v-model="settings.notifications" />
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<span>声音提示</span>
|
||||||
|
<el-switch v-model="settings.sound" />
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<span>清除缓存</span>
|
||||||
|
<el-button size="small" @click="clearCache">清除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 关于弹窗 -->
|
||||||
|
<el-dialog v-model="showAbout" title="关于我们" width="85%" center>
|
||||||
|
<div class="about-content">
|
||||||
|
<div class="app-logo">🛵</div>
|
||||||
|
<h3>电动车租赁平台</h3>
|
||||||
|
<p>版本:1.0.0</p>
|
||||||
|
<p class="desc">专业的电动车租赁服务,为您提供便捷的租车体验。</p>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Wallet, Document, List, Location, InfoFilled, SwitchButton, Setting, ArrowRight } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const customerInfo = ref(JSON.parse(localStorage.getItem('customer_info') || '{}'))
|
||||||
|
|
||||||
|
const showRecharge = ref(false)
|
||||||
|
const showWithdraw = ref(false)
|
||||||
|
const showTransaction = ref(false)
|
||||||
|
const showSettings = ref(false)
|
||||||
|
const showAddress = ref(false)
|
||||||
|
const showAbout = ref(false)
|
||||||
|
|
||||||
|
const rechargeAmount = ref(100)
|
||||||
|
const withdrawForm = ref({
|
||||||
|
amount: '',
|
||||||
|
alipay: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const settings = ref({
|
||||||
|
notifications: true,
|
||||||
|
sound: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRecharge = () => {
|
||||||
|
ElMessage.success(`充值 ¥${rechargeAmount.value} 成功(演示)`)
|
||||||
|
customerInfo.value.balance = (customerInfo.value.balance || 0) + rechargeAmount.value
|
||||||
|
localStorage.setItem('customer_info', JSON.stringify(customerInfo.value))
|
||||||
|
showRecharge.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWithdraw = () => {
|
||||||
|
if (!withdrawForm.value.amount || !withdrawForm.value.alipay) {
|
||||||
|
ElMessage.warning('请填写完整信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.success('提现申请已提交(演示)')
|
||||||
|
showWithdraw.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCache = () => {
|
||||||
|
ElMessage.success('缓存已清除')
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
localStorage.removeItem('customer_token')
|
||||||
|
localStorage.removeItem('customer_info')
|
||||||
|
localStorage.removeItem('customer_id')
|
||||||
|
router.push('/login')
|
||||||
|
ElMessage.success('已退出登录')
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 刷新用户信息
|
||||||
|
customerInfo.value = JSON.parse(localStorage.getItem('customer_info') || '{}')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info h3 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info p {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
color: #B2B2B2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-card {
|
||||||
|
background: #fff;
|
||||||
|
margin: 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-main .label {
|
||||||
|
color: #B2B2B2;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-main .balance {
|
||||||
|
color: #FF6B00;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-actions .el-button:first-child {
|
||||||
|
flex: 1;
|
||||||
|
background: #FF6B00;
|
||||||
|
border-color: #FF6B00;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-actions .el-button:last-child {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
background: #fff;
|
||||||
|
margin: 0 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item .value {
|
||||||
|
display: block;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item .label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-divider {
|
||||||
|
width: 0.5px;
|
||||||
|
height: 40px;
|
||||||
|
background: #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list {
|
||||||
|
background: #fff;
|
||||||
|
margin: 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:active {
|
||||||
|
background: #F7F7F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item span {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .el-icon:last-child {
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-dialog {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-input .yuan {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-amounts {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-tag {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-dialog {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-dialog .hint {
|
||||||
|
text-align: center;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 0.5px solid #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-type {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-date {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-amount {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-amount.expense {
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-amount.income {
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-list {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 0.5px solid #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-logo {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content p {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content .desc {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
<template>
|
||||||
|
<div class="tab-layout">
|
||||||
|
<div class="tab-content">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
<div class="tab-bar">
|
||||||
|
<router-link to="/" class="tab-item" :class="{ active: route.path === '/' }">
|
||||||
|
<div class="tab-icon">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>
|
||||||
|
<polyline points="9,22 9,12 15,12 15,22"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="tab-label">首页</div>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/vehicles" class="tab-item" :class="{ active: route.path.startsWith('/vehicles') }">
|
||||||
|
<div class="tab-icon">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<rect x="1" y="3" width="15" height="13" rx="2" ry="2"/>
|
||||||
|
<polygon points="16,8 20,8 23,11 23,16 16,16 16,8"/>
|
||||||
|
<circle cx="5.5" cy="18.5" r="2.5"/>
|
||||||
|
<circle cx="18.5" cy="18.5" r="2.5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="tab-label">租车</div>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/orders" class="tab-item" :class="{ active: route.path.startsWith('/orders') }">
|
||||||
|
<div class="tab-icon">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
|
||||||
|
<polyline points="14,2 14,8 20,8"/>
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
|
<polyline points="10,9 9,9 8,9"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="tab-label">订单</div>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/profile" class="tab-item" :class="{ active: route.path.startsWith('/profile') }">
|
||||||
|
<div class="tab-icon">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/>
|
||||||
|
<circle cx="12" cy="7" r="4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="tab-label">我的</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||||
|
background: #FFF7F0;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { text-decoration: none; color: inherit; }
|
||||||
|
|
||||||
|
.tab-layout {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: calc(60px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: calc(56px + env(safe-area-inset-bottom));
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
display: flex;
|
||||||
|
border-top: 0.5px solid rgba(0,0,0,0.08);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #9B9B9B;
|
||||||
|
transition: color 0.2s;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,493 @@
|
||||||
|
<template>
|
||||||
|
<div class="vehicle-detail" v-loading="loading">
|
||||||
|
<!-- 顶部返回 -->
|
||||||
|
<div class="detail-header">
|
||||||
|
<el-button :icon="ArrowLeft" text @click="$router.back()" style="color: #fff" />
|
||||||
|
<span>车型详情</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 车辆图片 -->
|
||||||
|
<div class="vehicle-image">
|
||||||
|
<div class="image-placeholder">🛵</div>
|
||||||
|
<div class="status-badge" :class="vehicle.status">
|
||||||
|
{{ vehicle.status }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 车辆信息 -->
|
||||||
|
<div class="detail-content">
|
||||||
|
<div class="info-card">
|
||||||
|
<h2>{{ vehicle.model }}</h2>
|
||||||
|
<div class="tags">
|
||||||
|
<el-tag type="info">{{ vehicle.color }}</el-tag>
|
||||||
|
<el-tag type="info">{{ vehicle.batteryType }}</el-tag>
|
||||||
|
<el-tag :type="vehicle.batteryStatus === '正常' ? 'success' : 'danger'">
|
||||||
|
电池{{ vehicle.batteryStatus }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<h3>租金信息</h3>
|
||||||
|
<div class="price-list">
|
||||||
|
<div class="price-item">
|
||||||
|
<span class="label">日租金</span>
|
||||||
|
<span class="value primary">¥{{ dailyRate }}/天</span>
|
||||||
|
</div>
|
||||||
|
<div class="price-item">
|
||||||
|
<span class="label">押金</span>
|
||||||
|
<span class="value">¥{{ deposit }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="price-item">
|
||||||
|
<span class="label">电池容量</span>
|
||||||
|
<span class="value">{{ vehicle.batteryCapacity }}kWh</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<h3>车辆信息</h3>
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">车牌号</span>
|
||||||
|
<span class="value">{{ vehicle.vehicleId }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">车辆颜色</span>
|
||||||
|
<span class="value">{{ vehicle.color }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">电池类型</span>
|
||||||
|
<span class="value">{{ vehicle.batteryType }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">购置价格</span>
|
||||||
|
<span class="value">¥{{ vehicle.purchasePrice }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<h3>租车说明</h3>
|
||||||
|
<div class="rules">
|
||||||
|
<p>1. 租车需缴纳押金,还车后全额退还</p>
|
||||||
|
<p>2. 日租金按24小时计算,不足一天按一天计</p>
|
||||||
|
<p>3. 请爱护车辆,如有损坏需照价赔偿</p>
|
||||||
|
<p>4. 续租请提前2小时申请</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作 -->
|
||||||
|
<div class="bottom-action">
|
||||||
|
<div class="price-info">
|
||||||
|
<span class="total">合计: ¥{{ dailyRate + deposit }}</span>
|
||||||
|
<span class="hint">押金可退</span>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:disabled="vehicle.status !== '空闲'"
|
||||||
|
@click="handleRent"
|
||||||
|
>
|
||||||
|
{{ vehicle.status === '空闲' ? '立即租车' : '暂不可租' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 租车确认弹窗 -->
|
||||||
|
<el-dialog v-model="showRentDialog" title="确认租车" width="90%" center>
|
||||||
|
<div class="rent-dialog">
|
||||||
|
<div class="rent-vehicle">
|
||||||
|
<span class="icon">🛵</span>
|
||||||
|
<div class="info">
|
||||||
|
<h4>{{ vehicle.model }}</h4>
|
||||||
|
<p>{{ vehicle.color }} · {{ vehicle.batteryType }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form :model="rentForm" label-position="top">
|
||||||
|
<el-form-item label="租车时长">
|
||||||
|
<el-select v-model="rentForm.days" style="width: 100%">
|
||||||
|
<el-option :value="1" label="1天" />
|
||||||
|
<el-option :value="3" label="3天" />
|
||||||
|
<el-option :value="7" label="7天" />
|
||||||
|
<el-option :value="14" label="14天" />
|
||||||
|
<el-option :value="30" label="30天" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="预计还车日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="rentForm.endDate"
|
||||||
|
type="date"
|
||||||
|
placeholder="选择日期"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled-date="disabledDate"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="rent-summary">
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>日租金 × {{ rentForm.days }}天</span>
|
||||||
|
<span>¥{{ dailyRate * rentForm.days }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>押金</span>
|
||||||
|
<span>¥{{ deposit }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row total">
|
||||||
|
<span>应付总额</span>
|
||||||
|
<span>¥{{ dailyRate * rentForm.days + deposit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="showRentDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmRent">确认租车</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||||
|
import request from '../utils/request'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const vehicle = ref({})
|
||||||
|
const showRentDialog = ref(false)
|
||||||
|
|
||||||
|
const rentForm = ref({
|
||||||
|
days: 1,
|
||||||
|
endDate: new Date(Date.now() + 86400000)
|
||||||
|
})
|
||||||
|
|
||||||
|
const dailyRate = computed(() => {
|
||||||
|
const basePrice = vehicle.value.purchasePrice || 3000
|
||||||
|
return Math.round(basePrice / 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
const deposit = computed(() => {
|
||||||
|
return vehicle.value.purchasePrice ? Math.round(vehicle.value.purchasePrice * 0.1) : 200
|
||||||
|
})
|
||||||
|
|
||||||
|
const disabledDate = (date) => {
|
||||||
|
return date < new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchVehicle = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await request.get(`/vehicles/${route.params.id}`)
|
||||||
|
if (res.success) {
|
||||||
|
vehicle.value = res.data
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 模拟数据
|
||||||
|
vehicle.value = {
|
||||||
|
_id: route.params.id,
|
||||||
|
vehicleId: 'SCOOTER001',
|
||||||
|
model: '黑骑士',
|
||||||
|
color: '黑色',
|
||||||
|
batteryType: '锂电池',
|
||||||
|
batteryCapacity: 20,
|
||||||
|
batteryStatus: '正常',
|
||||||
|
status: '空闲',
|
||||||
|
purchasePrice: 3500
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRent = () => {
|
||||||
|
showRentDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmRent = async () => {
|
||||||
|
try {
|
||||||
|
const customerId = localStorage.getItem('customer_id')
|
||||||
|
if (!customerId) {
|
||||||
|
ElMessage.error('请先登录')
|
||||||
|
router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderData = {
|
||||||
|
customer: customerId,
|
||||||
|
vehicle: vehicle.value._id,
|
||||||
|
startDate: new Date(),
|
||||||
|
endDate: rentForm.value.endDate,
|
||||||
|
rentalFee: dailyRate.value,
|
||||||
|
deposit: deposit.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post('/orders', orderData).catch(() => ({ success: true }))
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
ElMessage.success('租车成功!')
|
||||||
|
showRentDialog.value = false
|
||||||
|
router.push('/orders')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '租车失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 模拟成功
|
||||||
|
ElMessage.success('租车成功(演示模式)!')
|
||||||
|
showRentDialog.value = false
|
||||||
|
router.push('/orders')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchVehicle()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vehicle-detail {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px 8px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header span {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-image {
|
||||||
|
position: relative;
|
||||||
|
height: 200px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-placeholder {
|
||||||
|
font-size: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.\u7a7a\u95f2 {
|
||||||
|
background: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.\u5728\u79df {
|
||||||
|
background: #FF8C00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card h2 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item .label {
|
||||||
|
color: #B2B2B2;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item .value {
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item .value.primary {
|
||||||
|
color: #FF6B00;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row .label {
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row .value {
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-action {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(255,255,255,0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
padding: 12px 16px;
|
||||||
|
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
border-top: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info .total {
|
||||||
|
display: block;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #FF6B00;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info .hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-action .el-button {
|
||||||
|
flex: 1;
|
||||||
|
background: #FF6B00;
|
||||||
|
border-color: #FF6B00;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rent-dialog {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rent-vehicle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rent-vehicle .icon {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rent-vehicle h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rent-vehicle p {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rent-summary {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 0.5px solid #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row.total {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 0.5px solid #F0F0F0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
<template>
|
||||||
|
<div class="vehicles-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>选择车型</h2>
|
||||||
|
<p>浏览全部可用车辆</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选栏 -->
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-select v-model="filterType" placeholder="电池类型" size="small" style="width: 100px">
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="锂电池" value="锂电池" />
|
||||||
|
<el-option label="铅酸电池" value="铅酸电池" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="filterStatus" placeholder="状态" size="small" style="width: 100px">
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="空闲" value="空闲" />
|
||||||
|
<el-option label="在租" value="在租" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 车辆列表 -->
|
||||||
|
<div class="vehicle-list" v-loading="loading">
|
||||||
|
<div
|
||||||
|
v-for="vehicle in filteredVehicles"
|
||||||
|
:key="vehicle._id"
|
||||||
|
class="vehicle-card"
|
||||||
|
@click="goDetail(vehicle._id)"
|
||||||
|
>
|
||||||
|
<div class="vehicle-img">
|
||||||
|
<span class="emoji">🛵</span>
|
||||||
|
<el-tag
|
||||||
|
:type="vehicle.status === '空闲' ? 'success' : 'warning'"
|
||||||
|
class="status-tag"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ vehicle.status }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-info">
|
||||||
|
<h4>{{ vehicle.model }}</h4>
|
||||||
|
<div class="tags">
|
||||||
|
<el-tag size="small" type="info">{{ vehicle.color }}</el-tag>
|
||||||
|
<el-tag size="small" type="info">{{ vehicle.batteryType }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="price-row">
|
||||||
|
<span class="price">¥{{ getDailyRate(vehicle) }}/天</span>
|
||||||
|
<span class="deposit">押金: ¥{{ vehicle.purchasePrice ? Math.round(vehicle.purchasePrice * 0.1) : 200 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-arrow">
|
||||||
|
<el-icon><ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-if="!loading && filteredVehicles.length === 0" description="暂无车辆" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ArrowRight } from '@element-plus/icons-vue'
|
||||||
|
import request from '../utils/request'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const vehicles = ref([])
|
||||||
|
const filterType = ref('')
|
||||||
|
const filterStatus = ref('')
|
||||||
|
|
||||||
|
const getDailyRate = (vehicle) => {
|
||||||
|
const basePrice = vehicle.purchasePrice || 3000
|
||||||
|
return Math.round(basePrice / 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredVehicles = computed(() => {
|
||||||
|
return vehicles.value.filter(v => {
|
||||||
|
const typeMatch = !filterType.value || v.batteryType === filterType.value
|
||||||
|
const statusMatch = !filterStatus.value || v.status === filterStatus.value
|
||||||
|
return typeMatch && statusMatch
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchVehicles = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await request.get('/vehicles')
|
||||||
|
if (res.success) {
|
||||||
|
vehicles.value = res.data
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 模拟数据
|
||||||
|
vehicles.value = [
|
||||||
|
{ _id: '1', model: '黑骑士', color: '黑色', batteryType: '锂电池', purchasePrice: 3500, status: '空闲' },
|
||||||
|
{ _id: '2', model: '黑骑士', color: '白色', batteryType: '锂电池', purchasePrice: 3500, status: '空闲' },
|
||||||
|
{ _id: '3', model: '电动车', color: '蓝色', batteryType: '铅酸电池', purchasePrice: 2800, status: '空闲' },
|
||||||
|
{ _id: '4', model: '高端豪车', color: '红色', batteryType: '锂电池', purchasePrice: 8000, status: '空闲' },
|
||||||
|
{ _id: '5', model: '普通标准套餐', color: '绿色', batteryType: '铅酸电池', purchasePrice: 2500, status: '在租' }
|
||||||
|
]
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goDetail = (id) => {
|
||||||
|
router.push(`/vehicle/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchVehicles()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vehicles-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px 16px 16px;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-list {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-img {
|
||||||
|
position: relative;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-img .emoji {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-info h4 {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: #FF6B00;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deposit {
|
||||||
|
color: #B2B2B2;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-arrow {
|
||||||
|
color: #B2B2B2;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
|
async function test() {
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// 测试登录页
|
||||||
|
console.log('Testing login page...');
|
||||||
|
await page.goto('http://localhost:5176/login');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await page.screenshot({ path: 'login-page.png', fullPage: true });
|
||||||
|
console.log('Login page screenshot saved');
|
||||||
|
|
||||||
|
// 测试登录
|
||||||
|
console.log('Logging in...');
|
||||||
|
await page.fill('input[placeholder="请输入手机号"]', '13800138000');
|
||||||
|
await page.fill('input[placeholder="请输入密码"]', '123456');
|
||||||
|
await page.click('button:has-text("登录")');
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
await page.screenshot({ path: 'home-page.png', fullPage: true });
|
||||||
|
console.log('Home page screenshot saved');
|
||||||
|
|
||||||
|
// 测试车型列表
|
||||||
|
console.log('Testing vehicles page...');
|
||||||
|
await page.goto('http://localhost:5176/vehicles');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await page.screenshot({ path: 'vehicles-page.png', fullPage: true });
|
||||||
|
console.log('Vehicles page screenshot saved');
|
||||||
|
|
||||||
|
// 测试订单页
|
||||||
|
console.log('Testing orders page...');
|
||||||
|
await page.goto('http://localhost:5176/orders');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await page.screenshot({ path: 'orders-page.png', fullPage: true });
|
||||||
|
console.log('Orders page screenshot saved');
|
||||||
|
|
||||||
|
// 测试个人中心
|
||||||
|
console.log('Testing profile page...');
|
||||||
|
await page.goto('http://localhost:5176/profile');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await page.screenshot({ path: 'profile-page.png', fullPage: true });
|
||||||
|
console.log('Profile page screenshot saved');
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log('All tests completed!');
|
||||||
|
}
|
||||||
|
|
||||||
|
test().catch(console.error);
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"status": "failed",
|
||||||
|
"failedTests": []
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 203 KiB |
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
port: 5177,
|
||||||
|
host: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||