feat: initial panel (Vue + Tailwind)
This commit is contained in:
@@ -1,2 +0,0 @@
|
|||||||
VITE_API_URL=https://api.montime.ir
|
|
||||||
VITE_APP_MODE=admin
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
VITE_API_URL=http://localhost:3000
|
VITE_API_URL=http://localhost:3000
|
||||||
# web = montime.ir | admin = adm.montime.ir (also auto-detected from hostname)
|
|
||||||
VITE_APP_MODE=web
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-admin
|
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
import{_ as r}from"./AdminLayout.vue_vue_type_script_setup_true_lang-X12EOjkr.js";import{i as s,b as o,O as a,a as t,u as d}from"./index--aiJRDg4.js";import"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const x=s({__name:"AdminBusinessesView",setup(n){return(m,e)=>(d(),o(r,null,{default:a(()=>[...e[0]||(e[0]=[t("h1",{class:"text-xl font-bold text-text"},"کسبوکارها",-1),t("div",{class:"mt-4 rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted"}," بهزودی — CRUD کسبوکار و واگذاری مدیر ",-1)])]),_:1}))}});export{x as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{_ as s}from"./AdminLayout.vue_vue_type_script_setup_true_lang-X12EOjkr.js";import{i as r,b as o,O as a,a as t,u as d}from"./index--aiJRDg4.js";import"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const i=r({__name:"AdminHomeView",setup(x){return(l,e)=>(d(),o(s,null,{default:a(()=>[...e[0]||(e[0]=[t("h1",{class:"text-xl font-bold text-text"},"داشبورد مدیریت",-1),t("p",{class:"mt-2 text-sm text-muted"},"فقط سوپر ادمین — adm.montime.ir",-1),t("div",{class:"mt-6 grid gap-3 sm:grid-cols-2"},[t("div",{class:"rounded-2xl border border-border bg-surface-2 p-4"},[t("p",{class:"text-xs text-muted"},"کسبوکارها"),t("p",{class:"mt-1 text-2xl font-bold text-text"},"—")]),t("div",{class:"rounded-2xl border border-border bg-surface-2 p-4"},[t("p",{class:"text-xs text-muted"},"کاربران"),t("p",{class:"mt-1 text-2xl font-bold text-text"},"—")])],-1)])]),_:1}))}});export{i as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{i as b,I as x,b as _,O as o,a as e,g as a,y as f,e as g,x as h,F as y,K as k,J as v,B,u as n,q as C,H as L,D as r,f as A}from"./index--aiJRDg4.js";import{A as R,_ as S}from"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const N={class:"sticky top-0 z-10 flex items-center justify-between border-b border-amber-500/30 bg-surface/95 px-4 py-3 backdrop-blur"},V={class:"flex items-center gap-2"},w={class:"flex-1 space-y-4 px-4 py-4"},z={class:"sticky bottom-0 border-t border-border bg-surface/95 px-2 py-2"},D={class:"grid grid-cols-3 gap-1"},F={class:"text-lg"},E=b({__name:"AdminLayout",setup($){const l=v(),c=k(),i=x(),d=[{to:"/admin",name:"admin-home",label:"داشبورد",icon:"📊"},{to:"/admin/businesses",name:"admin-businesses",label:"کسبوکار",icon:"🏪"},{to:"/admin/users",name:"admin-users",label:"کاربران",icon:"👥"}];function u(){i.logout(),c.push({name:"admin-login"})}return(m,s)=>{const p=B("RouterLink");return n(),_(R,null,{default:o(()=>[e("header",N,[s[0]||(s[0]=e("div",null,[e("p",{class:"font-bold text-text"},"پنل مدیریت"),e("p",{class:"text-xs text-amber-600 dark:text-amber-400"},"adm.montime.ir")],-1)),e("div",V,[a(S),e("button",{type:"button",class:"rounded-xl px-3 py-2 text-sm text-muted hover:bg-surface-3",onClick:u}," خروج ")])]),e("main",w,[f(m.$slots,"default")]),e("nav",z,[e("div",D,[(n(),g(y,null,h(d,t=>a(p,{key:t.to,to:t.to,class:C(["flex flex-col items-center rounded-xl py-2 text-xs",L(l).name===t.name?"bg-amber-500/15 text-amber-700 dark:text-amber-400":"text-muted hover:bg-surface-3"])},{default:o(()=>[e("span",F,r(t.icon),1),A(" "+r(t.label),1)]),_:2},1032,["to","class"])),64))])])]),_:3})}}});export{E as _};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{_ as r}from"./AdminLayout.vue_vue_type_script_setup_true_lang-X12EOjkr.js";import{i as s,b as o,O as a,a as t,u as d}from"./index--aiJRDg4.js";import"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const c=s({__name:"AdminUsersView",setup(n){return(m,e)=>(d(),o(r,null,{default:a(()=>[...e[0]||(e[0]=[t("h1",{class:"text-xl font-bold text-text"},"کاربران",-1),t("div",{class:"mt-4 rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted"}," بهزودی ",-1)])]),_:1}))}});export{c as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{_ as r}from"./WebAppLayout.vue_vue_type_script_setup_true_lang-D1g6DvZW.js";import{i as o,b as s,O as a,a as t,u as d}from"./index--aiJRDg4.js";import"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const x=o({__name:"AppBookingsView",setup(n){return(m,e)=>(d(),s(r,null,{default:a(()=>[...e[0]||(e[0]=[t("h1",{class:"mb-4 text-xl font-bold text-text"},"رزروهای من",-1),t("div",{class:"rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted"}," بهزودی ",-1)])]),_:1}))}});export{x as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{_ as s}from"./WebAppLayout.vue_vue_type_script_setup_true_lang-D1g6DvZW.js";import{i as r,b as o,O as a,a as e,u as d}from"./index--aiJRDg4.js";import"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const x=r({__name:"AppHomeView",setup(n){return(c,t)=>(d(),o(s,null,{default:a(()=>[...t[0]||(t[0]=[e("section",{class:"space-y-4"},[e("h1",{class:"text-xl font-bold text-text"},"کشف خدمات محلی"),e("p",{class:"text-sm text-muted"}," بهزودی: دستهبندیها، جستجو و لیست کسبوکارها "),e("div",{class:"rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted"}," فاز بعدی — صفحهٔ خانه ")],-1)])]),_:1}))}});export{x as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{_ as r}from"./WebAppLayout.vue_vue_type_script_setup_true_lang-D1g6DvZW.js";import{i as s,b as o,O as a,a as e,u as d}from"./index--aiJRDg4.js";import"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const c=s({__name:"AppOwnerView",setup(m){return(n,t)=>(d(),o(r,null,{default:a(()=>[...t[0]||(t[0]=[e("h1",{class:"mb-4 text-xl font-bold text-text"},"مدیریت کسبوکار",-1),e("p",{class:"mb-4 text-sm text-muted"},"خدمات، ویژگیها و رزروهای ورودی",-1),e("div",{class:"rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted"}," بهزودی — پنل صاحب کسبوکار ",-1)])]),_:1}))}});export{c as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{_ as n}from"./WebAppLayout.vue_vue_type_script_setup_true_lang-D1g6DvZW.js";import{i as u,I as i,b as m,O as c,a as t,H as r,e as x,D as d,d as f,c as p,u as l}from"./index--aiJRDg4.js";import"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const _={key:0,class:"space-y-3 rounded-2xl border border-border bg-surface-2 p-4"},b={class:"font-medium",dir:"ltr"},k={class:"font-medium"},v={class:"font-medium"},N=u({__name:"AppProfileView",setup(w){const s=i(),a=p(()=>{var o;switch((o=s.user)==null?void 0:o.role){case"admin":return"مدیر سیستم";case"business_owner":return"صاحب کسبوکار";default:return"مشتری"}});return(o,e)=>(l(),m(n,null,{default:c(()=>[e[3]||(e[3]=t("h1",{class:"mb-4 text-xl font-bold text-text"},"پروفایل",-1)),r(s).user?(l(),x("dl",_,[t("div",null,[e[0]||(e[0]=t("dt",{class:"text-xs text-muted"},"موبایل",-1)),t("dd",b,d(r(s).user.mobile),1)]),t("div",null,[e[1]||(e[1]=t("dt",{class:"text-xs text-muted"},"نام",-1)),t("dd",k,d(r(s).user.fullName??"—"),1)]),t("div",null,[e[2]||(e[2]=t("dt",{class:"text-xs text-muted"},"نقش",-1)),t("dd",v,d(a.value),1)])])):f("",!0)]),_:1}))}});export{N as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{i as r,e as l,y as d,q as u,c as f,u as s,D as o,d as i,a,o as b,b as m,O as p,g as x}from"./index--aiJRDg4.js";import{A as h,_ as y}from"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const _=["type","disabled"],V=r({__name:"AppButton",props:{variant:{default:"primary"},type:{default:"button"},disabled:{type:Boolean,default:!1},loading:{type:Boolean,default:!1}},setup(e){const t=e,n=f(()=>{switch(t.variant){case"ghost":return"bg-transparent text-muted hover:bg-surface-3 hover:text-text";case"danger":return"bg-red-500/15 text-red-600 dark:text-red-400 hover:bg-red-500/25";default:return"bg-brand-600 text-white hover:bg-brand-700"}});return(c,C)=>(s(),l("button",{type:e.type,class:u(["inline-flex w-full items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold transition disabled:cursor-not-allowed disabled:opacity-50",n.value]),disabled:e.disabled||e.loading},[d(c.$slots,"default")],10,_))}}),g={class:"space-y-1.5"},v=["for"],$=["id"],j=r({inheritAttrs:!1,__name:"AppInput",props:{label:{},id:{},ltr:{type:Boolean,default:!1}},setup(e){return(t,n)=>(s(),l("div",g,[e.label?(s(),l("label",{key:0,for:e.id,class:"block text-sm font-medium text-muted"},o(e.label),9,v)):i("",!0),a("input",b({id:e.id,class:["w-full rounded-xl border border-border bg-surface-2 px-4 py-3 text-text outline-none transition placeholder:text-muted focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30",e.ltr?"text-left":""]},t.$attrs),null,16,$)]))}}),B={class:"flex items-center justify-between px-4 py-3"},k={class:"text-lg font-bold text-text"},w={key:0,class:"text-sm text-muted"},A={class:"flex flex-1 flex-col px-4 pb-8"},D=r({__name:"AuthLayout",props:{title:{},subtitle:{}},setup(e){return(t,n)=>(s(),m(h,null,{default:p(()=>[a("header",B,[a("div",null,[a("p",k,o(e.title),1),e.subtitle?(s(),l("p",w,o(e.subtitle),1)):i("",!0)]),x(y)]),a("main",A,[d(t.$slots,"default")])]),_:3}))}});export{D as _,j as a,V as b};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{i as _,b as y,c as l,O as u,K as x,J as g,u as n,e as V,D as w,d as C,a as S,P as k,g as d,f as A,w as i,z as h,A as B,j as N}from"./index--aiJRDg4.js";import{_ as q,a as $,b as D}from"./AuthLayout.vue_vue_type_script_setup_true_lang-Bbht7KM7.js";import"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const E={key:0,class:"mb-4 rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"},j=_({__name:"LoginView",setup(M){const m=x(),c=g(),s=l(()=>N()==="admin"||c.meta.adminAuth===!0),p=l(()=>s.value?"پنل مدیریت":"مونتایم"),f=l(()=>s.value?"ورود سوپر ادمین":"ورود با شماره موبایل"),t=i(""),r=i(!1),a=i("");async function v(){a.value="",r.value=!0;try{const e=await h(t.value.trim());await m.push({name:s.value?"admin-verify":"verify",query:{mobile:t.value.trim(),expires:String(e.expiresInSeconds),...e.otpCode?{devCode:e.otpCode}:{}}})}catch(e){a.value=e instanceof B?e.message:"خطا در ارسال درخواست. دوباره تلاش کنید."}finally{r.value=!1}}return(e,o)=>(n(),y(q,{title:p.value,subtitle:f.value},{default:u(()=>[a.value?(n(),V("p",E,w(a.value),1)):C("",!0),S("form",{class:"mt-auto space-y-4",onSubmit:k(v,["prevent"])},[d($,{id:"mobile",modelValue:t.value,"onUpdate:modelValue":o[0]||(o[0]=b=>t.value=b),label:"شماره موبایل",type:"tel",inputmode:"numeric",placeholder:"09121234567",autocomplete:"tel",ltr:"",required:""},null,8,["modelValue"]),d(D,{type:"submit",loading:r.value},{default:u(()=>[...o[1]||(o[1]=[A("دریافت کد تأیید",-1)])]),_:1},8,["loading"])],32)]),_:1},8,["title","subtitle"]))}});export{j as default};
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{i as I,I as N,w as r,r as $,t as B,b as S,O as i,J as O,K as L,c as g,e as x,D as y,d as V,a as M,g as _,f as k,P as D,H as E,z as R,A as h,u as l,L as T,j as U}from"./index--aiJRDg4.js";import{_ as j,a as z,b as C}from"./AuthLayout.vue_vue_type_script_setup_true_lang-Bbht7KM7.js";import"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const H={key:0,class:"mb-4 rounded-xl border border-brand-500/40 bg-brand-500/10 px-4 py-3 text-sm text-brand-700 dark:text-brand-500"},J={key:1,class:"mb-4 rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"},K={key:2,class:"mt-4 text-center text-sm text-muted"},W=I({__name:"VerifyOtpView",setup(P){const s=O(),o=L(),d=N(),m=g(()=>U()==="admin"||s.meta.adminAuth===!0),u=g(()=>String(s.query.mobile??"")),c=g(()=>s.query.devCode),p=r(c.value??""),v=r(!1),f=r(!1),a=r(""),n=r(Number(s.query.expires)||120);let b=null;$(()=>{if(!u.value){o.replace({name:m.value?"admin-login":"login"});return}b=setInterval(()=>{n.value>0&&(n.value-=1)},1e3)}),B(()=>{b&&clearInterval(b)});async function q(){a.value="",v.value=!0;try{const e=await T(u.value,p.value.trim());if(d.setSession(e.accessToken,e.user),m.value){if(!d.isAdmin){d.logout(),a.value="دسترسی ادمین ندارید.";return}await o.replace({name:"admin-home"});return}await o.replace({name:"app-home"})}catch(e){a.value=e instanceof h?e.message:"کد نامعتبر است. دوباره تلاش کنید."}finally{v.value=!1}}async function A(){a.value="",f.value=!0;try{const e=await R(u.value);n.value=e.expiresInSeconds,e.otpCode&&await o.replace({query:{...s.query,devCode:e.otpCode,expires:String(e.expiresInSeconds)}})}catch(e){a.value=e instanceof h?e.message:"ارسال مجدد ناموفق بود."}finally{f.value=!1}}return(e,t)=>(l(),S(j,{title:"کد تأیید",subtitle:`کد ارسالشده به ${u.value}`},{default:i(()=>[c.value?(l(),x("p",H," حالت توسعه — کد: "+y(c.value),1)):V("",!0),a.value?(l(),x("p",J,y(a.value),1)):V("",!0),M("form",{class:"space-y-4",onSubmit:D(q,["prevent"])},[_(z,{id:"code",modelValue:p.value,"onUpdate:modelValue":t[0]||(t[0]=w=>p.value=w),label:"کد ۶ رقمی",type:"text",inputmode:"numeric",maxlength:"8",placeholder:"123456",autocomplete:"one-time-code",ltr:"",required:""},null,8,["modelValue"]),_(C,{type:"submit",loading:v.value},{default:i(()=>[...t[2]||(t[2]=[k("ورود",-1)])]),_:1},8,["loading"])],32),n.value>0?(l(),x("p",K," ارسال مجدد تا "+y(n.value)+" ثانیه ",1)):(l(),S(C,{key:3,variant:"ghost",class:"mt-2",loading:f.value,onClick:A},{default:i(()=>[...t[3]||(t[3]=[k(" ارسال مجدد کد ",-1)])]),_:1},8,["loading"])),_(C,{variant:"ghost",class:"mt-2",onClick:t[1]||(t[1]=w=>E(o).push({name:m.value?"admin-login":"login"}))},{default:i(()=>[...t[4]||(t[4]=[k(" تغییر شماره موبایل ",-1)])]),_:1})]),_:1},8,["subtitle"]))}});export{W as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{i as f,I as g,b as p,O as u,a as e,H as n,e as i,D as r,d as h,g as k,y,q as d,x as v,F as B,c as C,K as w,J as L,B as A,u as a}from"./index--aiJRDg4.js";import{A as R,_ as S}from"./ThemeToggle.vue_vue_type_script_setup_true_lang-MhHgjOGX.js";const N={class:"sticky top-0 z-10 flex items-center justify-between border-b border-border bg-surface/95 px-4 py-3 backdrop-blur"},V={key:0,class:"text-xs text-muted"},z={class:"flex items-center gap-2"},D={class:"flex-1 px-4 py-4"},F={class:"sticky bottom-0 border-t border-border bg-surface/95 px-2 py-2 backdrop-blur"},I={class:"text-lg","aria-hidden":"true"},q=f({__name:"WebAppLayout",setup(O){const o=g(),b=L(),m=w(),l=C(()=>{const s=[{to:"/app",name:"app-home",label:"خانه",icon:"🏠"},{to:"/app/bookings",name:"app-bookings",label:"رزروها",icon:"📅"},{to:"/app/profile",name:"app-profile",label:"پروفایل",icon:"👤"}];return o.isBusinessOwner&&s.splice(2,0,{to:"/app/owner",name:"app-owner",label:"کسبوکار",icon:"🏪"}),s});function x(){o.logout(),m.push({name:"login"})}return(s,c)=>{const _=A("RouterLink");return a(),p(R,null,{default:u(()=>[e("header",N,[e("div",null,[c[0]||(c[0]=e("p",{class:"font-bold text-text"},"مونتایم",-1)),n(o).user?(a(),i("p",V,r(n(o).user.mobile),1)):h("",!0)]),e("div",z,[k(S),e("button",{type:"button",class:"rounded-xl px-3 py-2 text-sm text-muted hover:bg-surface-3 hover:text-text",onClick:x}," خروج ")])]),e("main",D,[y(s.$slots,"default")]),e("nav",F,[e("div",{class:d(["grid gap-1",l.value.length===4?"grid-cols-4":"grid-cols-3"])},[(a(!0),i(B,null,v(l.value,t=>(a(),p(_,{key:t.to,to:t.to,class:d(["flex flex-col items-center rounded-xl px-2 py-2 text-xs transition",n(b).name===t.name?"bg-brand-500/15 text-brand-700 dark:text-brand-500":"text-muted hover:bg-surface-3 hover:text-text"])},{default:u(()=>[e("span",I,r(t.icon),1),e("span",null,r(t.label),1)]),_:2},1032,["to","class"]))),128))],2)])]),_:3})}}});export{q as _};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>مونتایم</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<script type="module" crossorigin src="/assets/index--aiJRDg4.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-bUNY5WyE.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>␍
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+1
-4
@@ -5,12 +5,9 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:admin": "vite --port 5174 --mode admin",
|
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:admin": "vite build --mode admin",
|
|
||||||
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json",
|
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"preview:admin": "vite preview --port 5174 --mode admin"
|
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
export type AppMode = 'web' | 'admin';
|
|
||||||
|
|
||||||
/** `web` = montime.ir (customers + business owners). `admin` = adm.montime.ir only. */
|
|
||||||
export function getAppMode(): AppMode {
|
|
||||||
const fromEnv = import.meta.env.VITE_APP_MODE as AppMode | undefined;
|
|
||||||
if (fromEnv === 'web' || fromEnv === 'admin') {
|
|
||||||
return fromEnv;
|
|
||||||
}
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const host = window.location.hostname;
|
|
||||||
if (host === 'adm.montime.ir' || host.startsWith('adm.')) {
|
|
||||||
return 'admin';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'web';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAppMode() {
|
|
||||||
return { mode: getAppMode(), isAdmin: getAppMode() === 'admin', isWeb: getAppMode() === 'web' };
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<AppShell>
|
|
||||||
<header
|
|
||||||
class="sticky top-0 z-10 flex items-center justify-between border-b border-amber-500/30 bg-surface/95 px-4 py-3 backdrop-blur"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p class="font-bold text-text">پنل مدیریت</p>
|
|
||||||
<p class="text-xs text-amber-600 dark:text-amber-400">adm.montime.ir</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<ThemeToggle />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-xl px-3 py-2 text-sm text-muted hover:bg-surface-3"
|
|
||||||
@click="onLogout"
|
|
||||||
>
|
|
||||||
خروج
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="flex-1 space-y-4 px-4 py-4">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<nav class="sticky bottom-0 border-t border-border bg-surface/95 px-2 py-2">
|
|
||||||
<div class="grid grid-cols-3 gap-1">
|
|
||||||
<RouterLink
|
|
||||||
v-for="item in items"
|
|
||||||
:key="item.to"
|
|
||||||
:to="item.to"
|
|
||||||
class="flex flex-col items-center rounded-xl py-2 text-xs"
|
|
||||||
:class="
|
|
||||||
route.name === item.name
|
|
||||||
? 'bg-amber-500/15 text-amber-700 dark:text-amber-400'
|
|
||||||
: 'text-muted hover:bg-surface-3'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<span class="text-lg">{{ item.icon }}</span>
|
|
||||||
{{ item.label }}
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</AppShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import AppShell from '@/components/shell/AppShell.vue';
|
|
||||||
import ThemeToggle from '@/components/ui/ThemeToggle.vue';
|
|
||||||
import { useAuthStore } from '@/stores/auth';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const auth = useAuthStore();
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
{ to: '/admin', name: 'admin-home', label: 'داشبورد', icon: '📊' },
|
|
||||||
{ to: '/admin/businesses', name: 'admin-businesses', label: 'کسبوکار', icon: '🏪' },
|
|
||||||
{ to: '/admin/users', name: 'admin-users', label: 'کاربران', icon: '👥' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function onLogout() {
|
|
||||||
auth.logout();
|
|
||||||
router.push({ name: 'admin-login' });
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
+41
-93
@@ -1,103 +1,59 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import { getAppMode } from '@/composables/useAppMode';
|
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
|
||||||
const mode = getAppMode();
|
|
||||||
|
|
||||||
const webRoutes = [
|
|
||||||
{ path: '/', redirect: '/login' },
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
name: 'login',
|
|
||||||
component: () => import('@/views/auth/LoginView.vue'),
|
|
||||||
meta: { guest: true, shell: 'auth' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/verify',
|
|
||||||
name: 'verify',
|
|
||||||
component: () => import('@/views/auth/VerifyOtpView.vue'),
|
|
||||||
meta: { guest: true, shell: 'auth' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/app',
|
|
||||||
name: 'app-home',
|
|
||||||
component: () => import('@/views/app/AppHomeView.vue'),
|
|
||||||
meta: { requiresAuth: true, shell: 'web' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/app/bookings',
|
|
||||||
name: 'app-bookings',
|
|
||||||
component: () => import('@/views/app/AppBookingsView.vue'),
|
|
||||||
meta: { requiresAuth: true, shell: 'web' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/app/profile',
|
|
||||||
name: 'app-profile',
|
|
||||||
component: () => import('@/views/app/AppProfileView.vue'),
|
|
||||||
meta: { requiresAuth: true, shell: 'web' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/app/owner',
|
|
||||||
name: 'app-owner',
|
|
||||||
component: () => import('@/views/app/AppOwnerView.vue'),
|
|
||||||
meta: { requiresAuth: true, requiresBusinessOwner: true, shell: 'web' },
|
|
||||||
},
|
|
||||||
{ path: '/home', redirect: '/app' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const adminRoutes = [
|
|
||||||
{ path: '/', redirect: '/admin/login' },
|
|
||||||
{
|
|
||||||
path: '/admin/login',
|
|
||||||
name: 'admin-login',
|
|
||||||
component: () => import('@/views/auth/LoginView.vue'),
|
|
||||||
meta: { guest: true, shell: 'auth', adminAuth: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/verify',
|
|
||||||
name: 'admin-verify',
|
|
||||||
component: () => import('@/views/auth/VerifyOtpView.vue'),
|
|
||||||
meta: { guest: true, shell: 'auth', adminAuth: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin',
|
|
||||||
name: 'admin-home',
|
|
||||||
component: () => import('@/views/admin/AdminHomeView.vue'),
|
|
||||||
meta: { requiresAuth: true, requiresAdmin: true, shell: 'admin' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/businesses',
|
|
||||||
name: 'admin-businesses',
|
|
||||||
component: () => import('@/views/admin/AdminBusinessesView.vue'),
|
|
||||||
meta: { requiresAuth: true, requiresAdmin: true, shell: 'admin' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/admin/users',
|
|
||||||
name: 'admin-users',
|
|
||||||
component: () => import('@/views/admin/AdminUsersView.vue'),
|
|
||||||
meta: { requiresAuth: true, requiresAdmin: true, shell: 'admin' },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: mode === 'admin' ? adminRoutes : webRoutes,
|
routes: [
|
||||||
|
{ path: '/', redirect: '/login' },
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('@/views/auth/LoginView.vue'),
|
||||||
|
meta: { guest: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/verify',
|
||||||
|
name: 'verify',
|
||||||
|
component: () => import('@/views/auth/VerifyOtpView.vue'),
|
||||||
|
meta: { guest: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/app',
|
||||||
|
name: 'app-home',
|
||||||
|
component: () => import('@/views/app/AppHomeView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/app/bookings',
|
||||||
|
name: 'app-bookings',
|
||||||
|
component: () => import('@/views/app/AppBookingsView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/app/profile',
|
||||||
|
name: 'app-profile',
|
||||||
|
component: () => import('@/views/app/AppProfileView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/app/owner',
|
||||||
|
name: 'app-owner',
|
||||||
|
component: () => import('@/views/app/AppOwnerView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresBusinessOwner: true },
|
||||||
|
},
|
||||||
|
{ path: '/home', redirect: '/app' },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to) => {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const appMode = getAppMode();
|
|
||||||
|
|
||||||
if (auth.token && !auth.user) {
|
if (auth.token && !auth.user) {
|
||||||
await auth.loadUser();
|
await auth.loadUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.meta.requiresAuth && !auth.isLoggedIn) {
|
if (to.meta.requiresAuth && !auth.isLoggedIn) {
|
||||||
return appMode === 'admin' ? { name: 'admin-login' } : { name: 'login' };
|
return { name: 'login' };
|
||||||
}
|
|
||||||
|
|
||||||
if (to.meta.requiresAdmin && !auth.isAdmin) {
|
|
||||||
return appMode === 'admin' ? { name: 'admin-login' } : { name: 'login' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.meta.requiresBusinessOwner && !auth.isBusinessOwner && !auth.isAdmin) {
|
if (to.meta.requiresBusinessOwner && !auth.isBusinessOwner && !auth.isAdmin) {
|
||||||
@@ -105,16 +61,8 @@ router.beforeEach(async (to) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (to.meta.guest && auth.isLoggedIn) {
|
if (to.meta.guest && auth.isLoggedIn) {
|
||||||
if (to.meta.adminAuth) {
|
|
||||||
return auth.isAdmin ? { name: 'admin-home' } : undefined;
|
|
||||||
}
|
|
||||||
return { name: 'app-home' };
|
return { name: 'app-home' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appMode === 'admin' && auth.isLoggedIn && !auth.isAdmin && !to.meta.guest) {
|
|
||||||
auth.logout();
|
|
||||||
return { name: 'admin-login' };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<template>
|
|
||||||
<AdminLayout>
|
|
||||||
<h1 class="text-xl font-bold text-text">کسبوکارها</h1>
|
|
||||||
<div class="mt-4 rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted">
|
|
||||||
بهزودی — CRUD کسبوکار و واگذاری مدیر
|
|
||||||
</div>
|
|
||||||
</AdminLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
||||||
</script>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<template>
|
|
||||||
<AdminLayout>
|
|
||||||
<h1 class="text-xl font-bold text-text">داشبورد مدیریت</h1>
|
|
||||||
<p class="mt-2 text-sm text-muted">فقط سوپر ادمین — adm.montime.ir</p>
|
|
||||||
<div class="mt-6 grid gap-3 sm:grid-cols-2">
|
|
||||||
<div class="rounded-2xl border border-border bg-surface-2 p-4">
|
|
||||||
<p class="text-xs text-muted">کسبوکارها</p>
|
|
||||||
<p class="mt-1 text-2xl font-bold text-text">—</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-2xl border border-border bg-surface-2 p-4">
|
|
||||||
<p class="text-xs text-muted">کاربران</p>
|
|
||||||
<p class="mt-1 text-2xl font-bold text-text">—</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AdminLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
||||||
</script>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<template>
|
|
||||||
<AdminLayout>
|
|
||||||
<h1 class="text-xl font-bold text-text">کاربران</h1>
|
|
||||||
<div class="mt-4 rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted">
|
|
||||||
بهزودی
|
|
||||||
</div>
|
|
||||||
</AdminLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
||||||
</script>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AuthLayout :title="brandTitle" :subtitle="brandSubtitle">
|
<AuthLayout title="مونتایم" subtitle="ورود با شماره موبایل">
|
||||||
<p
|
<p
|
||||||
v-if="error"
|
v-if="error"
|
||||||
class="mb-4 rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
class="mb-4 rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
||||||
@@ -25,24 +25,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { requestOtp } from '@/api/auth';
|
import { requestOtp } from '@/api/auth';
|
||||||
import { ApiError } from '@/api/client';
|
import { ApiError } from '@/api/client';
|
||||||
import { getAppMode } from '@/composables/useAppMode';
|
|
||||||
import AppButton from '@/components/ui/AppButton.vue';
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
import AppInput from '@/components/ui/AppInput.vue';
|
import AppInput from '@/components/ui/AppInput.vue';
|
||||||
import AuthLayout from '@/layouts/AuthLayout.vue';
|
import AuthLayout from '@/layouts/AuthLayout.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
|
||||||
const isAdmin = computed(() => getAppMode() === 'admin' || route.meta.adminAuth === true);
|
|
||||||
|
|
||||||
const brandTitle = computed(() => (isAdmin.value ? 'پنل مدیریت' : 'مونتایم'));
|
|
||||||
const brandSubtitle = computed(() =>
|
|
||||||
isAdmin.value ? 'ورود سوپر ادمین' : 'ورود با شماره موبایل',
|
|
||||||
);
|
|
||||||
|
|
||||||
const mobile = ref('');
|
const mobile = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
@@ -53,7 +44,7 @@ async function onSubmit() {
|
|||||||
try {
|
try {
|
||||||
const res = await requestOtp(mobile.value.trim());
|
const res = await requestOtp(mobile.value.trim());
|
||||||
await router.push({
|
await router.push({
|
||||||
name: isAdmin.value ? 'admin-verify' : 'verify',
|
name: 'verify',
|
||||||
query: {
|
query: {
|
||||||
mobile: mobile.value.trim(),
|
mobile: mobile.value.trim(),
|
||||||
expires: String(res.expiresInSeconds),
|
expires: String(res.expiresInSeconds),
|
||||||
|
|||||||
@@ -32,21 +32,11 @@
|
|||||||
<p v-if="secondsLeft > 0" class="mt-4 text-center text-sm text-muted">
|
<p v-if="secondsLeft > 0" class="mt-4 text-center text-sm text-muted">
|
||||||
ارسال مجدد تا {{ secondsLeft }} ثانیه
|
ارسال مجدد تا {{ secondsLeft }} ثانیه
|
||||||
</p>
|
</p>
|
||||||
<AppButton
|
<AppButton v-else variant="ghost" class="mt-2" :loading="resendLoading" @click="resend">
|
||||||
v-else
|
|
||||||
variant="ghost"
|
|
||||||
class="mt-2"
|
|
||||||
:loading="resendLoading"
|
|
||||||
@click="resend"
|
|
||||||
>
|
|
||||||
ارسال مجدد کد
|
ارسال مجدد کد
|
||||||
</AppButton>
|
</AppButton>
|
||||||
|
|
||||||
<AppButton
|
<AppButton variant="ghost" class="mt-2" @click="router.push({ name: 'login' })">
|
||||||
variant="ghost"
|
|
||||||
class="mt-2"
|
|
||||||
@click="router.push({ name: isAdmin ? 'admin-login' : 'login' })"
|
|
||||||
>
|
|
||||||
تغییر شماره موبایل
|
تغییر شماره موبایل
|
||||||
</AppButton>
|
</AppButton>
|
||||||
</AuthLayout>
|
</AuthLayout>
|
||||||
@@ -57,7 +47,6 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { requestOtp, verifyOtp } from '@/api/auth';
|
import { requestOtp, verifyOtp } from '@/api/auth';
|
||||||
import { ApiError } from '@/api/client';
|
import { ApiError } from '@/api/client';
|
||||||
import { getAppMode } from '@/composables/useAppMode';
|
|
||||||
import AppButton from '@/components/ui/AppButton.vue';
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
import AppInput from '@/components/ui/AppInput.vue';
|
import AppInput from '@/components/ui/AppInput.vue';
|
||||||
import AuthLayout from '@/layouts/AuthLayout.vue';
|
import AuthLayout from '@/layouts/AuthLayout.vue';
|
||||||
@@ -66,7 +55,6 @@ import { useAuthStore } from '@/stores/auth';
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const isAdmin = computed(() => getAppMode() === 'admin' || route.meta.adminAuth === true);
|
|
||||||
|
|
||||||
const mobile = computed(() => String(route.query.mobile ?? ''));
|
const mobile = computed(() => String(route.query.mobile ?? ''));
|
||||||
const devCode = computed(() => route.query.devCode as string | undefined);
|
const devCode = computed(() => route.query.devCode as string | undefined);
|
||||||
@@ -80,7 +68,7 @@ let timer: ReturnType<typeof setInterval> | null = null;
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!mobile.value) {
|
if (!mobile.value) {
|
||||||
router.replace({ name: isAdmin.value ? 'admin-login' : 'login' });
|
router.replace({ name: 'login' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
@@ -98,17 +86,6 @@ async function onSubmit() {
|
|||||||
try {
|
try {
|
||||||
const res = await verifyOtp(mobile.value, code.value.trim());
|
const res = await verifyOtp(mobile.value, code.value.trim());
|
||||||
auth.setSession(res.accessToken, res.user);
|
auth.setSession(res.accessToken, res.user);
|
||||||
|
|
||||||
if (isAdmin.value) {
|
|
||||||
if (!auth.isAdmin) {
|
|
||||||
auth.logout();
|
|
||||||
error.value = 'دسترسی ادمین ندارید.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.replace({ name: 'admin-home' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await router.replace({ name: 'app-home' });
|
await router.replace({ name: 'app-home' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value =
|
error.value =
|
||||||
|
|||||||
Vendored
-1
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_URL: string;
|
readonly VITE_API_URL: string;
|
||||||
readonly VITE_APP_MODE?: 'web' | 'admin';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
+3
-6
@@ -2,10 +2,7 @@ import { defineConfig } from 'vite';
|
|||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import { fileURLToPath, URL } from 'node:url';
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig({
|
||||||
build: {
|
|
||||||
outDir: mode === 'admin' ? 'dist-admin' : 'dist',
|
|
||||||
},
|
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@@ -13,6 +10,6 @@ export default defineConfig(({ mode }) => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: mode === 'admin' ? 5174 : 5173,
|
port: 5173,
|
||||||
},
|
},
|
||||||
}));
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user