Compare commits

...

2 Commits

Author SHA1 Message Date
xjs 5ab100265a chore: 部分函数收敛 2026-07-23 13:07:48 +08:00
xjs 7d408c778d feat: 页面编写 2026-07-21 09:40:21 +08:00
69 changed files with 2983 additions and 603 deletions

6
.env.production Normal file
View File

@ -0,0 +1,6 @@
VITE_SECRET_KEY=8aWi3cPzmEB8zrzLato9jj6D9xZuJdML
VITE_API_BASE_URL=https://api.zsfz.shenquanedu.com
# 企业微信 JS-SDK填入 corpId / agentId 后 main.ts 才会执行 ww.register()
VITE_WECOM_CORP_ID=
VITE_WECOM_AGENT_ID=

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.invitation-data{--van-search-padding: 4px 15px 0;--van-search-content-background: #f3f4f8;--van-search-input-height: 38px;--van-tab-font-size: 15px}.invitation-data .van-search__content{border-radius:81px;--van-field-placeholder-text-color: #c0c4cc;--van-padding-sm: 15px}.van-field__left-icon{--van-field-icon-size: 14px;--van-padding-base: 5px;line-height:1}.van-tabs--line .van-tabs__wrap{--van-tabs-line-height: 38px}[data-v-f5944d2b] .van-tab{--van-tab-font-size: 15px;--van-font-bold: 400}[data-v-f5944d2b] .van-tab--active{--van-tab-font-size: 16px;--van-font-bold: 500}

View File

@ -0,0 +1 @@
.visitor-invitation-form{--van-cell-font-size: 15px;--van-cell-line-height: 1;--van-cell-vertical-padding: 23px;--van-cell-horizontal-padding: 0;--van-cell-border-color: #f3f3f3;--van-field-label-color: #111;--van-field-input-text-color: #333333;--van-field-placeholder-text-color: #bababa;--van-cell-right-icon-color: #999999;--van-button-primary-background: #1580ff;--van-button-primary-border-color: #1580ff}[data-v-4ae0ebea] .van-field__right-icon{padding:0 0 0 10px;margin-right:0}.visitor-invitation-form .visitor-invitation-form__field{padding:0;overflow:visible}.visitor-invitation-form .visitor-invitation-form__field+.visitor-invitation-form__field{margin-top:24px}.visitor-invitation-form .visitor-invitation-form__field .van-field__label--top{width:100%;margin:0 0 10px}.visitor-invitation-form .visitor-invitation-form__field .van-field__value{flex:0 0 100%;width:100%;overflow:visible}.visitor-invitation-form .visitor-invitation-form__field .van-field__body{box-sizing:border-box;min-height:42px;padding:0 10px;border:1px solid #d9d9d9;border-radius:5px}.visitor-invitation-form .van-field__label{font-weight:500}.visitor-invitation-form .van-field__control{font-size:15px}.visitor-invitation-form .van-field__control:read-only:not(:disabled){cursor:pointer}.visitor-invitation-form .visitor-invitation-form__submit{height:42px;border-radius:8px;--van-button-normal-font-size: 16px}.visitor-invitation-form .visitor-invitation-form__submit.van-button--disabled{opacity:1;background:#a1ccff;border-color:#a1ccff;color:#fff}[data-v-4ae0ebea] .van-field--disabled .van-field__label{--van-field-disabled-text-color: #000}[data-v-4ae0ebea] .van-field--disabled .van-field__body{background-color:#f7f8fa}

View File

@ -0,0 +1 @@
.page-shell[data-v-250ccd76]{box-sizing:border-box;min-height:100%;padding:56px 24px;color:#17201d;background:#f5f7f6}.page-content[data-v-250ccd76]{width:min(100%,640px);margin:0 auto}.page-section[data-v-250ccd76]{margin:0 0 12px;color:#25754a;font-size:14px;font-weight:600;letter-spacing:0}h1[data-v-250ccd76]{margin:0;font-size:32px;line-height:1;letter-spacing:0}.page-description[data-v-250ccd76]{max-width:560px;margin:20px 0 0;color:#56625e;font-size:16px;line-height:1}@media(max-width:640px){.page-shell[data-v-250ccd76]{padding:40px 20px}h1[data-v-250ccd76]{font-size:28px}}

View File

@ -0,0 +1 @@
.visitor-pass-code[data-v-023608c9]{background-image:url(https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/jiaozhang_BG.png);background-repeat:no-repeat;background-position:top center;background-size:100% 93px}.visitor-pass-code__divider[data-v-023608c9]{position:relative;height:0;border-top:1px dashed #d9d9d9}.visitor-pass-code__divider[data-v-023608c9]:before,.visitor-pass-code__divider[data-v-023608c9]:after{position:absolute;top:50%;width:16px;height:16px;border-radius:50%;background:#1580ff;content:"";transform:translateY(-50%)}.visitor-pass-code__divider[data-v-023608c9]:before{left:-9px}.visitor-pass-code__divider[data-v-023608c9]:after{right:-9px}

View File

@ -0,0 +1 @@
.reception-data{--van-search-padding: 4px 15px 0;--van-search-content-background: #f3f4f8;--van-search-input-height: 38px;--van-tab-font-size: 15px}.reception-data .van-search__content{border-radius:81px;--van-field-placeholder-text-color: #c0c4cc;--van-padding-sm: 15px}.van-field__left-icon{--van-field-icon-size: 14px;--van-padding-base: 5px;line-height:1}.reception-data .van-tabs--line .van-tabs__wrap{--van-tabs-line-height: 38px}[data-v-36bf4673] .van-tab{--van-tab-font-size: 15px;--van-font-bold: 400}[data-v-36bf4673] .van-tab--active{--van-tab-font-size: 16px;--van-font-bold: 500}

View File

@ -0,0 +1 @@
.van-tabbar-item__icon img[data-v-a621e2f0]{display:block;height:22px}

View File

@ -0,0 +1 @@
.reception-mine__watermark[data-v-bd9b19d4]{background-image:url(https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_xuexiaojianying.png);background-repeat:no-repeat;background-position:right top;background-size:auto 60px}

View File

@ -0,0 +1 @@
.reception-record{--van-button-primary-background: #1580ff;--van-button-primary-border-color: #1580ff}.reception-record .reception-record__save{height:44px;--van-button-radius: 8px;--van-button-normal-font-size: 16px}.reception-record .reception-record__reason{padding:0;--van-cell-background: transparent}

View File

@ -0,0 +1 @@
.reception-task{--van-tab-font-size: 15px}.reception-task .van-tabs--line .van-tabs__wrap{--van-tabs-line-height: 38px}[data-v-f6c8a443] .van-tab{--van-tab-font-size: 15px;--van-font-bold: 400}[data-v-f6c8a443] .van-tab--active{--van-tab-font-size: 16px;--van-font-bold: 500}[data-v-f6c8a443] .van-empty__description{--van-empty-description-margin-top: 20px;--van-empty-description-color: #9E9E9E;--van-empty-description-font-size: 14px;--van-empty-description-line-height: 1}

View File

@ -0,0 +1 @@
.custom-bg[data-v-a3094bb5]{background-image:url(https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tijiao_BG.png);background-repeat:no-repeat;background-position:right top;background-size:auto 263px}

View File

@ -0,0 +1 @@
.teacher-invitation-form{--van-cell-font-size: 15px;--van-cell-line-height: 1;--van-cell-vertical-padding: 23px;--van-cell-horizontal-padding: 0;--van-cell-border-color: #f3f3f3;--van-field-label-color: #111;--van-field-input-text-color: #333333;--van-field-placeholder-text-color: #bababa;--van-cell-right-icon-color: #999999;--van-button-primary-background: #1580ff;--van-button-primary-border-color: #1580ff}[data-v-c7de0471] .van-field__right-icon{padding:0 0 0 10px;margin-right:0}.teacher-invitation-form .teacher-invitation-form__field{padding:0;overflow:visible}.teacher-invitation-form .teacher-invitation-form__field+.teacher-invitation-form__field{margin-top:24px}.teacher-invitation-form .teacher-invitation-form__field .van-field__label--top{width:100%;margin:0 0 10px}.teacher-invitation-form .teacher-invitation-form__field .van-field__value{flex:0 0 100%;width:100%;overflow:visible}.teacher-invitation-form .teacher-invitation-form__field .van-field__body{box-sizing:border-box;min-height:42px;padding:0 10px;border:1px solid #d9d9d9;border-radius:5px}.teacher-invitation-form .van-field__label{font-weight:500}.teacher-invitation-form .van-field__control{font-size:15px}.teacher-invitation-form .van-field__control:read-only:not(:disabled){cursor:pointer}.teacher-invitation-form .teacher-invitation-form__submit{height:42px;border-radius:8px;--van-button-normal-font-size: 16px}.teacher-invitation-form .teacher-invitation-form__submit.van-button--disabled{opacity:1;background:#a1ccff;border-color:#a1ccff;color:#fff}[data-v-c7de0471] .van-field--disabled .van-field__label{--van-field-disabled-text-color: #000}[data-v-c7de0471] .van-field--disabled .van-field__body{background-color:#f7f8fa}

View File

@ -0,0 +1 @@
.van-tabbar-item__icon img[data-v-5b6bffe6]{display:block;height:22px}

View File

@ -0,0 +1 @@
.teacher-mine{--van-button-primary-background: #1580ff;--van-button-primary-border-color: #1580ff}.teacher-mine .teacher-mine__button{height:40px;--van-button-radius: 8px}.teacher-mine__watermark[data-v-2c45dfd0]{background-image:url(https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_xuexiaojianying.png);background-repeat:no-repeat;background-position:right top;background-size:auto 60px}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.school-select-popup{--van-search-padding: 15px;--van-search-content-background: #f5f5f5;--van-search-input-height: 34px;--van-sidebar-width: 112px;--van-sidebar-font-size: 14px;--van-sidebar-line-height: 1;--van-sidebar-padding: 15px 20px;--van-sidebar-background: #f5f5f5;--van-sidebar-selected-background: #fff;--van-sidebar-selected-font-weight: 500;--van-sidebar-selected-text-color: #1580ff;--van-sidebar-selected-border-width: 3px;--van-sidebar-selected-border-height: 100%;--van-sidebar-selected-border-color: #1580ff;--van-empty-image-size: 128px;--van-empty-description-color: #999;--van-empty-description-font-size: 15px;--van-padding-base: 6px}.school-select-popup .van-search__content{border-radius:5px}.school-select-popup .school-select-popup__search{padding-right:0}.school-select-popup .van-field__clear{margin-right:0;padding:0 6px}.school-select-popup .van-sidebar-item{display:flex;align-items:center}.school-select-popup .van-sidebar-item__text{word-break:keep-all}.school-select-popup .school-select-popup__region--before-selected{border-radius:0 0 8px}.school-select-popup .school-select-popup__region--after-selected{border-radius:0 8px 0 0}.school-select-popup .van-empty__description{display:flex;align-items:center;justify-content:center;white-space:nowrap}.visit-date-calendar__popup{--van-calendar-header-title-height: 1;--van-text-color: #000;--van-calendar-month-title-font-size: 15px;--van-calendar-day-height: 36px;--van-calendar-selected-day-size: 32px;--van-calendar-selected-day-size: 36px;--van-calendar-selected-day-background: #1580FF}[data-v-6bb4a750] .van-calendar__month{padding-top:20px}.visit-date-calendar__popup .van-calendar__selected-day{border-radius:5px}.van-calendar{--van-calendar-weekdays-font-size: 13px}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{d as g,o as s,p as l,q as a,b as u,u as i,S as y,w as k,a4 as N,c as w,N as D,A as f,O as _,r as p,x as V,a5 as F,E as n,a6 as T,v as r}from"./.pnpm.CF2hm8if.js";import{_ as B}from"./index.Bg6AlVg6.js";const C={class:"invitation-data min-h-full bg-[#F5F5F5]"},S={class:"sticky top-0 z-10 bg-white"},E={key:1,class:"flex flex-col gap-12 px-15 py-15"},I={class:"flex items-center justify-between gap-10"},z={class:"min-w-0 flex-1 truncate text-[4rem] font-600 leading-none text-[#000]"},K={class:"mt-15 text-[3.25rem] text-[#333] flex flex-col gap-row-[8px]"},U={class:""},j={class:""},q={class:""},A=g({__name:"InvitationData",setup(L){const m={pending:{label:"待到访",color:"#06C764"},visited:{label:"已到访",color:"#1580FF"},notVisited:{label:"未到访",color:"#969696"},paid:{label:"已缴费",color:"#FF7C38"}},h=[{label:"全部邀约",value:"all"},{label:"待到访",value:"pending"},{label:"已到访",value:"visited"},{label:"未到访",value:"notVisited"},{label:"已缴费",value:"paid"}],c=p(""),o=p("all"),x=p([{id:1,studentName:"张凯文",status:"notVisited",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部"},{id:2,studentName:"张凯文",status:"pending",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部"},{id:3,studentName:"张凯文",status:"visited",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部"},{id:4,studentName:"张凯文",status:"paid",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部"},{id:5,studentName:"李思远",status:"pending",appointmentDate:"2026-07-26",school:"济南市历城第二中学",consultation:"深泉外国语-初中部"},{id:6,studentName:"王梓萱",status:"visited",appointmentDate:"2026-07-27",school:"济南协和双语实验学校(初中部)",consultation:"深泉外国语-高中部"}]),v=V(()=>{const d=c.value.trim();return x.value.filter(e=>{const t=o.value==="all"||e.status===o.value,b=d===""||e.studentName.includes(d);return t&&b})});return(d,e)=>(s(),l("main",C,[a("div",S,[u(i(y),{modelValue:c.value,"onUpdate:modelValue":e[0]||(e[0]=t=>c.value=t),class:"invitation-data__search",background:"#fff",placeholder:"请输入学生姓名",autocomplete:"off",enterkeyhint:"search",clearable:""},null,8,["modelValue"]),u(i(N),{active:o.value,"onUpdate:active":e[1]||(e[1]=t=>o.value=t),class:"invitation-data__tabs","line-width":"24px","line-height":"2px",color:"#1580ff","title-active-color":"#000","title-inactive-color":"#999"},{default:k(()=>[(s(),l(f,null,_(h,t=>u(i(F),{key:t.value,name:t.value,title:t.label},null,8,["name","title"])),64))]),_:1},8,["active"])]),v.value.length===0?(s(),w(i(D),{key:0,class:"pt-40",description:"暂无邀约数据"})):(s(),l("div",E,[(s(!0),l(f,null,_(v.value,t=>(s(),l("article",{key:t.id,class:"rounded-8 bg-white p-15"},[a("div",I,[a("h2",z,n(t.studentName),1),a("span",{class:"shrink-0 text-[3.25rem] leading-none",style:T({color:m[t.status].color})},n(m[t.status].label),5)]),a("div",K,[a("p",U,[e[2]||(e[2]=a("span",{class:"text-[#666]"},"预约:",-1)),r(n(t.appointmentDate),1)]),a("p",j,[e[3]||(e[3]=a("span",{class:"text-[#666]"},"院校:",-1)),r(n(t.school),1)]),a("p",q,[e[4]||(e[4]=a("span",{class:"text-[#666]"},"咨询:",-1)),r(n(t.consultation),1)])])]))),128))]))]))}}),R=B(A,[["__scopeId","data-v-f5944d2b"]]);export{R as default};

View File

@ -0,0 +1 @@
import{_ as V}from"./right-arrow.Ca_LZHAz.js";import{d as T,n as P,o as q,p as A,q as r,b as t,w as c,u as n,F as M,I as j,r as o,t as p,B as E,v as L,x as i,y as O,z as Y,s as _}from"./.pnpm.CF2hm8if.js";import{V as G,_ as H,S as J,s as K}from"./invitationOptions.C9q7AXPg.js";import{_ as Q}from"./index.Bg6AlVg6.js";const W={class:"visitor-invitation-form min-h-full overflow-hidden bg-[#f3f5f8] text-[#111]"},X={class:"mx-15 mt-15 overflow-hidden rounded-15 bg-white px-15 pt-20"},Z={class:"px-28 my-30"},ee={class:"m-0 flex items-center justify-center gap-5 px-16 py-10 text-[3rem] text-[#86909C]"},le=T({__name:"InvitationForm",setup(ae){const k=O(),N=Y(),C=i(()=>{const a=k.params.token;return Array.isArray(a)?a[0]??"":a??""}),f=o(""),d=o(""),m=o(null),g=o(""),u=o(null),b=o(""),s=o(null),S=o(!1),w=o(!1),y=o(!1),h=o(!1),F=(a,e)=>{b.value=e.label},U=(a,e)=>{g.value=e.label},x=i(()=>!!b.value&&K.includes(b.value)),v=i(()=>u.value===null||x.value),D=i(()=>u.value===null?"请先选择咨询学段":x.value?"当前学段无需填写":"请选择毕业院校");P(v,a=>{a&&(m.value=null,g.value="")});const $=i(()=>{if(!s.value)return"";const a=s.value.getFullYear(),e=String(s.value.getMonth()+1).padStart(2,"0"),l=String(s.value.getDate()).padStart(2,"0");return`${a}-${e}-${l}`}),B=a=>a.replace(/\D/g,"").slice(0,11),I=i(()=>!(!f.value.trim()||!d.value.trim()||u.value===null||!v.value&&m.value===null||s.value===null)),z=()=>{v.value||(S.value=!0)},R=async()=>{const a=f.value.trim()?d.value.trim()?u.value===null?"咨询学段不能为空":!v.value&&m.value===null?"毕业院校不能为空":s.value===null?"到访日期不能为空":"":"联系方式不能为空":"学生姓名不能为空";if(a){_(a);return}if(!/^1[3-9]\d{9}$/.test(d.value)){_("请输入正确的家长手机号");return}h.value=!0;try{await N.push({name:"visitor-submission-success",params:{token:C.value}})}finally{h.value=!1}};return(a,e)=>(q(),A("main",W,[r("section",X,[t(n(M),{"label-align":"top","scroll-to-error":"",onSubmit:R},{default:c(()=>[t(n(p),{modelValue:f.value,"onUpdate:modelValue":e[0]||(e[0]=l=>f.value=l),class:"visitor-invitation-form__field",name:"studentName",label:"学生姓名",placeholder:"请输入学生姓名",autocomplete:"name",border:!1,clearable:""},null,8,["modelValue"]),t(n(p),{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=l=>d.value=l),class:"visitor-invitation-form__field",name:"contactNumber",label:"联系方式",placeholder:"请输入家长手机号",type:"tel",inputmode:"numeric",autocomplete:"tel",clearable:"",maxlength:"11",border:!1,formatter:B},null,8,["modelValue"]),t(n(p),{"model-value":b.value,class:"visitor-invitation-form__field",name:"stage",label:"咨询学段",placeholder:"请选择咨询学段",readonly:"",clickable:"",border:!1,"aria-haspopup":"dialog",onClick:e[2]||(e[2]=l=>w.value=!0)},{"right-icon":c(()=>[...e[10]||(e[10]=[r("img",{class:"h-18 w-9",src:V,alt:"","aria-hidden":"true"},null,-1)])]),_:1},8,["model-value"]),t(n(p),{"model-value":g.value,class:"visitor-invitation-form__field",name:"school",label:"毕业院校",placeholder:D.value,readonly:"",clickable:"",disabled:v.value,border:!1,"aria-haspopup":"dialog",onClick:z},{"right-icon":c(()=>[...e[11]||(e[11]=[r("img",{class:"h-18 w-9",src:V,alt:"","aria-hidden":"true"},null,-1)])]),_:1},8,["model-value","placeholder","disabled"]),t(n(p),{"model-value":$.value,class:"visitor-invitation-form__field",name:"visitDate",label:"到访日期",placeholder:"请选择到访日期",readonly:"",clickable:"",border:!1,"aria-haspopup":"dialog",onClick:e[3]||(e[3]=l=>y.value=!0)},{"right-icon":c(()=>[...e[12]||(e[12]=[r("img",{class:"h-18 w-9",src:V,alt:"","aria-hidden":"true"},null,-1)])]),_:1},8,["model-value"]),r("div",Z,[t(n(E),{class:"visitor-invitation-form__submit",block:"",type:"primary","native-type":"submit",loading:h.value,"loading-text":"提交中...",disabled:h.value||!I.value},{default:c(()=>[...e[13]||(e[13]=[L(" 提交 ",-1)])]),_:1},8,["loading","disabled"])])]),_:1})]),r("p",ee,[t(n(j),{class:"shrink-0",name:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_tishi.png","aria-hidden":"true"}),e[14]||(e[14]=r("span",null,"二维码自到访当日起3天内有效",-1))]),t(G,{modelValue:s.value,"onUpdate:modelValue":e[4]||(e[4]=l=>s.value=l),show:y.value,"onUpdate:show":e[5]||(e[5]=l=>y.value=l)},null,8,["modelValue","show"]),t(H,{modelValue:u.value,"onUpdate:modelValue":e[6]||(e[6]=l=>u.value=l),show:w.value,"onUpdate:show":e[7]||(e[7]=l=>w.value=l),title:"咨询学段",onSelect:F},null,8,["modelValue","show"]),t(J,{modelValue:m.value,"onUpdate:modelValue":e[8]||(e[8]=l=>m.value=l),show:S.value,"onUpdate:show":e[9]||(e[9]=l=>S.value=l),title:"毕业院校","page-size":20,onSelect:U},null,8,["modelValue","show"])]))}}),re=Q(le,[["__scopeId","data-v-4ae0ebea"]]);export{re as default};

View File

@ -0,0 +1 @@
import{P as e}from"./PagePlaceholder.CH5iciU9.js";import{d as o,o as t,c as r}from"./.pnpm.CF2hm8if.js";import"./index.Bg6AlVg6.js";const m=o({__name:"NotFound",setup(a){return(c,n)=>(t(),r(e,{section:"系统提示",title:"页面不存在",description:"当前访问地址无对应页面,请检查链接是否完整或已失效。"}))}});export{m as default};

View File

@ -0,0 +1 @@
import{d as o,o as c,p as a,q as e,E as t}from"./.pnpm.CF2hm8if.js";import{_ as n}from"./index.Bg6AlVg6.js";const i={class:"page-shell"},l={class:"page-content"},p={class:"page-section"},r={class:"page-description"},d=o({__name:"PagePlaceholder",props:{section:{},title:{},description:{}},setup(s){return(_,h)=>(c(),a("main",i,[e("div",l,[e("p",p,t(s.section),1),e("h1",null,t(s.title),1),e("p",r,t(s.description),1)])]))}}),f=n(d,[["__scopeId","data-v-250ccd76"]]);export{f as P};

View File

@ -0,0 +1 @@
import{d as E,n as I,o as l,p as d,q as e,A as h,v as f,D as P,E as R,r as x,G as w,s as m,H as D,y as A,Q as v}from"./.pnpm.CF2hm8if.js";import{_ as O}from"./index.Bg6AlVg6.js";const j={class:"visitor-pass-code box-border min-h-full overflow-y-auto bg-[#1580ff] px-16 pb-safe pt-60 text-[#333]"},F={class:"overflow-hidden rounded-15 bg-white"},M={class:"flex flex-col items-center px-50 pb-40 pt-50"},q={class:"flex h-244 w-244 items-center justify-center bg-white"},B=["src"],b="山东省济南市平阴榆山街道深泉路1号",H=244,T=38,W="/images/logo.png",G=E({__name:"PassCode",setup(N){const u=A(),r=x(""),g=x(!0),y={latitude:36.243169,longitude:116.432803},_=n=>new Promise((t,s)=>{const o=new Image;o.crossOrigin="anonymous",o.onload=()=>t(o),o.onerror=s,o.src=n}),k=async(n,t)=>{const s=await _(n),o=document.createElement("canvas");o.width=t,o.height=t;const a=o.getContext("2d");if(!a)throw new Error("2D context unavailable");a.beginPath(),a.arc(t/2,t/2,t/2,0,Math.PI*2),a.closePath(),a.clip();const i=Math.max(t/s.width,t/s.height),c=s.width*i,p=s.height*i;return a.drawImage(s,(t-c)/2,(t-p)/2,c,p),o.toDataURL("image/png")},C=()=>{const n=u.params.token;return Array.isArray(n)?n[0]??"":n??""},L=async()=>{if(g.value){r.value="";return}const n=C(),t=Math.min(Math.max(Math.round(window.devicePixelRatio||1),1),3),s=H*t,o=T*t,a={text:n,width:s,height:s,correctLevel:v.CorrectLevel.H,quietZone:0,onRenderingEnd:(i,c)=>{r.value=c}};try{const i=await k(W,o);Object.assign(a,{logo:i,logoWidth:o,logoHeight:o,logoBackgroundTransparent:!0})}catch{}try{new v(document.createElement("div"),a)}catch{r.value="",m("通行码生成失败")}},S=async()=>{if(!w.isWeChat&&!w.isWeCom){m("请在微信或企业微信中打开导航");return}try{await D({...y,name:"深泉学校",address:b,scale:16})}catch{m("地图打开失败,请检查 JS-SDK 配置")}};return I(()=>u.params.token,L,{immediate:!0}),(n,t)=>(l(),d("main",j,[e("section",F,[e("div",M,[g.value?(l(),d(h,{key:0},[t[0]||(t[0]=e("img",{class:"h-244 w-244",src:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_guoqi.png",role:"img","aria-label":"二维码已过期"},null,-1)),t[1]||(t[1]=e("p",{class:"m-0 mt-20 text-center text-[3.5rem] leading-[6rem] text-[#666]"},[f(" 二维码已过期,"),e("br"),f("请联系招生老师重新获取 ")],-1))],64)):(l(),d(h,{key:1},[e("div",q,[r.value?(l(),d("img",{key:0,class:"block h-244 w-244",src:r.value,alt:"入校通行二维码"},null,8,B)):P("",!0)]),t[2]||(t[2]=e("p",{class:"m-0 mt-20 flex items-center justify-center gap-5 text-[3.5rem] text-[#333] leading-none"},[e("span",{class:"flex h-18 w-18 shrink-0 items-center justify-center rounded-full bg-[#35bf70] text-[3.5rem] font-700 text-white","aria-hidden":"true"}," ↑ "),e("span",null,"入校时请向门卫出示二维码")],-1))],64))]),t[5]||(t[5]=e("div",{class:"visitor-pass-code__divider","aria-hidden":"true"},null,-1)),e("div",{class:"flex min-h-118 items-center gap-10 px-16 pt-13 pb-15 bg-[#F7FAFF]"},[t[4]||(t[4]=e("img",{src:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_dizhi.png",class:"h-90 w-90 shrink-0 rounded-8 bg-[#f3f5f8]",role:"img","aria-label":"地图占位图"},null,-1)),e("div",{class:"min-w-0 flex-1"},[e("p",{class:"m-0 text-[3.75rem] font-500 leading-[5rem] text-[#333]"},R(b)),e("button",{class:"mt-7 flex h-34 items-center gap-6 rounded-full border border-solid border-[#1580ff] bg-white px-14 text-[3.25rem] text-[#1580ff]",type:"button",onClick:S},[...t[3]||(t[3]=[e("span",{class:"flex h-20 w-20 items-center justify-center rounded-full text-white","aria-hidden":"true"},[e("img",{src:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_daohang.png",class:"h-16 w-16",alt:""})],-1),e("span",null,"导航到校",-1)])])])])]),t[6]||(t[6]=e("p",{class:"m-0 mt-15 text-center text-[3.25rem] leading-[5rem] text-[rgba(255,255,255,0.58)]"},"有效期7月25日至7月27日共3天",-1))]))}}),Q=O(G,[["__scopeId","data-v-023608c9"]]);export{Q as default};

View File

@ -0,0 +1 @@
import{d as g,o as s,p as l,q as a,b as u,u as c,S as F,w as N,a4 as T,c as w,N as D,A as b,O as _,r as p,x as V,a5 as C,E as n,a6 as L,v as r,D as R,z as S}from"./.pnpm.CF2hm8if.js";import{_ as B}from"./index.Bg6AlVg6.js";const E={class:"reception-data min-h-full bg-[#F5F5F5]"},z={class:"sticky top-0 z-10 bg-white"},K={key:1,class:"flex flex-col gap-12 px-15 py-15"},U={class:"flex items-center gap-8"},q={class:"text-[4rem] font-600 leading-none text-[#000]"},A={class:"rounded-2 bg-[#E7F2FF] px-4 py-3 text-[2.5rem] leading-none text-[#1580FF]"},I={class:"mt-14 flex items-center"},M={class:"min-w-0 flex-1 text-[3.25rem] flex flex-col gap-8 text-[#333]"},O={class:"m-0"},$={class:"m-0 truncate"},j={class:"m-0"},G=["onClick"],H=g({__name:"ReceptionData",setup(J){const m={paid:{label:"已缴费",color:"#1580FF"},unpaid:{label:"未缴费",color:"#FF2928"},refunded:{label:"已退费",color:"#969696"},toVerify:{label:"待核销",color:"#FF7C38"}},v=[{label:"总接待",value:"all"},{label:"已缴费",value:"paid"},{label:"未缴费",value:"unpaid"},{label:"已退费",value:"refunded"},{label:"待核销",value:"toVerify"}],d=p(""),i=p("all"),x=S(),k=p([{id:1,studentName:"张凯文",phone:"13800000001",status:"unpaid",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部",actionLabel:"接待记录"},{id:2,studentName:"张凯文",phone:"13800000002",status:"unpaid",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部",actionLabel:"查看记录"},{id:3,studentName:"张凯文",phone:"13800000003",status:"paid",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部"},{id:4,studentName:"张凯文",phone:"13800000004",status:"toVerify",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部"},{id:5,studentName:"张凯文",phone:"13800000005",status:"refunded",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部"}]),h=V(()=>{const o=d.value.trim();return k.value.filter(t=>{const e=i.value==="all"||t.status===i.value,f=o===""||t.studentName.includes(o)||t.phone.includes(o);return e&&f})}),y=o=>{x.push({name:"reception-record",params:{id:String(o.id)}})};return(o,t)=>(s(),l("main",E,[a("div",z,[u(c(F),{modelValue:d.value,"onUpdate:modelValue":t[0]||(t[0]=e=>d.value=e),class:"reception-data__search",background:"#fff",placeholder:"搜索学生姓名或手机号",autocomplete:"off",enterkeyhint:"search",clearable:""},null,8,["modelValue"]),u(c(T),{active:i.value,"onUpdate:active":t[1]||(t[1]=e=>i.value=e),class:"reception-data__tabs","line-width":"24px","line-height":"2px",color:"#1580ff","title-active-color":"#000","title-inactive-color":"#999"},{default:N(()=>[(s(),l(b,null,_(v,e=>u(c(C),{key:e.value,name:e.value,title:e.label},null,8,["name","title"])),64))]),_:1},8,["active"])]),h.value.length===0?(s(),w(c(D),{key:0,class:"pt-40",description:"暂无接待数据"})):(s(),l("div",K,[(s(!0),l(b,null,_(h.value,e=>(s(),l("article",{key:e.id,class:"rounded-8 bg-white p-15"},[a("div",U,[a("span",q,n(e.studentName),1),a("span",A,"签到:"+n(e.checkinTime),1),a("span",{class:"ml-auto shrink-0 text-[3.25rem] leading-none",style:L({color:m[e.status].color})},n(m[e.status].label),5)]),a("div",I,[a("div",M,[a("p",O,[t[2]||(t[2]=a("span",{class:"text-[#666]"},"预约:",-1)),r(" "+n(e.appointmentDate),1)]),a("p",$,[t[3]||(t[3]=a("span",{class:"text-[#666]"},"院校:",-1)),r(" "+n(e.school),1)]),a("p",j,[t[4]||(t[4]=a("span",{class:"text-[#666]"},"咨询:",-1)),r(" "+n(e.consultation),1)])]),e.actionLabel?(s(),l("button",{key:0,class:"ml-20 h-32 shrink-0 rounded-4 border border-solid border-[#1580ff] bg-white px-12 text-[3.25rem] leading-none text-[#1580ff] self-end",type:"button",onClick:f=>y(e)},n(e.actionLabel),9,G)):R("",!0)])]))),128))]))]))}}),W=B(H,[["__scopeId","data-v-36bf4673"]]);export{W as default};

View File

@ -0,0 +1 @@
import{d as u,o as c,p as d,q as s,b as n,u as e,R as p,c as _,w as t,J as f,D as m,y as h,v as i,L as r}from"./.pnpm.CF2hm8if.js";import{_ as w}from"./index.Bg6AlVg6.js";const b={class:"box-border flex h-full min-h-0 w-full flex-col overflow-hidden bg-[#f5f7f6]"},v={class:"min-h-0 flex-1 overflow-y-auto"},g=["src"],x=["src"],k=u({__name:"ReceptionLayout",setup(y){const l=h();return(z,o)=>(c(),d("div",b,[s("div",v,[n(e(p))]),e(l).meta.hideTabbar?m("",!0):(c(),_(e(f),{key:0,route:"",placeholder:"","safe-area-inset-bottom":"","active-color":"#1989fa","inactive-color":"#646566","z-index":100},{default:t(()=>[n(e(r),{to:{name:"reception-task"}},{icon:t(({active:a})=>[s("img",{class:"h-22 w-22",src:a?"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tab_renwu_set.png":"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tab_renwu_def.png",alt:""},null,8,g)]),default:t(()=>[o[0]||(o[0]=i(" 任务 ",-1))]),_:1}),n(e(r),{to:{name:"reception-mine"}},{icon:t(({active:a})=>[s("img",{class:"h-22 w-22",src:a?"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tab_wode_set.png":"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tab_wode_def.png",alt:""},null,8,x)]),default:t(()=>[o[1]||(o[1]=i(" 我的 ",-1))]),_:1})]),_:1}))]))}}),B=w(k,[["__scopeId","data-v-a621e2f0"]]);export{B as default};

View File

@ -0,0 +1 @@
import{_ as w}from"./logo.C0Q7FAT1.js";import{_ as y}from"./right-arrow.Ca_LZHAz.js";import{d as F,o as a,p as i,q as e,E as n,A as d,O as p,r,z as N,Y as u,v as c,s as m}from"./.pnpm.CF2hm8if.js";import{_ as T}from"./index.Bg6AlVg6.js";const z={class:"reception-mine box-border min-h-full bg-[#F5F5F5] px-15 pb-20 pt-15"},C={class:"relative overflow-hidden rounded-12 bg-[linear-gradient(135deg,#e8f1ff_0%,#f5f8ff_100%)] px-18 py-20"},R={class:"relative flex items-center gap-12"},D={class:"min-w-0 flex-1"},$={class:"truncate text-[4.5rem] font-500 leading-none text-[#000]"},j={class:"mt-10 truncate text-[3rem] leading-none text-[#999]"},B={class:"mt-12 overflow-hidden rounded-12 bg-white p-15"},E={class:"mt-20 flex items-stretch"},A={class:"mt-4 text-[3.25rem] leading-none text-[#666]"},L={class:"mt-12 flex flex-col gap-12"},M={class:"flex items-center gap-8"},V={class:"text-[4rem] font-600 leading-none text-[#000]"},q={class:"rounded-2 bg-[#E7F2FF] px-4 py-3 text-[2.5rem] leading-none text-[#1580FF]"},I={class:"mt-14 flex items-center"},O={class:"min-w-0 flex-1 text-[3.25rem] text-[#333] flex flex-col gap-8"},S={class:"m-0"},Y={class:"m-0 truncate"},G={class:"m-0"},H={class:"ml-20 flex shrink-0 items-center self-end gap-8"},J=["onClick"],K=["disabled","onClick"],P=F({__name:"ReceptionMine",setup(Q){const f=N(),h=r("王刚"),x=r("深泉外国语学校接待老师"),g=r([{label:"总接待",value:256},{label:"已缴费",value:56},{label:"未缴费",value:180,highlight:!0},{label:"已退费",value:10},{label:"待核销",value:10}]),l=r([{id:1,studentName:"张凯文",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)实验校区高中部",consultation:"深泉外国语-高中部",completable:!1},{id:2,studentName:"张凯文",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部",completable:!0},{id:3,studentName:"张凯文",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部",completable:!0}]),_=()=>{f.push({name:"reception-data"})},b=o=>{l.value=l.value.filter(t=>t.id!==o.id),m(`已取消 ${o.studentName}`)},v=o=>{o.completable&&(l.value=l.value.filter(t=>t.id!==o.id),m(`已完成 ${o.studentName}`))};return(o,t)=>(a(),i("main",z,[e("section",C,[t[1]||(t[1]=e("div",{class:"reception-mine__watermark pointer-events-none absolute right-0 top-0 h-60 w-210","aria-hidden":"true"},null,-1)),e("div",R,[t[0]||(t[0]=e("img",{class:"h-[12rem] w-[12rem] shrink-0 rounded-full bg-white object-cover",src:w,alt:"学校头像"},null,-1)),e("div",D,[e("p",$,n(h.value)+"老师,辛苦了",1),e("p",j,n(x.value),1)])])]),e("section",B,[e("div",{class:"flex items-center"},[t[3]||(t[3]=e("img",{src:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_yaoyueshuju.png",class:"h-16 w-16 shrink-0 rounded-4","aria-hidden":"true"},null,-1)),t[4]||(t[4]=e("h2",{class:"ml-5 flex-1 text-[4.25rem] font-600 leading-none text-[#000]"},"累计数据",-1)),e("button",{class:"flex items-center gap-5 border-0 bg-transparent p-0 text-[3.25rem] leading-none text-[#666]",type:"button",onClick:_},[...t[2]||(t[2]=[e("span",null,"详情",-1),e("img",{class:"h-14 w-7",src:y,alt:"","aria-hidden":"true"},null,-1)])])]),e("div",E,[(a(!0),i(d,null,p(g.value,s=>(a(),i("div",{key:s.label,class:"flex flex-1 flex-col items-center"},[e("span",{class:u(["din-bold text-[5rem] font-700 leading-none",s.highlight?"text-[#FA5151]":"text-[#000]"])},n(s.value),3),e("span",A,n(s.label),1)]))),128))])]),t[8]||(t[8]=e("div",{class:"mt-15 flex items-center gap-6"},[e("img",{src:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_jiedaizhong.png",class:"h-18 w-18 shrink-0 rounded-4 bg-[#e6efff]","aria-hidden":"true"}),e("h2",{class:"text-[4rem] font-600 leading-none text-[#000]"},"接待中")],-1)),e("div",L,[(a(!0),i(d,null,p(l.value,s=>(a(),i("section",{key:s.id,class:"rounded-12 bg-white p-15"},[e("div",M,[e("span",V,n(s.studentName),1),e("span",q,"签到:"+n(s.checkinTime),1)]),e("div",I,[e("div",O,[e("p",S,[t[5]||(t[5]=e("span",{class:"text-[#666]"},"预约:",-1)),c(" "+n(s.appointmentDate),1)]),e("p",Y,[t[6]||(t[6]=e("span",{class:"text-[#666]"},"院校:",-1)),c(" "+n(s.school),1)]),e("p",G,[t[7]||(t[7]=e("span",{class:"text-[#666]"},"咨询:",-1)),c(" "+n(s.consultation),1)])]),e("div",H,[e("button",{class:"h-32 w-50 rounded-4 border border-solid border-[#1580ff] bg-white text-[3.25rem] leading-none text-[#1580ff]",type:"button",onClick:k=>b(s)}," 取消 ",8,J),e("button",{class:u(["h-32 w-50 rounded-4 border-0 text-[3.25rem] leading-none text-white",s.completable?"bg-[#1580ff]":"bg-[#ccc]"]),disabled:!s.completable,type:"button",onClick:k=>v(s)}," 完成 ",10,K)])])]))),128))])]))}}),ee=T(P,[["__scopeId","data-v-bd9b19d4"]]);export{ee as default};

View File

@ -0,0 +1 @@
import{d as b,a1 as F,s as r,o as v,p as g,q as e,E as o,a6 as h,v as a,b as d,u as p,t as y,w as k,B as w,y as R,z as V,r as u}from"./.pnpm.CF2hm8if.js";import{_ as z}from"./index.Bg6AlVg6.js";const E={class:"reception-record box-border min-h-full bg-white px-15 pb-30 pt-15"},B={class:"rounded-10 bg-[#F7F8FA] p-15"},N={class:"flex items-center gap-8"},C={class:"text-[4rem] font-600 leading-none text-[#000]"},T={class:"rounded-2 bg-[#E7F2FF] px-4 py-3 text-[2.5rem] leading-none text-[#1580FF]"},A={class:"mt-14 flex flex-col gap-8 text-[3.25rem] text-[#333]"},D={class:"m-0"},I={class:"m-0 truncate"},M={class:"m-0"},S={class:"mt-[-7px] rounded-10 bg-[#F3F4F8] p-[15px_11px_7px_15px]"},q=b({__name:"ReceptionRecord",setup(H){const i={paid:{label:"已缴费",color:"#1580FF"},unpaid:{label:"未缴费",color:"#FF2928"},refunded:{label:"已退费",color:"#969696"},toVerify:{label:"待核销",color:"#FF7C38"}},m=R(),c=V(),n=m.params.id,_=Array.isArray(n)?n[0]??"":n??"",s=u({studentName:"张凯文",status:"unpaid",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部"}),l=u("");F(()=>{if(!_){r("记录不存在"),c.back();return}});const x=()=>{if(!l.value.trim()){r("请填写未缴费原因");return}r("保存成功"),c.back()};return(U,t)=>(v(),g("main",E,[e("section",B,[e("div",N,[e("span",C,o(s.value.studentName),1),e("span",T,"签到:"+o(s.value.checkinTime),1),e("span",{class:"ml-auto shrink-0 text-[3.25rem] leading-none",style:h({color:i[s.value.status].color})},o(i[s.value.status].label),5)]),e("div",A,[e("p",D,[t[1]||(t[1]=e("span",{class:"text-[#666]"},"预约:",-1)),a(" "+o(s.value.appointmentDate),1)]),e("p",I,[t[2]||(t[2]=e("span",{class:"text-[#666]"},"院校:",-1)),a(" "+o(s.value.school),1)]),e("p",M,[t[3]||(t[3]=e("span",{class:"text-[#666]"},"咨询:",-1)),a(" "+o(s.value.consultation),1)])])]),t[5]||(t[5]=e("div",{class:"mt-30 h-43 w-192 pt-10 pl-16 bg-[linear-gradient(to_right,#E2EFFE_0%,#fff_100%)] rounded-[10px_0_0_0]"},[e("img",{class:"block h-15",src:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/wenzi_biaoti.png",alt:"请填写未缴费原因"})],-1)),e("div",S,[d(p(y),{modelValue:l.value,"onUpdate:modelValue":t[0]||(t[0]=f=>l.value=f),class:"reception-record__reason",type:"textarea",maxlength:"200",autosize:{minHeight:300},"show-word-limit":"",border:!1,placeholder:"请简要记录原因,便于后续跟进。"},null,8,["modelValue"])]),d(p(w),{class:"reception-record__save mt-40!",block:"",type:"primary",onClick:x},{default:k(()=>[...t[4]||(t[4]=[a("保存",-1)])]),_:1})]))}}),J=z(q,[["__scopeId","data-v-ae49d964"]]);export{J as default};

View File

@ -0,0 +1 @@
import{d as f,o as n,p as a,q as t,b as p,w as k,u as d,a4 as w,A as m,O as _,c as y,N as b,r as x,x as F,a5 as T,E as i,D as r,v as c,s as N}from"./.pnpm.CF2hm8if.js";import{_ as z}from"./index.Bg6AlVg6.js";const E={class:"reception-task min-h-full flex flex-col"},C={class:"sticky top-0 z-10 bg-white"},D={key:0,class:"flex flex-col gap-12 p-15"},R={class:"flex items-center gap-8"},B={class:"text-[4rem] font-600 leading-none text-[#000]"},V={class:"rounded-2 bg-[#E7F2FF] px-4 py-3 text-[2.5rem] leading-none text-[#1580FF]"},M={key:0,class:"rounded-2 bg-[#FEF3E8] px-4 py-3 text-[2.5rem] leading-none text-[#F68E1D]"},q={key:1,class:"ml-auto text-[3.25rem] leading-none text-[#06C764]"},L={class:"mt-15 flex items-end"},$={class:"min-w-0 flex-1 text-[3.25rem] text-[#333] flex flex-col gap-8"},j={class:"m-0"},A={class:"m-0"},I={class:"m-0"},O=["onClick"],S=f({__name:"ReceptionTask",setup(U){const g=[{label:"待接待",value:"pending"},{label:"今日已接待",value:"received"}],o=x("pending"),v=x([{id:1,studentName:"张凯文",checkinTime:"07-26 14:31",waitingMinutes:13,appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部",status:"pending"},{id:2,studentName:"张凯文",checkinTime:"07-26 14:31",appointmentDate:"2026-07-25",school:"济南高新区海川中学(民办)",consultation:"深泉外国语-高中部",status:"pending"}]),u=F(()=>v.value.filter(l=>l.status===o.value)),h=l=>{l.status="received",N(`已接待 ${l.studentName}`)};return(l,s)=>(n(),a("main",E,[t("div",C,[p(d(w),{active:o.value,"onUpdate:active":s[0]||(s[0]=e=>o.value=e),class:"reception-task__tabs","line-width":"24px","line-height":"2px",color:"#1580ff","title-active-color":"#000","title-inactive-color":"#999"},{default:k(()=>[(n(),a(m,null,_(g,e=>p(d(T),{key:e.value,name:e.value,title:e.label},null,8,["name","title"])),64))]),_:1},8,["active"])]),u.value.length?(n(),a("div",D,[(n(!0),a(m,null,_(u.value,e=>(n(),a("section",{key:e.id,class:"rounded-8 bg-white p-15"},[t("div",R,[t("span",B,i(e.studentName),1),t("span",V,"签到:"+i(e.checkinTime),1),o.value==="pending"&&e.waitingMinutes?(n(),a("span",M," 已等待"+i(e.waitingMinutes)+"分钟 ",1)):r("",!0),o.value==="received"?(n(),a("span",q,"已接待")):r("",!0)]),t("div",L,[t("div",$,[t("p",j,[s[1]||(s[1]=t("span",{class:"text-[#666]"},"预约:",-1)),c(" "+i(e.appointmentDate),1)]),t("p",A,[s[2]||(s[2]=t("span",{class:"text-[#666]"},"院校:",-1)),c(" "+i(e.school),1)]),t("p",I,[s[3]||(s[3]=t("span",{class:"text-[#666]"},"咨询:",-1)),c(" "+i(e.consultation),1)])]),o.value==="pending"?(n(),a("button",{key:0,class:"ml-auto shrink-0 border-0 h-32 w-74 rounded-4 bg-[#1580FF] flex items-center justify-center text-[3.25rem] leading-none text-white",type:"button",onClick:G=>h(e)},[...s[4]||(s[4]=[c(" 接待 ",-1),t("img",{class:"w-12 h-5 ml-4",src:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_qianwang.png",alt:""},null,-1)])],8,O)):r("",!0)])]))),128))])):(n(),y(d(b),{key:1,class:"my-auto",image:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_zanwu.png","image-size":[186,144],description:"今日暂无接待记录"}))]))}}),K=z(S,[["__scopeId","data-v-f6c8a443"]]);export{K as default};

View File

@ -0,0 +1 @@
import{d as s,o as r,p as a,q as t,b as i,w as m,u as l,B as c,v as u,z as f}from"./.pnpm.CF2hm8if.js";import{_ as p}from"./index.Bg6AlVg6.js";const d={class:"custom-bg box-border flex h-full min-h-full flex-col items-center px-37 pt-[20vh] bg-[linear-gradient(180deg,#eef4fe_0%,#f5f6f8_42%)]"},b=s({__name:"SubmissionSuccess",setup(x){const n=f(),o=async()=>{await n.replace({name:"teacher-invitation-form"})};return(g,e)=>(r(),a("main",d,[e[1]||(e[1]=t("img",{class:"w-88 h-103",src:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_chenggong.png",alt:""},null,-1)),e[2]||(e[2]=t("h1",{class:"mt-[6.25rem] text-[5rem] font-500 leading-none text-[#000]"},"提交成功",-1)),e[3]||(e[3]=t("p",{class:"mt-[2.5rem] text-[3.5rem] leading-none text-[#666]"},"短信已发送,请查收",-1)),i(l(c),{class:"mt-[7.5rem]! h-[44px]! [--van-button-radius:2rem] [--van-button-normal-font-size:16px] [--van-button-primary-background:#1580ff] [--van-button-primary-border-color:#1580ff]",block:"",type:"primary",onClick:o},{default:m(()=>[...e[0]||(e[0]=[u(" 返回工作台 ",-1)])]),_:1})]))}}),h=p(b,[["__scopeId","data-v-a3094bb5"]]);export{h as default};

View File

@ -0,0 +1 @@
import{P as e}from"./PagePlaceholder.CH5iciU9.js";import{d as o,o as t,c as s}from"./.pnpm.CF2hm8if.js";import"./index.Bg6AlVg6.js";const m=o({__name:"SubmissionSuccess",setup(c){return(r,a)=>(t(),s(e,{section:"家长端",title:"信息提交成功",description:"家长提交的参观信息已记录,当前页面用于展示本次登记的确认结果。"}))}});export{m as default};

View File

@ -0,0 +1 @@
import{_ as x}from"./right-arrow.Ca_LZHAz.js";import{d as z,n as I,o as P,p as R,q as u,b as t,w as c,u as n,F as q,I as H,r as o,z as M,t as v,B as j,v as E,x as f,s as _}from"./.pnpm.CF2hm8if.js";import{V as L,_ as O,S as Y,s as A}from"./invitationOptions.C9q7AXPg.js";import{_ as G}from"./index.Bg6AlVg6.js";const J={class:"teacher-invitation-form min-h-full overflow-hidden bg-[#f3f5f8] text-[#111]"},K={class:"mx-15 mt-15 overflow-hidden rounded-15 bg-white px-15 pt-20"},Q={class:"px-28 my-30"},W={class:"m-0 flex items-center justify-center gap-5 px-16 py-10 text-[3rem] text-[#86909C]"},X=z({__name:"TeacherHome",setup(Z){const k=M(),p=o(""),i=o(""),d=o(null),g=o(""),r=o(null),h=o(""),s=o(null),S=o(!1),w=o(!1),V=o(!1),b=o(!1),N=(a,e)=>{h.value=e.label},C=(a,e)=>{g.value=e.label},y=f(()=>!!h.value&&A.includes(h.value)),m=f(()=>r.value===null||y.value),U=f(()=>r.value===null?"请先选择咨询学段":y.value?"当前学段无需填写":"请选择毕业院校");I(m,a=>{a&&(d.value=null,g.value="")});const D=f(()=>{if(!s.value)return"";const a=s.value.getFullYear(),e=String(s.value.getMonth()+1).padStart(2,"0"),l=String(s.value.getDate()).padStart(2,"0");return`${a}-${e}-${l}`}),$=a=>a.replace(/\D/g,"").slice(0,11),F=f(()=>!(!p.value.trim()||!i.value.trim()||r.value===null||!m.value&&d.value===null||s.value===null)),B=()=>{m.value||(S.value=!0)},T=async()=>{const a=p.value.trim()?i.value.trim()?r.value===null?"咨询学段不能为空":!m.value&&d.value===null?"毕业院校不能为空":s.value===null?"到访日期不能为空":"":"联系方式不能为空":"学生姓名不能为空";if(a){_(a);return}if(!/^1[3-9]\d{9}$/.test(i.value)){_("请输入正确的家长手机号");return}b.value=!0;try{await k.push({name:"teacher-submission-success"})}finally{b.value=!1}};return(a,e)=>(P(),R("main",J,[u("section",K,[t(n(q),{"label-align":"top","scroll-to-error":"",onSubmit:T},{default:c(()=>[t(n(v),{modelValue:p.value,"onUpdate:modelValue":e[0]||(e[0]=l=>p.value=l),class:"teacher-invitation-form__field",name:"studentName",label:"学生姓名",placeholder:"请输入学生姓名",autocomplete:"name",border:!1,clearable:""},null,8,["modelValue"]),t(n(v),{modelValue:i.value,"onUpdate:modelValue":e[1]||(e[1]=l=>i.value=l),class:"teacher-invitation-form__field",name:"contactNumber",label:"联系方式",placeholder:"请输入家长手机号",type:"tel",inputmode:"numeric",autocomplete:"tel",clearable:"",maxlength:"11",border:!1,formatter:$},null,8,["modelValue"]),t(n(v),{"model-value":h.value,class:"teacher-invitation-form__field",name:"stage",label:"咨询学段",placeholder:"请选择咨询学段",readonly:"",clickable:"",border:!1,"aria-haspopup":"dialog",onClick:e[2]||(e[2]=l=>w.value=!0)},{"right-icon":c(()=>[...e[10]||(e[10]=[u("img",{class:"h-18 w-9",src:x,alt:"","aria-hidden":"true"},null,-1)])]),_:1},8,["model-value"]),t(n(v),{"model-value":g.value,class:"teacher-invitation-form__field",name:"school",label:"毕业院校",placeholder:U.value,readonly:"",clickable:"",disabled:m.value,border:!1,"aria-haspopup":"dialog",onClick:B},{"right-icon":c(()=>[...e[11]||(e[11]=[u("img",{class:"h-18 w-9",src:x,alt:"","aria-hidden":"true"},null,-1)])]),_:1},8,["model-value","placeholder","disabled"]),t(n(v),{"model-value":D.value,class:"teacher-invitation-form__field",name:"visitDate",label:"到访日期",placeholder:"请选择到访日期",readonly:"",clickable:"",border:!1,"aria-haspopup":"dialog",onClick:e[3]||(e[3]=l=>V.value=!0)},{"right-icon":c(()=>[...e[12]||(e[12]=[u("img",{class:"h-18 w-9",src:x,alt:"","aria-hidden":"true"},null,-1)])]),_:1},8,["model-value"]),u("div",Q,[t(n(j),{class:"teacher-invitation-form__submit",block:"",type:"primary","native-type":"submit",loading:b.value,"loading-text":"提交中...",disabled:b.value||!F.value},{default:c(()=>[...e[13]||(e[13]=[E(" 提交 ",-1)])]),_:1},8,["loading","disabled"])])]),_:1})]),u("p",W,[t(n(H),{class:"shrink-0",name:"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_tishi.png","aria-hidden":"true"}),e[14]||(e[14]=u("span",null,"二维码自到访当日起3天内有效",-1))]),t(L,{modelValue:s.value,"onUpdate:modelValue":e[4]||(e[4]=l=>s.value=l),show:V.value,"onUpdate:show":e[5]||(e[5]=l=>V.value=l)},null,8,["modelValue","show"]),t(O,{modelValue:r.value,"onUpdate:modelValue":e[6]||(e[6]=l=>r.value=l),show:w.value,"onUpdate:show":e[7]||(e[7]=l=>w.value=l),title:"咨询学段",onSelect:N},null,8,["modelValue","show"]),t(Y,{modelValue:d.value,"onUpdate:modelValue":e[8]||(e[8]=l=>d.value=l),show:S.value,"onUpdate:show":e[9]||(e[9]=l=>S.value=l),title:"毕业院校","page-size":20,onSelect:C},null,8,["modelValue","show"])]))}}),oe=G(X,[["__scopeId","data-v-c7de0471"]]);export{oe as default};

View File

@ -0,0 +1 @@
import{d,o as c,p as u,q as s,b as n,u as e,R as _,c as f,w as t,J as h,D as m,y as p,v as i,L as r}from"./.pnpm.CF2hm8if.js";import{_ as b}from"./index.Bg6AlVg6.js";const w={class:"box-border flex h-full min-h-0 w-full flex-col overflow-hidden bg-[#f5f7f6]"},v={class:"min-h-0 flex-1 overflow-y-auto"},g=["src"],x=["src"],y=d({__name:"TeacherLayout",setup(z){const l=p();return(k,o)=>(c(),u("div",w,[s("div",v,[n(e(_))]),e(l).meta.hideTabbar?m("",!0):(c(),f(e(h),{key:0,route:"",placeholder:"","safe-area-inset-bottom":"","active-color":"#1989fa","inactive-color":"#646566","z-index":100},{default:t(()=>[n(e(r),{to:{name:"teacher-invitation-form"}},{icon:t(({active:a})=>[s("img",{class:"h-22 w-22",src:a?"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tab_biaodan_set.png":"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tab_biaodan_def.png",alt:""},null,8,g)]),default:t(()=>[o[0]||(o[0]=i(" 表单 ",-1))]),_:1}),n(e(r),{to:{name:"teacher-mine"}},{icon:t(({active:a})=>[s("img",{class:"h-22 w-22",src:a?"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tab_wode_set.png":"https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tab_wode_def.png",alt:""},null,8,x)]),default:t(()=>[o[1]||(o[1]=i(" 我的 ",-1))]),_:1})]),_:1}))]))}}),V=b(y,[["__scopeId","data-v-5b6bffe6"]]);export{V 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

View File

@ -0,0 +1 @@
const o=""+new URL("../images/logo.png",import.meta.url).href;export{o as _};

View File

@ -0,0 +1 @@
const r=""+new URL("../images/right-arrow.png",import.meta.url).href;export{r as _};

View File

@ -0,0 +1 @@
import{P as e}from"./PagePlaceholder.CH5iciU9.js";import{d as o,o as t,c as r}from"./.pnpm.CF2hm8if.js";import"./index.Bg6AlVg6.js";const m=o({__name:"unauthorized",setup(a){return(c,n)=>(t(),r(e,{section:"系统提示",title:"无访问权限",description:"当前企业微信账号没有访问该页面的权限。"}))}});export{m as default};

1560
docs/api.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,154 @@
# 前端架构与关键决策(招生协作 H5
> 面向后续开发 / AI 的speedrun 文档。记录本项目前端的结构、约定和「为什么这么做」,以及待后端配合的事项。配套接口文档见 [api.md](./api.md)。
## 技术栈
- Vue 3 `<script setup>` + TypeScript + Vue Routerhistory 模式)
- Pinia`pinia-plugin-persistedstate` 持久化,存储经 AES 加密,见 `src/store/secureStore.ts`
- Vant 4 组件库(按需引入,每个组件手动 `import "vant/es/xxx/style"`
- UnoCSS`preset-wind4` + `preset-rem-to-px`
- 企业微信 JS-SDK`@wecom/jssdk`
- 运行环境:企业微信 / 微信 webview老内核 X5/TBS 兼容要求高)
---
## 1. HTTP 层
**已弃用 axios**,统一用 fetch 封装。不要再引入 axios 或恢复 `customAxios.ts` / `encryptUrl.ts`(已删除)。
- **`src/api/customFetch.ts`** — 纯粹的请求组装:拼 headers / body / query、相对路径拼 `baseUrl`、跑拦截器管线。导出 `getRequest / postRequest / putRequest / deleteRequest`,均返回 `Promise<ApiResponse>`
- **`src/api/interceptors.ts`** — 类 axios 的拦截器注册中心,`interceptors.request/response/error.use()`。内置默认拦截器:
- 请求:注入 `Authorization: Bearer <token>`token 取自 `useUserStore().getToken`,本项目 store 字段是 `token`,不是 accessToken`FormData` 直传不设 Content-Type非 GET JSON body 自动加 `application/json`
- 全局 loading并发计数 + 800ms 延迟才显示 Vant loading toast`options.silent` 可跳过。
- 响应:非 2xx 统一 `showToast` 报错并短路返回 `{error, code}`;业务码 `result.code === 2``location.reload()`(登录失效)。
- 网络异常:关 loading + toast + 返回 `{error, code:-1}`
- 扩展新行为请用 `interceptors.*.use()`,不要写进 customFetch。
- **`src/api/fetchUrl.ts`** — 所有接口 URL 构造函数集中在此。`baseUrl = import.meta.env.VITE_API_BASE_URL ?? "https://api.zsfz.jinzejk.com"`。开发环境不设该变量 → 走相对 `/api`,由 `vite.config.ts` 的 dev proxy 转发到内网后端;生产由 `.env.production` 注入域名。
**Admin.NET 统一响应**`{ code, type, message, result, extras, time }``code === 200` 为成功,业务数据在 `result`。各 API 服务里用 `unwrap``result`
---
## 2. API 服务层
- **`src/api/admission.ts`** — 招生辅助数据:
- `fetchConsultStages()``GET /options/consult-stages`,返回排序后的 `{label,value,disabled}[]`
- `fetchSchoolRegions()``GET /school/regions`,返回 `{label, value(=region 精确筛选值)}[]`
- `searchSchools({page,pageSize,region,keyword})``POST /school/search`,分页。
- (历史上有过 `fetchInvitationFormOptions`/`fetchAllSchools`,已废弃删除——改为各选择器按需请求。)
- **`src/api/wecom.ts`** — `getWecomSignature(url, type)`:换取企业微信 JS-SDK 签名,见第 5 节。
---
## 3. 选择器组件「数据自取」
要点:**下拉/选择器的数据请求收敛在组件内部**父页面TeacherHome / 家长端 InvitationForm不再重复写取数逻辑`v-model` 绑值 + `@select` 拿回显示名。
- **`SingleSelectPopup.vue`**(咨询学段):
- `onBeforeMount` 里请求一次 `fetchConsultStages()`(不用 `watch(show)`——被 keep-alive 缓存时不会随激活重复请求)。
- 仍保留通用 `options` prop父级显式传 `options` 就用外部的、不请求(保持组件通用性)。
- **`SchoolSelectPopup.vue`**(毕业院校):
- 区域列表在 `onBeforeMount` 请求一次(`loadRegions()` 带 in-flight 去重)。
- **兜底**:区域为空(首次请求失败)时,`watch(show)` 在打开时补拉一次;补齐且列表确空已结束时才 `resetList()` 触发加载(避免与 VanList 自动加载重复)。
- 学校**按区域服务端分页**`VanList` 的 `@load` 每次请求下一页;切换区域 / 搜索 → `resetList` 重载;搜索态跨区域查询(不带 region
- 选中通过 `@select` 回传 `{label,value}`,父级存 `selectedSchoolName` 用于字段展示(父级不再持有完整学校列表)。
父页面里「毕业院校是否必填」的规则用 `schoolOptionalStageLabels``src/views/teacher/invitationOptions.ts`)按**学段显示名**匹配(不依赖后端 value 是编码还是中文)。
---
## 4. 路由与布局
- 两套布局 + 底部 tabbar
- `TeacherLayout``/teacher`):表单 / 我的。
- `ReceptionLayout``/reception`):任务 / 我的。
- tabbar 图标用 OSS 图片按 active 态切换(`#icon="{ active }"` 插槽)。
- 主要页面:教师端 `TeacherHome`(邀约表) / `TeacherMine` / `InvitationData`;接待端 `ReceptionTask` / `ReceptionMine` / `ReceptionData` / `ReceptionRecord`;家长端(公开路由 `/invite/:token``InvitationForm` / `SubmissionSuccess` / `PassCode`
- `router/index.ts``userRole` 目前写死 `"admin"`,登录 / 角色路由待接(见待办)。
---
## 5. 企业微信 JS-SDK`ww.register`
`main.ts` 初始化(`app.use(store/router)` 之后、`mount` 之前)。未配置 `corpId` 或非微信/企微环境时跳过。
```ts
register({
corpId: VITE_WECOM_CORP_ID,
agentId: VITE_WECOM_AGENT_ID,
jsApiList: ['shareWechatMessage','shareAppMessage','shareToExternalContact','getLocation','openLocation'],
getConfigSignature: (url) => getWecomSignature(url, 'config'),
getAgentConfigSignature: (url) => getWecomSignature(url, 'agent'),
})
```
### corpId / agentId / Secret 的区别
| 值 | 级别 | 从哪拿 | 放哪 |
|---|---|---|---|
| corpId企业ID | 企业级,所有自建应用共用 | 管理后台 → 我的企业 → 企业信息 → 底部「企业ID」 | 前端 `VITE_WECOM_CORP_ID`(非敏感) |
| AgentId | 每个应用不同 | 应用管理 → 该应用页 | 前端 `VITE_WECOM_AGENT_ID`(非敏感) |
| Secret | 每个应用不同 | 应用页 | **只放后端**,绝不进前端 |
### getConfigSignature vs getAgentConfigSignature —— 后端要做什么
两者都是「对当前页面 URL 生成 JS-SDK 签名」,**算法完全相同,只是用的 jsapi_ticket 不同**
- **getConfigSignature → `wx.config`(企业级鉴权)**:用**企业级** jsapi_ticket。基础 JS-SDK 能力的入口,必须先成功。
- **getAgentConfigSignature → `agentConfig`(应用级鉴权)**:用**应用级** jsapi_ticket。企业微信很多「应用级」接口`shareToExternalContact`、外部联系人/客户群相关,以及需要应用身份的分享)必须在 config 之后再做 agentConfig 才能调用。`@wecom/jssdk` 的 `register` 会自动先 config 后 agentConfig。
**后端每次收到 `{ url, type }` 要做的**`type` = `config` | `agent`
1. 用 `corpId + 应用Secret``GET /cgi-bin/gettoken``access_token`(缓存 ~7200s
2. 用 access_token 取对应 jsapi_ticket都缓存 ~7200s
- `type=config``GET /cgi-bin/get_jsapi_ticket?access_token=...`**企业** ticket
- `type=agent``GET /cgi-bin/ticket/get?access_token=...&type=agent_config`**应用** ticket
3. 生成 `noncestr`、`timestamp`;取前端传来的 `url`**去掉 `#` 及其后面部分**)。
4. `signature = sha1("jsapi_ticket=<ticket>&noncestr=<nonce>&timestamp=<ts>&url=<url>")`(小写十六进制)。
5. 返回 `{ timestamp, nonceStr, signature }`
**关键坑**
- 签名的 `url` **必须**是前端传来的当前页面 URL工厂参数不能用固定值。SPA/iOS 上企业微信按「进入页 URL」签名——若线上 iOS 报签名无效,需前端在入口缓存 URL 后传给后端。
- 一个应用的 Secret 就能算出这两种 ticket企业 ticket 和 agent_config ticket 都用该应用的 access_token 换)。
- 签名接口本身不依赖用户 JWT是企业/应用级),可做成公开或轻鉴权;前端 `getWecomSignature``postRequest`,若已登录会自动带 token未登录则不带。
- 前端签名接口路径暂定 `POST /api/admission/wecom/jsapi-signature``fetchUrl.ts` 的 `getWecomSignatureUrl`**待与后端确认**。
### 已接的 JS-SDK 能力
- `TeacherMine.shareToParent()``shareWechatMessage`(转发到微信,把邀约链接发给家长);未 register 成功时 catch 降级为「右上角菜单分享」提示。
- `TeacherMine.saveImage()``previewImage`打开原生图片预览长按保存到相册。webview 里 `<a download>` 无效,故改用它;未 config / 不支持时降级提示长按保存。**注意**WeChat/WeCom 的 `previewImage` 对 base64 dataURL 支持不稳定(官方要求 http/https二维码目前是前端 canvas 生成的 dataURL——要稳妥保存应让后端把二维码作为可公网访问的图片 URL 返回(或用 `uploadImage` 换 URL再传给 previewImage。
- `PassCode.openSchoolLocation()``openLocation`(导航到校)。
---
## 6. 样式 / 资源约定
- **UnoCSS px 缩放**`preset-rem-to-px` baseFontSize=4 + spacing DEFAULT=1px → 「数字即 px」。间距类 `px-15`=15px、`rounded-12`=12px、`h-22`=22px字号用 `text-[Nrem]`N×4=px`text-[4.5rem]`=18px。渐变/复杂值用字面量 `bg-[linear-gradient(...)]`**别用** preset-wind4 的 `from-*/to-*`(会生成 oklab/CSS 变量,老版微信 webview 不支持)。
- **远程图片资源**:用户给的 OSS 图片 URL`lw-zk.oss-cn-hangzhou.aliyuncs.com/...`**直接引用,不要下载到 `public/images`**。验证 URL / 读尺寸可临时 curl用完删。
- **字体**`public/fonts/din-bold.ttf` 已在 `src/style.css` 注册 `@font-face`family `DIN-Bold`),用全局类 `.din-bold`(故意不叫 `font-din`,避开 UnoCSS `font-` 命名空间)给数字用。
- **禁用态按钮**:邀约表单提交按钮未填完/提交中禁用,禁用色 `#A1CCFF`(覆盖 Vant 默认半透明降级)。
---
## 7. 本地测试(后端未就绪)
分两层:
**① 普通页面 / 表单流程(浏览器即可测)** —— 用 dev-only 的 mock 中间件拦截接口,不需要真后端:
- `plugins/mockServer.ts`Vite 插件(`apply: "serve"`mock 了 `options/consult-stages`、`school/regions`、`school/search`(含分页 + 关键字过滤,「市中区」特意补到 24 条便于测 VanList 滚动分页)。返回 Admin.NET 统一响应结构。
- 开关:`.env` 里 `VITE_USE_MOCK=true``vite.config.ts` 才挂载该插件;后端就绪后置 `false` 或删除。
- **前提**dev 的 `VITE_API_BASE_URL` 必须**留空**(走相对 `/api`才能被本地中间件拦截若填了绝对域名请求会直连该域名、mock 拦不到。线上域名放在 `.env.production`
- 用法:`pnpm dev` 打开表单页 → 咨询学段 / 毕业院校下拉、区域切换、滚动加载、搜索都能跑。接待端页面本就用本地 mock ref直接可看。
**② 企业微信能力(分享 / 导航)** —— 普通浏览器测不了真实调起:
- 浏览器里 `env.isWeCom` 为 false → `main.ts` 跳过 `register``shareToParent` / `openLocation` 走 catch 降级 toast页面不报错、可验证 UI。
- 真正调起需在**企业微信开发者工具**或真机企微里打开,且后端签名接口就绪 + 可信域名配置好。
## 8. 待办 / 需后端或配置配合
- [ ] 填 `VITE_WECOM_CORP_ID` / `VITE_WECOM_AGENT_ID``.env.production`)。
- [ ] 后端实现 JS-SDK 签名接口config 用企业 ticket、agent 用 agent_config ticket确认路径。
- [ ] 登录流程:企业微信免登 / Mock 登录(`POST /api/admission/wecom/login``code` 可传 `MOCK:<SysUser.Id>`),拿 `accessToken``userStore.setToken``router/index.ts` 按角色路由(现写死 admin
- [ ] 各页面 mock 数据替换为真实接口:教师端提交邀约(`invitation/create`)、接待端任务(`checkin/reception/*`)、接待数据 / 记录、数据看板等。
- [ ] 家长端 `/invite/:token` 是公开链接、无用户 token`consult-stages`/`school/*` 等辅助数据接口按现有文档需登录授权,需后端提供按 token 的公开取数方式,否则家长端下拉取不到数据。
- [ ] 分享缩略图 `imgUrl` 换成公网可访问的线上 logo`link` 域名要在企业微信后台配为可信域名。

View File

@ -15,7 +15,6 @@
"@types/crypto-js": "^4.2.2",
"@types/node": "^22.10.1",
"@wecom/jssdk": "^2.4.2",
"axios": "^1.7.9",
"crypto-js": "^4.2.0",
"easyqrcodejs": "^4.6.2",
"pinia": "^2.3.0",

124
plugins/mockServer.ts Normal file
View File

@ -0,0 +1,124 @@
import type { Plugin } from "vite";
import type { IncomingMessage, ServerResponse } from "node:http";
/*
* mock Admin.NET
* dev serve apply: "serve" vite.config VITE_USE_MOCK
* .env VITE_USE_MOCK /false
*/
// value 为后端整数枚举(见 docs/api.md 10.8),前端按整数判断学段。
const CONSULT_STAGES = [
{ label: "高中", value: 1, sort: 1, disabled: false },
{ label: "职教高考", value: 2, sort: 2, disabled: false },
{ label: "综合高中", value: 3, sort: 3, disabled: false },
{ label: "小学", value: 4, sort: 4, disabled: false },
{ label: "初中", value: 5, sort: 5, disabled: false },
{ label: "国际/出国", value: 6, sort: 6, disabled: false },
{ label: "复读", value: 7, sort: 7, disabled: false },
];
const REGION_SCHOOLS: Record<string, string[]> = {
: ["济南市历城第二中学", "济南稼轩学校", "济南市历城区洪家楼高级中学"],
: [
"济南协和双语实验学校(初中部)",
"济南市安生学校(初中部)",
"济南高新区海川中学(民办)",
"济南高新区瀚阳学校(初中部)",
"济南高新区凤凰路学校",
"济南高新区奥体中路学校",
"济南高新区伯乐实验学校",
"济南市育英中学",
"山东省济南第三中学",
],
: ["山东省济南第九中学", "济南市槐荫中学", "济南市济微中学"],
: ["山东省济南第十一中学", "济南市天桥区泺口实验学校"],
: ["山东省平阴县第一中学", "平阴县实验高级中学"],
: ["济南市长清第一中学", "济南市长清中学"],
: ["山东省章丘第四中学", "济南市章丘区实验中学"],
: ["山东省济北中学", "济阳区第一中学"],
};
// 给「市中区」补一些学校,方便测试 VanList 滚动分页(超过 1 页)。
for (let i = 1; i <= 15; i++) {
REGION_SCHOOLS["市中区"].push(`济南市中区实验学校(分校${i}`);
}
const REGION_ORDER = Object.keys(REGION_SCHOOLS);
const REGIONS = REGION_ORDER.map((region, index) => ({
region,
label: region,
schoolCount: REGION_SCHOOLS[region].length,
sort: index + 1,
}));
let autoId = 0;
const ALL_SCHOOLS = REGION_ORDER.flatMap((region) => REGION_SCHOOLS[region].map((schoolName) => ({ id: ++autoId, region, schoolName })));
function sendResult(res: ServerResponse, result: unknown): void {
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.statusCode = 200;
res.end(JSON.stringify({ code: 200, type: "success", message: "", result, extras: null, time: new Date().toISOString() }));
}
function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {
return new Promise((resolve) => {
let raw = "";
req.on("data", (chunk) => (raw += chunk));
req.on("end", () => {
try {
resolve(raw ? JSON.parse(raw) : {});
} catch {
resolve({});
}
});
});
}
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
export function mockServer(): Plugin {
return {
name: "admission-mock-server",
apply: "serve",
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const path = (req.url || "").split("?")[0];
const method = req.method || "GET";
const handler =
method === "GET" && path === "/api/admission/options/consult-stages"
? () => sendResult(res, CONSULT_STAGES)
: method === "GET" && path === "/api/admission/school/regions"
? () => sendResult(res, REGIONS)
: method === "POST" && path === "/api/admission/school/search"
? async () => {
const body = await readJson(req);
const page = Math.max(1, Number(body.page) || 1);
const pageSize = Math.min(100, Math.max(1, Number(body.pageSize) || 20));
let list = ALL_SCHOOLS;
if (body.region) {
list = list.filter((school) => school.region === body.region);
}
if (body.keyword) {
list = list.filter((school) => school.schoolName.includes(String(body.keyword)));
}
const total = list.length;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const items = list.slice((page - 1) * pageSize, page * pageSize);
sendResult(res, { page, pageSize, total, totalPages, items, hasPrevPage: page > 1, hasNextPage: page < totalPages });
}
: null;
if (!handler) {
next();
return;
}
await delay(250); // 模拟网络延迟
await handler();
});
},
};
}

View File

@ -17,9 +17,6 @@ importers:
'@wecom/jssdk':
specifier: ^2.4.2
version: 2.4.2
axios:
specifier: ^1.7.9
version: 1.15.0
crypto-js:
specifier: ^4.2.0
version: 4.2.0
@ -1065,9 +1062,6 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
atob@2.1.2:
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
engines: {node: '>= 4.5.0'}
@ -1077,9 +1071,6 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
axios@1.15.0:
resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@ -1154,10 +1145,6 @@ packages:
colorjs.io@0.5.2:
resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
@ -1239,10 +1226,6 @@ packages:
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
destr@2.0.5:
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
@ -1383,15 +1366,6 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
follow-redirects@1.16.0:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@ -1400,10 +1374,6 @@ packages:
resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==}
engines: {node: '>=0.10.0'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
fragment-cache@0.2.1:
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
engines: {node: '>=0.10.0'}
@ -1770,14 +1740,6 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'}
@ -1963,10 +1925,6 @@ packages:
engines: {node: '>=14'}
hasBin: true
proxy-from-env@2.1.0:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
@ -3334,22 +3292,12 @@ snapshots:
async-function@1.0.0: {}
asynckit@0.4.0: {}
atob@2.1.2: {}
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
axios@1.15.0:
dependencies:
follow-redirects: 1.16.0
form-data: 4.0.5
proxy-from-env: 2.1.0
transitivePeerDependencies:
- debug
balanced-match@1.0.2: {}
base@0.11.2:
@ -3455,10 +3403,6 @@ snapshots:
colorjs.io@0.5.2: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
component-emitter@1.3.1: {}
confbox@0.1.8: {}
@ -3533,8 +3477,6 @@ snapshots:
defu@6.1.7: {}
delayed-stream@1.0.0: {}
destr@2.0.5: {}
detect-libc@2.1.2:
@ -3788,22 +3730,12 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
follow-redirects@1.16.0: {}
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
for-in@1.0.2: {}
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fragment-cache@0.2.1:
dependencies:
map-cache: 0.2.2
@ -4188,12 +4120,6 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.2
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@9.0.9:
dependencies:
brace-expansion: 2.1.0
@ -4404,8 +4330,6 @@ snapshots:
prettier@3.4.2: {}
proxy-from-env@2.1.0: {}
quansync@0.2.11: {}
quansync@1.0.0: {}

81
src/api/admission.ts Normal file
View File

@ -0,0 +1,81 @@
import { getRequest, postRequest, type ApiResponse } from "./customFetch";
import { getConsultStagesUrl, getSchoolRegionsUrl, getSchoolSearchUrl } from "./fetchUrl";
/** 咨询学段选项(接口返回) */
export interface ConsultStageDTO {
/** 页面显示文字 */
label: string;
/** 提交到邀约 consultStage 的整数枚举值(见 docs/api.md 10.8 */
value: number;
/** 展示顺序 */
sort: number;
/** 是否禁止选择 */
disabled: boolean;
}
/** 学校区域(不含学校明细) */
export interface SchoolRegionDTO {
region: string;
label: string;
schoolCount: number;
sort: number;
}
/** 学校明细 */
export interface SchoolItemDTO {
id: number;
region: string;
schoolName: string;
}
/** 单选选项(供 SingleSelectPopup 使用) */
export interface SelectOption {
label: string;
/** 咨询学段场景为整数枚举;保留 string 以兼容其它通用单选场景 */
value: string | number;
disabled?: boolean;
}
/** 区域选项(供 SchoolSelectPopup 使用value 为 search 接口的 region 精确筛选值 */
export interface RegionOption {
label: string;
value: string;
}
interface PagedResult<T> {
page: number;
pageSize: number;
total: number;
totalPages: number;
items: T[];
hasPrevPage: boolean;
hasNextPage: boolean;
}
export interface SchoolSearchParams {
page?: number;
pageSize?: number;
region?: string;
keyword?: string;
}
// Admin.NET 统一响应code === 200 时取 result否则视为无数据错误提示已在拦截器处理
const unwrap = <T>(res: ApiResponse): T | null => (res.code === 200 ? (res.result as T) : null);
/** 查询咨询学段,按 sort 排序后转成下拉选项。 */
export const fetchConsultStages = async (): Promise<SelectOption[]> => {
const list = unwrap<ConsultStageDTO[]>(await getRequest(getConsultStagesUrl())) ?? [];
return [...list]
.sort((a, b) => a.sort - b.sort)
.map((stage) => ({ label: stage.label, value: stage.value, disabled: stage.disabled }));
};
/** 查询学校区域,按 sort 排序后转成区域选项value = region 精确筛选值)。 */
export const fetchSchoolRegions = async (): Promise<RegionOption[]> => {
const list = unwrap<SchoolRegionDTO[]>(await getRequest(getSchoolRegionsUrl())) ?? [];
return [...list].sort((a, b) => a.sort - b.sort).map((region) => ({ label: region.label, value: region.region }));
};
/** 分页查询学校。 */
export const searchSchools = async (params: SchoolSearchParams = {}): Promise<PagedResult<SchoolItemDTO> | null> =>
unwrap<PagedResult<SchoolItemDTO>>(await postRequest(getSchoolSearchUrl(), params));

View File

@ -1,205 +0,0 @@
import axios, { AxiosRequestConfig } from 'axios'
import { handleUrl } from './encryptUrl'
// 自定义判断元素类型JS
function toType(obj: any): string {
return {}.toString
.call(obj)
.match(/\s([a-zA-Z]+)/)![1]
.toLowerCase()
}
// 参数过滤函数
function filterNull(o: any) {
for (var key in o) {
if (o[key] === null) {
delete o[key]
}
if (toType(o[key]) === 'string') {
o[key] = o[key].trim()
} else if (toType(o[key]) === 'object') {
o[key] = filterNull(o[key])
} else if (toType(o[key]) === 'array') {
o[key] = filterNull(o[key])
}
}
return o
}
/*
https://cnodejs.org/api/v1 的接口,如果是其他接口
https://cnodejs.org/topic/5378720ed6e2d16149fa16bd
alert
*/
function apiAxios(
method: string,
url: string,
params: null | string | object,
success: any,
failure: any,
unEncrypt: boolean = false // 是否不加密
) {
let contentTypeIsJson = false
if (params && typeof params != 'string') {
params = filterNull(params)
} else contentTypeIsJson = true
axios({
method: method,
url: url,
data: method === 'POST' || method === 'PUT' ? params : null,
params: method === 'GET' || method === 'DELETE' ? params : null,
withCredentials: true,
crossDomain: true,
unEncrypt,
transformRequest: [
function (data) {
if (contentTypeIsJson) return data
let ret = ''
for (let it in data) {
ret +=
encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
},
],
headers: {
'Content-Type': contentTypeIsJson
? 'application/json'
: 'application/x-www-form-urlencoded',
},
} as AxiosRequestConfig<any>)
.then(function (res) {
let response = res.data
if (response.code == 200) {
if (success) {
success(response)
}
} else {
if (failure) {
failure(response)
} else {
if (response.code == 2) {
//错误处理
setTimeout(() => {
location.reload()
}, 1000)
} else {
//错误处理
}
}
}
})
.catch(function (err) {
let res = err.response
console.error(res || err)
if (res) {
// 清楚所有的错误提示
clearTimeout(timeObj)
if (res.data.msg) {
//错误处理
} else {
//错误处理
}
return
}
})
}
let requestCount = 0
let timeObj: NodeJS.Timeout
// http request 拦截器
axios.interceptors.request.use((config) => {
requestCount++
if (requestCount == 1) {
timeObj = setTimeout(() => {
//加载中提示
}, 800)
}
if (
config.data &&
Object.prototype.toString.call(config.data) == '[object FormData]'
) {
config.headers!!['Content-Type'] = 'multipart/form-data;charset=utf-8'
config.transformRequest = [
function (data) {
return data
},
]
}
// 拦截配置有新的配置在这里新增函数处理然后合并config
let _config = handleUrl(config)
config = Object.assign(config, _config)
return config
})
// http response 拦截器
axios.interceptors.response.use((response) => {
requestCount--
if (requestCount === 0) {
setTimeout(() => {
// 关闭所有提示
}, 1500)
clearTimeout(timeObj)
}
return response
},error =>{
// let xhrErrL = { type: "XHRERR", data: error.response };
if (error.response) {
const { status, data } = error.response;
if (status === 422) {
alert(data)
}
}
})
// 返回在vue模板中的调用接口
export default {
get: function (
url: string,
params: string | object | null,
success: any,
failure: any
) {
return apiAxios('GET', url, params, success, failure)
},
post: function (
url: string,
params: string | object,
success: any,
failure: any
) {
return apiAxios('POST', url, params, success, failure)
},
put: function (
url: string,
params: string | object,
success: any,
failure: any
) {
return apiAxios('PUT', url, params, success, failure)
},
delete: function (
url: string,
params: string | object,
success: any,
failure: any
) {
return apiAxios('DELETE', url, params, success, failure)
},
// 不加密的post请求
unEncryptPost: function (
url: string,
params: string | object,
success: any,
failure: any,
) {
return apiAxios('POST', url, params, success, failure,true)
}
}

69
src/api/customFetch.ts Normal file
View File

@ -0,0 +1,69 @@
import { baseUrl } from "./fetchUrl";
import {
runRequestInterceptors,
runResponseInterceptors,
runErrorInterceptors,
type FetchOptions,
type ApiResponse,
type RequestContext,
} from "./interceptors";
// 便于在别处按需注册自定义拦截器 / 复用类型。
export { interceptors } from "./interceptors";
export type { ApiResponse, FetchOptions } from "./interceptors";
/*
* @Description: fetch url / / body
* ./interceptors runRequest/Response/ErrorInterceptors
*/
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
const apiClient = (method: HttpMethod) => {
return async (
url: string,
data?: unknown,
options: FetchOptions = {}
): Promise<ApiResponse> => {
// 取出调用方自定义 headers 与 silent其余 options 原样透传给 fetch。
const { headers: optionHeaders, silent, ...restOptions } = options;
const headers: Record<string, string> = { ...optionHeaders };
const config: RequestContext["config"] = { method, ...restOptions, headers, silent };
if (method !== "GET" && data) {
if (data instanceof FormData) {
// FormData 直接透传,不手动设置 Content-Type浏览器自动带 multipart 边界)。
config.body = data;
} else {
config.body = JSON.stringify(data);
if (!("Content-Type" in headers)) {
headers["Content-Type"] = "application/json";
}
}
} else if (method === "GET" && data) {
const _params = [];
for (const [key, value] of Object.entries(data)) {
_params.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
}
url += `?${_params.join("&")}`;
}
// 相对路径统一拼接 baseUrl已是绝对地址http/https则原样使用。
const requestUrl = /^https?:\/\//i.test(url) ? url : `${baseUrl}${url}`;
const ctx: RequestContext = { url: requestUrl, config };
await runRequestInterceptors(ctx);
try {
const response = await fetch(ctx.url, ctx.config);
const body = await response.json().catch(() => ({}));
return await runResponseInterceptors({ ctx, response, data: body });
} catch (error) {
return await runErrorInterceptors({ ctx, error });
}
};
};
export const getRequest = apiClient("GET");
export const postRequest = apiClient("POST");
export const putRequest = apiClient("PUT");
export const deleteRequest = apiClient("DELETE");

View File

@ -1,12 +0,0 @@
// 对指定请求链接进行加密
import { AxiosRequestConfig } from "axios";
import { useUserStore } from "@/store/user";
export const handleUrl = async (config: (AxiosRequestConfig & {unEncrypt?: boolean})) => {
const userStore = useUserStore()
if(userStore.getToken){
config.headers!!["Authorization"] = `Bearer ${userStore.getToken}`
}
return config;
}

20
src/api/fetchUrl.ts Normal file
View File

@ -0,0 +1,20 @@
// 后端接口基础地址。
// - 开发环境不设置 VITE_API_BASE_URL → baseUrl 为空串,请求走相对路径 /api由 vite.config 的 dev proxy 转发到内网后端。
// - 生产环境由 .env.production 注入线上域名。
export const baseUrl: string = import.meta.env.VITE_API_BASE_URL ?? "https://api.zsfz.shenquanedu.com";
// —— 招生辅助数据 ——
/** 初始化邀约表单:一次返回咨询学段与学校区域 */
export const getInvitationFormOptionsUrl = () => `${baseUrl}/api/admission/options/invitation-form`;
/** 查询咨询学段 */
export const getConsultStagesUrl = () => `${baseUrl}/api/admission/options/consult-stages`;
/** 查询学校区域 */
export const getSchoolRegionsUrl = () => `${baseUrl}/api/admission/school/regions`;
/** 分页查询学校 */
export const getSchoolSearchUrl = () => `${baseUrl}/api/admission/school/search`;
/** 查询学校详情 */
export const getSchoolDetailUrl = (id: string | number) => `${baseUrl}/api/admission/school/detail?id=${id}`;
// —— 企业微信 JS-SDK ——
/** 换取 JS-SDK 签名config / agentConfig后端按当前 url 计算)。路径待与后端确认。 */
export const getWecomSignatureUrl = () => `${baseUrl}/api/admission/wecom/jsapi-signature`;

150
src/api/interceptors.ts Normal file
View File

@ -0,0 +1,150 @@
import { useUserStore } from "@/store/user";
import { showLoadingToast, closeToast, showToast } from "vant";
import "vant/es/toast/style";
/*
* / customFetch 便
* - interceptors.request / response / error use()
* - customFetch customFetch
*/
export interface FetchOptions extends RequestInit {
headers?: Record<string, string>;
/** 跳过全局 loading轮询 / 静默请求等场景) */
silent?: boolean;
}
export interface ApiResponse {
result?: unknown;
error?: string;
code?: number;
success?: boolean;
message?: string;
}
export interface RequestContext {
url: string;
config: RequestInit & { headers: Record<string, string>; silent?: boolean };
}
export interface ResponsePayload {
ctx: RequestContext;
response: Response;
data: any;
}
export interface ErrorPayload {
ctx: RequestContext;
error: unknown;
}
// 请求拦截器可修改 ctx如加 header
// 响应 / 错误拦截器返回 ApiResponse 即作为最终结果短路返回;返回 void 则继续执行后续拦截器。
export type RequestInterceptor = (ctx: RequestContext) => void | Promise<void>;
export type ResponseInterceptor = (payload: ResponsePayload) => ApiResponse | void | Promise<ApiResponse | void>;
export type ErrorInterceptor = (payload: ErrorPayload) => ApiResponse | void | Promise<ApiResponse | void>;
const requestInterceptors: RequestInterceptor[] = [];
const responseInterceptors: ResponseInterceptor[] = [];
const errorInterceptors: ErrorInterceptor[] = [];
/** 拦截器注册入口,可在任意模块 import 后追加自定义拦截器。 */
export const interceptors = {
request: { use: (fn: RequestInterceptor) => requestInterceptors.push(fn) },
response: { use: (fn: ResponseInterceptor) => responseInterceptors.push(fn) },
error: { use: (fn: ErrorInterceptor) => errorInterceptors.push(fn) },
};
export const runRequestInterceptors = async (ctx: RequestContext): Promise<void> => {
for (const fn of requestInterceptors) {
await fn(ctx);
}
};
export const runResponseInterceptors = async (payload: ResponsePayload): Promise<ApiResponse> => {
for (const fn of responseInterceptors) {
const handled = await fn(payload);
if (handled !== undefined) return handled;
}
return payload.data as ApiResponse;
};
export const runErrorInterceptors = async (payload: ErrorPayload): Promise<ApiResponse> => {
let result: ApiResponse = { error: "Network error", code: -1 };
for (const fn of errorInterceptors) {
const handled = await fn(payload);
if (handled !== undefined) result = handled;
}
return result;
};
/* —— 全局 loading并发请求共享超过 800ms 未结束才显示,避免快请求闪烁 —— */
let pendingCount = 0;
let loadingTimer: ReturnType<typeof setTimeout> | null = null;
const startLoading = (): void => {
pendingCount++;
if (pendingCount === 1 && !loadingTimer) {
loadingTimer = setTimeout(() => {
showLoadingToast({ message: "加载中...", forbidClick: true, duration: 0 });
}, 800);
}
};
const stopLoading = (): void => {
pendingCount = Math.max(0, pendingCount - 1);
if (pendingCount === 0) {
if (loadingTimer) {
clearTimeout(loadingTimer);
loadingTimer = null;
}
closeToast();
}
};
/* —— 默认拦截器(对应原 axios 拦截器逻辑)—— */
// 请求:注入 token
interceptors.request.use((ctx) => {
const token = useUserStore().getToken;
if (token && !("Authorization" in ctx.config.headers)) {
ctx.config.headers.Authorization = `Bearer ${token}`;
}
});
// 请求:开启 loading
interceptors.request.use((ctx) => {
if (!ctx.config.silent) startLoading();
});
// 响应:关闭 loading须在错误提示前执行避免把错误 toast 一起关掉)
interceptors.response.use(({ ctx }) => {
if (!ctx.config.silent) stopLoading();
});
// 响应HTTP 错误统一提示并短路返回
interceptors.response.use(({ response, data }) => {
if (!response.ok) {
showToast(data.message || data.error || `请求失败(${response.status})`);
return { error: data.message || "Request failed", code: response.status };
}
});
// 响应:业务码 2登录失效等→ 重新加载
interceptors.response.use(({ data }) => {
if (data && data.code === 2) {
setTimeout(() => location.reload(), 1000);
}
});
// 错误:关闭 loading
interceptors.error.use(({ ctx }) => {
if (!ctx.config.silent) stopLoading();
});
// 错误:网络异常提示 + 统一错误结构
interceptors.error.use(({ error }) => {
showToast("网络异常,请稍后重试");
console.error(error);
return { error: "Network error", code: -1 };
});

18
src/api/wecom.ts Normal file
View File

@ -0,0 +1,18 @@
import { postRequest, type ApiResponse } from "./customFetch";
import { getWecomSignatureUrl } from "./fetchUrl";
import type { SignatureData } from "@wecom/jssdk";
/**
* JS-SDK
* - type=configwx.config
* - type=agentagentConfig
* jsapi_ticket url { timestamp, nonceStr, signature }
*/
export const getWecomSignature = async (url: string, type: "config" | "agent"): Promise<SignatureData> => {
const res: ApiResponse = await postRequest(getWecomSignatureUrl(), { url, type });
const result = (res.code === 200 ? res.result : null) as SignatureData | null;
if (!result) {
throw new Error("获取企业微信 JS-SDK 签名失败");
}
return result;
};

View File

@ -59,9 +59,19 @@
</div>
<div ref="listScroller" class="min-w-0 flex-1 overflow-y-auto">
<VanEmpty v-if="showRegionEmpty" class="h-full" image="search" image-size="108" description="该区域暂无学校" />
<VanList
ref="listRef"
v-model:loading="loading"
:finished="finished"
:error="error"
:finished-text="visibleSchools.length ? finishedText : ''"
error-text="加载失败,点击重试"
:scroller="listScroller ?? undefined"
role="radiogroup"
@load="handleLoad"
@update:error="(value: boolean) => (error = value)">
<VanEmpty v-if="showRegionEmpty" class="h-full" image="search" image-size="108" description="该区域暂无学校" />
<VanList v-else ref="listRef" v-model:loading="loading" :finished="finished" :scroller="listScroller ?? undefined" role="radiogroup" @load="handleLoad">
<button
v-for="school in visibleSchools"
:key="school.value"
@ -70,7 +80,6 @@
type="button"
role="radio"
:aria-checked="isSelected(school)"
:disabled="school.disabled"
@click="handleSelect(school)">
<span class="min-w-0 flex-1">{{ school.label }}</span>
<VanIcon v-if="isSelected(school)" class="shrink-0 text-[5rem]" name="success" aria-hidden="true" />
@ -82,7 +91,7 @@
</template>
<script setup lang="ts">
import { computed, nextTick, ref, useId, watch, type HTMLAttributes } from "vue";
import { computed, nextTick, onBeforeMount, ref, useId, watch, type HTMLAttributes } from "vue";
import {
Empty as VanEmpty,
Icon as VanIcon,
@ -100,24 +109,24 @@
import "vant/es/search/style";
import "vant/es/sidebar/style";
import "vant/es/sidebar-item/style";
import { searchSchools, fetchSchoolRegions } from "@/api/admission";
type SchoolValue = string | number;
interface SchoolOption {
label: string;
value: SchoolValue;
disabled?: boolean;
}
interface SchoolRegion {
/** 页面显示名称 */
label: string;
value: SchoolValue;
schools: readonly SchoolOption[];
/** 区域精确筛选值(对应 /api/admission/school/search 的 region 参数) */
value: string;
}
interface Props {
show: boolean;
regions: readonly SchoolRegion[];
modelValue?: SchoolValue | null;
title?: string;
cancelText?: string;
@ -125,6 +134,7 @@
searchText?: string;
emptyText?: string;
showAllText?: string;
finishedText?: string;
pageSize?: number;
}
@ -136,7 +146,8 @@
searchText: "搜索",
emptyText: "未找到相关学校",
showAllText: "查看全部学校",
pageSize: 10,
finishedText: "没有更多了",
pageSize: 20,
});
const emit = defineEmits<{
@ -155,35 +166,46 @@
const currentPage = ref(0);
const loading = ref(false);
const finished = ref(false);
const error = ref(false);
const listRef = ref<ListInstance | null>(null);
const listScroller = ref<HTMLElement | null>(null);
const componentId = useId();
const titleId = `${componentId}-title`;
const searchInputId = `${componentId}-search`;
const normalizeSearchText = (value: string): string => value.normalize("NFKC").toLocaleLowerCase();
// onBeforeMountkeep-alive
const regions = ref<SchoolRegion[]>([]);
let regionsLoaded: Promise<void> = Promise.resolve();
let regionsLoading = false;
// Promise
const loadRegions = (): Promise<void> => {
if (regionsLoading) {
return regionsLoaded;
}
regionsLoading = true;
regionsLoaded = fetchSchoolRegions()
.then((list) => {
regions.value = list;
})
.finally(() => {
regionsLoading = false;
});
return regionsLoaded;
};
const clampRegionIndex = (index: number): number => {
const lastRegionIndex = Math.max(0, props.regions.length - 1);
const lastRegionIndex = Math.max(0, regions.value.length - 1);
return Math.min(Math.max(0, index), lastRegionIndex);
};
const normalizedPageSize = computed(() => (Number.isFinite(props.pageSize) ? Math.max(1, Math.floor(props.pageSize)) : 10));
const normalizedPageSize = computed(() => (Number.isFinite(props.pageSize) ? Math.max(1, Math.floor(props.pageSize)) : 20));
const isSearchMode = computed(() => submittedKeyword.value.length > 0);
const activeRegion = computed(() => props.regions[activeRegionIndex.value]);
const allSchools = computed<SchoolOption[]>(() => props.regions.flatMap((region) => [...region.schools]));
const activeRegion = computed(() => regions.value[activeRegionIndex.value]);
const listSource = computed<readonly SchoolOption[]>(() => {
if (!isSearchMode.value) {
return activeRegion.value?.schools ?? [];
}
const keyword = normalizeSearchText(submittedKeyword.value);
return allSchools.value.filter((school) => normalizeSearchText(school.label).includes(keyword));
});
const showSearchEmpty = computed(() => isSearchMode.value && listSource.value.length === 0);
const showRegionEmpty = computed(() => !isSearchMode.value && listSource.value.length === 0);
//
const showSearchEmpty = computed(() => isSearchMode.value && finished.value && visibleSchools.value.length === 0);
const showRegionEmpty = computed(() => !isSearchMode.value && finished.value && visibleSchools.value.length === 0);
const resetList = (): void => {
const validRegionIndex = clampRegionIndex(activeRegionIndex.value);
@ -198,36 +220,43 @@
visibleSchools.value = [];
currentPage.value = 0;
loading.value = false;
finished.value = listSource.value.length === 0;
finished.value = false;
error.value = false;
// VanList @load
nextTick(() => listRef.value?.check());
};
const findSelectedRegionIndex = (): number => {
if (props.modelValue === null || props.modelValue === undefined) {
return -1;
// VanList region
const handleLoad = async (): Promise<void> => {
// onBeforeMount
if (!isSearchMode.value) {
await regionsLoaded;
if (!activeRegion.value) {
loading.value = false;
finished.value = true;
return;
}
}
return props.regions.findIndex((region) => region.schools.some((school) => school.value === props.modelValue));
};
const nextPage = currentPage.value + 1;
const paged = await searchSchools({
page: nextPage,
pageSize: normalizedPageSize.value,
keyword: isSearchMode.value ? submittedKeyword.value : undefined,
region: isSearchMode.value ? undefined : activeRegion.value?.value,
});
const initializePopup = (): void => {
searchKeyword.value = "";
submittedKeyword.value = "";
if (!paged) {
// VanList
loading.value = false;
error.value = true;
return;
}
const selectedRegionIndex = findSelectedRegionIndex();
activeRegionIndex.value = selectedRegionIndex >= 0 ? selectedRegionIndex : clampRegionIndex(activeRegionIndex.value);
resetList();
};
const handleLoad = async (): Promise<void> => {
const start = currentPage.value * normalizedPageSize.value;
const end = start + normalizedPageSize.value;
const nextSchools = listSource.value.slice(start, end);
visibleSchools.value.push(...nextSchools);
currentPage.value += 1;
finished.value = end >= listSource.value.length;
visibleSchools.value.push(...paged.items.map((item) => ({ label: item.schoolName, value: item.id })));
currentPage.value = nextPage;
finished.value = !paged.hasNextPage;
await nextTick();
loading.value = false;
@ -251,7 +280,7 @@
};
const handleRegionChange = (index: number): void => {
const region = props.regions[index];
const region = regions.value[index];
if (region) {
emit("region-change", region, index);
}
@ -261,14 +290,10 @@
const getSchoolClass = (school: SchoolOption): HTMLAttributes["class"] => [
isSelected(school) ? "font-medium text-[#1580ff]" : "font-normal text-[#333]",
school.disabled ? "cursor-not-allowed opacity-40" : "cursor-pointer active:bg-[#f7f8fa]",
"cursor-pointer active:bg-[#f7f8fa]",
];
const handleSelect = (school: SchoolOption): void => {
if (school.disabled) {
return;
}
emit("update:modelValue", school.value);
emit("select", school.value, school);
emit("update:show", false);
@ -283,19 +308,26 @@
emit("cancel");
};
watch([activeRegionIndex, submittedKeyword, () => props.pageSize, () => props.regions], resetList, {
deep: true,
immediate: true,
});
// /
watch([activeRegionIndex, submittedKeyword], resetList);
// onBeforeMount watch(show)
// keep-alive VanList
onBeforeMount(loadRegions);
// onBeforeMount
//
watch(
() => props.show,
(isShown) => {
if (isShown) {
initializePopup();
async (isShown) => {
if (!isShown || regions.value.length > 0) {
return;
}
await loadRegions();
if (regions.value.length > 0 && finished.value && visibleSchools.value.length === 0) {
resetList();
}
},
{ immediate: true },
);
</script>

View File

@ -17,7 +17,7 @@
<div class="grid gap-x-10 gap-y-10 px-16 pt-14 pb-24" :class="columnClass" role="radiogroup" :aria-label="title">
<button
v-for="option in options"
v-for="option in displayOptions"
:key="option.value"
class="h-[9rem] rounded-[1.5rem] border-0 px-8 text-[3.5rem] leading-none transition-colors"
:class="getOptionClass(option)"
@ -35,9 +35,10 @@
</template>
<script setup lang="ts">
import { computed } from "vue";
import { computed, onBeforeMount, ref } from "vue";
import { Popup as VanPopup } from "vant";
import "vant/es/popup/style";
import { fetchConsultStages } from "@/api/admission";
type SelectValue = string | number;
type ColumnCount = 1 | 2 | 3 | 4;
@ -50,7 +51,8 @@
interface Props {
show: boolean;
options: readonly SelectOption[];
/** 选项;不传则组件自行请求咨询学段(咨询学段选择场景)。 */
options?: readonly SelectOption[];
modelValue?: SelectValue | null;
title?: string;
cancelText?: string;
@ -58,6 +60,7 @@
}
const props = withDefaults(defineProps<Props>(), {
options: () => [],
modelValue: null,
title: "请选择",
cancelText: "取消",
@ -71,6 +74,12 @@
cancel: [];
}>();
// options
// onBeforeMount watch(show) keep-alive
const fetchedOptions = ref<SelectOption[]>([]);
const displayOptions = computed<readonly SelectOption[]>(() => (props.options.length ? props.options : fetchedOptions.value));
const columnClasses: Record<ColumnCount, string> = {
1: "grid-cols-1",
2: "grid-cols-2",
@ -105,4 +114,11 @@
emit("select", option.value, option);
emit("update:show", false);
};
onBeforeMount(async () => {
if (props.options.length) {
return;
}
fetchedOptions.value = await fetchConsultStages();
});
</script>

View File

@ -0,0 +1,31 @@
// 咨询学段枚举与「毕业院校」必填规则。
// 学段 value 为后端整数枚举(见 docs/api.md 10.8),前端一律按整数判断,不再依赖中文 label。
// 校区由后端根据 consultStage 自动确定,前端不参与。
/** 咨询学段枚举值(= 后端 consultStage 整数枚举) */
export const ConsultStage = {
/** 高中 → 南校 */
senior: 1,
/** 职教高考 → 北校 */
vocational: 2,
/** 综合高中 → 北校 */
comprehensive: 3,
/** 小学 → 南校 */
primary: 4,
/** 初中 → 南校 */
junior: 5,
/** 国际/出国 → 南校 */
international: 6,
/** 复读 → 南校 */
repeat: 7,
} as const;
export type ConsultStageValue = (typeof ConsultStage)[keyof typeof ConsultStage];
// 无需填写「毕业院校」的咨询学段:小学(4)、初中(5)、国际/出国(6)、复读(7)。
// 需要填写的是高中(1)、职教高考(2)、综合高中(3)。
const schoolOptionalStages: readonly number[] = [ConsultStage.primary, ConsultStage.junior, ConsultStage.international, ConsultStage.repeat];
/** 该咨询学段是否无需填写毕业院校(按后端整数枚举判断,不依赖中文 label。 */
export const isSchoolOptionalStage = (stage: number | string | null | undefined): boolean =>
stage != null && stage !== "" && schoolOptionalStages.includes(Number(stage));

View File

@ -0,0 +1,126 @@
import { computed, ref } from "vue";
import { showToast } from "vant";
import "vant/es/toast/style";
import { formatDate, formatPhoneNumber, isValidPhone } from "@/utils/format";
import { useSchoolRequirement } from "./useSchoolRequirement";
/** 校验通过后交给页面提交的邀约数据consultStage 为后端整数枚举)。 */
export interface InvitationFormPayload {
studentName: string;
parentPhone: string;
consultStage: number;
/** 毕业院校名称;当前学段无需填写时为空串 */
schoolName: string;
visitDate: Date;
}
/**
* TeacherHome / InvitationForm
*
* onSubmit /loading
*/
export function useInvitationForm(onSubmit: (payload: InvitationFormPayload) => Promise<void>) {
// —— 字段状态 ——
const studentName = ref("");
const contactNumber = ref("");
const selectedSchool = ref<string | number | null>(null);
const selectedSchoolName = ref("");
const consultationStage = ref<string | number | null>(null);
const selectedStageName = ref("");
const visitDate = ref<Date | null>(null);
// —— 弹层/提交开关 ——
const showSchoolSelector = ref(false);
const showStageSelector = ref(false);
const showCalendar = ref(false);
const submitting = ref(false);
// 学段 ↔ 毕业院校 联动规则
const { isSchoolNotRequired, isSchoolDisabled, schoolPlaceholder } = useSchoolRequirement(consultationStage, selectedSchool, selectedSchoolName);
// 选择器回传显示名(选项数据都在选择器组件内请求,父级不持有列表)。
const onStageSelect = (_value: string | number, option: { label: string; value: string | number }): void => {
selectedStageName.value = option.label;
};
const onSchoolSelect = (_value: string | number, school: { label: string; value: string | number }): void => {
selectedSchoolName.value = school.label;
};
const selectedDateText = computed(() => (visitDate.value ? formatDate(visitDate.value) : ""));
// 必填项是否都已填写完整(决定提交按钮是否可点;手机号格式在提交时再校验)。
const isFormComplete = computed(() => {
if (!studentName.value.trim()) return false;
if (!contactNumber.value.trim()) return false;
if (consultationStage.value === null) return false;
if (!isSchoolDisabled.value && selectedSchool.value === null) return false;
if (visitDate.value === null) return false;
return true;
});
const openSchoolSelector = (): void => {
if (isSchoolDisabled.value) return;
// 学校由选择器按当前区域分页请求VanList 滚动加载),此处只负责打开。
showSchoolSelector.value = true;
};
// 返回首个未通过的校验提示,全部通过返回空串。
const validate = (): string => {
if (!studentName.value.trim()) return "学生姓名不能为空";
if (!contactNumber.value.trim()) return "联系方式不能为空";
if (consultationStage.value === null) return "咨询学段不能为空";
if (!isSchoolDisabled.value && selectedSchool.value === null) return "毕业院校不能为空";
if (visitDate.value === null) return "到访日期不能为空";
if (!isValidPhone(contactNumber.value)) return "请输入正确的家长手机号";
return "";
};
const handleSubmit = async (): Promise<void> => {
const message = validate();
if (message) {
showToast(message);
return;
}
submitting.value = true;
try {
await onSubmit({
studentName: studentName.value.trim(),
parentPhone: contactNumber.value,
consultStage: Number(consultationStage.value),
schoolName: isSchoolDisabled.value ? "" : selectedSchoolName.value,
visitDate: visitDate.value as Date,
});
} finally {
submitting.value = false;
}
};
return {
// 字段状态
studentName,
contactNumber,
selectedSchool,
selectedSchoolName,
consultationStage,
selectedStageName,
visitDate,
// 弹层/提交开关
showSchoolSelector,
showStageSelector,
showCalendar,
submitting,
// 派生状态
isSchoolNotRequired,
isSchoolDisabled,
schoolPlaceholder,
selectedDateText,
isFormComplete,
// 事件处理
onStageSelect,
onSchoolSelect,
formatPhoneNumber,
openSchoolSelector,
handleSubmit,
};
}

View File

@ -0,0 +1,33 @@
import { computed, watch, type Ref } from "vue";
import { isSchoolOptionalStage } from "./invitationOptions";
type StageValue = string | number | null;
type SchoolValue = string | number | null;
/**
*
* -
* - //·/
* - //
*/
export function useSchoolRequirement(consultationStage: Ref<StageValue>, selectedSchool: Ref<SchoolValue>, selectedSchoolName: Ref<string>) {
const isSchoolNotRequired = computed(() => isSchoolOptionalStage(consultationStage.value));
const isSchoolDisabled = computed(() => consultationStage.value === null || isSchoolNotRequired.value);
const schoolPlaceholder = computed(() => {
if (consultationStage.value === null) return "请先选择咨询学段";
if (isSchoolNotRequired.value) return "当前学段无需填写";
return "请选择毕业院校";
});
// 禁用(未选学段或该学段无需填写)时清空已选的毕业院校,避免残留脏数据。
watch(isSchoolDisabled, (disabled) => {
if (disabled) {
selectedSchool.value = null;
selectedSchoolName.value = "";
}
});
return { isSchoolNotRequired, isSchoolDisabled, schoolPlaceholder };
}

View File

@ -6,6 +6,8 @@ import App from './App.vue'
import store from '@/store/index'
//导入路由
import router from '@/router/index'
import { register, env } from '@wecom/jssdk'
import { getWecomSignature } from '@/api/wecom'
const app = createApp(App)
@ -14,4 +16,22 @@ app.use(store)
app.use(router)
// 企业微信 JS-SDK 初始化:注册 config + agentConfig分享 / 定位等能力依赖它。
// corpId / agentId 由环境变量注入;签名由后端按当前 URL 下发(见 @/api/wecom
// 未配置 corpId 或非微信 / 企微环境(普通浏览器调试)时跳过,避免无意义的注册报错。
const wecomCorpId = import.meta.env.VITE_WECOM_CORP_ID as string | undefined
if (wecomCorpId && (env.isWeCom || env.isWeChat)) {
register({
corpId: wecomCorpId,
agentId: import.meta.env.VITE_WECOM_AGENT_ID as string | undefined,
// 需要用到的 JS-SDK 接口,新增能力时在此登记。
jsApiList: ['shareWechatMessage', 'shareAppMessage', 'shareToExternalContact', 'previewImage', 'getLocation', 'openLocation'],
getConfigSignature: (url) => getWecomSignature(url, 'config'),
getAgentConfigSignature: (url) => getWecomSignature(url, 'agent'),
onConfigFail: (err) => console.error('[wecom] config 失败', err),
onAgentConfigFail: (err) => console.error('[wecom] agentConfig 失败', err),
})
}
app.mount('#app')

15
src/utils/format.ts Normal file
View File

@ -0,0 +1,15 @@
// 通用格式化 / 校验纯函数(无状态,供 composable 与页面复用)。
/** Date → `YYYY-MM-DD` */
export const formatDate = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
/** 手机号输入格式化:仅保留数字并截断到 11 位。 */
export const formatPhoneNumber = (value: string): string => value.replace(/\D/g, "").slice(0, 11);
/** 中国大陆手机号校验。 */
export const isValidPhone = (value: string): boolean => /^1[3-9]\d{9}$/.test(value);

View File

@ -23,7 +23,7 @@
statusMeta[record.status].label }}</span>
</div>
<div class="mt-15 text-[3.25rem] text-[#999] flex flex-col gap-row-[8px]">
<div class="mt-15 text-[3.25rem] text-[#333] flex flex-col gap-row-[8px]">
<p class=""> <span class="text-[#666]">预约</span>{{ record.appointmentDate }}</p>
<p class=""> <span class="text-[#666]">院校</span>{{ record.school }}</p>
<p class=""> <span class="text-[#666]">咨询</span>{{ record.consultation }}</p>

View File

@ -1,12 +1,7 @@
<template>
<main
class="box-border flex h-full min-h-full flex-col items-center px-37 pt-[20vh] bg-[linear-gradient(180deg,#eef4fe_0%,#f5f6f8_42%)]">
<div
class="flex size-88 items-center justify-center rounded-full bg-[linear-gradient(180deg,#4a9bff_0%,#1580ff_100%)] [box-shadow:0_10px_20px_rgba(21,128,255,0.25)]"
aria-hidden="true">
</div>
class="custom-bg box-border flex h-full min-h-full flex-col items-center px-37 pt-[20vh] bg-[linear-gradient(180deg,#eef4fe_0%,#f5f6f8_42%)]">
<img class="w-88 h-103" src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_chenggong.png" alt="">
<h1 class="mt-[6.25rem] text-[5rem] font-500 leading-none text-[#000]">提交成功</h1>
<p class="mt-[2.5rem] text-[3.5rem] leading-none text-[#666]">短信已发送请查收</p>
@ -31,3 +26,12 @@
await router.replace({ name: "teacher-invitation-form" });
};
</script>
<style type="scss" scoped>
.custom-bg{
background-image: url("https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/tijiao_BG.png");
background-repeat: no-repeat;
background-position: right top;
background-size: auto 263px;
}
</style>

View File

@ -27,7 +27,7 @@
:formatter="formatPhoneNumber" />
<VanField
:model-value="selectedStageLabel"
:model-value="selectedStageName"
class="teacher-invitation-form__field"
name="stage"
label="咨询学段"
@ -43,7 +43,7 @@
</VanField>
<VanField
:model-value="selectedSchoolLabel"
:model-value="selectedSchoolName"
class="teacher-invitation-form__field"
name="school"
label="毕业院校"
@ -53,7 +53,7 @@
:disabled="isSchoolDisabled"
:border="false"
aria-haspopup="dialog"
@click="!isSchoolDisabled && (showSchoolSelector = true)">
@click="openSchoolSelector">
<template #right-icon>
<img class="h-18 w-9" src="/images/right-arrow.png" alt="" aria-hidden="true" />
</template>
@ -76,7 +76,16 @@
</VanField>
<div class="px-28 my-30">
<VanButton class="teacher-invitation-form__submit" block type="primary" native-type="submit">提交</VanButton>
<VanButton
class="teacher-invitation-form__submit"
block
type="primary"
native-type="submit"
:loading="submitting"
loading-text="提交中..."
:disabled="submitting || !isFormComplete">
提交
</VanButton>
</div>
</VanForm>
</section>
@ -87,108 +96,46 @@
</p>
<VisitDateCalendar v-model="visitDate" v-model:show="showCalendar" />
<SingleSelectPopup v-model="consultationStage" v-model:show="showStageSelector" title="咨询学段" :options="consultationStageOptions" />
<SchoolSelectPopup v-model="selectedSchool" v-model:show="showSchoolSelector" title="毕业院校" :regions="schoolRegions" :page-size="8" />
<SingleSelectPopup v-model="consultationStage" v-model:show="showStageSelector" title="咨询学段" @select="onStageSelect" />
<SchoolSelectPopup v-model="selectedSchool" v-model:show="showSchoolSelector" title="毕业院校" :page-size="20" @select="onSchoolSelect" />
</main>
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { Button as VanButton, Field as VanField, Form as VanForm, Icon as VanIcon, showToast } from "vant";
import { Button as VanButton, Field as VanField, Form as VanForm, Icon as VanIcon } from "vant";
import SchoolSelectPopup from "@/components/SchoolSelectPopup.vue";
import SingleSelectPopup from "@/components/SingleSelectPopup.vue";
import VisitDateCalendar from "@/components/VisitDateCalendar.vue";
import { consultationStageOptions, schoolOptionalStages, schoolRegions } from "./invitationOptions";
import { useInvitationForm } from "@/composables/useInvitationForm";
const router = useRouter();
const studentName = ref("");
const contactNumber = ref("");
const selectedSchool = ref<string | number | null>(null);
const consultationStage = ref<string | number | null>(null);
const visitDate = ref<Date | null>(null);
const showSchoolSelector = ref(false);
const showStageSelector = ref(false);
const showCalendar = ref(false);
const selectedSchoolLabel = computed(() => {
for (const region of schoolRegions) {
for (const school of region.schools) {
if (school.value === selectedSchool.value) {
return school.label;
}
}
}
return "";
});
const selectedStageLabel = computed(() => {
return consultationStageOptions.find((option) => option.value === consultationStage.value)?.label ?? "";
});
const isSchoolNotRequired = computed(() => {
return consultationStage.value !== null && schoolOptionalStages.includes(consultationStage.value as string);
});
const isSchoolDisabled = computed(() => {
return consultationStage.value === null || isSchoolNotRequired.value;
});
const schoolPlaceholder = computed(() => {
if (consultationStage.value === null) {
return "请先选择咨询学段";
}
if (isSchoolNotRequired.value) {
return "当前学段无需填写";
}
return "请选择毕业院校";
});
watch(isSchoolDisabled, (disabled) => {
if (disabled) {
selectedSchool.value = null;
}
});
const selectedDateText = computed(() => {
if (!visitDate.value) {
return "";
}
const year = visitDate.value.getFullYear();
const month = String(visitDate.value.getMonth() + 1).padStart(2, "0");
const day = String(visitDate.value.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
});
const formatPhoneNumber = (value: string): string => value.replace(/\D/g, "").slice(0, 11);
const handleSubmit = async (): Promise<void> => {
const emptyFieldMessage = (() => {
if (!studentName.value.trim()) return "学生姓名不能为空";
if (!contactNumber.value.trim()) return "联系方式不能为空";
if (consultationStage.value === null) return "咨询学段不能为空";
if (!isSchoolDisabled.value && selectedSchool.value === null) return "毕业院校不能为空";
if (visitDate.value === null) return "到访日期不能为空";
return "";
})();
if (emptyFieldMessage) {
showToast(emptyFieldMessage);
return;
}
if (!/^1[3-9]\d{9}$/.test(contactNumber.value)) {
showToast("请输入正确的家长手机号");
return;
}
//
const {
studentName,
contactNumber,
selectedSchool,
selectedSchoolName,
consultationStage,
selectedStageName,
visitDate,
showSchoolSelector,
showStageSelector,
showCalendar,
submitting,
isSchoolDisabled,
schoolPlaceholder,
selectedDateText,
isFormComplete,
onStageSelect,
onSchoolSelect,
formatPhoneNumber,
openSchoolSelector,
handleSubmit,
} = useInvitationForm(async (_payload) => {
// _payload consultStage
await router.push({ name: "teacher-submission-success" });
};
});
</script>
<style scoped>
@ -256,6 +203,14 @@
border-radius: 8px;
--van-button-normal-font-size: 16px;
}
/* 未填写完整 / 提交中:禁用态用浅蓝 #A1CCFF覆盖 Vant 默认的半透明降级)。 */
:global(.teacher-invitation-form .teacher-invitation-form__submit.van-button--disabled) {
opacity: 1;
background: #a1ccff;
border-color: #a1ccff;
color: #fff;
}
:deep(.van-field--disabled .van-field__label){
--van-field-disabled-text-color: #000;
}

View File

@ -76,6 +76,7 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { env, previewImage, shareWechatMessage } from "@wecom/jssdk";
import { Button as VanButton, showToast } from "vant";
import "vant/es/button/style";
import "vant/es/toast/style";
@ -183,18 +184,47 @@
router.push({ name: "teacher-invitation-data" });
};
const shareToParent = (): void => {
// JS-SDK updateAppMessageShareData /
showToast("请点击右上角菜单分享给家长");
const shareToParent = async (): Promise<void> => {
//
// ww.registerconfig + agentConfigjsApiList shareWechatMessage
if (!env.isWeCom) {
showToast("请在企业微信中打开后分享给家长");
return;
}
try {
await shareWechatMessage({
title: `${teacherName.value}老师邀请您参观`,
desc: "点击填写学生信息,预约到校参观",
link: invitationLink,
// 访线 logo
imgUrl: `${window.location.origin}/images/logo.png`,
});
} catch {
// register / agentConfig
showToast("请点击右上角菜单分享给家长");
}
};
const saveImage = (): void => {
const saveImage = async (): Promise<void> => {
if (!qrCodeDataUrl.value) {
showToast("邀约码尚未生成");
return;
}
// / webview
// / webview <a download> JS-SDK
// ww.registerconfigjsApiList previewImage
if (env.isWeCom || env.isWeChat) {
try {
await previewImage({ current: qrCodeDataUrl.value, urls: [qrCodeDataUrl.value] });
} catch {
// config / dataURL
showToast("请长按二维码保存到相册");
}
return;
}
//
const link = document.createElement("a");
link.href = qrCodeDataUrl.value;
link.download = "invitation-qrcode.png";

View File

@ -1,88 +0,0 @@
export const consultationStageOptions = [
{ label: "高中", value: "senior" },
{ label: "职教高考", value: "vocational" },
{ label: "综合高中班", value: "comprehensive" },
{ label: "小学", value: "primary" },
{ label: "初中", value: "junior" },
{ label: "国际", value: "international" },
{ label: "复读", value: "repeat" },
] as const;
// 以下咨询学段无需填写毕业院校:字段展示“当前学段无需填写”、不可进入学校选择页、不参与提交校验。
export const schoolOptionalStages: readonly string[] = ["primary", "junior", "international", "repeat"];
export const schoolRegions = [
{
label: "历城区",
value: "licheng",
schools: [
{ label: "济南市历城第二中学", value: "licheng-no2" },
{ label: "济南稼轩学校", value: "jiaxuan" },
{ label: "济南市历城区洪家楼高级中学", value: "hongjialou" },
],
},
{
label: "市中区",
value: "shizhong",
schools: [
{ label: "济南协和双语实验学校(初中部)", value: "xiehe" },
{ label: "济南市安生学校(初中部)", value: "ansheng" },
{ label: "济南高新区海川中学(民办)", value: "haichuan" },
{ label: "济南高新区瀚阳学校(初中部)", value: "hanyang" },
{ label: "济南高新区凤凰路学校", value: "fenghuanglu" },
{ label: "济南高新区奥体中路学校", value: "aotizhonglu" },
{ label: "济南高新区伯乐实验学校", value: "bole" },
{ label: "济南市育英中学", value: "yuying" },
{ label: "山东省济南第三中学", value: "jinan-no3" },
],
},
{
label: "槐荫区",
value: "huaiyin",
schools: [
{ label: "山东省济南第九中学", value: "jinan-no9" },
{ label: "济南市槐荫中学", value: "huaiyin-middle" },
{ label: "济南市济微中学", value: "jiwei" },
],
},
{
label: "天桥区",
value: "tianqiao",
schools: [
{ label: "山东省济南第十一中学", value: "jinan-no11" },
{ label: "济南市天桥区泺口实验学校", value: "luokou" },
],
},
{
label: "平阴县",
value: "pingyin",
schools: [
{ label: "山东省平阴县第一中学", value: "pingyin-no1" },
{ label: "平阴县实验高级中学", value: "pingyin-experimental" },
],
},
{
label: "长清区",
value: "changqing",
schools: [
{ label: "济南市长清第一中学", value: "changqing-no1" },
{ label: "济南市长清中学", value: "changqing-middle" },
],
},
{
label: "章丘区",
value: "zhangqiu",
schools: [
{ label: "山东省章丘第四中学", value: "zhangqiu-no4" },
{ label: "济南市章丘区实验中学", value: "zhangqiu-experimental" },
],
},
{
label: "济阳区",
value: "jiyang",
schools: [
{ label: "山东省济北中学", value: "jibei" },
{ label: "济阳区第一中学", value: "jiyang-no1" },
],
},
] as const;

View File

@ -1,7 +1,230 @@
<template>
<PagePlaceholder section="家长端" title="家长参观信息" description="当前页面供受邀家长查看参观安排并填写到访信息。" />
<main class="visitor-invitation-form min-h-full overflow-hidden bg-[#f3f5f8] text-[#111]">
<section class="mx-15 mt-15 overflow-hidden rounded-15 bg-white px-15 pt-20">
<VanForm label-align="top" scroll-to-error @submit="handleSubmit">
<VanField
v-model="studentName"
class="visitor-invitation-form__field"
name="studentName"
label="学生姓名"
placeholder="请输入学生姓名"
autocomplete="name"
:border="false"
clearable />
<VanField
v-model="contactNumber"
class="visitor-invitation-form__field"
name="contactNumber"
label="联系方式"
placeholder="请输入家长手机号"
type="tel"
inputmode="numeric"
autocomplete="tel"
clearable
maxlength="11"
:border="false"
:formatter="formatPhoneNumber" />
<VanField
:model-value="selectedStageName"
class="visitor-invitation-form__field"
name="stage"
label="咨询学段"
placeholder="请选择咨询学段"
readonly
clickable
:border="false"
aria-haspopup="dialog"
@click="showStageSelector = true">
<template #right-icon>
<img class="h-18 w-9" src="/images/right-arrow.png" alt="" aria-hidden="true" />
</template>
</VanField>
<VanField
:model-value="selectedSchoolName"
class="visitor-invitation-form__field"
name="school"
label="毕业院校"
:placeholder="schoolPlaceholder"
readonly
clickable
:disabled="isSchoolDisabled"
:border="false"
aria-haspopup="dialog"
@click="openSchoolSelector">
<template #right-icon>
<img class="h-18 w-9" src="/images/right-arrow.png" alt="" aria-hidden="true" />
</template>
</VanField>
<VanField
:model-value="selectedDateText"
class="visitor-invitation-form__field"
name="visitDate"
label="到访日期"
placeholder="请选择到访日期"
readonly
clickable
:border="false"
aria-haspopup="dialog"
@click="showCalendar = true">
<template #right-icon>
<img class="h-18 w-9" src="/images/right-arrow.png" alt="" aria-hidden="true" />
</template>
</VanField>
<div class="px-28 my-30">
<VanButton
class="visitor-invitation-form__submit"
block
type="primary"
native-type="submit"
:loading="submitting"
loading-text="提交中..."
:disabled="submitting || !isFormComplete">
提交
</VanButton>
</div>
</VanForm>
</section>
<p class="m-0 flex items-center justify-center gap-5 px-16 py-10 text-[3rem] text-[#86909C]">
<VanIcon class="shrink-0" name="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/inviteReception/icon_tishi.png" aria-hidden="true" />
<span>二维码自到访当日起3天内有效</span>
</p>
<VisitDateCalendar v-model="visitDate" v-model:show="showCalendar" />
<SingleSelectPopup v-model="consultationStage" v-model:show="showStageSelector" title="咨询学段" @select="onStageSelect" />
<SchoolSelectPopup v-model="selectedSchool" v-model:show="showSchoolSelector" title="毕业院校" :page-size="20" @select="onSchoolSelect" />
</main>
</template>
<script setup lang="ts">
import PagePlaceholder from "@/components/PagePlaceholder.vue";
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Button as VanButton, Field as VanField, Form as VanForm, Icon as VanIcon } from "vant";
import SchoolSelectPopup from "@/components/SchoolSelectPopup.vue";
import SingleSelectPopup from "@/components/SingleSelectPopup.vue";
import VisitDateCalendar from "@/components/VisitDateCalendar.vue";
import { useInvitationForm } from "@/composables/useInvitationForm";
const route = useRoute();
const router = useRouter();
// ref token使
const inviteToken = computed(() => {
const raw = route.params.token;
return Array.isArray(raw) ? (raw[0] ?? "") : (raw ?? "");
});
const {
studentName,
contactNumber,
selectedSchool,
selectedSchoolName,
consultationStage,
selectedStageName,
visitDate,
showSchoolSelector,
showStageSelector,
showCalendar,
submitting,
isSchoolDisabled,
schoolPlaceholder,
selectedDateText,
isFormComplete,
onStageSelect,
onSchoolSelect,
formatPhoneNumber,
openSchoolSelector,
handleSubmit,
} = useInvitationForm(async (_payload) => {
// _payload consultStage + inviteToken
await router.push({ name: "visitor-submission-success", params: { token: inviteToken.value } });
});
</script>
<style scoped>
:global(.visitor-invitation-form) {
--van-cell-font-size: 15px;
--van-cell-line-height: 1;
--van-cell-vertical-padding: 23px;
--van-cell-horizontal-padding: 0;
--van-cell-border-color: #f3f3f3;
--van-field-label-color: #111;
--van-field-input-text-color: #333333;
--van-field-placeholder-text-color: #bababa;
--van-cell-right-icon-color: #999999;
--van-button-primary-background: #1580ff;
--van-button-primary-border-color: #1580ff;
}
:deep(.van-field__right-icon) {
padding: 0 0 0 10px;
margin-right: 0;
}
:global(.visitor-invitation-form .visitor-invitation-form__field) {
padding: 0;
overflow: visible;
}
:global(.visitor-invitation-form .visitor-invitation-form__field + .visitor-invitation-form__field) {
margin-top: 24px;
}
:global(.visitor-invitation-form .visitor-invitation-form__field .van-field__label--top) {
width: 100%;
margin: 0 0 10px;
}
:global(.visitor-invitation-form .visitor-invitation-form__field .van-field__value) {
flex: 0 0 100%;
width: 100%;
overflow: visible;
}
:global(.visitor-invitation-form .visitor-invitation-form__field .van-field__body) {
box-sizing: border-box;
min-height: 42px;
padding: 0 10px;
border: 1px solid #d9d9d9;
border-radius: 5px;
}
:global(.visitor-invitation-form .van-field__label) {
font-weight: 500;
}
:global(.visitor-invitation-form .van-field__control) {
font-size: 15px;
}
:global(.visitor-invitation-form .van-field__control:read-only:not(:disabled)) {
cursor: pointer;
}
:global(.visitor-invitation-form .visitor-invitation-form__submit) {
height: 42px;
border-radius: 8px;
--van-button-normal-font-size: 16px;
}
/* 未填写完整 / 提交中:禁用态用浅蓝 #A1CCFF覆盖 Vant 默认的半透明降级)。 */
:global(.visitor-invitation-form .visitor-invitation-form__submit.van-button--disabled) {
opacity: 1;
background: #a1ccff;
border-color: #a1ccff;
color: #fff;
}
:deep(.van-field--disabled .van-field__label) {
--van-field-disabled-text-color: #000;
}
:deep(.van-field--disabled .van-field__body) {
background-color: #f7f8fa;
}
</style>

View File

@ -4,7 +4,7 @@
# 服务器信息
SERVER_USER="root"
SERVER_HOST="106.14.30.150"
SERVER_PATH="/opt/1panel/apps/openresty/openresty/www/sites/zsfz.jinzejk.com/index"
SERVER_PATH="/opt/1panel/apps/openresty/openresty/www/sites/zsfz.shenquanedu.com/index"
PRIVATE_KEY="ALIYUN.pem"
BACKUP_PATH="${SERVER_PATH}-backup-$(date +%Y%m%d%H%M%S).zip"
DINGDING_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=fca104958fea6273c9c7ef3f08b3d552645c214f929066785e8caf6e1885a5a6"

View File

@ -1,4 +1,4 @@
import { defineConfig } from "vite";
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
import AutoComplete from "unplugin-auto-import/vite";
@ -6,58 +6,69 @@ import UnoCSS from 'unocss/vite'
import svgBuilder from "./plugins/svgBuilder";
import { mockServer } from "./plugins/mockServer";
const pathSrc = resolve(__dirname, "src");
// https://vite.dev/config/
export default defineConfig({
assetsInclude: ["**/*.svg"],
base: "./" /* 这个就是webpack里面的publicPath */,
build: {
rollupOptions: {
output: {
// 最小化拆分包
manualChunks: (id) => {
if (id.includes("node_modules")) {
return id.toString().split("node_modules/")[1].split("/")[0].toString();
}
}, // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
entryFileNames: "js/[name].[hash].js", // 用于命名代码拆分时创建的共享块的输出命名
chunkFileNames: "js/[name].[hash].js", // 用于输出静态资源的命名,[ext]表示文件扩展名
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
// 后端未就绪时用本地 mock 拦截招生辅助数据接口(仅 dev serve 生效)。
const useMock = env.VITE_USE_MOCK === "true";
return {
assetsInclude: ["**/*.svg"],
// SPA history 模式 + 嵌套路由必须用绝对根 "/"
// 若用相对 "./",访问 /teacher/mine 时资源会按 /teacher/ 解析成 /teacher/js/xxx.js
// 命中 nginx try_files 兜底返回 index.html(text/html),触发 module script MIME 报错、白屏。
base: "/" /* 这个就是webpack里面的publicPath */,
build: {
rollupOptions: {
output: {
// 最小化拆分包
manualChunks: (id) => {
if (id.includes("node_modules")) {
return id.toString().split("node_modules/")[1].split("/")[0].toString();
}
}, // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
entryFileNames: "js/[name].[hash].js", // 用于命名代码拆分时创建的共享块的输出命名
chunkFileNames: "js/[name].[hash].js", // 用于输出静态资源的命名,[ext]表示文件扩展名
},
},
},
},
plugins: [
vue(),
UnoCSS(),
svgBuilder(resolve("./public/icons")), // svg存储的名字
AutoComplete({
imports: [
"vue",
"vue-router",
"pinia", // 自动导入 Pinia API如 defineStore
],
dts: resolve(pathSrc, "auto-imports.d.ts"),
}),
],
resolve: {
alias: {
"@/": `${pathSrc}/`,
plugins: [
vue(),
UnoCSS(),
svgBuilder(resolve("./public/icons")), // svg存储的名字
AutoComplete({
imports: [
"vue",
"vue-router",
"pinia", // 自动导入 Pinia API如 defineStore
],
dts: resolve(pathSrc, "auto-imports.d.ts"),
}),
...(useMock ? [mockServer()] : []),
],
resolve: {
alias: {
"@/": `${pathSrc}/`,
},
},
},
server: {
host: "localhost",
proxy:{
'/api':{
target:'http://192.168.31.149:5006',
changeOrigin:true,
server: {
host: "localhost",
proxy: {
'/api': {
target: "https://api.zsfz.shenquanedu.com",
changeOrigin: true,
}
}
}
},
esbuild: {
drop: ["debugger"],
},
optimizeDeps: { // 在开发服务器启动时预构建依赖,将 CommonJS/UMD 模块转换为 ESM 格式,并缓存结果以提高性能。
include: [],
},
},
esbuild: {
drop: ["debugger"],
},
optimizeDeps: { // 在开发服务器启动时预构建依赖,将 CommonJS/UMD 模块转换为 ESM 格式,并缓存结果以提高性能。
include: [],
},
};
});