Initial commit: 门店端
This commit is contained in:
commit
cbc2f0f95b
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.home-page[data-v-cf2eec66]{min-height:100vh;background:#f7f7f7}.home-header[data-v-cf2eec66]{position:sticky;top:0;z-index:10;background:#fff;padding:16px 16px 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:.5px solid rgba(0,0,0,.06)}.header-left[data-v-cf2eec66]{display:flex;align-items:center;gap:10px}.store-avatar[data-v-cf2eec66]{width:44px;height:44px;background:#fff4e0;border-radius:22px;display:flex;align-items:center;justify-content:center;font-size:22px}.store-name[data-v-cf2eec66]{font-size:17px;font-weight:600;color:#1a1a1a}.store-sub[data-v-cf2eec66]{font-size:12px;color:#b2b2b2;margin-top:2px}.header-badge[data-v-cf2eec66]{background:#e8f8ee;color:#ff6b00;font-size:11px;font-weight:500;padding:3px 10px;border-radius:10px}.quick-nav[data-v-cf2eec66]{display:flex;gap:10px;padding:16px 16px 12px;background:#fff}.nav-card[data-v-cf2eec66]{flex:1;background:#f7f7f7;border-radius:16px;padding:14px 8px 12px;text-align:center;text-decoration:none;display:flex;flex-direction:column;align-items:center;gap:8px}.nav-icon-box[data-v-cf2eec66]{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center}.nav-label[data-v-cf2eec66]{font-size:12px;color:#666}.section[data-v-cf2eec66]{margin:0 12px 12px;background:#fff;border-radius:16px;overflow:hidden}.section-header[data-v-cf2eec66]{display:flex;justify-content:space-between;align-items:center;padding:16px 16px 12px}.section-title[data-v-cf2eec66]{font-size:16px;font-weight:600;color:#1a1a1a}.section-more[data-v-cf2eec66]{font-size:12px;color:#b2b2b2}.stat-grid[data-v-cf2eec66]{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;padding:0 16px 16px;gap:8px}.stat-card[data-v-cf2eec66]{background:#f7f7f7;border-radius:12px;padding:14px 4px 12px;text-align:center}.stat-num[data-v-cf2eec66]{font-size:24px;font-weight:700;line-height:1}.stat-label[data-v-cf2eec66]{font-size:10px;color:#b2b2b2;margin-top:6px}.order-list[data-v-cf2eec66]{padding:0 4px}.loading-tip[data-v-cf2eec66],.empty-tip[data-v-cf2eec66]{padding:24px;text-align:center;color:#b2b2b2;font-size:13px}.order-item[data-v-cf2eec66]{display:flex;justify-content:space-between;align-items:center;padding:14px 12px;border-top:.5px solid #F0F0F0}.order-customer[data-v-cf2eec66]{font-size:14px;color:#1a1a1a;font-weight:500}.order-vehicle[data-v-cf2eec66]{font-size:11px;color:#b2b2b2;margin-top:4px}.order-status[data-v-cf2eec66]{font-size:12px;padding:3px 10px;border-radius:12px;flex-shrink:0}.status-renting[data-v-cf2eec66]{background:#e8f8ee;color:#ff6b00}.status-ok[data-v-cf2eec66]{background:#f0f0f0;color:#999}.status-warning[data-v-cf2eec66]{background:#fff0e8;color:#ff6b35}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{o as h,v as y}from"./api-CXZzdW5u.js";import{_ as F,e as b,o as i,c as r,a as t,t as l,b as c,w as v,f as k,F as x,g as O,h as u,r as w,i as S,n as B}from"./index-DH2ZjwZR.js";const V={class:"home-page"},E={class:"home-header"},C={class:"header-left"},I={class:"store-name"},N={class:"quick-nav"},$={class:"section"},z={class:"stat-grid"},T={class:"stat-card"},A={class:"stat-num",style:{color:"#FF6B00"}},D={class:"stat-card"},H={class:"stat-num",style:{color:"#576BFF"}},M={class:"stat-card"},j={class:"stat-num",style:{color:"#FF6B35"}},q={class:"stat-card"},L={class:"stat-num",style:{color:"#333"}},G={class:"section"},J={class:"section-header"},K={class:"order-list"},P={key:0,class:"loading-tip"},Q={key:1,class:"empty-tip"},R={class:"order-left"},U={class:"order-customer"},W={class:"order-vehicle"},X={__name:"Home",setup(Y){const m=u({}),d=u([]),_=u(!0),n=u({totalOrders:0,rentingOrders:0,warningOrders:0,totalVehicles:0}),g=e=>({renting:"status-renting",returned:"status-ok",overdue:"status-warning"})[e]||"",p=e=>({renting:"在租",returned:"已还",overdue:"逾期",pending:"待处理"})[e]||e,f=e=>{if(!e)return"-";const s=new Date(e);return`${s.getMonth()+1}/${s.getDate()} ${s.getHours().toString().padStart(2,"0")}:${s.getMinutes().toString().padStart(2,"0")}`};return b(async()=>{const e=localStorage.getItem("storeId")||"demo-store";if(m.value={name:"示例门店"},e!=="demo-store")try{const a=await(await fetch(`/api/stores/${e}`)).json();a.name&&(m.value=a)}catch{}try{const s=await h.list({storeId:e,limit:5});d.value=s.data.data||s.data||[];const a=s.data.data||s.data||[];n.value.totalOrders=a.length,n.value.rentingOrders=a.filter(o=>o.status==="renting").length,n.value.warningOrders=a.filter(o=>o.status==="overdue").length}catch{d.value=[]}_.value=!1;try{const s=await y.list({storeId:e,limit:100}),a=s.data.data||s.data||[];n.value.totalVehicles=a.length}catch{}}),(e,s)=>{const a=w("router-link");return i(),r("div",V,[t("div",E,[t("div",C,[s[1]||(s[1]=t("div",{class:"store-avatar"},"🚲",-1)),t("div",null,[t("div",I,l(m.value.name||"加载中..."),1),s[0]||(s[0]=t("div",{class:"store-sub"},"门店端",-1))])]),s[2]||(s[2]=t("div",{class:"header-badge"},"营业中",-1))]),t("div",N,[c(a,{to:"/vehicle-types",class:"nav-card"},{default:v(()=>[...s[3]||(s[3]=[t("div",{class:"nav-icon-box",style:{background:"#E8F8EE"}},[t("span",{style:{"font-size":"20px"}},"🚗")],-1),t("div",{class:"nav-label"},"车型管理",-1)])]),_:1}),c(a,{to:"/vehicles",class:"nav-card"},{default:v(()=>[...s[4]||(s[4]=[t("div",{class:"nav-icon-box",style:{background:"#FFF4E0"}},[t("span",{style:{"font-size":"20px"}},"🏍️")],-1),t("div",{class:"nav-label"},"车辆管理",-1)])]),_:1}),c(a,{to:"/orders",class:"nav-card"},{default:v(()=>[...s[5]||(s[5]=[t("div",{class:"nav-icon-box",style:{background:"#E8F0FF"}},[t("span",{style:{"font-size":"20px"}},"📋")],-1),t("div",{class:"nav-label"},"订单管理",-1)])]),_:1})]),t("div",$,[s[10]||(s[10]=t("div",{class:"section-header"},[t("div",{class:"section-title"},"今日概览"),t("div",{class:"section-more"},"实时数据")],-1)),t("div",z,[t("div",T,[t("div",A,l(n.value.totalOrders),1),s[6]||(s[6]=t("div",{class:"stat-label"},"总订单",-1))]),t("div",D,[t("div",H,l(n.value.rentingOrders),1),s[7]||(s[7]=t("div",{class:"stat-label"},"在租订单",-1))]),t("div",M,[t("div",j,l(n.value.warningOrders),1),s[8]||(s[8]=t("div",{class:"stat-label"},"预警订单",-1))]),t("div",q,[t("div",L,l(n.value.totalVehicles),1),s[9]||(s[9]=t("div",{class:"stat-label"},"车辆总数",-1))])])]),t("div",G,[t("div",J,[s[12]||(s[12]=t("div",{class:"section-title"},"最近订单",-1)),c(a,{to:"/orders",class:"section-more",style:{color:"#FF6B00"}},{default:v(()=>[...s[11]||(s[11]=[S("查看全部 ›",-1)])]),_:1})]),t("div",K,[_.value?(i(),r("div",P,"加载中...")):d.value.length===0?(i(),r("div",Q,"暂无订单")):k("",!0),(i(!0),r(x,null,O(d.value,o=>(i(),r("div",{key:o._id,class:"order-item"},[t("div",R,[t("div",U,l(o.customerName||"客户"),1),t("div",W,l(o.vehicleType||"车型")+" · "+l(f(o.createdAt)),1)]),t("div",{class:B(["order-status",g(o.status)])},l(p(o.status)),3)]))),128))])])])}}},ts=F(X,[["__scopeId","data-v-cf2eec66"]]);export{ts as default};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{_ as k,e as y,o as u,c,a as s,t as m,u as x,j as I,k as n,v as i,f as S,h as v}from"./index-DH2ZjwZR.js";import{s as p}from"./api-CXZzdW5u.js";const _={class:"page"},C={class:"mine-header"},V={class:"header-info"},D={class:"info-text"},M={class:"store-name"},F={class:"store-id"},U={class:"dialog"},B={class:"dialog-body"},E={class:"form-item"},N={class:"form-item"},$={class:"form-item"},j={class:"form-item"},A={class:"dialog-footer"},L={__name:"Mine",setup(T){const r=localStorage.getItem("storeId")||"demo-store",t=v({}),a=v(!1),o=v({}),f=async()=>{try{const d=await p.getStore(r);t.value=d.data||{}}catch{t.value={name:"示例门店"}}},g=()=>{o.value={...t.value},a.value=!0},b=async()=>{try{await p.updateStore(r,o.value),t.value={...o.value},a.value=!1}catch{alert("保存失败")}},w=()=>{confirm("确定退出登录?")&&(localStorage.removeItem("storeId"),window.location.reload())};return y(f),(d,l)=>(u(),c("div",_,[s("div",C,[s("div",V,[l[6]||(l[6]=s("div",{class:"avatar-box"},"🏪",-1)),s("div",D,[s("div",M,m(t.value.name||"加载中..."),1),s("div",F,"ID: "+m(x(r)),1)])])]),s("div",{class:"card-section"},[s("div",{class:"card-row",onClick:g},[...l[7]||(l[7]=[s("div",{class:"card-left"},[s("span",{class:"card-icon"},"🏢"),s("span",{class:"card-text"},"门店信息")],-1),s("span",{class:"card-arrow"},"›",-1)])]),l[9]||(l[9]=s("div",{class:"card-divider"},null,-1)),s("div",{class:"card-row",onClick:w},[...l[8]||(l[8]=[s("div",{class:"card-left"},[s("span",{class:"card-icon"},"🚪"),s("span",{class:"card-text",style:{color:"#FF4D4F"}},"退出登录")],-1)])])]),a.value?(u(),c("div",{key:0,class:"dialog-overlay",onClick:l[5]||(l[5]=I(e=>a.value=!1,["self"]))},[s("div",U,[l[14]||(l[14]=s("div",{class:"dialog-handle"},null,-1)),l[15]||(l[15]=s("div",{class:"dialog-title"},"编辑门店信息",-1)),s("div",B,[s("div",E,[l[10]||(l[10]=s("label",null,"门店名称",-1)),n(s("input",{"onUpdate:modelValue":l[0]||(l[0]=e=>o.value.name=e),placeholder:"请输入门店名称"},null,512),[[i,o.value.name]])]),s("div",N,[l[11]||(l[11]=s("label",null,"联系人",-1)),n(s("input",{"onUpdate:modelValue":l[1]||(l[1]=e=>o.value.contact=e),placeholder:"请输入联系人"},null,512),[[i,o.value.contact]])]),s("div",$,[l[12]||(l[12]=s("label",null,"联系电话",-1)),n(s("input",{"onUpdate:modelValue":l[2]||(l[2]=e=>o.value.phone=e),placeholder:"请输入联系电话"},null,512),[[i,o.value.phone]])]),s("div",j,[l[13]||(l[13]=s("label",null,"地址",-1)),n(s("input",{"onUpdate:modelValue":l[3]||(l[3]=e=>o.value.address=e),placeholder:"请输入地址"},null,512),[[i,o.value.address]])])]),s("div",A,[s("button",{class:"btn-cancel",onClick:l[4]||(l[4]=e=>a.value=!1)},"取消"),s("button",{class:"btn-confirm",onClick:b},"保存")])])])):S("",!0)]))}},z=k(L,[["__scopeId","data-v-999dd477"]]);export{z as default};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.page[data-v-999dd477]{min-height:100vh;background:#f7f7f7;padding-bottom:20px}.mine-header[data-v-999dd477]{position:sticky;top:0;z-index:10;background:#fff;padding:28px 16px 24px;border-bottom:.5px solid rgba(0,0,0,.06)}.header-info[data-v-999dd477]{display:flex;align-items:center;gap:14px}.avatar-box[data-v-999dd477]{width:60px;height:60px;background:#fff4e0;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:30px}.info-text[data-v-999dd477]{flex:1}.store-name[data-v-999dd477]{font-size:20px;font-weight:600;color:#1a1a1a}.store-id[data-v-999dd477]{font-size:12px;color:#b2b2b2;margin-top:4px}.card-section[data-v-999dd477]{margin:12px;background:#fff;border-radius:16px;overflow:hidden}.card-row[data-v-999dd477]{display:flex;align-items:center;justify-content:space-between;padding:16px}.card-left[data-v-999dd477]{display:flex;align-items:center;gap:10px}.card-icon[data-v-999dd477]{font-size:18px}.card-text[data-v-999dd477]{font-size:15px;color:#1a1a1a}.card-arrow[data-v-999dd477]{font-size:20px;color:#d1d1d6}.card-divider[data-v-999dd477]{height:.5px;background:#f0f0f0;margin:0 16px}.dialog-overlay[data-v-999dd477]{position:fixed;inset:0;background:#0006;display:flex;align-items:flex-end;z-index:200}.dialog[data-v-999dd477]{background:#fff;width:100%;border-radius:20px 20px 0 0;padding-bottom:calc(16px + env(safe-area-inset-bottom))}.dialog-handle[data-v-999dd477]{width:36px;height:4px;background:#ddd;border-radius:2px;margin:10px auto 0}.dialog-title[data-v-999dd477]{text-align:center;font-size:17px;font-weight:600;color:#1a1a1a;padding:18px 16px 14px}.dialog-body[data-v-999dd477]{padding:0 16px 16px;max-height:60vh;overflow-y:auto}.form-item[data-v-999dd477]{margin-bottom:14px}.form-item label[data-v-999dd477]{display:block;font-size:13px;color:#8e8e93;margin-bottom:8px}.form-item input[data-v-999dd477]{width:100%;padding:12px 14px;background:#f7f7f7;border:none;border-radius:10px;font-size:15px;color:#1a1a1a;outline:none;box-sizing:border-box}.form-item input[data-v-999dd477]::placeholder{color:#b2b2b2}.dialog-footer[data-v-999dd477]{display:flex;gap:12px;padding:0 16px}.btn-cancel[data-v-999dd477],.btn-confirm[data-v-999dd477]{flex:1;padding:13px;border-radius:24px;font-size:16px;font-weight:500;cursor:pointer;border:none}.btn-cancel[data-v-999dd477]{background:#f0f0f0;color:#666}.btn-confirm[data-v-999dd477]{background:#ff6b00;color:#fff}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.page[data-v-8ae6e53c]{min-height:100vh;background:#f7f7f7}.page-header[data-v-8ae6e53c]{position:sticky;top:0;z-index:10;background:#fff;padding:18px 16px 14px;border-bottom:.5px solid rgba(0,0,0,.06)}.page-title[data-v-8ae6e53c]{font-size:20px;font-weight:600;color:#1a1a1a}.filter-tabs[data-v-8ae6e53c]{display:flex;background:#fff;padding:0 16px;gap:24px;border-bottom:.5px solid #F0F0F0}.sticky-tabs[data-v-8ae6e53c]{position:sticky;top:calc(53px + env(safe-area-inset-top));z-index:9}.filter-tab[data-v-8ae6e53c]{padding:12px 0;font-size:15px;color:#999;cursor:pointer;position:relative;transition:color .2s}.filter-tab.active[data-v-8ae6e53c]{color:#ff6b00;font-weight:600}.filter-tab.active[data-v-8ae6e53c]:after{content:"";position:absolute;bottom:-.5px;left:0;right:0;height:2px;background:#ff6b00;border-radius:1px}.list-wrap[data-v-8ae6e53c]{padding:12px}.loading[data-v-8ae6e53c]{display:flex;flex-direction:column;align-items:center;gap:12px;padding:60px;color:#b2b2b2;font-size:14px}.loading-ring[data-v-8ae6e53c]{width:32px;height:32px;border:3px solid #E5E5E5;border-top-color:#ff6b00;border-radius:50%;animation:spin-8ae6e53c .8s linear infinite}@keyframes spin-8ae6e53c{to{transform:rotate(360deg)}}.empty[data-v-8ae6e53c]{text-align:center;padding:60px 40px}.empty-icon[data-v-8ae6e53c]{font-size:48px;margin-bottom:12px}.empty-text[data-v-8ae6e53c]{font-size:14px;color:#b2b2b2}.order-card[data-v-8ae6e53c]{background:#fff;border-radius:16px;padding:16px;margin-bottom:10px}.order-top[data-v-8ae6e53c]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding-bottom:12px;border-bottom:.5px solid #F0F0F0}.order-id[data-v-8ae6e53c]{font-size:13px;color:#8e8e93;font-weight:500;letter-spacing:.5px}.order-status[data-v-8ae6e53c]{font-size:12px;padding:3px 10px;border-radius:12px;font-weight:500}.status-renting[data-v-8ae6e53c]{background:#e8f8ee;color:#ff6b00}.status-ok[data-v-8ae6e53c]{background:#f0f0f0;color:#999}.status-warning[data-v-8ae6e53c]{background:#fff0e8;color:#ff6b35}.order-info .info-line[data-v-8ae6e53c]{display:flex;justify-content:space-between;padding:5px 0;font-size:14px}.info-label[data-v-8ae6e53c]{color:#8e8e93}.info-value[data-v-8ae6e53c]{color:#1a1a1a}.info-value.price[data-v-8ae6e53c]{color:#ff6b35;font-weight:600}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{o as h}from"./api-CXZzdW5u.js";import{_ as k,e as C,o,c as n,a as s,F as v,g as p,f as D,h as c,m as x,n as _,t as l}from"./index-DH2ZjwZR.js";const S={class:"page"},w={class:"filter-tabs sticky-tabs"},I=["onClick"],N={class:"list-wrap"},A={key:0,class:"loading"},B={key:1,class:"empty"},L={class:"order-top"},z={class:"order-id"},F={class:"order-info"},O={class:"info-line"},T={class:"info-value"},V={class:"info-line"},E={class:"info-value"},M={class:"info-line"},U={class:"info-value price"},$={class:"info-line"},j={class:"info-value"},q={__name:"Orders",setup(G){const i=c([]),d=c(!0),r=c("all"),f=[{label:"全部",value:"all"},{label:"在租",value:"renting"},{label:"已还",value:"returned"},{label:"逾期",value:"overdue"}],g=localStorage.getItem("storeId")||"demo-store",u=x(()=>r.value==="all"?i.value:i.value.filter(t=>t.status===r.value)),m=t=>({renting:"status-renting",returned:"status-ok",overdue:"status-warning"})[t]||"",y=t=>({renting:"在租",returned:"已还",overdue:"逾期",pending:"待处理"})[t]||t,b=t=>t?new Date(t).toLocaleDateString("zh-CN"):"-";return C(async()=>{d.value=!0;try{const t=await h.list({storeId:g});i.value=t.data.data||t.data||[]}catch{i.value=[]}d.value=!1}),(t,e)=>(o(),n("div",S,[e[6]||(e[6]=s("div",{class:"page-header"},[s("div",{class:"page-title"},"订单管理")],-1)),s("div",w,[(o(),n(v,null,p(f,a=>s("div",{key:a.value,class:_(["filter-tab",{active:r.value===a.value}]),onClick:J=>r.value=a.value},l(a.label),11,I)),64))]),s("div",N,[d.value?(o(),n("div",A,[...e[0]||(e[0]=[s("div",{class:"loading-ring"},null,-1),s("div",null,"加载中...",-1)])])):u.value.length===0?(o(),n("div",B,[...e[1]||(e[1]=[s("div",{class:"empty-icon"},"📋",-1),s("div",{class:"empty-text"},"暂无订单",-1)])])):D("",!0),(o(!0),n(v,null,p(u.value,a=>(o(),n("div",{key:a._id,class:"order-card"},[s("div",L,[s("div",z,"订单 · "+l(a._id.slice(-6).toUpperCase()),1),s("div",{class:_(["order-status",m(a.status)])},l(y(a.status)),3)]),s("div",F,[s("div",O,[e[2]||(e[2]=s("span",{class:"info-label"},"客户",-1)),s("span",T,l(a.customerName||"-"),1)]),s("div",V,[e[3]||(e[3]=s("span",{class:"info-label"},"车型",-1)),s("span",E,l(a.vehicleType||"-"),1)]),s("div",M,[e[4]||(e[4]=s("span",{class:"info-label"},"金额",-1)),s("span",U,"¥"+l(a.totalAmount||0),1)]),s("div",$,[e[5]||(e[5]=s("span",{class:"info-label"},"时间",-1)),s("span",j,l(b(a.createdAt)),1)])])]))),128))])]))}},Q=k(q,[["__scopeId","data-v-8ae6e53c"]]);export{Q as default};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{o as d,c as v,a as t,b as s,w as i,n as l,u as n,d as p,r}from"./index-DH2ZjwZR.js";const u={class:"tab-layout"},b={class:"tab-content"},h={class:"tab-bar"},x={__name:"TabLayout",setup(_){const o=p();return(m,e)=>{const c=r("router-view"),a=r("router-link");return d(),v("div",u,[t("div",b,[s(c)]),t("div",h,[s(a,{to:"/",class:l(["tab-item",{active:n(o).path==="/"}])},{default:i(()=>[...e[0]||(e[0]=[t("div",{class:"tab-icon"},[t("svg",{width:"22",height:"22",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.8"},[t("path",{d:"M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"}),t("polyline",{points:"9,22 9,12 15,12 15,22"})])],-1),t("div",{class:"tab-label"},"首页",-1)])]),_:1},8,["class"]),s(a,{to:"/orders",class:l(["tab-item",{active:n(o).path==="/orders"}])},{default:i(()=>[...e[1]||(e[1]=[t("div",{class:"tab-icon"},[t("svg",{width:"22",height:"22",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.8"},[t("path",{d:"M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"}),t("polyline",{points:"14,2 14,8 20,8"}),t("line",{x1:"16",y1:"13",x2:"8",y2:"13"}),t("line",{x1:"16",y1:"17",x2:"8",y2:"17"}),t("polyline",{points:"10,9 9,9 8,9"})])],-1),t("div",{class:"tab-label"},"订单",-1)])]),_:1},8,["class"]),s(a,{to:"/mine",class:l(["tab-item",{active:n(o).path==="/mine"}])},{default:i(()=>[...e[2]||(e[2]=[t("div",{class:"tab-icon"},[t("svg",{width:"22",height:"22",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.8"},[t("path",{d:"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"}),t("circle",{cx:"12",cy:"7",r:"4"})])],-1),t("div",{class:"tab-label"},"我的",-1)])]),_:1},8,["class"])])])}}};export{x as default};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
*{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:#ffffffeb;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);display:flex;border-top:.5px solid rgba(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 .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}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{_ as N,e as U,o,c as n,a as e,f as b,F as A,g as B,j as F,t as v,k as f,v as g,b as k,w,h as r,r as D,i as M,u as S,p as E}from"./index-DH2ZjwZR.js";import{a as u}from"./api-CXZzdW5u.js";const L={class:"page"},R={class:"page-header"},j={class:"header-row"},q={class:"list-wrap"},z={key:0,class:"loading"},G={key:1,class:"empty"},H={class:"type-img"},J=["src"],K={key:1,class:"type-img-placeholder"},O={class:"type-info"},Q={class:"type-name"},W={class:"type-brand"},X={class:"type-price"},Y=["onClick"],Z=["onClick"],ee={class:"dialog"},se={class:"dialog-title"},ae={class:"dialog-body"},le={class:"form-item"},te={class:"form-item"},oe={class:"form-item"},ne={class:"cover-upload-wrap"},de={key:0,class:"cover-preview"},re=["src"],ie={key:1,class:"cover-placeholder"},ce={class:"form-item"},ve={__name:"VehicleTypes",setup(ue){const i=r([]),p=r(!0),c=r(!1),d=r(null),a=r({brand:"",name:"",cover:"",pricePerDay:""}),C=l=>{const s=new FileReader;s.onload=m=>{a.value.cover=m.target.result},s.readAsDataURL(l.raw)},h=localStorage.getItem("storeId")||"demo-store",y=async()=>{p.value=!0;try{const l=await u.list({storeId:h});i.value=l.data.data||l.data||[]}catch{i.value=[]}p.value=!1},V=()=>{d.value=null,a.value={brand:"",name:"",cover:"",pricePerDay:""},c.value=!0},P=l=>{d.value=l._id,a.value={brand:l.brand,name:l.name,cover:l.cover||"",pricePerDay:l.pricePerDay||""},c.value=!0},T=async()=>{if(!a.value.brand||!a.value.name){alert("请填写品牌和名称");return}try{d.value?await u.update(d.value,a.value):await u.create({...a.value,storeId:h}),_(),y()}catch{alert(d.value?"更新失败":"添加失败")}},_=()=>{c.value=!1,d.value=null,a.value={brand:"",name:"",cover:"",pricePerDay:""}},x=async l=>{if(confirm("确定删除该车型?"))try{await u.delete(l),y()}catch{alert("删除失败")}};return U(y),(l,s)=>{const m=D("el-icon"),I=D("el-upload");return o(),n("div",L,[e("div",R,[e("div",j,[e("button",{class:"btn-back",onClick:s[0]||(s[0]=t=>l.$router.back())},[...s[5]||(s[5]=[e("span",{class:"back-arrow"},"‹",-1)])]),s[6]||(s[6]=e("div",{class:"page-title"},"车型管理",-1)),e("button",{class:"btn-add-inline",onClick:V},"+ 新增")])]),e("div",q,[p.value?(o(),n("div",z,[...s[7]||(s[7]=[e("div",{class:"loading-ring"},null,-1)])])):i.value.length===0?(o(),n("div",G,[...s[8]||(s[8]=[e("div",{class:"empty-icon"},"🚗",-1),e("div",{class:"empty-text"},"暂无车型",-1),e("div",{class:"empty-sub"},"点击下方添加车型",-1)])])):b("",!0),(o(!0),n(A,null,B(i.value,t=>(o(),n("div",{key:t._id,class:"type-card"},[e("div",H,[t.cover?(o(),n("img",{key:0,src:t.cover,alt:"cover"},null,8,J)):(o(),n("div",K,"🚗"))]),e("div",O,[e("div",Q,v(t.name),1),e("div",W,v(t.brand),1),e("div",X,[M("¥"+v(t.pricePerDay||0),1),s[9]||(s[9]=e("span",{class:"per-day"},"/天",-1))])]),e("button",{class:"btn-edit",onClick:$=>P(t)},"编辑",8,Y),e("button",{class:"btn-del",onClick:$=>x(t._id)},"删除",8,Z)]))),128))]),c.value?(o(),n("div",{key:0,class:"dialog-overlay",onClick:F(_,["self"])},[e("div",ee,[s[16]||(s[16]=e("div",{class:"dialog-handle"},null,-1)),e("div",se,v(d.value?"编辑车型":"添加车型"),1),e("div",ae,[e("div",le,[s[10]||(s[10]=e("label",null,"品牌",-1)),f(e("input",{"onUpdate:modelValue":s[1]||(s[1]=t=>a.value.brand=t),placeholder:"如:雅迪"},null,512),[[g,a.value.brand]])]),e("div",te,[s[11]||(s[11]=e("label",null,"车型名称",-1)),f(e("input",{"onUpdate:modelValue":s[2]||(s[2]=t=>a.value.name=t),placeholder:"如:TDT1234"},null,512),[[g,a.value.name]])]),e("div",oe,[s[14]||(s[14]=e("label",null,"封面图",-1)),e("div",ne,[k(I,{class:"cover-uploader","auto-upload":!1,"show-file-list":!1,"on-change":C,accept:"image/*"},{default:w(()=>[a.value.cover?(o(),n("div",de,[e("img",{src:a.value.cover,alt:"封面"},null,8,re),s[12]||(s[12]=e("div",{class:"cover-mask"},[e("span",null,"点击更换")],-1))])):(o(),n("div",ie,[k(m,{class:"cover-icon"},{default:w(()=>[k(S(E))]),_:1}),s[13]||(s[13]=e("span",null,"上传封面",-1))]))]),_:1}),a.value.cover?(o(),n("div",{key:0,class:"cover-remove",onClick:s[3]||(s[3]=t=>a.value.cover="")},"删除图片")):b("",!0)])]),e("div",ce,[s[15]||(s[15]=e("label",null,"日租金(元)",-1)),f(e("input",{"onUpdate:modelValue":s[4]||(s[4]=t=>a.value.pricePerDay=t),type:"number",placeholder:"如:50"},null,512),[[g,a.value.pricePerDay,void 0,{number:!0}]])])]),e("div",{class:"dialog-footer"},[e("button",{class:"btn-cancel",onClick:_},"取消"),e("button",{class:"btn-confirm",onClick:T},"确定")])])])):b("",!0)])}}},_e=N(ve,[["__scopeId","data-v-7acc3da1"]]);export{_e as default};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.page[data-v-7acc3da1]{min-height:100vh;background:#f7f7f7}.page-header[data-v-7acc3da1]{position:sticky;top:0;z-index:10;background:#fff;padding:14px 16px 12px;border-bottom:.5px solid rgba(0,0,0,.06)}.header-row[data-v-7acc3da1]{display:flex;align-items:center;justify-content:space-between}.btn-back[data-v-7acc3da1]{width:34px;height:34px;border-radius:50%;background:#f7f7f7;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0}.back-arrow[data-v-7acc3da1]{font-size:22px;color:#1a1a1a;line-height:1;margin-top:-2px}.page-title[data-v-7acc3da1]{font-size:18px;font-weight:600;color:#1a1a1a;flex:1;text-align:center}.btn-add-inline[data-v-7acc3da1]{background:#ff6b00;color:#fff;border:none;border-radius:16px;padding:6px 14px;font-size:13px;font-weight:600;cursor:pointer;flex-shrink:0}.list-wrap[data-v-7acc3da1]{padding:12px}.loading[data-v-7acc3da1]{display:flex;justify-content:center;padding:60px}.loading-ring[data-v-7acc3da1]{width:32px;height:32px;border:3px solid #E5E5E5;border-top-color:#ff6b00;border-radius:50%;animation:spin-7acc3da1 .8s linear infinite}@keyframes spin-7acc3da1{to{transform:rotate(360deg)}}.empty[data-v-7acc3da1]{text-align:center;padding:60px 40px}.empty-icon[data-v-7acc3da1]{font-size:48px;margin-bottom:12px}.empty-text[data-v-7acc3da1]{font-size:15px;color:#1a1a1a;font-weight:500}.empty-sub[data-v-7acc3da1]{font-size:13px;color:#b2b2b2;margin-top:6px}.type-card[data-v-7acc3da1]{background:#fff;border-radius:16px;padding:14px;margin-bottom:10px;display:flex;align-items:center;gap:12px}.type-img[data-v-7acc3da1]{width:60px;height:60px;border-radius:12px;overflow:hidden;background:#f7f7f7;display:flex;align-items:center;justify-content:center;flex-shrink:0}.type-img img[data-v-7acc3da1]{width:100%;height:100%;object-fit:cover}.type-img-placeholder[data-v-7acc3da1]{font-size:28px}.type-info[data-v-7acc3da1]{flex:1;min-width:0}.type-name[data-v-7acc3da1]{font-size:15px;font-weight:600;color:#1a1a1a}.type-brand[data-v-7acc3da1]{font-size:12px;color:#8e8e93;margin-top:3px}.type-price[data-v-7acc3da1]{font-size:16px;color:#ff6b35;font-weight:600;margin-top:6px}.per-day[data-v-7acc3da1]{font-size:12px;font-weight:400;color:#8e8e93}.btn-edit[data-v-7acc3da1]{background:#fff7f0;color:#ff6b00;border:none;border-radius:16px;padding:6px 14px;font-size:12px;cursor:pointer;flex-shrink:0}.btn-del[data-v-7acc3da1]{background:#fff0f0;color:#ff4d4f;border:none;border-radius:16px;padding:6px 14px;font-size:12px;cursor:pointer;flex-shrink:0}.dialog-overlay[data-v-7acc3da1]{position:fixed;inset:0;background:#0006;display:flex;align-items:flex-end;z-index:200}.dialog[data-v-7acc3da1]{background:#fff;width:100%;border-radius:20px 20px 0 0;padding-bottom:calc(16px + env(safe-area-inset-bottom))}.dialog-handle[data-v-7acc3da1]{width:36px;height:4px;background:#ddd;border-radius:2px;margin:10px auto 0}.dialog-title[data-v-7acc3da1]{text-align:center;font-size:17px;font-weight:600;color:#1a1a1a;padding:18px 16px 14px}.dialog-body[data-v-7acc3da1]{padding:0 16px 16px}.form-item[data-v-7acc3da1]{margin-bottom:14px}.form-item label[data-v-7acc3da1]{display:block;font-size:13px;color:#8e8e93;margin-bottom:8px}.form-item input[data-v-7acc3da1]{width:100%;padding:12px 14px;background:#f7f7f7;border:none;border-radius:10px;font-size:15px;color:#1a1a1a;outline:none;box-sizing:border-box}.form-item input[data-v-7acc3da1]::placeholder{color:#b2b2b2}.dialog-footer[data-v-7acc3da1]{display:flex;gap:12px;padding:0 16px}.btn-cancel[data-v-7acc3da1],.btn-confirm[data-v-7acc3da1]{flex:1;padding:13px;border-radius:24px;font-size:16px;font-weight:500;cursor:pointer;border:none}.btn-cancel[data-v-7acc3da1]{background:#f0f0f0;color:#666}.btn-confirm[data-v-7acc3da1]{background:#ff6b00;color:#fff}.cover-upload-wrap[data-v-7acc3da1]{display:flex;align-items:center;gap:12px}.cover-uploader[data-v-7acc3da1]{width:80px;height:80px;flex-shrink:0}.cover-placeholder[data-v-7acc3da1]{width:80px;height:80px;border-radius:10px;border:1.5px dashed #DDD;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;cursor:pointer;background:#f7f7f7;transition:border-color .2s}.cover-placeholder[data-v-7acc3da1]:hover{border-color:#ff6b00}.cover-icon[data-v-7acc3da1]{font-size:22px;color:#b2b2b2}.cover-placeholder span[data-v-7acc3da1]{font-size:11px;color:#b2b2b2}.cover-preview[data-v-7acc3da1]{width:80px;height:80px;border-radius:10px;overflow:hidden;position:relative;cursor:pointer}.cover-preview img[data-v-7acc3da1]{width:100%;height:100%;object-fit:cover}.cover-mask[data-v-7acc3da1]{position:absolute;inset:0;background:#00000073;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s}.cover-preview:hover .cover-mask[data-v-7acc3da1]{opacity:1}.cover-mask span[data-v-7acc3da1]{color:#fff;font-size:11px}.cover-remove[data-v-7acc3da1]{font-size:12px;color:#ff4d4f;cursor:pointer;text-decoration:underline}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.page[data-v-9bb5a6e7]{min-height:100vh;background:#f7f7f7}.page-header[data-v-9bb5a6e7]{position:sticky;top:0;z-index:10;background:#fff;padding:14px 16px 12px;border-bottom:.5px solid rgba(0,0,0,.06)}.header-row[data-v-9bb5a6e7]{display:flex;align-items:center;justify-content:space-between}.btn-back[data-v-9bb5a6e7]{width:34px;height:34px;border-radius:50%;background:#f7f7f7;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0}.back-arrow[data-v-9bb5a6e7]{font-size:22px;color:#1a1a1a;line-height:1;margin-top:-2px}.page-title[data-v-9bb5a6e7]{font-size:18px;font-weight:600;color:#1a1a1a;flex:1;text-align:center}.btn-add-inline[data-v-9bb5a6e7]{background:#ff6b00;color:#fff;border:none;border-radius:16px;padding:6px 14px;font-size:13px;font-weight:600;cursor:pointer;flex-shrink:0}.list-wrap[data-v-9bb5a6e7]{padding:12px}.loading[data-v-9bb5a6e7]{display:flex;justify-content:center;padding:60px}.loading-ring[data-v-9bb5a6e7]{width:32px;height:32px;border:3px solid #E5E5E5;border-top-color:#ff6b00;border-radius:50%;animation:spin-9bb5a6e7 .8s linear infinite}@keyframes spin-9bb5a6e7{to{transform:rotate(360deg)}}.empty[data-v-9bb5a6e7]{text-align:center;padding:60px 40px}.empty-icon[data-v-9bb5a6e7]{font-size:48px;margin-bottom:12px}.empty-text[data-v-9bb5a6e7]{font-size:15px;color:#1a1a1a;font-weight:500}.empty-sub[data-v-9bb5a6e7]{font-size:13px;color:#b2b2b2;margin-top:6px}.vehicle-card[data-v-9bb5a6e7]{background:#fff;border-radius:16px;padding:16px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between}.vehicle-left[data-v-9bb5a6e7]{flex:1;min-width:0}.vehicle-plate[data-v-9bb5a6e7]{font-size:16px;font-weight:600;color:#1a1a1a}.vehicle-type[data-v-9bb5a6e7]{font-size:12px;color:#8e8e93;margin-top:4px}.vehicle-detail[data-v-9bb5a6e7]{font-size:12px;color:#b2b2b2;margin-top:3px}.vehicle-right[data-v-9bb5a6e7]{display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex-shrink:0}.vehicle-status[data-v-9bb5a6e7]{font-size:12px;padding:4px 14px;border-radius:16px;font-weight:500}.status-renting[data-v-9bb5a6e7]{background:#e8f8ee;color:#ff6b00}.status-idle[data-v-9bb5a6e7]{background:#f0f0f0;color:#999}.btn-edit[data-v-9bb5a6e7]{background:#fff7f0;color:#ff6b00;border:none;border-radius:16px;padding:6px 14px;font-size:12px;cursor:pointer}.btn-del[data-v-9bb5a6e7]{background:#fff0f0;color:#ff4d4f;border:none;border-radius:16px;padding:6px 14px;font-size:12px;cursor:pointer}.dialog-overlay[data-v-9bb5a6e7]{position:fixed;inset:0;background:#0006;display:flex;align-items:flex-end;z-index:200}.dialog[data-v-9bb5a6e7]{background:#fff;width:100%;border-radius:20px 20px 0 0;padding-bottom:calc(16px + env(safe-area-inset-bottom))}.dialog-handle[data-v-9bb5a6e7]{width:36px;height:4px;background:#ddd;border-radius:2px;margin:10px auto 0}.dialog-title[data-v-9bb5a6e7]{text-align:center;font-size:17px;font-weight:600;color:#1a1a1a;padding:18px 16px 14px}.dialog-body[data-v-9bb5a6e7]{padding:0 16px 16px}.form-item[data-v-9bb5a6e7]{margin-bottom:14px}.form-item label[data-v-9bb5a6e7]{display:block;font-size:13px;color:#8e8e93;margin-bottom:8px}.form-item input[data-v-9bb5a6e7]{width:100%;padding:12px 14px;background:#f7f7f7;border:none;border-radius:10px;font-size:15px;color:#1a1a1a;outline:none;box-sizing:border-box}.form-item input[data-v-9bb5a6e7]::placeholder{color:#b2b2b2}.dialog-footer[data-v-9bb5a6e7]{display:flex;gap:12px;padding:0 16px}.btn-cancel[data-v-9bb5a6e7],.btn-confirm[data-v-9bb5a6e7]{flex:1;padding:13px;border-radius:24px;font-size:16px;font-weight:500;cursor:pointer;border:none}.btn-cancel[data-v-9bb5a6e7]{background:#f0f0f0;color:#666}.btn-confirm[data-v-9bb5a6e7]{background:#ff6b00;color:#fff}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{_ as U,e as $,o,c as n,a as e,f as p,F as T,g as w,j as M,t as d,k as g,v as k,b as S,w as E,h as r,r as C,n as F,l as R}from"./index-DH2ZjwZR.js";import{v as y,a as j}from"./api-CXZzdW5u.js";const z={class:"page"},L={class:"page-header"},q={class:"header-row"},G={class:"list-wrap"},H={key:0,class:"loading"},J={key:1,class:"empty"},K={class:"vehicle-left"},O={class:"vehicle-plate"},P={class:"vehicle-type"},Q={class:"vehicle-detail"},W={key:0},X={key:1},Y={class:"vehicle-right"},Z=["onClick"],ee=["onClick"],le={class:"dialog"},te={class:"dialog-title"},ae={class:"dialog-body"},se={class:"form-item"},oe={class:"form-item"},ne={class:"form-item"},ie={class:"form-item"},de={__name:"Vehicles",setup(re){const u=r([]),b=r(!0),v=r(!1),i=r(null),s=r({plateNumber:"",vehicleType:"",color:"",batteryType:""}),c=r([]),h=localStorage.getItem("storeId")||"demo-store",_=async()=>{b.value=!0;try{const a=await y.list({storeId:h});u.value=a.data.data||a.data||[]}catch{u.value=[]}b.value=!1},m=async()=>{try{const a=await j.list({storeId:h});c.value=a.data.data||a.data||[]}catch{c.value=[]}},N=()=>{i.value=null,s.value={plateNumber:"",vehicleType:"",color:"",batteryType:""},c.value.length===0&&m(),v.value=!0},V=a=>{i.value=a._id,s.value={plateNumber:a.plateNumber||"",vehicleType:a.vehicleType||"",color:a.color||"",batteryType:a.batteryType||""},c.value.length===0&&m(),v.value=!0},D=async()=>{if(!s.value.plateNumber){alert("请填写车牌号");return}try{i.value?await y.update(i.value,s.value):await y.create({...s.value,storeId:h}),f(),_()}catch{alert(i.value?"更新失败":"添加失败")}},f=()=>{v.value=!1,i.value=null,s.value={plateNumber:"",vehicleType:"",color:"",batteryType:""}},x=async a=>{if(confirm("确定删除该车辆?"))try{await y.delete(a),_()}catch{alert("删除失败")}};return $(()=>{_(),m()}),(a,l)=>{const I=C("el-option"),A=C("el-select");return o(),n("div",z,[e("div",L,[e("div",q,[e("button",{class:"btn-back",onClick:l[0]||(l[0]=t=>a.$router.back())},[...l[5]||(l[5]=[e("span",{class:"back-arrow"},"‹",-1)])]),l[6]||(l[6]=e("div",{class:"page-title"},"车辆管理",-1)),e("button",{class:"btn-add-inline",onClick:N},"+ 新增")])]),e("div",G,[b.value?(o(),n("div",H,[...l[7]||(l[7]=[e("div",{class:"loading-ring"},null,-1)])])):u.value.length===0?(o(),n("div",J,[...l[8]||(l[8]=[e("div",{class:"empty-icon"},"🏍️",-1),e("div",{class:"empty-text"},"暂无车辆",-1),e("div",{class:"empty-sub"},"点击上方添加车辆",-1)])])):p("",!0),(o(!0),n(T,null,w(u.value,t=>(o(),n("div",{key:t._id,class:"vehicle-card"},[e("div",K,[e("div",O,d(t.plateNumber),1),e("div",P,d(t.vehicleType||"车型"),1),e("div",Q,[t.color?(o(),n("span",W,"颜色:"+d(t.color),1)):p("",!0),t.batteryType?(o(),n("span",X," | 电池:"+d(t.batteryType),1)):p("",!0)])]),e("div",Y,[e("div",{class:F(["vehicle-status",t.isRented?"status-renting":"status-idle"])},d(t.isRented?"在租":"空闲"),3),e("button",{class:"btn-edit",onClick:B=>V(t)},"编辑",8,Z),e("button",{class:"btn-del",onClick:B=>x(t._id)},"删除",8,ee)])]))),128))]),v.value?(o(),n("div",{key:0,class:"dialog-overlay",onClick:M(f,["self"])},[e("div",le,[l[13]||(l[13]=e("div",{class:"dialog-handle"},null,-1)),e("div",te,d(i.value?"编辑车辆":"添加车辆"),1),e("div",ae,[e("div",se,[l[9]||(l[9]=e("label",null,"车牌号",-1)),g(e("input",{"onUpdate:modelValue":l[1]||(l[1]=t=>s.value.plateNumber=t),placeholder:"如:京A12345"},null,512),[[k,s.value.plateNumber]])]),e("div",oe,[l[10]||(l[10]=e("label",null,"车型",-1)),S(A,{modelValue:s.value.vehicleType,"onUpdate:modelValue":l[2]||(l[2]=t=>s.value.vehicleType=t),placeholder:"请选择车型",style:{width:"100%"}},{default:E(()=>[(o(!0),n(T,null,w(c.value,t=>(o(),R(I,{key:t._id,label:t.name,value:t.name},null,8,["label","value"]))),128))]),_:1},8,["modelValue"])]),e("div",ne,[l[11]||(l[11]=e("label",null,"颜色",-1)),g(e("input",{"onUpdate:modelValue":l[3]||(l[3]=t=>s.value.color=t),placeholder:"如:黑色"},null,512),[[k,s.value.color]])]),e("div",ie,[l[12]||(l[12]=e("label",null,"电池类型",-1)),g(e("input",{"onUpdate:modelValue":l[4]||(l[4]=t=>s.value.batteryType=t),placeholder:"如:锂电池"},null,512),[[k,s.value.batteryType]])])]),e("div",{class:"dialog-footer"},[e("button",{class:"btn-cancel",onClick:f},"取消"),e("button",{class:"btn-confirm",onClick:D},"确定")])])])):p("",!0)])}}},ve=U(de,[["__scopeId","data-v-9bb5a6e7"]]);export{ve as default};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!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>
|
||||||
|
<script type="module" crossorigin src="/assets/index-DH2ZjwZR.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-DYAfUsQo.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!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>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "e-scooter-store-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"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": "^5.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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,22 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: () => import('../views/TabLayout.vue'),
|
||||||
|
children: [
|
||||||
|
{ path: '/', name: 'Home', component: () => import('../views/Home.vue') },
|
||||||
|
{ path: '/vehicle-types', name: 'VehicleTypes', component: () => import('../views/VehicleTypes.vue') },
|
||||||
|
{ path: '/vehicles', name: 'Vehicles', component: () => import('../views/Vehicles.vue') },
|
||||||
|
{ path: '/orders', name: 'Orders', component: () => import('../views/Orders.vue') },
|
||||||
|
{ path: '/mine', name: 'Mine', component: () => import('../views/Mine.vue') }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/* ============================================
|
||||||
|
橙色主题 - 全局样式覆盖
|
||||||
|
Orange Theme - Global Overrides
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用:绿色状态改为橙色 */
|
||||||
|
.status-renting,
|
||||||
|
.status-active,
|
||||||
|
.status-success {
|
||||||
|
background: #FFF0E0 !important;
|
||||||
|
color: #FF6B00 !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 */
|
||||||
|
.order-status.status-renting,
|
||||||
|
.order-status.status-ok {
|
||||||
|
background: #FFF0E0 !important;
|
||||||
|
color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* loading spinner */
|
||||||
|
.loading-ring {
|
||||||
|
border-top-color: #FF6B00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局 a 链接橙色 */
|
||||||
|
a.link-orange {
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 门店相关
|
||||||
|
export const storesApi = {
|
||||||
|
getStore: (id) => api.get(`/stores/${id}`),
|
||||||
|
updateStore: (id, data) => api.put(`/stores/${id}`, data),
|
||||||
|
getMyStore: () => api.get('/stores/me')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 车型相关
|
||||||
|
export const vehicleTypesApi = {
|
||||||
|
list: (params) => api.get('/vehicle-types', { params }),
|
||||||
|
create: (data) => api.post('/vehicle-types', data),
|
||||||
|
update: (id, data) => api.put(`/vehicle-types/${id}`, data),
|
||||||
|
delete: (id) => api.delete(`/vehicle-types/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 车辆相关
|
||||||
|
export const vehiclesApi = {
|
||||||
|
list: (params) => api.get('/vehicles', { params }),
|
||||||
|
create: (data) => api.post('/vehicles', data),
|
||||||
|
update: (id, data) => api.put(`/vehicles/${id}`, data),
|
||||||
|
delete: (id) => api.delete(`/vehicles/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单相关
|
||||||
|
export const ordersApi = {
|
||||||
|
list: (params) => api.get('/orders', { params }),
|
||||||
|
detail: (id) => api.get(`/orders/${id}`),
|
||||||
|
create: (data) => api.post('/orders', data),
|
||||||
|
update: (id, data) => api.put(`/orders/${id}`, data),
|
||||||
|
delete: (id) => api.delete(`/orders/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api
|
||||||
|
|
@ -0,0 +1,335 @@
|
||||||
|
<template>
|
||||||
|
<div class="home-page">
|
||||||
|
<!-- 顶部 -->
|
||||||
|
<div class="home-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="store-avatar">🚲</div>
|
||||||
|
<div>
|
||||||
|
<div class="store-name">{{ storeInfo.name || '加载中...' }}</div>
|
||||||
|
<div class="store-sub">门店端</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-badge">营业中</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快捷入口 -->
|
||||||
|
<div class="quick-nav">
|
||||||
|
<router-link to="/vehicle-types" class="nav-card">
|
||||||
|
<div class="nav-icon-box" style="background:#E8F8EE;">
|
||||||
|
<span style="font-size:20px;">🚗</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-label">车型管理</div>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/vehicles" class="nav-card">
|
||||||
|
<div class="nav-icon-box" style="background:#FFF4E0;">
|
||||||
|
<span style="font-size:20px;">🏍️</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-label">车辆管理</div>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/orders" class="nav-card">
|
||||||
|
<div class="nav-icon-box" style="background:#E8F0FF;">
|
||||||
|
<span style="font-size:20px;">📋</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-label">订单管理</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">今日概览</div>
|
||||||
|
<div class="section-more">实时数据</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-num" style="color:#FF6B00;">{{ stats.totalOrders }}</div>
|
||||||
|
<div class="stat-label">总订单</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-num" style="color:#576BFF;">{{ stats.rentingOrders }}</div>
|
||||||
|
<div class="stat-label">在租订单</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-num" style="color:#FF6B35;">{{ stats.warningOrders }}</div>
|
||||||
|
<div class="stat-label">预警订单</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-num" style="color:#333;">{{ stats.totalVehicles }}</div>
|
||||||
|
<div class="stat-label">车辆总数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近订单 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">最近订单</div>
|
||||||
|
<router-link to="/orders" class="section-more" style="color:#FF6B00;">查看全部 ›</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="order-list">
|
||||||
|
<div v-if="loadingOrders" class="loading-tip">加载中...</div>
|
||||||
|
<div v-else-if="recentOrders.length === 0" class="empty-tip">暂无订单</div>
|
||||||
|
<div v-for="order in recentOrders" :key="order._id" class="order-item">
|
||||||
|
<div class="order-left">
|
||||||
|
<div class="order-customer">{{ order.customerName || '客户' }}</div>
|
||||||
|
<div class="order-vehicle">{{ order.vehicleType || '车型' }} · {{ formatTime(order.createdAt) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-status" :class="getStatusClass(order.status)">
|
||||||
|
{{ getStatusText(order.status) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ordersApi, vehiclesApi } from '../utils/api.js'
|
||||||
|
|
||||||
|
const storeInfo = ref({})
|
||||||
|
const recentOrders = ref([])
|
||||||
|
const loadingOrders = ref(true)
|
||||||
|
const stats = ref({ totalOrders: 0, rentingOrders: 0, warningOrders: 0, totalVehicles: 0 })
|
||||||
|
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
const map = { renting: 'status-renting', returned: 'status-ok', overdue: 'status-warning' }
|
||||||
|
return map[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const map = { renting: '在租', returned: '已还', overdue: '逾期', pending: '待处理' }
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (date) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
const d = new Date(date)
|
||||||
|
return `${d.getMonth()+1}/${d.getDate()} ${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const storeId = localStorage.getItem('storeId') || 'demo-store'
|
||||||
|
storeInfo.value = { name: '示例门店' }
|
||||||
|
if (storeId !== 'demo-store') {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/stores/${storeId}`)
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.name) storeInfo.value = data
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await ordersApi.list({ storeId, limit: 5 })
|
||||||
|
recentOrders.value = res.data.data || res.data || []
|
||||||
|
const allOrders = res.data.data || res.data || []
|
||||||
|
stats.value.totalOrders = allOrders.length
|
||||||
|
stats.value.rentingOrders = allOrders.filter(o => o.status === 'renting').length
|
||||||
|
stats.value.warningOrders = allOrders.filter(o => o.status === 'overdue').length
|
||||||
|
} catch (e) {
|
||||||
|
recentOrders.value = []
|
||||||
|
}
|
||||||
|
loadingOrders.value = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await vehiclesApi.list({ storeId, limit: 100 })
|
||||||
|
const vehicles = res.data.data || res.data || []
|
||||||
|
stats.value.totalVehicles = vehicles.length
|
||||||
|
} catch (e) {}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px 16px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-avatar {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
background: #FFF4E0;
|
||||||
|
border-radius: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-name {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-badge {
|
||||||
|
background: #E8F8EE;
|
||||||
|
color: #FF6B00;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 16px 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-card {
|
||||||
|
flex: 1;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 14px 8px 12px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon-box {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-more {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #F7F7F7;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 4px 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-tip, .empty-tip {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #B2B2B2;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 12px;
|
||||||
|
border-top: 0.5px solid #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-customer {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-vehicle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-renting {
|
||||||
|
background: #E8F8EE;
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-ok {
|
||||||
|
background: #F0F0F0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warning {
|
||||||
|
background: #FFF0E8;
|
||||||
|
color: #FF6B35;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,287 @@
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<!-- 头部信息卡 -->
|
||||||
|
<div class="mine-header">
|
||||||
|
<div class="header-info">
|
||||||
|
<div class="avatar-box">🏪</div>
|
||||||
|
<div class="info-text">
|
||||||
|
<div class="store-name">{{ storeInfo.name || '加载中...' }}</div>
|
||||||
|
<div class="store-id">ID: {{ storeId }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 门店信息卡片 -->
|
||||||
|
<div class="card-section">
|
||||||
|
<div class="card-row" @click="handleEdit">
|
||||||
|
<div class="card-left">
|
||||||
|
<span class="card-icon">🏢</span>
|
||||||
|
<span class="card-text">门店信息</span>
|
||||||
|
</div>
|
||||||
|
<span class="card-arrow">›</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-divider"></div>
|
||||||
|
<div class="card-row" @click="handleLogout">
|
||||||
|
<div class="card-left">
|
||||||
|
<span class="card-icon">🚪</span>
|
||||||
|
<span class="card-text" style="color:#FF4D4F;">退出登录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑弹窗 -->
|
||||||
|
<div v-if="showEditDialog" class="dialog-overlay" @click.self="showEditDialog = false">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-handle"></div>
|
||||||
|
<div class="dialog-title">编辑门店信息</div>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<div class="form-item">
|
||||||
|
<label>门店名称</label>
|
||||||
|
<input v-model="editForm.name" placeholder="请输入门店名称" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>联系人</label>
|
||||||
|
<input v-model="editForm.contact" placeholder="请输入联系人" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>联系电话</label>
|
||||||
|
<input v-model="editForm.phone" placeholder="请输入联系电话" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>地址</label>
|
||||||
|
<input v-model="editForm.address" placeholder="请输入地址" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button class="btn-cancel" @click="showEditDialog = false">取消</button>
|
||||||
|
<button class="btn-confirm" @click="handleSave">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { storesApi } from '../utils/api.js'
|
||||||
|
|
||||||
|
const storeId = localStorage.getItem('storeId') || 'demo-store'
|
||||||
|
const storeInfo = ref({})
|
||||||
|
const showEditDialog = ref(false)
|
||||||
|
const editForm = ref({})
|
||||||
|
|
||||||
|
const loadStore = async () => {
|
||||||
|
try {
|
||||||
|
const res = await storesApi.getStore(storeId)
|
||||||
|
storeInfo.value = res.data || {}
|
||||||
|
} catch (e) {
|
||||||
|
storeInfo.value = { name: '示例门店' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
editForm.value = { ...storeInfo.value }
|
||||||
|
showEditDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
await storesApi.updateStore(storeId, editForm.value)
|
||||||
|
storeInfo.value = { ...editForm.value }
|
||||||
|
showEditDialog.value = false
|
||||||
|
} catch (e) {
|
||||||
|
alert('保存失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
if (confirm('确定退出登录?')) {
|
||||||
|
localStorage.removeItem('storeId')
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadStore)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: #fff;
|
||||||
|
padding: 28px 16px 24px;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-box {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: #FFF4E0;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-section {
|
||||||
|
margin: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #D1D1D6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-divider {
|
||||||
|
height: 0.5px;
|
||||||
|
background: #F0F0F0;
|
||||||
|
margin: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部弹窗 */
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background: #fff;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-handle {
|
||||||
|
width: 36px;
|
||||||
|
height: 4px;
|
||||||
|
background: #DDD;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 10px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
padding: 18px 16px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8E8E93;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input::placeholder {
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel, .btn-confirm {
|
||||||
|
flex: 1;
|
||||||
|
padding: 13px;
|
||||||
|
border-radius: 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #F0F0F0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
background: #FF6B00;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,872 @@
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-row">
|
||||||
|
<div class="page-title">订单管理</div>
|
||||||
|
<button class="btn-add-inline" @click="openDialog">+ 创建订单</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-wrap">
|
||||||
|
<div v-if="loading" class="loading">
|
||||||
|
<div class="loading-ring"></div>
|
||||||
|
<div>加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计栏 -->
|
||||||
|
<div v-else-if="filteredList.length > 0" class="stats-bar">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num">{{ list.length }}</span>
|
||||||
|
<span class="stat-label">全部</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num renting">{{ statusCount.renting }}</span>
|
||||||
|
<span class="stat-label">进行中</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num completed">{{ statusCount.completed }}</span>
|
||||||
|
<span class="stat-label">已完成</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num cancelled">{{ statusCount.cancelled }}</span>
|
||||||
|
<span class="stat-label">已取消</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="filteredList.length === 0 && !loading" class="empty">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<div class="empty-text">暂无订单</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="item in filteredList" :key="item._id" class="order-card">
|
||||||
|
<!-- 卡片头部:订单号 + 状态 -->
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="order-id-row">
|
||||||
|
<span class="order-icon">📋</span>
|
||||||
|
<span class="order-number">{{ item.orderNumber || item._id.slice(-8).toUpperCase() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-badge" :class="getStatusBadgeClass(item.status)">
|
||||||
|
<span class="badge-dot" :class="getDotClass(item.status)"></span>
|
||||||
|
{{ item.status }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片信息区 -->
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">客户姓名</span>
|
||||||
|
<span class="info-value">{{ item.customer?.name || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">联系电话</span>
|
||||||
|
<span class="info-value">{{ item.customer?.phone || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">车牌号</span>
|
||||||
|
<span class="info-value">{{ item.vehicle?.plateNumber || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">车型</span>
|
||||||
|
<span class="info-value">{{ item.vehicle?.vehicleType || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">租金</span>
|
||||||
|
<span class="info-value price">¥{{ item.rentalFee || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">押金</span>
|
||||||
|
<span class="info-value">¥{{ item.deposit || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-group full">
|
||||||
|
<span class="info-label">租赁日期</span>
|
||||||
|
<span class="info-value">{{ formatDateRange(item.startDate, item.endDate) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.actualEndDate" class="info-row">
|
||||||
|
<div class="info-group full">
|
||||||
|
<span class="info-label">实际归还</span>
|
||||||
|
<span class="info-value">{{ formatDate(item.actualEndDate) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片底部操作栏 -->
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="footer-hint">{{ formatDate(item.createdAt) }}</div>
|
||||||
|
<div class="action-btns">
|
||||||
|
<!-- 完成订单按钮(进行中状态显示) -->
|
||||||
|
<button
|
||||||
|
v-if="item.status === '进行中' || item.status === '待支付'"
|
||||||
|
class="btn-action btn-complete"
|
||||||
|
@click="handleComplete(item._id)"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
|
</svg>
|
||||||
|
完成
|
||||||
|
</button>
|
||||||
|
<!-- 取消订单按钮(待支付/进行中状态显示) -->
|
||||||
|
<button
|
||||||
|
v-if="item.status === '待支付' || item.status === '进行中'"
|
||||||
|
class="btn-action btn-cancel-order"
|
||||||
|
@click="handleCancel(item._id)"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button class="btn-action btn-edit" @click="handleEdit(item)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
|
</svg>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button class="btn-action btn-del" @click="handleDelete(item._id)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
||||||
|
</svg>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 创建/编辑订单弹窗 -->
|
||||||
|
<div v-if="showDialog" class="dialog-overlay" @click.self="closeDialog">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-handle"></div>
|
||||||
|
<div class="dialog-title">{{ editingId ? '编辑订单' : '创建订单' }}</div>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<div class="form-item">
|
||||||
|
<label>客户姓名 <span class="required">*</span></label>
|
||||||
|
<input v-model="form.customerName" placeholder="请输入客户姓名" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>客户电话</label>
|
||||||
|
<input v-model="form.customerPhone" placeholder="请输入手机号" type="tel" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>品牌车型 <span class="required">*</span></label>
|
||||||
|
<select v-model="form.vehicleType" @change="onVehicleTypeChange">
|
||||||
|
<option value="" disabled>请选择品牌车型</option>
|
||||||
|
<option v-for="vt in vehicleTypeList" :key="vt._id" :value="vt.name">{{ vt.brand }} - {{ vt.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>车辆选择 <span class="required">*</span></label>
|
||||||
|
<select v-model="form.vehicleId" :disabled="!form.vehicleType">
|
||||||
|
<option value="" disabled>请选择车辆</option>
|
||||||
|
<option v-for="v in filteredVehicleList" :key="v._id" :value="v._id">
|
||||||
|
{{ v.plateNumber || '未上牌' }} ({{ v.frameNumber || v._id.slice(-6) }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>租金(元) <span class="required">*</span></label>
|
||||||
|
<input v-model="form.rentalFee" type="number" placeholder="如:1500" min="0" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>押金(元)</label>
|
||||||
|
<input v-model="form.deposit" type="number" placeholder="如:500" min="0" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>租赁时长(月)</label>
|
||||||
|
<input v-model="form.contractMonths" type="number" placeholder="如:12" min="1" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>备注</label>
|
||||||
|
<input v-model="form.notes" placeholder="可选备注" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button class="btn-cancel" @click="closeDialog">取消</button>
|
||||||
|
<button class="btn-confirm" @click="handleSave">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { ordersApi, vehicleTypesApi, vehiclesApi } from '../utils/api.js'
|
||||||
|
|
||||||
|
const list = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const filter = ref('all')
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const editingId = ref(null)
|
||||||
|
const vehicleTypeList = ref([])
|
||||||
|
const vehicleList = ref([])
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
customerName: '',
|
||||||
|
customerPhone: '',
|
||||||
|
vehicleType: '',
|
||||||
|
vehicleId: '',
|
||||||
|
rentalFee: '',
|
||||||
|
deposit: '',
|
||||||
|
contractMonths: '',
|
||||||
|
notes: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
{ label: '进行中', value: 'renting' },
|
||||||
|
{ label: '已完成', value: 'completed' },
|
||||||
|
{ label: '已取消', value: 'cancelled' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const storeId = localStorage.getItem('storeId') || 'demo-store'
|
||||||
|
|
||||||
|
// 状态计数
|
||||||
|
const statusCount = computed(() => {
|
||||||
|
const counts = { renting: 0, completed: 0, cancelled: 0 }
|
||||||
|
list.value.forEach(o => {
|
||||||
|
if (o.status === '进行中' || o.status === '待支付' || o.status === '逾期') counts.renting++
|
||||||
|
else if (o.status === '已完成') counts.completed++
|
||||||
|
else if (o.status === '已取消') counts.cancelled++
|
||||||
|
})
|
||||||
|
return counts
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredList = computed(() => {
|
||||||
|
if (filter.value === 'all') return list.value
|
||||||
|
if (filter.value === 'renting') return list.value.filter(o => ['进行中', '待支付', '逾期'].includes(o.status))
|
||||||
|
if (filter.value === 'completed') return list.value.filter(o => o.status === '已完成')
|
||||||
|
if (filter.value === 'cancelled') return list.value.filter(o => o.status === '已取消')
|
||||||
|
return list.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredVehicleList = computed(() => {
|
||||||
|
if (!form.value.vehicleType) return []
|
||||||
|
return vehicleList.value.filter(v => v.vehicleType === form.value.vehicleType)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onVehicleTypeChange = () => {
|
||||||
|
form.value.vehicleId = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态标签样式
|
||||||
|
const getStatusBadgeClass = (status) => {
|
||||||
|
if (status === '待支付') return 'badge-pending'
|
||||||
|
if (status === '进行中' || status === '逾期') return 'badge-renting'
|
||||||
|
if (status === '已完成') return 'badge-completed'
|
||||||
|
if (status === '已取消' || status === '已退款') return 'badge-cancelled'
|
||||||
|
return 'badge-default'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDotClass = (status) => {
|
||||||
|
if (status === '待支付') return 'dot-pending'
|
||||||
|
if (status === '进行中' || status === '逾期') return 'dot-renting'
|
||||||
|
if (status === '已完成') return 'dot-completed'
|
||||||
|
if (status === '已取消' || status === '已退款') return 'dot-cancelled'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
return new Date(date).toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDateRange = (start, end) => {
|
||||||
|
if (!start) return '-'
|
||||||
|
const s = formatDate(start)
|
||||||
|
if (!end) return s + ' 起'
|
||||||
|
return `${s} ~ ${formatDate(end)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await ordersApi.list({ storeId })
|
||||||
|
list.value = res.data.data || res.data || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载订单失败', e)
|
||||||
|
list.value = []
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadVehicleTypesAndVehicles = async () => {
|
||||||
|
try {
|
||||||
|
const [vtRes, vRes] = await Promise.all([
|
||||||
|
vehicleTypesApi.list({ storeId }),
|
||||||
|
vehiclesApi.list({ storeId })
|
||||||
|
])
|
||||||
|
vehicleTypeList.value = vtRes.data.data || vtRes.data || []
|
||||||
|
vehicleList.value = vRes.data.data || vRes.data || []
|
||||||
|
} catch (e) {
|
||||||
|
vehicleTypeList.value = []
|
||||||
|
vehicleList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDialog = () => {
|
||||||
|
editingId.value = null
|
||||||
|
form.value = { customerName: '', customerPhone: '', vehicleType: '', vehicleId: '', rentalFee: '', deposit: '', contractMonths: '', notes: '' }
|
||||||
|
loadVehicleTypesAndVehicles()
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (item) => {
|
||||||
|
editingId.value = item._id
|
||||||
|
form.value = {
|
||||||
|
customerName: item.customer?.name || '',
|
||||||
|
customerPhone: item.customer?.phone || '',
|
||||||
|
vehicleType: item.vehicle?.vehicleType || '',
|
||||||
|
vehicleId: item.vehicle?._id || '',
|
||||||
|
rentalFee: item.rentalFee || '',
|
||||||
|
deposit: item.deposit || '',
|
||||||
|
contractMonths: '',
|
||||||
|
notes: item.notes || ''
|
||||||
|
}
|
||||||
|
loadVehicleTypesAndVehicles()
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showDialog.value = false
|
||||||
|
editingId.value = null
|
||||||
|
form.value = { customerName: '', customerPhone: '', vehicleType: '', vehicleId: '', rentalFee: '', deposit: '', contractMonths: '', notes: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!form.value.customerName) {
|
||||||
|
alert('请填写客户姓名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.value.vehicleType) {
|
||||||
|
alert('请选择品牌车型')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.value.vehicleId) {
|
||||||
|
alert('请选择车辆')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.value.rentalFee) {
|
||||||
|
alert('请填写租金')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
storeId,
|
||||||
|
customerName: form.value.customerName,
|
||||||
|
customerPhone: form.value.customerPhone || undefined,
|
||||||
|
vehicleId: form.value.vehicleId,
|
||||||
|
vehicleType: form.value.vehicleType,
|
||||||
|
rentalFee: Number(form.value.rentalFee),
|
||||||
|
deposit: Number(form.value.deposit) || 0,
|
||||||
|
contractMonths: Number(form.value.contractMonths) || 0,
|
||||||
|
notes: form.value.notes || undefined,
|
||||||
|
status: editingId.value ? undefined : '草稿'
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingId.value) {
|
||||||
|
await ordersApi.update(editingId.value, payload)
|
||||||
|
} else {
|
||||||
|
await ordersApi.create(payload)
|
||||||
|
}
|
||||||
|
closeDialog()
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e?.response?.data?.message || e?.message || '操作失败'
|
||||||
|
alert(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleComplete = async (id) => {
|
||||||
|
if (!confirm('确定完成此订单?')) return
|
||||||
|
try {
|
||||||
|
await ordersApi.update(id, { status: '已完成' })
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
alert('操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = async (id) => {
|
||||||
|
if (!confirm('确定取消此订单?')) return
|
||||||
|
try {
|
||||||
|
await ordersApi.update(id, { status: '已取消' })
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
alert('操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (!confirm('确定删除该订单?')) return
|
||||||
|
try {
|
||||||
|
await ordersApi.delete(id)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
// 后端若无 delete 路由则提示
|
||||||
|
alert('删除失败:后端暂无此接口')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadData)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: #fff;
|
||||||
|
padding: 14px 16px 12px;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-inline {
|
||||||
|
background: #FF6B00;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表容器 */
|
||||||
|
.list-wrap {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计栏 */
|
||||||
|
.stats-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num.renting { color: #FF6B00; }
|
||||||
|
.stat-num.completed { color: #52C41A; }
|
||||||
|
.stat-num.cancelled { color: #8E8E93; }
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8E8E93;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 32px;
|
||||||
|
background: #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载 */
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 60px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-ring {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid #E5E5E5;
|
||||||
|
border-top-color: #FF6B00;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单卡片 */
|
||||||
|
.order-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片头部 */
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px 12px;
|
||||||
|
border-bottom: 1px solid #F5F5F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-id-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-number {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1A1A1A;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态标签 */
|
||||||
|
.status-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pending {
|
||||||
|
background: #FFF7E6;
|
||||||
|
color: #FA8C16;
|
||||||
|
}
|
||||||
|
.badge-pending .badge-dot { background: #FA8C16; }
|
||||||
|
|
||||||
|
.badge-renting {
|
||||||
|
background: #FFF7F0;
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
.badge-renting .badge-dot {
|
||||||
|
background: #FF6B00;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-completed {
|
||||||
|
background: #F0FFF4;
|
||||||
|
color: #52C41A;
|
||||||
|
}
|
||||||
|
.badge-completed .badge-dot { background: #52C41A; }
|
||||||
|
|
||||||
|
.badge-cancelled {
|
||||||
|
background: #F5F5F5;
|
||||||
|
color: #8E8E93;
|
||||||
|
}
|
||||||
|
.badge-cancelled .badge-dot { background: #8E8E93; }
|
||||||
|
|
||||||
|
.badge-default {
|
||||||
|
background: #F5F5F5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片信息区 */
|
||||||
|
.card-body {
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group.full {
|
||||||
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.price {
|
||||||
|
color: #FF6B35;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片底部 */
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #FAFAFA;
|
||||||
|
border-top: 1px solid #F5F5F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-complete {
|
||||||
|
background: #F0FFF4;
|
||||||
|
color: #52C41A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel-order {
|
||||||
|
background: #FFF7E6;
|
||||||
|
color: #FA8C16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
background: #FFF7F0;
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-del {
|
||||||
|
background: #FFF0F0;
|
||||||
|
color: #FF4D4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗 */
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background: #fff;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-handle {
|
||||||
|
width: 36px;
|
||||||
|
height: 4px;
|
||||||
|
background: #DDD;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 10px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
padding: 18px 16px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8E8E93;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #FF4D4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input::placeholder {
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238E8E93' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 14px center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item select:disabled {
|
||||||
|
color: #B2B2B2;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel, .btn-confirm {
|
||||||
|
flex: 1;
|
||||||
|
padding: 13px;
|
||||||
|
border-radius: 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #F0F0F0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
background: #FF6B00;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
<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="/orders" class="tab-item" :class="{ active: route.path === '/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="/mine" class="tab-item" :class="{ active: route.path === '/mine' }">
|
||||||
|
<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,515 @@
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-row">
|
||||||
|
<button class="btn-back" @click="$router.back()">
|
||||||
|
<span class="back-arrow">‹</span>
|
||||||
|
</button>
|
||||||
|
<div class="page-title">车型管理</div>
|
||||||
|
<button class="btn-add-inline" @click="handleAdd">+ 新增</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-wrap">
|
||||||
|
<div v-if="loading" class="loading">
|
||||||
|
<div class="loading-ring"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="list.length === 0" class="empty">
|
||||||
|
<div class="empty-icon">🚗</div>
|
||||||
|
<div class="empty-text">暂无车型</div>
|
||||||
|
<div class="empty-sub">点击下方添加车型</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="item in list" :key="item._id" class="type-card">
|
||||||
|
<div class="type-img">
|
||||||
|
<img v-if="item.cover" :src="item.cover" alt="cover" />
|
||||||
|
<div v-else class="type-img-placeholder">🚗</div>
|
||||||
|
</div>
|
||||||
|
<div class="type-info">
|
||||||
|
<div class="type-name">{{ item.name }}</div>
|
||||||
|
<div class="type-brand">{{ item.brand }}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-edit" @click="handleEdit(item)">编辑</button>
|
||||||
|
<button class="btn-del" @click="handleDelete(item._id)">删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加/编辑弹窗 -->
|
||||||
|
<div v-if="showDialog" class="dialog-overlay" @click.self="closeDialog">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-handle"></div>
|
||||||
|
<div class="dialog-title">{{ editingId ? '编辑车型' : '添加车型' }}</div>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<div class="form-item">
|
||||||
|
<label>品牌</label>
|
||||||
|
<input v-model="form.brand" placeholder="如:雅迪" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>车型名称</label>
|
||||||
|
<input v-model="form.name" placeholder="如:TDT1234" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>封面图</label>
|
||||||
|
<div class="cover-upload-wrap">
|
||||||
|
<el-upload
|
||||||
|
class="cover-uploader"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="handleCoverChange"
|
||||||
|
accept="image/*"
|
||||||
|
>
|
||||||
|
<div v-if="form.cover" class="cover-preview">
|
||||||
|
<img :src="form.cover" alt="封面" />
|
||||||
|
<div class="cover-mask">
|
||||||
|
<span>点击更换</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="cover-placeholder">
|
||||||
|
<el-icon class="cover-icon"><Plus /></el-icon>
|
||||||
|
<span>上传封面</span>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
<div v-if="form.cover" class="cover-remove" @click="form.cover = ''">删除图片</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button class="btn-cancel" @click="closeDialog">取消</button>
|
||||||
|
<button class="btn-confirm" @click="handleSave">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
|
import { vehicleTypesApi } from '../utils/api.js'
|
||||||
|
|
||||||
|
const list = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const editingId = ref(null)
|
||||||
|
const form = ref({ brand: '', name: '', cover: '' })
|
||||||
|
|
||||||
|
// 将选中的本地图片转为 base64 存入 form.cover
|
||||||
|
const handleCoverChange = (file) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
form.value.cover = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file.raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeId = localStorage.getItem('storeId') || 'demo-store'
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await vehicleTypesApi.list({ storeId })
|
||||||
|
list.value = res.data.data || res.data || []
|
||||||
|
} catch (e) {
|
||||||
|
list.value = []
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
editingId.value = null
|
||||||
|
form.value = { brand: '', name: '', cover: '' }
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (item) => {
|
||||||
|
editingId.value = item._id
|
||||||
|
form.value = { brand: item.brand, name: item.name, cover: item.cover || '' }
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!form.value.brand || !form.value.name) {
|
||||||
|
alert('请填写品牌和名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (editingId.value) {
|
||||||
|
await vehicleTypesApi.update(editingId.value, form.value)
|
||||||
|
} else {
|
||||||
|
await vehicleTypesApi.create({ ...form.value, storeId })
|
||||||
|
}
|
||||||
|
closeDialog()
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
alert(editingId.value ? '更新失败' : '添加失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showDialog.value = false
|
||||||
|
editingId.value = null
|
||||||
|
form.value = { brand: '', name: '', cover: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (!confirm('确定删除该车型?')) return
|
||||||
|
try {
|
||||||
|
await vehicleTypesApi.delete(id)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
alert('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadData)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: #fff;
|
||||||
|
padding: 14px 16px 12px;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-arrow {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-inline {
|
||||||
|
background: #FF6B00;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-wrap {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-ring {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid #E5E5E5;
|
||||||
|
border-top-color: #FF6B00;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-sub {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #F7F7F7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-img img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-img-placeholder {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-brand {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8E8E93;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
background: #FFF7F0;
|
||||||
|
color: #FF6B00;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-del {
|
||||||
|
background: #FFF0F0;
|
||||||
|
color: #FF4D4F;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background: #fff;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-handle {
|
||||||
|
width: 36px;
|
||||||
|
height: 4px;
|
||||||
|
background: #DDD;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 10px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
padding: 18px 16px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8E8E93;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input::placeholder {
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel, .btn-confirm {
|
||||||
|
flex: 1;
|
||||||
|
padding: 13px;
|
||||||
|
border-radius: 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #F0F0F0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
background: #FF6B00;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 封面图上传 */
|
||||||
|
.cover-upload-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-uploader {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-placeholder {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1.5px dashed #DDD;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #F7F7F7;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-placeholder:hover {
|
||||||
|
border-color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-placeholder span {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-preview {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-mask {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-preview:hover .cover-mask {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-mask span {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-remove {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #FF4D4F;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,685 @@
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-row">
|
||||||
|
<button class="btn-back" @click="$router.back()">
|
||||||
|
<span class="back-arrow">‹</span>
|
||||||
|
</button>
|
||||||
|
<div class="page-title">车辆管理</div>
|
||||||
|
<button class="btn-add-inline" @click="handleAdd">+ 新增</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-wrap">
|
||||||
|
<div v-if="loading" class="loading">
|
||||||
|
<div class="loading-ring"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="list.length === 0" class="empty">
|
||||||
|
<div class="empty-icon">🏍️</div>
|
||||||
|
<div class="empty-text">暂无车辆</div>
|
||||||
|
<div class="empty-sub">点击上方添加车辆</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="stats-bar">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num">{{ list.length }}</span>
|
||||||
|
<span class="stat-label">全部</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num idle">{{ idleCount }}</span>
|
||||||
|
<span class="stat-label">空闲</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num rented">{{ rentedCount }}</span>
|
||||||
|
<span class="stat-label">在租</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="item in list" :key="item._id" class="vehicle-card">
|
||||||
|
<!-- 卡片头部:车牌 + 状态 -->
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="plate-row">
|
||||||
|
<div class="plate-icon">🛵</div>
|
||||||
|
<div class="plate-number">{{ item.plateNumber || '未上牌' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-badge" :class="item.isRented ? 'badge-rented' : 'badge-idle'">
|
||||||
|
<span class="badge-dot"></span>
|
||||||
|
{{ item.isRented ? '在租' : '空闲' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 信息区域 -->
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">车架号</span>
|
||||||
|
<span class="info-value">{{ item.frameNumber || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">品牌</span>
|
||||||
|
<span class="info-value">{{ item.brand || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">车型</span>
|
||||||
|
<span class="info-value">{{ item.vehicleType || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">颜色</span>
|
||||||
|
<span class="info-value">{{ item.color || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.batteryType" class="info-row">
|
||||||
|
<div class="info-group full">
|
||||||
|
<span class="info-label">电池</span>
|
||||||
|
<span class="info-value">{{ item.batteryType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片底部操作栏 -->
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="footer-hint">最后更新:{{ formatTime(item.updatedAt) }}</div>
|
||||||
|
<div class="action-btns">
|
||||||
|
<button class="btn-action btn-edit" @click="handleEdit(item)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||||
|
</svg>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button class="btn-action btn-del" @click="handleDelete(item._id)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
||||||
|
</svg>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加/编辑弹窗 -->
|
||||||
|
<div v-if="showDialog" class="dialog-overlay" @click.self="closeDialog">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-handle"></div>
|
||||||
|
<div class="dialog-title">{{ editingId ? '编辑车辆' : '添加车辆' }}</div>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<div class="form-item">
|
||||||
|
<label>车架号 <span class="required">*</span></label>
|
||||||
|
<input v-model="form.frameNumber" placeholder="请输入车架号" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>车牌号</label>
|
||||||
|
<input v-model="form.plateNumber" placeholder="如:京A12345" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>品牌</label>
|
||||||
|
<el-select v-model="form.brand" placeholder="请选择品牌" style="width:100%" @change="onBrandChange">
|
||||||
|
<el-option v-for="b in brands" :key="b" :label="b" :value="b" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>车型</label>
|
||||||
|
<el-select v-model="form.vehicleType" placeholder="请先选择品牌" style="width:100%" :disabled="!form.brand">
|
||||||
|
<el-option v-for="vt in filteredVehicleTypes" :key="vt._id" :label="vt.name" :value="vt.name" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>颜色</label>
|
||||||
|
<input v-model="form.color" placeholder="如:黑色、白色" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>电池类型</label>
|
||||||
|
<input v-model="form.batteryType" placeholder="如:铅酸电池、锂电池" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button class="btn-cancel" @click="closeDialog">取消</button>
|
||||||
|
<button class="btn-confirm" @click="handleSave">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { vehiclesApi, vehicleTypesApi } from '../utils/api.js'
|
||||||
|
|
||||||
|
const list = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const editingId = ref(null)
|
||||||
|
const form = ref({ frameNumber: '', plateNumber: '', brand: '', vehicleType: '', color: '', batteryType: '' })
|
||||||
|
const vehicleTypes = ref([])
|
||||||
|
|
||||||
|
const idleCount = computed(() => list.value.filter(v => !v.isRented).length)
|
||||||
|
const rentedCount = computed(() => list.value.filter(v => v.isRented).length)
|
||||||
|
|
||||||
|
const brands = computed(() => {
|
||||||
|
const set = new Set(vehicleTypes.value.map(vt => vt.brand).filter(Boolean))
|
||||||
|
return [...set]
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredVehicleTypes = computed(() => {
|
||||||
|
if (!form.value.brand) return []
|
||||||
|
return vehicleTypes.value.filter(vt => vt.brand === form.value.brand)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onBrandChange = () => {
|
||||||
|
form.value.vehicleType = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (ts) => {
|
||||||
|
if (!ts) return '-'
|
||||||
|
const d = new Date(ts)
|
||||||
|
return `${d.getMonth()+1}/${d.getDate()} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeId = localStorage.getItem('storeId') || 'demo-store'
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await vehiclesApi.list({ storeId })
|
||||||
|
list.value = res.data.data || res.data || []
|
||||||
|
} catch (e) {
|
||||||
|
list.value = []
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadVehicleTypes = async () => {
|
||||||
|
try {
|
||||||
|
const res = await vehicleTypesApi.list({ storeId })
|
||||||
|
vehicleTypes.value = res.data.data || res.data || []
|
||||||
|
} catch (e) {
|
||||||
|
vehicleTypes.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
editingId.value = null
|
||||||
|
form.value = { frameNumber: '', plateNumber: '', brand: '', vehicleType: '', color: '', batteryType: '' }
|
||||||
|
if (vehicleTypes.value.length === 0) loadVehicleTypes()
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (item) => {
|
||||||
|
editingId.value = item._id
|
||||||
|
form.value = {
|
||||||
|
frameNumber: item.frameNumber || '',
|
||||||
|
plateNumber: item.plateNumber || '',
|
||||||
|
brand: '',
|
||||||
|
vehicleType: item.vehicleType || '',
|
||||||
|
color: item.color || '',
|
||||||
|
batteryType: item.batteryType || ''
|
||||||
|
}
|
||||||
|
const findBrand = (list) => {
|
||||||
|
const vt = list.find(v => v.name === item.vehicleType)
|
||||||
|
if (vt) form.value.brand = vt.brand
|
||||||
|
}
|
||||||
|
if (vehicleTypes.value.length === 0) {
|
||||||
|
loadVehicleTypes().then(findBrand)
|
||||||
|
} else {
|
||||||
|
findBrand(vehicleTypes.value)
|
||||||
|
}
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!form.value.frameNumber) {
|
||||||
|
alert('请填写车架号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (editingId.value) {
|
||||||
|
await vehiclesApi.update(editingId.value, form.value)
|
||||||
|
} else {
|
||||||
|
await vehiclesApi.create({ ...form.value, storeId })
|
||||||
|
}
|
||||||
|
closeDialog()
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
alert(editingId.value ? '更新失败' : '添加失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showDialog.value = false
|
||||||
|
editingId.value = null
|
||||||
|
form.value = { frameNumber: '', plateNumber: '', brand: '', vehicleType: '', color: '', batteryType: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (!confirm('确定删除该车辆?')) return
|
||||||
|
try {
|
||||||
|
await vehiclesApi.delete(id)
|
||||||
|
loadData()
|
||||||
|
} catch (e) {
|
||||||
|
alert('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
loadVehicleTypes()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F7F7F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: #fff;
|
||||||
|
padding: 14px 16px 12px;
|
||||||
|
border-bottom: 0.5px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-arrow {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-inline {
|
||||||
|
background: #FF6B00;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-wrap {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计栏 */
|
||||||
|
.stats-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num.idle {
|
||||||
|
color: #52C41A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num.rented {
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8E8E93;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 32px;
|
||||||
|
background: #F0F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-ring {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid #E5E5E5;
|
||||||
|
border-top-color: #FF6B00;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-sub {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 车辆卡片 */
|
||||||
|
.vehicle-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片头部 */
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px 12px;
|
||||||
|
border-bottom: 1px solid #F5F5F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plate-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plate-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plate-number {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1A1A1A;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-idle {
|
||||||
|
background: #F0FFF4;
|
||||||
|
color: #52C41A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-idle .badge-dot {
|
||||||
|
background: #52C41A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-rented {
|
||||||
|
background: #FFF7F0;
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-rented .badge-dot {
|
||||||
|
background: #FF6B00;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片信息区 */
|
||||||
|
.card-body {
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group.full {
|
||||||
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片底部 */
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #FAFAFA;
|
||||||
|
border-top: 1px solid #F5F5F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
background: #FFF7F0;
|
||||||
|
color: #FF6B00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-del {
|
||||||
|
background: #FFF0F0;
|
||||||
|
color: #FF4D4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗 */
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background: #fff;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-handle {
|
||||||
|
width: 36px;
|
||||||
|
height: 4px;
|
||||||
|
background: #DDD;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 10px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1A1A1A;
|
||||||
|
padding: 18px 16px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8E8E93;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #FF4D4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #F7F7F7;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item input::placeholder {
|
||||||
|
color: #B2B2B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel, .btn-confirm {
|
||||||
|
flex: 1;
|
||||||
|
padding: 13px;
|
||||||
|
border-radius: 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #F0F0F0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
background: #FF6B00;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
port: 5174,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue