Update latest code

This commit is contained in:
MaDaLei 2026-04-12 23:35:31 +08:00
parent 3e01102898
commit 494aaa8c72
45 changed files with 171 additions and 2927 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules/ node_modules/
node_modules

1
.gitignore 2 Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

View File

@ -1 +1 @@
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./common/vendor.js"),n=require("./utils/session.js");Math;function r(){setTimeout(()=>{try{if(n.isLoggedIn())return;const r=getCurrentPages(),t=r[r.length-1];if((e=>!(!e||!e.includes("pages/login/Login")&&!e.includes("pages/report-view/reportView")))(t&&t.route?t.route:""))return;e.index.reLaunch({url:"/pages/login/Login"})}catch(r){e.index.reLaunch({url:"/pages/login/Login"})}},50)}const t={onLaunch(){r()},onShow(){r()}};function o(){return{app:e.createSSRApp(t)}}o().app.mount("#app"),exports.createApp=o; "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./common/vendor.js"),n=require("./utils/session.js");Math;function t(){setTimeout(()=>{try{if(n.isLoggedIn())return;const t=getCurrentPages();if(0===t.length)return;const r=t[t.length-1];if((e=>!(!e||!e.includes("pages/login/Login")&&!e.includes("pages/report-view/reportView")))(r&&r.route?r.route:""))return;e.index.reLaunch({url:"/pages/login/Login"})}catch(t){e.index.reLaunch({url:"/pages/login/Login"})}},50)}const r={onLaunch(){t()},onShow(){t()}};function o(){return{app:e.createSSRApp(r)}}o().app.mount("#app"),exports.createApp=o;

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

@ -1 +1 @@
"use strict";const e=require("../../common/vendor.js"),n=require("../../utils/session.js"),o=require("../../composables/useNavigator.js");Math||(t+r)();const r=()=>"../../components/TabBar.js",t=()=>"../../components/AppIcon.js",s={__name:"Mine",emits:["change-page"],setup(r,{emit:t}){const s=n.getUserSession(),{goPage:a,navigateTo:i}=o.useNavigator(),u=(()=>{var n,o,r,t;let s=((null==(o=(n=e.index).getSystemInfoSync)?void 0:o.call(n).statusBarHeight)||20)+12;const a=null==(t=(r=e.index).getMenuButtonBoundingClientRect)?void 0:t.call(r);return a&&a.top&&a.height&&(s=a.top+a.height+8),`padding-top:${s}px;`})(),c=e.computed(()=>s.name?s.name.slice(0,1).toUpperCase():"?"),p=e.computed(()=>{var e;if(s.avatar)return{};const n=["#ff7c43","#07c160","#8b6914","#e06040","#5090d0"];return{background:n[((null==(e=s.name)?void 0:e.charCodeAt(0))||0)%n.length]}}),f=()=>{e.index.showModal({title:"提示",content:"确定退出登录?",success:e=>{e.confirm&&(n.clearSession(),i("login"))}})};return(n,o)=>e.e({a:e.unref(s).avatar},e.unref(s).avatar?{b:e.unref(s).avatar}:{c:e.t(c.value)},{d:e.s(p.value),e:e.t(e.unref(s).name),f:e.t(e.unref(s).phone),g:e.t("boss"===e.unref(s).role?"店长":"员工"),h:e.s(e.unref(u)),i:e.o(n=>e.unref(i)("profile")),j:"boss"===e.unref(s).role},"boss"===e.unref(s).role?{k:e.p({name:"staff",size:15}),l:e.o(n=>e.unref(i)("staff")),m:e.p({name:"service",size:15}),n:e.o(n=>e.unref(i)("serviceType")),o:e.p({name:"store",size:15}),p:e.o(n=>e.unref(i)("store"))}:{},{q:e.p({name:"profile",size:15}),r:e.o(n=>e.unref(i)("profile")),s:e.p({name:"report",size:15}),t:e.o(n=>e.unref(i)("myReports")),v:e.p({name:"orders",size:15}),w:e.o(n=>e.unref(i)("myOrders")),x:e.p({name:"logout",size:15,color:"#dc2626"}),y:e.o(f),z:e.o(e.unref(a)),A:e.p({"current-page":"mine"})})}},a=e._export_sfc(s,[["__scopeId","data-v-27768e55"]]);wx.createPage(a); "use strict";const e=require("../../common/vendor.js"),r=require("../../utils/session.js"),n=require("../../composables/useNavigator.js");Math||(t+o)();const o=()=>"../../components/TabBar.js",t=()=>"../../components/AppIcon.js",s={__name:"Mine",emits:["change-page"],setup(o,{emit:t}){const s=r.getUserSession(),{goPage:a,navigateTo:u}=n.useNavigator(),i=(()=>{var r,n,o,t;let s=((null==(n=(r=e.index).getSystemInfoSync)?void 0:n.call(r).statusBarHeight)||20)+12;const a=null==(t=(o=e.index).getMenuButtonBoundingClientRect)?void 0:t.call(o);return a&&a.top&&a.height&&(s=a.top+a.height+8),`padding-top:${s}px;`})(),c=e.computed(()=>s.name?s.name.slice(0,1).toUpperCase():"?"),p=e.computed(()=>{var e;if(s.avatar)return{};const r=["#ff7c43","#07c160","#8b6914","#e06040","#5090d0"];return{background:r[((null==(e=s.name)?void 0:e.charCodeAt(0))||0)%r.length]}}),f=()=>{e.index.showModal({title:"提示",content:"确定退出登录?",success:e=>{e.confirm&&(r.clearSession(),u("login"))}})};return(r,n)=>e.e({a:e.unref(s).avatar},e.unref(s).avatar?{b:e.unref(s).avatar}:{c:e.t(c.value)},{d:e.s(p.value),e:e.t(e.unref(s).name),f:e.t(e.unref(s).phone),g:e.t("boss"===e.unref(s).role?"店长":"customer"===e.unref(s).role?"客户":"员工"),h:e.s(e.unref(i)),i:e.o(r=>e.unref(u)("profile")),j:"boss"===e.unref(s).role},"boss"===e.unref(s).role?{k:e.p({name:"staff",size:15}),l:e.o(r=>e.unref(u)("staff")),m:e.p({name:"service",size:15}),n:e.o(r=>e.unref(u)("serviceType")),o:e.p({name:"store",size:15}),p:e.o(r=>e.unref(u)("store"))}:{},{q:e.p({name:"profile",size:15}),r:e.o(r=>e.unref(u)("profile")),s:"customer"!==e.unref(s).role},"customer"!==e.unref(s).role?{t:e.p({name:"report",size:15}),v:e.o(r=>e.unref(u)("myReports"))}:{},{w:e.p({name:"orders",size:15}),x:e.o(r=>e.unref(u)("myOrders")),y:e.p({name:"logout",size:15,color:"#dc2626"}),z:e.o(f),A:e.o(e.unref(a)),B:e.p({"current-page":"mine"})})}},a=e._export_sfc(s,[["__scopeId","data-v-5428c025"]]);wx.createPage(a);

View File

@ -1 +1 @@
<view class="mine-page data-v-27768e55"><view class="user-card data-v-27768e55" style="{{h}}" bindtap="{{i}}"><view class="user-info data-v-27768e55"><view class="user-avatar data-v-27768e55" style="{{d}}"><image wx:if="{{a}}" src="{{b}}" class="avatar-img data-v-27768e55"/><label wx:else class="avatar-initials data-v-27768e55">{{c}}</label></view><view class="user-info-content data-v-27768e55"><view class="user-name data-v-27768e55">{{e}}</view><view class="user-phone data-v-27768e55">{{f}}</view><view class="role-pill data-v-27768e55">{{g}}</view></view></view></view><view wx:if="{{j}}" class="menu-section data-v-27768e55"><view class="menu-card data-v-27768e55"><view class="menu-title data-v-27768e55">店铺管理</view><view class="menu-item data-v-27768e55" bindtap="{{l}}"><view class="menu-left data-v-27768e55"><label class="menu-icon data-v-27768e55"><app-icon wx:if="{{k}}" class="data-v-27768e55" u-i="27768e55-0" bind:__l="__l" u-p="{{k}}"/></label><label class="menu-text data-v-27768e55">员工管理</label></view><label class="menu-arrow data-v-27768e55"></label></view><view class="menu-item data-v-27768e55" bindtap="{{n}}"><view class="menu-left data-v-27768e55"><label class="menu-icon data-v-27768e55"><app-icon wx:if="{{m}}" class="data-v-27768e55" u-i="27768e55-1" bind:__l="__l" u-p="{{m}}"/></label><label class="menu-text data-v-27768e55">服务类型</label></view><label class="menu-arrow data-v-27768e55"></label></view><view class="menu-item data-v-27768e55" bindtap="{{p}}"><view class="menu-left data-v-27768e55"><label class="menu-icon data-v-27768e55"><app-icon wx:if="{{o}}" class="data-v-27768e55" u-i="27768e55-2" bind:__l="__l" u-p="{{o}}"/></label><label class="menu-text data-v-27768e55">店铺设置</label></view><label class="menu-arrow data-v-27768e55"></label></view></view></view><view class="menu-section data-v-27768e55"><view class="menu-card data-v-27768e55"><view class="menu-title data-v-27768e55">个人中心</view><view class="menu-item data-v-27768e55" bindtap="{{r}}"><view class="menu-left data-v-27768e55"><label class="menu-icon data-v-27768e55"><app-icon wx:if="{{q}}" class="data-v-27768e55" u-i="27768e55-3" bind:__l="__l" u-p="{{q}}"/></label><label class="menu-text data-v-27768e55">个人信息</label></view><label class="menu-arrow data-v-27768e55"></label></view><view class="menu-item data-v-27768e55" bindtap="{{t}}"><view class="menu-left data-v-27768e55"><label class="menu-icon data-v-27768e55"><app-icon wx:if="{{s}}" class="data-v-27768e55" u-i="27768e55-4" bind:__l="__l" u-p="{{s}}"/></label><label class="menu-text data-v-27768e55">我的报告</label></view><label class="menu-arrow data-v-27768e55"></label></view><view class="menu-item data-v-27768e55" bindtap="{{w}}"><view class="menu-left data-v-27768e55"><label class="menu-icon data-v-27768e55"><app-icon wx:if="{{v}}" class="data-v-27768e55" u-i="27768e55-5" bind:__l="__l" u-p="{{v}}"/></label><label class="menu-text data-v-27768e55">我的订单</label></view><label class="menu-arrow data-v-27768e55"></label></view></view></view><view class="menu-section data-v-27768e55"><button class="logout-btn data-v-27768e55" bindtap="{{y}}"><app-icon wx:if="{{x}}" class="data-v-27768e55" u-i="27768e55-6" bind:__l="__l" u-p="{{x}}"/>退出登录</button></view></view><tab-bar wx:if="{{A}}" class="data-v-27768e55" bindchange="{{z}}" u-i="27768e55-7" bind:__l="__l" u-p="{{A}}"/> <view class="mine-page data-v-5428c025"><view class="user-card data-v-5428c025" style="{{h}}" bindtap="{{i}}"><view class="user-info data-v-5428c025"><view class="user-avatar data-v-5428c025" style="{{d}}"><image wx:if="{{a}}" src="{{b}}" class="avatar-img data-v-5428c025"/><label wx:else class="avatar-initials data-v-5428c025">{{c}}</label></view><view class="user-info-content data-v-5428c025"><view class="user-name data-v-5428c025">{{e}}</view><view class="user-phone data-v-5428c025">{{f}}</view><view class="role-pill data-v-5428c025">{{g}}</view></view></view></view><view wx:if="{{j}}" class="menu-section data-v-5428c025"><view class="menu-card data-v-5428c025"><view class="menu-title data-v-5428c025">店铺管理</view><view class="menu-item data-v-5428c025" bindtap="{{l}}"><view class="menu-left data-v-5428c025"><label class="menu-icon data-v-5428c025"><app-icon wx:if="{{k}}" class="data-v-5428c025" u-i="5428c025-0" bind:__l="__l" u-p="{{k}}"/></label><label class="menu-text data-v-5428c025">员工管理</label></view><label class="menu-arrow data-v-5428c025"></label></view><view class="menu-item data-v-5428c025" bindtap="{{n}}"><view class="menu-left data-v-5428c025"><label class="menu-icon data-v-5428c025"><app-icon wx:if="{{m}}" class="data-v-5428c025" u-i="5428c025-1" bind:__l="__l" u-p="{{m}}"/></label><label class="menu-text data-v-5428c025">服务类型</label></view><label class="menu-arrow data-v-5428c025"></label></view><view class="menu-item data-v-5428c025" bindtap="{{p}}"><view class="menu-left data-v-5428c025"><label class="menu-icon data-v-5428c025"><app-icon wx:if="{{o}}" class="data-v-5428c025" u-i="5428c025-2" bind:__l="__l" u-p="{{o}}"/></label><label class="menu-text data-v-5428c025">店铺设置</label></view><label class="menu-arrow data-v-5428c025"></label></view></view></view><view class="menu-section data-v-5428c025"><view class="menu-card data-v-5428c025"><view class="menu-title data-v-5428c025">个人中心</view><view class="menu-item data-v-5428c025" bindtap="{{r}}"><view class="menu-left data-v-5428c025"><label class="menu-icon data-v-5428c025"><app-icon wx:if="{{q}}" class="data-v-5428c025" u-i="5428c025-3" bind:__l="__l" u-p="{{q}}"/></label><label class="menu-text data-v-5428c025">个人信息</label></view><label class="menu-arrow data-v-5428c025"></label></view><view wx:if="{{s}}" class="menu-item data-v-5428c025" bindtap="{{v}}"><view class="menu-left data-v-5428c025"><label class="menu-icon data-v-5428c025"><app-icon wx:if="{{t}}" class="data-v-5428c025" u-i="5428c025-4" bind:__l="__l" u-p="{{t}}"/></label><label class="menu-text data-v-5428c025">我的报告</label></view><label class="menu-arrow data-v-5428c025"></label></view><view class="menu-item data-v-5428c025" bindtap="{{x}}"><view class="menu-left data-v-5428c025"><label class="menu-icon data-v-5428c025"><app-icon wx:if="{{w}}" class="data-v-5428c025" u-i="5428c025-5" bind:__l="__l" u-p="{{w}}"/></label><label class="menu-text data-v-5428c025">我的订单</label></view><label class="menu-arrow data-v-5428c025"></label></view></view></view><view class="menu-section data-v-5428c025"><button class="logout-btn data-v-5428c025" bindtap="{{z}}"><app-icon wx:if="{{y}}" class="data-v-5428c025" u-i="5428c025-6" bind:__l="__l" u-p="{{y}}"/>退出登录</button></view></view><tab-bar wx:if="{{B}}" class="data-v-5428c025" bindchange="{{A}}" u-i="5428c025-7" bind:__l="__l" u-p="{{B}}"/>

View File

@ -1 +1 @@
.mine-page.data-v-27768e55{padding-bottom:140rpx;background:#f5f7fb;min-height:100vh}.user-card.data-v-27768e55{background:linear-gradient(135deg,#22c55e,#16a34a);border-radius:0 0 24px 24px;padding:24px 16px 22px;color:#fff;cursor:pointer;box-shadow:0 10px 24px rgba(34,197,94,.25)}.user-info.data-v-27768e55{display:flex;align-items:center;gap:12px}.user-info-content.data-v-27768e55{flex:1}.user-avatar.data-v-27768e55{width:58px;height:58px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;overflow:hidden;flex-shrink:0;border:2px solid rgba(255,255,255,.3)}.avatar-img.data-v-27768e55{width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.avatar-initials.data-v-27768e55{color:#fff;font-size:22px;font-weight:700}.user-name.data-v-27768e55{font-size:21px;font-weight:700;color:#fff;line-height:1.1}.user-phone.data-v-27768e55{font-size:14px;opacity:.92;margin-top:4px}.role-pill.data-v-27768e55{margin-top:8px;display:inline-flex;align-items:center;height:24px;padding:0 10px;border-radius:999px;font-size:12px;font-weight:700;background:rgba(255,255,255,.92);color:#166534}.menu-section.data-v-27768e55{padding:0 14px;margin-top:12px}.menu-card.data-v-27768e55{background:#fff;border:1px solid #e6ecf4;border-radius:16px;box-shadow:0 8px 20px rgba(15,23,42,.05);overflow:hidden}.menu-title.data-v-27768e55{font-size:12px;color:#94a3b8;font-weight:700;padding:12px 14px 8px}.menu-item.data-v-27768e55{min-height:54px;padding:0 14px;display:flex;align-items:center;justify-content:space-between}.menu-item+.menu-item.data-v-27768e55{border-top:1px solid #eef2f7}.menu-left.data-v-27768e55{display:flex;align-items:center;gap:10px}.menu-icon.data-v-27768e55{width:28px;height:28px;border-radius:8px;background:#f1f5f9;display:inline-flex;align-items:center;justify-content:center;font-size:16px}.menu-text.data-v-27768e55{font-size:16px;color:#1f2937;font-weight:600}.menu-arrow.data-v-27768e55{color:#c4cfdd;font-size:18px}.logout-btn.data-v-27768e55{width:100%;height:44px;display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:12px;border:1px solid #fecaca;background:#fff1f2;color:#dc2626;font-size:15px;font-weight:700} .mine-page.data-v-5428c025{padding-bottom:140rpx;background:#f5f7fb;min-height:100vh}.user-card.data-v-5428c025{background:linear-gradient(135deg,#22c55e,#16a34a);border-radius:0 0 24px 24px;padding:24px 16px 22px;color:#fff;cursor:pointer;box-shadow:0 10px 24px rgba(34,197,94,.25)}.user-info.data-v-5428c025{display:flex;align-items:center;gap:12px}.user-info-content.data-v-5428c025{flex:1}.user-avatar.data-v-5428c025{width:58px;height:58px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;overflow:hidden;flex-shrink:0;border:2px solid rgba(255,255,255,.3)}.avatar-img.data-v-5428c025{width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.avatar-initials.data-v-5428c025{color:#fff;font-size:22px;font-weight:700}.user-name.data-v-5428c025{font-size:21px;font-weight:700;color:#fff;line-height:1.1}.user-phone.data-v-5428c025{font-size:14px;opacity:.92;margin-top:4px}.role-pill.data-v-5428c025{margin-top:8px;display:inline-flex;align-items:center;height:24px;padding:0 10px;border-radius:999px;font-size:12px;font-weight:700;background:rgba(255,255,255,.92);color:#166534}.menu-section.data-v-5428c025{padding:0 14px;margin-top:12px}.menu-card.data-v-5428c025{background:#fff;border:1px solid #e6ecf4;border-radius:16px;box-shadow:0 8px 20px rgba(15,23,42,.05);overflow:hidden}.menu-title.data-v-5428c025{font-size:12px;color:#94a3b8;font-weight:700;padding:12px 14px 8px}.menu-item.data-v-5428c025{min-height:54px;padding:0 14px;display:flex;align-items:center;justify-content:space-between}.menu-item+.menu-item.data-v-5428c025{border-top:1px solid #eef2f7}.menu-left.data-v-5428c025{display:flex;align-items:center;gap:10px}.menu-icon.data-v-5428c025{width:28px;height:28px;border-radius:8px;background:#f1f5f9;display:inline-flex;align-items:center;justify-content:center;font-size:16px}.menu-text.data-v-5428c025{font-size:16px;color:#1f2937;font-weight:600}.menu-arrow.data-v-5428c025{color:#c4cfdd;font-size:18px}.logout-btn.data-v-5428c025{width:100%;height:44px;display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:12px;border:1px solid #fecaca;background:#fff1f2;color:#dc2626;font-size:15px;font-weight:700}

View File

@ -26,6 +26,7 @@ function scheduleAuthGuard() {
try { try {
if (utils_session.isLoggedIn()) return; if (utils_session.isLoggedIn()) return;
const pages = getCurrentPages(); const pages = getCurrentPages();
if (pages.length === 0) return;
const cur = pages[pages.length - 1]; const cur = pages[pages.length - 1];
const route = cur && cur.route ? cur.route : ""; const route = cur && cur.route ? cur.route : "";
if (isPublicRoute(route)) return; if (isPublicRoute(route)) return;

View File

@ -182,9 +182,32 @@ const _sfc_main = {
}); });
return (_ctx, _cache) => { return (_ctx, _cache) => {
return common_vendor.e({ return common_vendor.e({
a: common_vendor.o(($event) => showNewAppt.value = true), a: common_vendor.unref(userInfo).role === "customer"
}, common_vendor.unref(userInfo).role === "customer" ? {
b: common_vendor.s(common_vendor.unref(navSafeStyle)), b: common_vendor.s(common_vendor.unref(navSafeStyle)),
c: common_vendor.f(statusTabs, (tab, k0, i0) => { c: common_vendor.t(common_vendor.unref(storeInfo).name || "宠伴生活馆"),
d: newAppt.value.petName,
e: common_vendor.o(($event) => newAppt.value.petName = $event.detail.value),
f: common_vendor.t(newAppt.value.petType || "请选择"),
g: petTypes,
h: common_vendor.o((e) => newAppt.value.petType = petTypes[e.detail.value].value),
i: common_vendor.t(newAppt.value.serviceType || "请选择"),
j: serviceTypes.value,
k: common_vendor.o((e) => newAppt.value.serviceType = serviceTypes.value[e.detail.value].value),
l: common_vendor.t(appointmentDate.value || "请选择日期"),
m: appointmentDate.value,
n: common_vendor.o(onAppointmentDateChange),
o: common_vendor.t(appointmentTime.value || "请选择时间"),
p: appointmentTime.value,
q: common_vendor.o(onAppointmentTimeOnlyChange),
r: newAppt.value.remark,
s: common_vendor.o(($event) => newAppt.value.remark = $event.detail.value),
t: creatingAppt.value,
v: common_vendor.o(confirmNewAppt)
} : common_vendor.e({
w: common_vendor.o(($event) => showNewAppt.value = true),
x: common_vendor.s(common_vendor.unref(navSafeStyle)),
y: common_vendor.f(statusTabs, (tab, k0, i0) => {
return common_vendor.e({ return common_vendor.e({
a: common_vendor.t(tab.title), a: common_vendor.t(tab.title),
b: tab.name === "new" && newApptCount.value > 0 b: tab.name === "new" && newApptCount.value > 0
@ -198,9 +221,9 @@ const _sfc_main = {
f: common_vendor.o(($event) => currentStatus.value = tab.name, tab.name) f: common_vendor.o(($event) => currentStatus.value = tab.name, tab.name)
}); });
}), }),
d: filteredOrders.value.length > 0 z: filteredOrders.value.length > 0
}, filteredOrders.value.length > 0 ? { }, filteredOrders.value.length > 0 ? {
e: common_vendor.f(filteredOrders.value, (item, k0, i0) => { A: common_vendor.f(filteredOrders.value, (item, k0, i0) => {
return common_vendor.e({ return common_vendor.e({
a: common_vendor.n(`dot-${item.status}`), a: common_vendor.n(`dot-${item.status}`),
b: "bd9bb22b-0-" + i0, b: "bd9bb22b-0-" + i0,
@ -225,45 +248,45 @@ const _sfc_main = {
q: item.id q: item.id
}); });
}), }),
f: common_vendor.p({ B: common_vendor.p({
name: "profile", name: "profile",
size: 14 size: 14
}), }),
g: common_vendor.p({ C: common_vendor.p({
name: "orders", name: "orders",
size: 13 size: 13
}) })
} : {}, { } : {}, {
h: filteredOrders.value.length === 0 D: filteredOrders.value.length === 0
}, filteredOrders.value.length === 0 ? {} : {}, { }, filteredOrders.value.length === 0 ? {} : {}, {
i: showNewAppt.value E: showNewAppt.value
}, showNewAppt.value ? { }, showNewAppt.value ? {
j: common_vendor.o(($event) => showNewAppt.value = false), F: common_vendor.o(($event) => showNewAppt.value = false),
k: newAppt.value.petName, G: newAppt.value.petName,
l: common_vendor.o(($event) => newAppt.value.petName = $event.detail.value), H: common_vendor.o(($event) => newAppt.value.petName = $event.detail.value),
m: common_vendor.t(newAppt.value.petType || "请选择"), I: common_vendor.t(newAppt.value.petType || "请选择"),
n: petTypes, J: petTypes,
o: common_vendor.o((e) => newAppt.value.petType = petTypes[e.detail.value].value), K: common_vendor.o((e) => newAppt.value.petType = petTypes[e.detail.value].value),
p: common_vendor.t(newAppt.value.serviceType || "请选择"), L: common_vendor.t(newAppt.value.serviceType || "请选择"),
q: serviceTypes.value, M: serviceTypes.value,
r: common_vendor.o((e) => newAppt.value.serviceType = serviceTypes.value[e.detail.value].value), N: common_vendor.o((e) => newAppt.value.serviceType = serviceTypes.value[e.detail.value].value),
s: common_vendor.t(appointmentDate.value || "请选择日期"), O: common_vendor.t(appointmentDate.value || "请选择日期"),
t: appointmentDate.value, P: appointmentDate.value,
v: common_vendor.o(onAppointmentDateChange), Q: common_vendor.o(onAppointmentDateChange),
w: common_vendor.t(appointmentTime.value || "请选择时间"), R: common_vendor.t(appointmentTime.value || "请选择时间"),
x: appointmentTime.value, S: appointmentTime.value,
y: common_vendor.o(onAppointmentTimeOnlyChange), T: common_vendor.o(onAppointmentTimeOnlyChange),
z: newAppt.value.remark, U: newAppt.value.remark,
A: common_vendor.o(($event) => newAppt.value.remark = $event.detail.value), V: common_vendor.o(($event) => newAppt.value.remark = $event.detail.value),
B: common_vendor.o(($event) => showNewAppt.value = false), W: common_vendor.o(($event) => showNewAppt.value = false),
C: creatingAppt.value, X: creatingAppt.value,
D: common_vendor.o(confirmNewAppt), Y: common_vendor.o(confirmNewAppt),
E: common_vendor.o(() => { Z: common_vendor.o(() => {
}), }),
F: common_vendor.o(($event) => showNewAppt.value = false) aa: common_vendor.o(($event) => showNewAppt.value = false)
} : {}, { } : {}), {
G: common_vendor.o(common_vendor.unref(goPage)), ab: common_vendor.o(common_vendor.unref(goPage)),
H: common_vendor.p({ ac: common_vendor.p({
["current-page"]: "home" ["current-page"]: "home"
}) })
}); });

File diff suppressed because one or more lines are too long

View File

@ -25,6 +25,26 @@
} }
.list-content.data-v-bd9bb22b { padding: 0; .list-content.data-v-bd9bb22b { padding: 0;
} }
.c-header.data-v-bd9bb22b {
background: linear-gradient(135deg, #07c160 0%, #10b76f 100%);
padding: 20px 20px 40px;
text-align: center;
color: #fff;
border-radius: 0 0 24px 24px;
}
.c-title.data-v-bd9bb22b { font-size: 24px; font-weight: 700; margin-bottom: 8px;
}
.c-sub.data-v-bd9bb22b { font-size: 14px; opacity: 0.8;
}
.c-booking-card.data-v-bd9bb22b {
background: #fff;
border-radius: 16px;
margin: -20px 16px 0;
padding: 24px 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.c-booking-title.data-v-bd9bb22b { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 20px; text-align: center;
}
/* 按钮 */ /* 按钮 */
.btn-primary.data-v-bd9bb22b { .btn-primary.data-v-bd9bb22b {

View File

@ -57,7 +57,7 @@ const _sfc_main = {
d: common_vendor.s(avatarStyle.value), d: common_vendor.s(avatarStyle.value),
e: common_vendor.t(common_vendor.unref(userInfo).name), e: common_vendor.t(common_vendor.unref(userInfo).name),
f: common_vendor.t(common_vendor.unref(userInfo).phone), f: common_vendor.t(common_vendor.unref(userInfo).phone),
g: common_vendor.t(common_vendor.unref(userInfo).role === "boss" ? "店长" : "员工"), g: common_vendor.t(common_vendor.unref(userInfo).role === "boss" ? "店长" : common_vendor.unref(userInfo).role === "customer" ? "客户" : "员工"),
h: common_vendor.s(common_vendor.unref(userCardSafeStyle)), h: common_vendor.s(common_vendor.unref(userCardSafeStyle)),
i: common_vendor.o(($event) => common_vendor.unref(navigateTo)("profile")), i: common_vendor.o(($event) => common_vendor.unref(navigateTo)("profile")),
j: common_vendor.unref(userInfo).role === "boss" j: common_vendor.unref(userInfo).role === "boss"
@ -83,24 +83,27 @@ const _sfc_main = {
size: 15 size: 15
}), }),
r: common_vendor.o(($event) => common_vendor.unref(navigateTo)("profile")), r: common_vendor.o(($event) => common_vendor.unref(navigateTo)("profile")),
s: common_vendor.p({ s: common_vendor.unref(userInfo).role !== "customer"
}, common_vendor.unref(userInfo).role !== "customer" ? {
t: common_vendor.p({
name: "report", name: "report",
size: 15 size: 15
}), }),
t: common_vendor.o(($event) => common_vendor.unref(navigateTo)("myReports")), v: common_vendor.o(($event) => common_vendor.unref(navigateTo)("myReports"))
v: common_vendor.p({ } : {}, {
w: common_vendor.p({
name: "orders", name: "orders",
size: 15 size: 15
}), }),
w: common_vendor.o(($event) => common_vendor.unref(navigateTo)("myOrders")), x: common_vendor.o(($event) => common_vendor.unref(navigateTo)("myOrders")),
x: common_vendor.p({ y: common_vendor.p({
name: "logout", name: "logout",
size: 15, size: 15,
color: "#dc2626" color: "#dc2626"
}), }),
y: common_vendor.o(logout), z: common_vendor.o(logout),
z: common_vendor.o(common_vendor.unref(goPage)), A: common_vendor.o(common_vendor.unref(goPage)),
A: common_vendor.p({ B: common_vendor.p({
["current-page"]: "mine" ["current-page"]: "mine"
}) })
}); });

View File

@ -1 +1 @@
<view class="mine-page data-v-02264ef7"><view class="user-card data-v-02264ef7" style="{{h}}" bindtap="{{i}}"><view class="user-info data-v-02264ef7"><view class="user-avatar data-v-02264ef7" style="{{d}}"><image wx:if="{{a}}" src="{{b}}" class="avatar-img data-v-02264ef7"/><label wx:else class="avatar-initials data-v-02264ef7">{{c}}</label></view><view class="user-info-content data-v-02264ef7"><view class="user-name data-v-02264ef7">{{e}}</view><view class="user-phone data-v-02264ef7">{{f}}</view><view class="role-pill data-v-02264ef7">{{g}}</view></view></view></view><view wx:if="{{j}}" class="menu-section data-v-02264ef7"><view class="menu-card data-v-02264ef7"><view class="menu-title data-v-02264ef7">店铺管理</view><view class="menu-item data-v-02264ef7" bindtap="{{l}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{k}}" class="data-v-02264ef7" u-i="02264ef7-0" bind:__l="__l" u-p="{{k}}"/></label><label class="menu-text data-v-02264ef7">员工管理</label></view><label class="menu-arrow data-v-02264ef7"></label></view><view class="menu-item data-v-02264ef7" bindtap="{{n}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{m}}" class="data-v-02264ef7" u-i="02264ef7-1" bind:__l="__l" u-p="{{m}}"/></label><label class="menu-text data-v-02264ef7">服务类型</label></view><label class="menu-arrow data-v-02264ef7"></label></view><view class="menu-item data-v-02264ef7" bindtap="{{p}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{o}}" class="data-v-02264ef7" u-i="02264ef7-2" bind:__l="__l" u-p="{{o}}"/></label><label class="menu-text data-v-02264ef7">店铺设置</label></view><label class="menu-arrow data-v-02264ef7"></label></view></view></view><view class="menu-section data-v-02264ef7"><view class="menu-card data-v-02264ef7"><view class="menu-title data-v-02264ef7">个人中心</view><view class="menu-item data-v-02264ef7" bindtap="{{r}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{q}}" class="data-v-02264ef7" u-i="02264ef7-3" bind:__l="__l" u-p="{{q}}"/></label><label class="menu-text data-v-02264ef7">个人信息</label></view><label class="menu-arrow data-v-02264ef7"></label></view><view class="menu-item data-v-02264ef7" bindtap="{{t}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{s}}" class="data-v-02264ef7" u-i="02264ef7-4" bind:__l="__l" u-p="{{s}}"/></label><label class="menu-text data-v-02264ef7">我的报告</label></view><label class="menu-arrow data-v-02264ef7"></label></view><view class="menu-item data-v-02264ef7" bindtap="{{w}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{v}}" class="data-v-02264ef7" u-i="02264ef7-5" bind:__l="__l" u-p="{{v}}"/></label><label class="menu-text data-v-02264ef7">我的订单</label></view><label class="menu-arrow data-v-02264ef7"></label></view></view></view><view class="menu-section data-v-02264ef7"><button class="logout-btn data-v-02264ef7" bindtap="{{y}}"><app-icon wx:if="{{x}}" class="data-v-02264ef7" u-i="02264ef7-6" bind:__l="__l" u-p="{{x}}"/>退出登录</button></view></view><tab-bar wx:if="{{A}}" class="data-v-02264ef7" bindchange="{{z}}" u-i="02264ef7-7" bind:__l="__l" u-p="{{A}}"/> <view class="mine-page data-v-02264ef7"><view class="user-card data-v-02264ef7" style="{{h}}" bindtap="{{i}}"><view class="user-info data-v-02264ef7"><view class="user-avatar data-v-02264ef7" style="{{d}}"><image wx:if="{{a}}" src="{{b}}" class="avatar-img data-v-02264ef7"/><label wx:else class="avatar-initials data-v-02264ef7">{{c}}</label></view><view class="user-info-content data-v-02264ef7"><view class="user-name data-v-02264ef7">{{e}}</view><view class="user-phone data-v-02264ef7">{{f}}</view><view class="role-pill data-v-02264ef7">{{g}}</view></view></view></view><view wx:if="{{j}}" class="menu-section data-v-02264ef7"><view class="menu-card data-v-02264ef7"><view class="menu-title data-v-02264ef7">店铺管理</view><view class="menu-item data-v-02264ef7" bindtap="{{l}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{k}}" class="data-v-02264ef7" u-i="02264ef7-0" bind:__l="__l" u-p="{{k}}"/></label><label class="menu-text data-v-02264ef7">员工管理</label></view><label class="menu-arrow data-v-02264ef7"></label></view><view class="menu-item data-v-02264ef7" bindtap="{{n}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{m}}" class="data-v-02264ef7" u-i="02264ef7-1" bind:__l="__l" u-p="{{m}}"/></label><label class="menu-text data-v-02264ef7">服务类型</label></view><label class="menu-arrow data-v-02264ef7"></label></view><view class="menu-item data-v-02264ef7" bindtap="{{p}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{o}}" class="data-v-02264ef7" u-i="02264ef7-2" bind:__l="__l" u-p="{{o}}"/></label><label class="menu-text data-v-02264ef7">店铺设置</label></view><label class="menu-arrow data-v-02264ef7"></label></view></view></view><view class="menu-section data-v-02264ef7"><view class="menu-card data-v-02264ef7"><view class="menu-title data-v-02264ef7">个人中心</view><view class="menu-item data-v-02264ef7" bindtap="{{r}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{q}}" class="data-v-02264ef7" u-i="02264ef7-3" bind:__l="__l" u-p="{{q}}"/></label><label class="menu-text data-v-02264ef7">个人信息</label></view><label class="menu-arrow data-v-02264ef7"></label></view><view wx:if="{{s}}" class="menu-item data-v-02264ef7" bindtap="{{v}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{t}}" class="data-v-02264ef7" u-i="02264ef7-4" bind:__l="__l" u-p="{{t}}"/></label><label class="menu-text data-v-02264ef7">我的报告</label></view><label class="menu-arrow data-v-02264ef7"></label></view><view class="menu-item data-v-02264ef7" bindtap="{{x}}"><view class="menu-left data-v-02264ef7"><label class="menu-icon data-v-02264ef7"><app-icon wx:if="{{w}}" class="data-v-02264ef7" u-i="02264ef7-5" bind:__l="__l" u-p="{{w}}"/></label><label class="menu-text data-v-02264ef7">我的订单</label></view><label class="menu-arrow data-v-02264ef7"></label></view></view></view><view class="menu-section data-v-02264ef7"><button class="logout-btn data-v-02264ef7" bindtap="{{z}}"><app-icon wx:if="{{y}}" class="data-v-02264ef7" u-i="02264ef7-6" bind:__l="__l" u-p="{{y}}"/>退出登录</button></view></view><tab-bar wx:if="{{B}}" class="data-v-02264ef7" bindchange="{{A}}" u-i="02264ef7-7" bind:__l="__l" u-p="{{B}}"/>

View File

@ -1,9 +0,0 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}

View File

@ -1,539 +0,0 @@
<template>
<view class="home-page">
<!-- C端视角微官网与预约 -->
<view v-if="userInfo.role === 'customer'" class="c-home">
<view class="c-header">
<view class="c-title">{{ storeInfo.name || '宠伴生活馆' }}</view>
<view class="c-sub">宠物服务让爱更专业</view>
</view>
<view class="c-booking-card">
<view class="c-booking-title">预约服务</view>
<view class="form-item">
<text class="form-label">宠物名字</text>
<input class="form-input" v-model="newAppt.petName" placeholder="请输入宠物名字" />
</view>
<view class="form-item">
<text class="form-label">宠物类型</text>
<picker :value="petTypeIndex" :range="petTypes" @change="onPetTypeChange">
<view class="picker-val">{{ newAppt.petType || '请选择' }} </view>
</picker>
</view>
<view class="form-item">
<text class="form-label">服务类型</text>
<picker :value="serviceTypeIndex" :range="serviceTypeOptions" :range-key="'name'" @change="onServiceTypeChange">
<view class="picker-val">{{ newAppt.serviceType || '请选择' }} </view>
</picker>
</view>
<view class="form-item">
<text class="form-label">预约时间</text>
<picker mode="datetime" :value="newAppt.appointmentTime" @change="onDateChange">
<view class="picker-val">{{ newAppt.appointmentTime || '请选择' }} </view>
</picker>
</view>
<view class="form-item">
<text class="form-label">备注</text>
<input class="form-input" v-model="newAppt.remark" placeholder="可选" />
</view>
<button class="btn-primary-full" :loading="creatingAppt" @click="onNewApptConfirm">提交预约</button>
</view>
</view>
<!-- B端视角订单管理 -->
<view v-else class="b-home">
<!-- 自定义导航栏 -->
<view class="nav-bar">
<view class="nav-title">宠伴生活馆</view>
<view class="nav-right">
<view class="new-appt-btn" @click="showNewAppt = true">+ 新建预约</view>
</view>
</view>
<!-- Tab切换 -->
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.key"
:class="['tab-item', { active: currentStatus === tab.key }]"
@click="currentStatus = tab.key"
>{{ tab.label }}</view>
</view>
<!-- 列表 -->
<view class="list-content">
<view v-if="filteredOrders.length > 0" class="timeline">
<view v-for="(item, index) in filteredOrders" :key="item.id" class="timeline-item">
<!-- 时间轴线+圆点 -->
<view class="timeline-dot" :class="'dot-' + item.status">
<view class="dot-inner"></view>
</view>
<view v-if="index < filteredOrders.length - 1" class="timeline-line"></view>
<!-- 卡片 -->
<view class="order-card" :class="'card-' + item.status">
<view class="card-header">
<view class="pet-info">
<text class="pet-emoji">{{ petEmoji(item.petType) }}</text>
<text class="pet-name">{{ item.petName }}</text>
<text class="service-tag" :class="'tag-' + item.status">{{ item.serviceType }}</text>
</view>
<view class="status-tag" :class="'status-' + item.status">{{ item.statusText }}</view>
</view>
<view class="card-body">
<text class="card-time">📅 {{ item.time }}</text>
</view>
<view class="card-footer">
<view v-if="item.status === 'new'" class="action-btns">
<view class="btn-sm btn-primary-sm" @click="startService(item)">开始服务</view>
<view class="btn-sm btn-default-sm" @click="cancelService(item)">取消</view>
</view>
<view v-else-if="item.status === 'doing'">
<view class="btn-sm btn-outline-sm" @click="goReport(item)">填写报告</view>
</view>
<text v-else class="done-label">{{ item.status === 'cancel' ? '已取消' : '已完成' }}</text>
</view>
</view>
</view>
</view>
<view v-else class="empty-wrap">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无数据</text>
</view>
</view>
<!-- 新建预约弹窗 -->
<view v-if="showNewAppt" class="dialog-mask" @click="showNewAppt = false">
<view class="dialog-content" @click.stop>
<view class="dialog-header">
<text class="dialog-title">新建预约</text>
<text class="dialog-close" @click="showNewAppt = false"></text>
</view>
<view class="dialog-body">
<view class="form-item">
<text class="form-label">宠物名字</text>
<input class="form-input" v-model="newAppt.petName" placeholder="请输入" />
</view>
<view class="form-item">
<text class="form-label">宠物类型</text>
<picker :value="petTypeIndex" :range="petTypes" @change="onPetTypeChange">
<view class="picker-val">{{ newAppt.petType || '请选择' }} </view>
</picker>
</view>
<view class="form-item">
<text class="form-label">服务类型</text>
<picker :value="serviceTypeIndex" :range="serviceTypeOptions" :range-key="'name'" @change="onServiceTypeChange">
<view class="picker-val">{{ newAppt.serviceType || '请选择' }} </view>
</picker>
</view>
<view class="form-item">
<text class="form-label">预约时间</text>
<picker mode="datetime" :value="newAppt.appointmentTime" @change="onDateChange">
<view class="picker-val">{{ newAppt.appointmentTime || '请选择' }} </view>
</picker>
</view>
<view class="form-item">
<text class="form-label">备注</text>
<input class="form-input" v-model="newAppt.remark" placeholder="可选" />
</view>
</view>
<view class="dialog-footer">
<view class="btn-cancel" @click="showNewAppt = false">取消</view>
<view class="btn-confirm" :class="{ loading: creatingAppt }" @click="onNewApptConfirm">确定</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import {
getAppointmentList, createAppointment, startAppointment, cancelAppointment, getServiceTypeList
} from '../../utils/api.js'
const userInfo = JSON.parse(uni.getStorageSync('petstore_user') || '{}')
const storeInfo = JSON.parse(uni.getStorageSync('petstore_store') || '{}')
const currentUserId = userInfo.id
const currentStatus = ref('new')
const orders = ref([])
const serviceTypes = ref([])
const showNewAppt = ref(false)
const creatingAppt = ref(false)
const newAppt = ref({ petName: '', petType: '', serviceType: '', appointmentTime: '', remark: '' })
const petTypeIndex = ref(0)
const serviceTypeIndex = ref(0)
const petTypes = ['猫', '狗', '其他']
const tabs = [
{ key: 'new', label: '待确认' },
{ key: 'doing', label: '进行中' },
{ key: 'done', label: '已完成' }
]
const serviceTypeOptions = computed(() => serviceTypes.value.map(s => ({ name: `${serviceEmoji(s.name)} ${s.name}`, id: s.id })))
const filteredOrders = computed(() => orders.value.filter(o => {
if (currentStatus.value === 'new') return o.status === 'new'
if (currentStatus.value === 'doing') return o.status === 'doing'
if (currentStatus.value === 'done') return o.status === 'done' || o.status === 'cancel'
return true
}))
const petEmoji = (petType) => {
const map = { '猫': '🐱', '狗': '🐶' }
return map[petType] || '🐾'
}
const serviceEmoji = (name) => {
if (!name) return ''
if (name.includes('洗澡') && name.includes('美容')) return '🛁✂️'
if (name.includes('洗澡')) return '🛁'
if (name.includes('美容')) return '✂️'
if (name.includes('剪指甲')) return '💅'
if (name.includes('驱虫')) return '🐛'
return '✨'
}
const statusTagClass = (status) => {
const map = { new: 'warning', doing: 'primary', done: 'success', cancel: 'default' }
return map[status] || 'default'
}
const fetchAppointments = async () => {
if (!currentUserId) return
const res = await getAppointmentList(currentUserId)
if (res.code === 200) {
orders.value = res.data.map(appt => ({
id: appt.id,
title: appt.serviceType || '洗澡美容预约',
desc: `${appt.petType || ''} - ${appt.petName || ''}`,
time: appt.appointmentTime ? new Date(appt.appointmentTime).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '',
status: appt.status || 'new',
statusText: { new: '待确认', doing: '服务中', done: '已完成', cancel: '已取消' }[appt.status] || '待确认',
petName: appt.petName,
petType: appt.petType,
serviceType: appt.serviceType,
appointmentTime: appt.appointmentTime
}))
}
}
const loadServiceTypes = async () => {
const sid = storeInfo.id || 1
const res = await getServiceTypeList(sid)
if (res.code === 200) serviceTypes.value = res.data
}
const onPetTypeChange = (e) => {
petTypeIndex.value = e.detail.value
newAppt.value.petType = petTypes[e.detail.value]
}
const onServiceTypeChange = (e) => {
serviceTypeIndex.value = e.detail.value
newAppt.value.serviceType = serviceTypes.value[e.detail.value]?.name || ''
}
const onDateChange = (e) => {
newAppt.value.appointmentTime = e.detail.value
}
const startService = async (item) => {
const res = await startAppointment(item.id, userInfo.id)
if (res.code === 200) {
uni.showToast({ title: '已开始服务', icon: 'success' })
fetchAppointments()
} else {
uni.showToast({ title: res.message || '操作失败', icon: 'none' })
}
}
const cancelService = async (item) => {
uni.showModal({
title: '确认取消',
content: '确定取消该预约吗?',
success: async (res) => {
if (res.confirm) {
const r = await cancelAppointment(item.id)
if (r.code === 200) {
uni.showToast({ title: '已取消', icon: 'success' })
fetchAppointments()
} else {
uni.showToast({ title: r.message || '操作失败', icon: 'none' })
}
}
}
})
}
const goReport = (item) => {
uni.setStorageSync('petstore_report_prefill', JSON.stringify({
appointmentId: item.id,
petName: item.petName,
serviceType: item.serviceType,
appointmentTime: item.appointmentTime
}))
uni.switchTab({ url: '/pages/report/report' })
}
const onNewApptConfirm = async () => {
const a = newAppt.value
if (!a.petName) { uni.showToast({ title: '请输入宠物名字', icon: 'none' }); return }
if (!a.petType) { uni.showToast({ title: '请选择宠物类型', icon: 'none' }); return }
if (!a.serviceType) { uni.showToast({ title: '请选择服务类型', icon: 'none' }); return }
if (!a.appointmentTime) { uni.showToast({ title: '请选择预约时间', icon: 'none' }); return }
creatingAppt.value = true
const res = await createAppointment({ ...a, storeId: storeInfo.id || 1, userId: userInfo.id })
creatingAppt.value = false
if (res.code === 200) {
uni.showToast({ title: '预约创建成功', icon: 'success' })
newAppt.value = { petName: '', petType: '', serviceType: '', appointmentTime: '', remark: '' }
showNewAppt.value = false
fetchAppointments()
} else {
uni.showToast({ title: res.message || '创建失败', icon: 'none' })
}
}
onMounted(() => {
fetchAppointments()
loadServiceTypes()
})
</script>
<style scoped>
.home-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 20px; }
/* C端样式 */
.c-home { padding-bottom: 20px; }
.c-header {
background: linear-gradient(135deg, #07c160 0%, #10b76f 100%);
padding: 60px 20px 40px;
text-align: center;
color: #fff;
border-radius: 0 0 24px 24px;
}
.c-title { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
.c-sub { font-size: 14px; opacity: 0.8; }
.c-booking-card {
background: #fff;
border-radius: 16px;
margin: -20px 16px 0;
padding: 24px 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.c-booking-title { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 20px; text-align: center; }
.btn-primary-full {
width: 100%;
height: 44px;
background: #07c160;
color: #fff;
border: none;
border-radius: 8px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 24px;
}
/* B端样式 */
.nav-bar {
background: linear-gradient(135deg, #07c160 0%, #10b76f 100%);
padding: 40px 16px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-title { color: #fff; font-size: 17px; font-weight: 600; }
.new-appt-btn {
background: rgba(255,255,255,0.2);
color: #fff;
font-size: 13px;
padding: 6px 14px;
border-radius: 20px;
border: 1px solid rgba(255,255,255,0.4);
}
.tab-bar {
display: flex;
background: #fff;
padding: 0 16px;
border-bottom: 1px solid #eee;
}
.tab-item {
flex: 1;
text-align: center;
padding: 12px 0;
font-size: 14px;
color: #999;
border-bottom: 2px solid transparent;
}
.tab-item.active { color: #07c160; border-bottom-color: #07c160; font-weight: 600; }
.list-content { padding: 12px 16px 0; }
.timeline { padding: 4px 0; }
.timeline-item {
display: flex;
align-items: flex-start;
margin-bottom: 4px;
position: relative;
}
.timeline-dot {
display: flex;
flex-direction: column;
align-items: center;
width: 24px;
flex-shrink: 0;
padding-top: 14px;
}
.dot-inner {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid;
background: #fff;
z-index: 1;
}
.dot-new .dot-inner { border-color: #ff9f00; }
.dot-doing .dot-inner { border-color: #07c160; }
.dot-done .dot-inner { border-color: #c0c0c0; }
.dot-cancel .dot-inner { border-color: #d0d0d0; }
.timeline-line {
position: absolute;
left: 11px;
top: 28px;
bottom: -4px;
width: 2px;
background: #e8e0d8;
}
.order-card {
flex: 1;
background: #fff;
border-radius: 12px;
padding: 14px 16px;
margin-left: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
border-left: 4px solid;
}
.card-new { border-left-color: #ff9f00; }
.card-doing { border-left-color: #07c160; }
.card-done { border-left-color: #c0c0c0; }
.card-cancel { border-left-color: #d8d8d8; opacity: 0.7; }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.pet-info { display: flex; align-items: center; gap: 8px; }
.pet-emoji { font-size: 20px; }
.pet-name { font-weight: 600; color: #333; font-size: 15px; }
.service-tag {
font-size: 12px;
background: #f0f9f4;
color: #52c41a;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.tag-new { background: #fff3e8; color: #ff9f00; }
.tag-doing { background: #e8f7ef; color: #07c160; }
.tag-done { background: #f0f0f0; color: #888; }
.status-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
}
.status-new { background: #fff3e8; color: #ff9f00; }
.status-doing { background: #e8f7ef; color: #07c160; }
.status-done { background: #f0f0f0; color: #888; }
.status-cancel { background: #f5f5f5; color: #999; }
.card-body { margin-bottom: 10px; }
.card-time { font-size: 13px; color: #999; }
.card-footer { display: flex; justify-content: flex-end; align-items: center; }
.action-btns { display: flex; gap: 8px; }
.done-label { font-size: 13px; color: #999; }
.btn-sm {
font-size: 12px;
padding: 5px 14px;
border-radius: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-primary-sm { background: #07c160; color: #fff; border: 1px solid #07c160; }
.btn-default-sm { background: #fff; color: #666; border: 1px solid #ddd; }
.btn-outline-sm { background: #fff; color: #07c160; border: 1px solid #07c160; }
.empty-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; }
.empty-icon { font-size: 48px; }
.empty-text { font-size: 14px; color: #999; margin-top: 12px; }
/* 弹窗 */
.dialog-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
}
.dialog-content {
width: 100%;
max-height: 80vh;
background: #fff;
border-radius: 16px 16px 0 0;
overflow: hidden;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.dialog-title { font-size: 16px; font-weight: 600; color: #333; }
.dialog-close { font-size: 18px; color: #999; }
.dialog-body { padding: 16px 20px; max-height: 55vh; overflow-y: auto; }
.dialog-footer { display: flex; border-top: 1px solid #eee; }
.btn-cancel, .btn-confirm {
flex: 1;
text-align: center;
padding: 14px 0;
font-size: 15px;
}
.btn-cancel { color: #666; border-right: 1px solid #eee; }
.btn-confirm { color: #07c160; font-weight: 600; }
.form-item { margin-bottom: 16px; }
.form-label { font-size: 13px; color: #999; display: block; margin-bottom: 6px; }
.form-input {
width: 100%;
height: 40px;
border: 1px solid #eee;
border-radius: 8px;
padding: 0 12px;
font-size: 14px;
box-sizing: border-box;
}
.picker-val {
height: 40px;
border: 1px solid #eee;
border-radius: 8px;
padding: 0 12px;
font-size: 14px;
line-height: 40px;
color: #333;
background: #fafafa;
}
</style>

View File

@ -1,348 +0,0 @@
<template>
<view class="login-page">
<!-- Logo区 -->
<view class="login-logo">
<view class="login-logo-text">宠伴生活馆</view>
<view class="login-logo-sub">宠物服务让爱更专业</view>
</view>
<!-- 老板注册成功 -->
<view v-if="bossRegistered" class="form-card">
<view class="success-icon">🎉</view>
<view class="success-title">入驻成功</view>
<view class="success-sub">欢迎加入宠伴生活馆</view>
<view class="success-info">
<view class="info-row"><span class="label">店铺名称</span><span class="value">{{ regResult.store.name }}</span></view>
<view class="info-row"><span class="label">您的账号</span><span class="value">{{ regResult.user.phone }}</span></view>
<view class="info-row"><span class="label">初始密码</span><span class="value">{{ regResult.user.password }}</span></view>
<view class="info-row"><span class="label">员工邀请码</span><span class="value">{{ regResult.store.inviteCode }}</span></view>
</view>
<button class="btn-primary-full" @click="copyCode">📋 复制员工邀请码</button>
<button class="btn-default-full" @click="goLogin" style="margin-top:12px;">前往登录 </button>
</view>
<!-- 员工注册成功 -->
<view v-else-if="staffRegistered" class="form-card">
<view class="success-icon"></view>
<view class="success-title">注册成功</view>
<view class="success-sub">您已成功加入 {{ regResult.store?.name }} 团队</view>
<view class="success-info">
<view class="info-row"><span class="label">所属店铺</span><span class="value">{{ regResult.store?.name }}</span></view>
<view class="info-row"><span class="label">您的账号</span><span class="value">{{ regResult.user?.phone }}</span></view>
</view>
<button class="btn-primary-full" @click="goLogin">前往登录 </button>
</view>
<!-- 登录/注册表单 -->
<view v-else class="form-card">
<!-- Tab切换 -->
<view class="tab-header">
<view
v-for="tab in tabs"
:key="tab.key"
:class="['tab-item', { active: activeTab === tab.key }]"
@click="activeTab = tab.key"
>{{ tab.label }}</view>
</view>
<!-- 用户登录 -->
<view v-if="activeTab === 'staff'" class="tab-content">
<view class="form-group">
<input class="form-input" v-model="loginForm.phone" type="tel" placeholder="请输入手机号" maxlength="11" />
</view>
<view class="form-group">
<view class="code-input-wrap">
<input class="form-input" v-model="loginForm.code" type="digit" placeholder="短信验证码" maxlength="6" />
<view class="code-btn" :class="{ disabled: smsCountdown > 0 }" @click="handleSendSms">
{{ smsCountdown > 0 ? smsCountdown + 's' : '获取验证码' }}
</view>
</view>
</view>
<button class="btn-primary-full" :loading="loginLoading" @click="handleLogin">登录</button>
<view class="login-divider">其他登录方式</view>
<button class="btn-default-full" @click="handleWechatLogin">
<text class="wechat-icon">📱</text> 微信授权登录
</button>
<view class="links">
<text class="link" @click="activeTab = 'staff-reg'">员工注册</text>
<text class="link" @click="activeTab = 'boss-reg'">商家入驻</text>
</view>
</view>
<!-- 老板登录 -->
<view v-if="activeTab === 'boss'" class="tab-content">
<view class="form-group">
<input class="form-input" v-model="loginForm.phone" type="tel" placeholder="请输入手机号" maxlength="11" />
</view>
<view class="form-group">
<view class="code-input-wrap">
<input class="form-input" v-model="loginForm.code" type="digit" placeholder="短信验证码" maxlength="6" />
<view class="code-btn" :class="{ disabled: smsCountdown > 0 }" @click="handleSendSms">
{{ smsCountdown > 0 ? smsCountdown + 's' : '获取验证码' }}
</view>
</view>
</view>
<button class="btn-primary-full" :loading="loginLoading" @click="handleLogin">登录</button>
<view class="links" style="margin-top:20px;">
<text class="link" @click="activeTab = 'boss-reg'">商家入驻</text>
</view>
</view>
<!-- 注册老板 -->
<view v-if="activeTab === 'boss-reg'" class="tab-content">
<view class="form-group">
<input class="form-input" v-model="bossForm.storeName" placeholder="店铺名称" />
</view>
<view class="form-group">
<input class="form-input" v-model="bossForm.bossName" placeholder="您的姓名" />
</view>
<view class="form-group">
<input class="form-input" v-model="bossForm.phone" type="tel" placeholder="手机号" maxlength="11" />
</view>
<view class="form-group">
<input class="form-input" v-model="bossForm.password" password placeholder="登录密码至少6位" />
</view>
<button class="btn-primary-full" :loading="regLoading" @click="handleRegisterBoss">提交申请</button>
<view class="links" style="margin-top:20px;">
<text class="link" @click="activeTab = 'staff'">返回登录</text>
</view>
</view>
<!-- 注册员工 -->
<view v-if="activeTab === 'staff-reg'" class="tab-content">
<view class="invite-hint">请输入店长提供的邀请码加入团队</view>
<view class="form-group">
<input class="form-input" v-model="staffForm.inviteCode" placeholder="请输入8位邀请码" maxlength="8" />
</view>
<view class="form-group">
<input class="form-input" v-model="staffForm.name" placeholder="您的姓名" />
</view>
<view class="form-group">
<input class="form-input" v-model="staffForm.phone" type="tel" placeholder="手机号" maxlength="11" />
</view>
<view class="form-group">
<input class="form-input" v-model="staffForm.password" password placeholder="登录密码至少6位" />
</view>
<button class="btn-primary-full" :loading="regLoading" @click="handleRegisterStaff">注册</button>
<view class="links" style="margin-top:20px;">
<text class="link" @click="activeTab = 'staff'">返回登录</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { sendSms, login, registerBoss, registerStaff } from '../../utils/api.js'
const activeTab = ref('staff')
const loginForm = reactive({ phone: '13800138001', code: '123456' })
const bossForm = reactive({ storeName: '', bossName: '', phone: '', password: '' })
const staffForm = reactive({ inviteCode: '', name: '', phone: '', password: '' })
const loginLoading = ref(false)
const regLoading = ref(false)
const smsCountdown = ref(0)
const bossRegistered = ref(false)
const staffRegistered = ref(false)
const regResult = ref({ store: {}, user: {} })
const tabs = [
{ key: 'staff', label: '用户登录' },
{ key: 'boss', label: '老板登录' },
{ key: 'boss-reg', label: '注册老板' },
{ key: 'staff-reg', label: '注册员工' }
]
let smsTimer = null
const showToast = (msg) => uni.showToast({ title: msg, icon: 'none' })
const handleSendSms = async () => {
if (!loginForm.phone || loginForm.phone.length !== 11) return showToast('请输入正确的手机号')
const res = await sendSms(loginForm.phone)
if (res.code === 200) {
showToast('验证码已发送')
smsCountdown.value = 60
smsTimer = setInterval(() => {
smsCountdown.value--
if (smsCountdown.value <= 0) clearInterval(smsTimer)
}, 1000)
} else {
showToast(res.message || '发送失败')
}
}
const handleLogin = async () => {
if (!loginForm.phone || loginForm.phone.length !== 11) return showToast('请输入正确的手机号')
if (!loginForm.code || loginForm.code.length !== 6) return showToast('请输入6位验证码')
loginLoading.value = true
const res = await login(loginForm.phone, loginForm.code)
loginLoading.value = false
if (res.code === 200) {
uni.setStorageSync('petstore_user', JSON.stringify(res.data.user))
uni.setStorageSync('petstore_store', JSON.stringify(res.data.store))
uni.switchTab({ url: '/pages/home/home' })
} else {
showToast(res.message || '登录失败')
}
}
const handleWechatLogin = () => {
showToast('跳转到微信授权...')
setTimeout(() => {
const demoUser = { id: 99, name: '微信用户', phone: '', role: 'staff' }
uni.setStorageSync('petstore_user', JSON.stringify(demoUser))
uni.setStorageSync('petstore_store', JSON.stringify({ id: 2, name: '宠伴生活馆测试店' }))
uni.switchTab({ url: '/pages/home/home' })
}, 1500)
}
const handleRegisterBoss = async () => {
const f = bossForm
if (!f.storeName) return showToast('请输入店铺名称')
if (!f.bossName) return showToast('请输入您的姓名')
if (!f.phone || f.phone.length !== 11) return showToast('请输入正确的手机号')
if (!f.password || f.password.length < 6) return showToast('密码至少6位')
regLoading.value = true
const res = await registerBoss(f)
regLoading.value = false
if (res.code === 200) {
regResult.value = res.data
bossRegistered.value = true
} else {
showToast(res.message || '注册失败')
}
}
const handleRegisterStaff = async () => {
const f = staffForm
if (!f.inviteCode || f.inviteCode.length !== 8) return showToast('请输入8位邀请码')
if (!f.name) return showToast('请输入您的姓名')
if (!f.phone || f.phone.length !== 11) return showToast('请输入正确的手机号')
if (!f.password || f.password.length < 6) return showToast('密码至少6位')
regLoading.value = true
const res = await registerStaff(f)
regLoading.value = false
if (res.code === 200) {
regResult.value = res.data
staffRegistered.value = true
} else {
showToast(res.message || '注册失败')
}
}
const copyCode = () => {
uni.setClipboardData({ data: regResult.value.store.inviteCode, success: () => showToast('邀请码已复制') })
}
const goLogin = () => {
activeTab.value = 'staff'
bossRegistered.value = false
staffRegistered.value = false
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
background: linear-gradient(135deg, #07c160 0%, #10b76f 100%);
display: flex;
flex-direction: column;
padding: 80px 32px 32px;
}
.login-logo { text-align: center; margin-bottom: 48px; }
.login-logo-text { font-size: 28px; font-weight: 700; color: #fff; letter-spacing: 2px; }
.login-logo-sub { font-size: 13px; color: rgba(255,255,255,0.7); margin-top: 4px; }
.form-card { background: #fff; border-radius: 16px; padding: 24px 20px; }
.tab-header {
display: flex;
flex-wrap: wrap;
border-bottom: 1px solid #eee;
margin-bottom: 4px;
}
.tab-item {
font-size: 14px;
color: #999;
padding: 10px 8px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.tab-item.active { color: #07c160; border-bottom-color: #07c160; font-weight: 600; }
.tab-content { padding-top: 16px; }
.form-group { margin-bottom: 12px; }
.form-input {
width: 100%;
height: 44px;
border: 1px solid #eee;
border-radius: 8px;
padding: 0 12px;
font-size: 14px;
box-sizing: border-box;
background: #fafafa;
}
.form-input:focus { border-color: #07c160; outline: none; }
.code-input-wrap { display: flex; align-items: center; gap: 8px; }
.code-input-wrap .form-input { flex: 1; }
.code-btn {
font-size: 13px;
color: #07c160;
white-space: nowrap;
padding: 8px 12px;
border: 1px solid #07c160;
border-radius: 6px;
flex-shrink: 0;
}
.code-btn.disabled { color: #999; border-color: #ddd; }
.login-divider {
text-align: center;
margin: 16px 0 12px;
color: #999;
font-size: 13px;
position: relative;
}
.links { display: flex; justify-content: space-between; font-size: 13px; margin-top: 12px; }
.link { color: #07c160; cursor: pointer; }
.invite-hint {
background: #f0f9f4;
border: 1px solid #b7eb8f;
border-radius: 8px;
padding: 12px;
font-size: 13px;
color: #52c41a;
margin-bottom: 12px;
}
.wechat-icon { margin-right: 6px; }
.success-icon { font-size: 60px; text-align: center; margin-bottom: 16px; }
.success-title { font-size: 20px; font-weight: 600; text-align: center; color: #333; margin-bottom: 8px; }
.success-sub { font-size: 14px; color: #999; text-align: center; margin-bottom: 24px; }
.success-info { background: #f9f9f9; border-radius: 12px; padding: 16px; margin-bottom: 20px; }
.info-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 14px; }
.info-row .label { color: #999; }
.info-row .value { color: #333; font-weight: 500; }
.btn-primary-full {
width: 100%;
height: 44px;
background: #07c160;
color: #fff;
border: none;
border-radius: 8px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-default-full {
width: 100%;
height: 44px;
background: #fff;
color: #333;
border: 1px solid #eee;
border-radius: 8px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -1,170 +0,0 @@
<template>
<view class="mine-page">
<!-- 用户卡片 -->
<view class="user-card">
<view class="user-info">
<view class="user-avatar" :style="avatarStyle">
<image v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar-img" />
<text v-else class="avatar-initials">{{ initials }}</text>
</view>
<view class="user-detail">
<text class="user-name">{{ userInfo.name }}</text>
<text class="user-phone">{{ userInfo.phone }}</text>
<view class="user-role">
<view v-if="userInfo.role === 'boss'" class="role-tag boss">🏠 店长</view>
<view v-else-if="userInfo.role === 'customer'" class="role-tag customer">🌟 客户</view>
<view v-else class="role-tag staff">👤 员工</view>
</view>
</view>
</view>
</view>
<!-- 老板菜单 -->
<view v-if="userInfo.role === 'boss'" class="menu-section">
<view class="menu-card">
<view class="menu-item" @click="goPage('/pages/mine/staff/staff')">
<text class="menu-icon">👥</text>
<text class="menu-text">员工管理</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-divider"></view>
<view class="menu-item" @click="goPage('/pages/mine/service-type/service-type')">
<text class="menu-icon">🛁</text>
<text class="menu-text">服务类型</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-divider"></view>
<view class="menu-item" @click="goPage('/pages/mine/store/store')">
<text class="menu-icon"></text>
<text class="menu-text">店铺设置</text>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<!-- C端菜单 -->
<view v-if="userInfo.role === 'customer'" class="menu-section">
<view class="menu-card">
<view class="menu-item" @click="goPage('/pages/mine/my-orders/my-orders')">
<text class="menu-icon">📅</text>
<text class="menu-text">我的预约</text>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<!-- B端通用菜单 -->
<view v-if="userInfo.role !== 'customer'" class="menu-section">
<view class="menu-card">
<view class="menu-item" @click="goPage('/pages/mine/my-reports/my-reports')">
<text class="menu-icon">📋</text>
<text class="menu-text">我的报告</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-divider"></view>
<view class="menu-item" @click="goPage('/pages/mine/my-orders/my-orders')">
<text class="menu-icon">📦</text>
<text class="menu-text">我的订单</text>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="menu-section">
<view class="menu-card">
<view class="menu-item logout" @click="logout">
<text class="menu-text">退出登录</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
const userInfo = JSON.parse(uni.getStorageSync('petstore_user') || '{}')
const initials = computed(() => {
if (!userInfo.name) return '?'
return userInfo.name.slice(0, 1).toUpperCase()
})
const avatarStyle = computed(() => {
if (userInfo.avatar) return {}
const colors = ['#ff7c43', '#07c160', '#8b6914', '#e06040', '#5090d0']
const idx = (userInfo.name?.charCodeAt(0) || 0) % colors.length
return { background: colors[idx] }
})
const goPage = (url) => {
uni.navigateTo({ url })
}
const logout = () => {
uni.showModal({
title: '确认退出',
content: '确定退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('petstore_user')
uni.removeStorageSync('petstore_store')
uni.reLaunch({ url: '/pages/login/login' })
}
}
})
}
</script>
<style scoped>
.mine-page { padding-bottom: 20px; background: #f5f5f5; min-height: 100vh; }
.user-card {
background: linear-gradient(135deg, #07c160 0%, #10b76f 100%);
border-radius: 0 0 24px 24px;
padding: 32px 20px 48px;
}
.user-info { display: flex; align-items: center; gap: 16px; }
.user-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
overflow: hidden;
flex-shrink: 0;
border: 2px solid rgba(255,255,255,0.3);
}
.avatar-img { width: 100%; height: 100%; object-fit: cover; }
.avatar-initials { color: #fff; font-size: 24px; font-weight: 600; }
.user-detail { display: flex; flex-direction: column; gap: 3px; }
.user-name { font-size: 18px; font-weight: 600; color: #fff; }
.user-phone { font-size: 13px; opacity: 0.8; }
.user-role { margin-top: 4px; }
.role-tag {
display: inline-block;
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
}
.role-tag.boss { background: rgba(255,159,0,0.3); color: #fff; }
.role-tag.customer { background: rgba(255,255,255,0.4); color: #fff; }
.role-tag.staff { background: rgba(255,255,255,0.3); color: #fff; }
.menu-section { padding: 16px 16px 0; }
.menu-card { background: #fff; border-radius: 12px; overflow: hidden; }
.menu-item {
display: flex;
align-items: center;
padding: 14px 16px;
}
.menu-icon { font-size: 18px; margin-right: 12px; }
.menu-text { flex: 1; font-size: 15px; color: #333; }
.menu-arrow { font-size: 18px; color: #ccc; }
.menu-divider { height: 1px; background: #f0f0f0; margin-left: 46px; }
.menu-item.logout { justify-content: center; }
.menu-item.logout .menu-text { color: #ff4d4f; text-align: center; flex: none; }
</style>

View File

@ -1,176 +0,0 @@
<template>
<view class="orders-page">
<!-- 导航栏 -->
<view class="nav-bar">
<text class="nav-back" @click="goBack"></text>
<text class="nav-title">我的订单</text>
<view style="width:40px;"></view>
</view>
<!-- Tab切换 -->
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.key"
:class="['tab-item', { active: currentStatus === tab.key }]"
@click="currentStatus = tab.key"
>{{ tab.label }}</view>
</view>
<!-- 列表 -->
<view class="order-list">
<view v-for="item in filteredOrders" :key="item.id" class="order-item">
<view class="order-title">{{ item.title }}</view>
<view class="order-desc">{{ item.desc }}</view>
<view class="order-footer">
<text class="order-time">{{ item.time }}</text>
<view class="status-tag" :class="'status-' + item.status">{{ item.statusText }}</view>
</view>
<view v-if="item.status === 'new'" class="action-btns">
<view class="btn-sm btn-primary-sm" @click="startService(item)">开始服务</view>
<view class="btn-sm btn-default-sm" @click="cancelService(item)">取消</view>
</view>
<view v-else-if="item.status === 'doing'">
<view class="btn-sm btn-outline-sm" @click="goReport(item)">填写报告</view>
</view>
</view>
<view v-if="filteredOrders.length === 0" class="empty-wrap">
<text class="empty-icon">📦</text>
<text class="empty-text">暂无数据</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { getAppointmentList, startAppointment, cancelAppointment } from '../../../utils/api.js'
const userInfo = JSON.parse(uni.getStorageSync('petstore_user') || '{}')
const currentUserId = userInfo.id
const currentStatus = ref('new')
const orders = ref([])
const tabs = [
{ key: 'new', label: '待确认' },
{ key: 'doing', label: '进行中' },
{ key: 'done', label: '已完成' }
]
const filteredOrders = computed(() => orders.value.filter(o => {
if (currentStatus.value === 'new') return o.status === 'new'
if (currentStatus.value === 'doing') return o.status === 'doing'
if (currentStatus.value === 'done') return o.status === 'done' || o.status === 'cancel'
return true
}))
const goBack = () => uni.navigateBack()
const fetchOrders = async () => {
if (!currentUserId) return
const res = await getAppointmentList(currentUserId)
if (res.code === 200) {
orders.value = res.data.map(appt => ({
id: appt.id,
title: appt.serviceType || '洗澡美容预约',
desc: `${appt.petType || ''} - ${appt.petName || ''}`,
time: appt.appointmentTime ? new Date(appt.appointmentTime).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '',
status: appt.status || 'new',
statusText: { new: '待确认', doing: '服务中', done: '已完成', cancel: '已取消' }[appt.status] || '待确认',
petName: appt.petName,
petType: appt.petType,
serviceType: appt.serviceType,
appointmentTime: appt.appointmentTime
}))
}
}
const startService = async (item) => {
const res = await startAppointment(item.id, userInfo.id)
if (res.code === 200) { uni.showToast({ title: '已开始服务', icon: 'success' }); fetchOrders() }
else { uni.showToast({ title: res.message || '操作失败', icon: 'none' }) }
}
const cancelService = async (item) => {
uni.showModal({
title: '确认取消',
content: '确定取消该预约?',
success: async (res) => {
if (res.confirm) {
const r = await cancelAppointment(item.id)
if (r.code === 200) { uni.showToast({ title: '已取消', icon: 'success' }); fetchOrders() }
else { uni.showToast({ title: r.message || '操作失败', icon: 'none' }) }
}
}
})
}
const goReport = (item) => {
uni.setStorageSync('petstore_report_prefill', JSON.stringify({
appointmentId: item.id, petName: item.petName, serviceType: item.serviceType, appointmentTime: item.appointmentTime
}))
uni.switchTab({ url: '/pages/report/report' })
}
onMounted(() => fetchOrders())
</script>
<style scoped>
.orders-page { padding-bottom: 20px; background: #f5f5f5; min-height: 100vh; }
.nav-bar {
background: #fff;
padding: 40px 16px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
.nav-back { font-size: 28px; color: #333; font-weight: 300; }
.nav-title { font-size: 16px; font-weight: 600; color: #333; }
.tab-bar {
display: flex;
background: #fff;
padding: 0 16px;
border-bottom: 1px solid #eee;
}
.tab-item {
flex: 1;
text-align: center;
padding: 12px 0;
font-size: 14px;
color: #999;
border-bottom: 2px solid transparent;
}
.tab-item.active { color: #07c160; border-bottom-color: #07c160; font-weight: 600; }
.order-list { padding: 12px 16px 0; }
.order-item {
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.order-title { font-size: 15px; font-weight: 500; color: #333; margin-bottom: 6px; }
.order-desc { font-size: 13px; color: #999; margin-bottom: 10px; }
.order-footer { display: flex; justify-content: space-between; align-items: center; }
.order-time { font-size: 12px; color: #999; }
.action-btns { display: flex; gap: 8px; margin-top: 10px; }
.btn-sm { font-size: 12px; padding: 5px 14px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; }
.btn-primary-sm { background: #07c160; color: #fff; border: 1px solid #07c160; }
.btn-default-sm { background: #fff; color: #666; border: 1px solid #ddd; }
.btn-outline-sm { background: #fff; color: #07c160; border: 1px solid #07c160; }
.status-tag { font-size: 12px; padding: 2px 8px; border-radius: 10px; }
.status-new { background: #fff3e8; color: #ff9f00; }
.status-doing { background: #e8f7ef; color: #07c160; }
.status-done { background: #f0f0f0; color: #888; }
.status-cancel { background: #f5f5f5; color: #999; }
.empty-wrap { display: flex; flex-direction: column; align-items: center; padding: 60px 0; }
.empty-icon { font-size: 48px; }
.empty-text { font-size: 14px; color: #999; margin-top: 12px; }
</style>

View File

@ -1,145 +0,0 @@
<template>
<view class="my-reports-page">
<!-- 导航栏 -->
<view class="nav-bar">
<text class="nav-back" @click="goBack"></text>
<text class="nav-title">我的报告</text>
<view style="width:40px;"></view>
</view>
<!-- 网格相册 -->
<view v-if="reportList.length > 0" class="gallery-grid">
<view
v-for="r in reportList"
:key="r.id"
class="gallery-item"
@click="viewReport(r)"
>
<view class="gallery-cover">
<image
v-if="r.beforePhoto"
:src="imgUrl(r.beforePhoto)"
class="cover-img"
mode="aspectFill"
/>
<view v-else class="cover-placeholder">
<text style="font-size:32px;">📷</text>
</view>
<!-- 叠加信息 -->
<view class="gallery-overlay">
<text class="overlay-name">🐾 {{ r.petName }}</text>
<text class="overlay-service">{{ serviceEmoji(r.serviceType) }} {{ r.serviceType }}</text>
</view>
</view>
</view>
</view>
<view v-if="!loading && reportList.length === 0" class="empty-wrap">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无报告</text>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getReportList, imgUrl } from '../../../utils/api.js'
const userInfo = JSON.parse(uni.getStorageSync('petstore_user') || '{}')
const storeInfo = JSON.parse(uni.getStorageSync('petstore_store') || '{}')
const loading = ref(false)
const reportList = ref([])
const goBack = () => uni.navigateBack()
const serviceEmoji = (name) => {
if (!name) return ''
if (name.includes('洗澡') && name.includes('美容')) return '🛁✂️'
if (name.includes('洗澡')) return '🛁'
if (name.includes('美容')) return '✂️'
if (name.includes('剪指甲')) return '💅'
if (name.includes('驱虫')) return '🐛'
return '✨'
}
const loadReports = async () => {
loading.value = true
const params = userInfo.role === 'boss'
? { storeId: storeInfo.id }
: { userId: userInfo.id }
const res = await getReportList(params)
loading.value = false
if (res.code === 200) reportList.value = res.data
}
const viewReport = (r) => {
// #ifdef MP-WEIXIN
const url = `/pages/report-view/reportView?token=${r.reportToken}`
// #endif
// #ifdef H5
const url = `/pages/report-view/reportView?token=${r.reportToken}`
// #endif
uni.navigateTo({ url })
}
onMounted(() => loadReports())
</script>
<style scoped>
.my-reports-page { padding: 0 12px 20px; background: #f5f5f5; min-height: 100vh; }
.nav-bar {
background: #fff;
padding: 40px 16px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 12px;
}
.nav-back { font-size: 28px; color: #333; font-weight: 300; }
.nav-title { font-size: 16px; font-weight: 600; color: #333; }
.gallery-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
padding-top: 4px;
}
.gallery-item {
border-radius: 12px;
overflow: hidden;
cursor: pointer;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.gallery-cover {
position: relative;
width: 100%;
aspect-ratio: 1;
overflow: hidden;
}
.cover-img { width: 100%; height: 100%; }
.cover-placeholder {
width: 100%;
height: 100%;
background: #f0ede8;
display: flex;
align-items: center;
justify-content: center;
}
.gallery-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 10px;
background: linear-gradient(transparent, rgba(0,0,0,0.65));
color: #fff;
}
.overlay-name { font-size: 13px; font-weight: 600; display: block; }
.overlay-service { font-size: 11px; opacity: 0.9; margin-top: 2px; display: block; }
.empty-wrap { display: flex; flex-direction: column; align-items: center; padding: 80px 0; }
.empty-icon { font-size: 60px; }
.empty-text { font-size: 14px; color: #999; margin-top: 12px; }
</style>

View File

@ -1,193 +0,0 @@
<template>
<view class="st-page">
<!-- 导航栏 -->
<view class="nav-bar">
<text class="nav-back" @click="goBack"></text>
<text class="nav-title">服务类型</text>
<view style="width:40px;"></view>
</view>
<view class="add-btn-wrap">
<view class="btn-add" @click="showNew = true">+ 新增服务类型</view>
</view>
<!-- 列表 -->
<view class="list-wrap">
<view v-for="s in serviceTypes" :key="s.id" class="list-item">
<text class="item-name">{{ s.name }}</text>
<view v-if="s.storeId" class="item-actions">
<text class="delete-btn" @click="deleteST(s.id)">删除</text>
</view>
<view v-else>
<view class="system-tag">系统默认</view>
</view>
</view>
<view v-if="serviceTypes.length === 0" class="empty-wrap">
<text class="empty-icon">🛁</text>
<text class="empty-text">暂无服务类型</text>
</view>
</view>
<!-- 新增弹窗 -->
<view v-if="showNew" class="dialog-mask" @click="showNew = false">
<view class="dialog-content" @click.stop>
<view class="dialog-header">
<text class="dialog-title">新增服务类型</text>
<text class="dialog-close" @click="showNew = false"></text>
</view>
<view class="dialog-body">
<view class="form-item">
<text class="form-label">服务类型名称</text>
<input class="form-input" v-model="newName" placeholder="请输入" />
</view>
</view>
<view class="dialog-footer">
<view class="btn-cancel" @click="showNew = false">取消</view>
<view class="btn-confirm" @click="onAddConfirm">确定</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getServiceTypeList, createServiceType, deleteServiceType } from '../../../utils/api.js'
const storeInfo = JSON.parse(uni.getStorageSync('petstore_store') || '{}')
const serviceTypes = ref([])
const showNew = ref(false)
const newName = ref('')
const goBack = () => uni.navigateBack()
const load = async () => {
const res = await getServiceTypeList(storeInfo.id)
if (res.code === 200) serviceTypes.value = res.data
}
const onAddConfirm = async () => {
if (!newName.value) { uni.showToast({ title: '请输入服务类型名称', icon: 'none' }); return }
const res = await createServiceType(storeInfo.id, newName.value)
if (res.code === 200) {
uni.showToast({ title: '添加成功', icon: 'success' })
newName.value = ''
showNew.value = false
load()
} else {
uni.showToast({ title: res.message || '添加失败', icon: 'none' })
}
}
const deleteST = async (id) => {
uni.showModal({
title: '确认删除',
content: '确定删除该服务类型?',
success: async (res) => {
if (res.confirm) {
const r = await deleteServiceType(id)
if (r.code === 200) {
uni.showToast({ title: '已删除', icon: 'success' })
load()
} else {
uni.showToast({ title: r.message || '删除失败', icon: 'none' })
}
}
}
})
}
onMounted(() => load())
</script>
<style scoped>
.st-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 20px; }
.nav-bar {
background: #fff;
padding: 40px 16px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
.nav-back { font-size: 28px; color: #333; font-weight: 300; }
.nav-title { font-size: 16px; font-weight: 600; color: #333; }
.add-btn-wrap { padding: 12px 16px; }
.btn-add {
width: 100%;
height: 44px;
background: #07c160;
color: #fff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
}
.list-wrap { padding: 0 16px; }
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border-radius: 12px;
padding: 14px 16px;
margin-bottom: 10px;
}
.item-name { font-size: 15px; color: #333; font-weight: 500; }
.delete-btn { font-size: 13px; color: #ff4d4f; }
.system-tag {
font-size: 12px;
background: #e8f7ef;
color: #07c160;
padding: 2px 8px;
border-radius: 4px;
}
.empty-wrap { display: flex; flex-direction: column; align-items: center; padding: 60px 0; }
.empty-icon { font-size: 48px; }
.empty-text { font-size: 14px; color: #999; margin-top: 12px; }
/* 弹窗 */
.dialog-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
}
.dialog-content {
width: 80%;
max-width: 340px;
background: #fff;
border-radius: 12px;
overflow: hidden;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.dialog-title { font-size: 16px; font-weight: 600; color: #333; }
.dialog-close { font-size: 18px; color: #999; }
.dialog-body { padding: 16px 20px; }
.dialog-footer { display: flex; border-top: 1px solid #eee; }
.btn-cancel, .btn-confirm { flex: 1; text-align: center; padding: 14px 0; font-size: 15px; }
.btn-cancel { color: #666; border-right: 1px solid #eee; }
.btn-confirm { color: #07c160; font-weight: 600; }
.form-item { }
.form-label { font-size: 13px; color: #999; display: block; margin-bottom: 6px; }
.form-input {
width: 100%;
height: 40px;
border: 1px solid #eee;
border-radius: 8px;
padding: 0 12px;
font-size: 14px;
box-sizing: border-box;
}
</style>

View File

@ -1,247 +0,0 @@
<template>
<view class="staff-page">
<!-- 导航栏 -->
<view class="nav-bar">
<text class="nav-back" @click="goBack"></text>
<text class="nav-title">员工管理</text>
<view style="width:40px;"></view>
</view>
<view class="invite-code-row">
<text class="invite-label">员工邀请码</text>
<text class="invite-code">{{ storeInfo.inviteCode }}</text>
<view class="copy-btn" @click="copyCode">复制</view>
</view>
<view class="add-btn-wrap">
<view class="btn-add" @click="showAddStaff = true">+ 新增员工</view>
</view>
<!-- 员工列表 -->
<view class="staff-list">
<view v-for="s in staffList" :key="s.id" class="staff-item">
<view class="staff-avatar" :style="avatarStyle(s)">
<image v-if="s.avatar" :src="s.avatar" class="avatar-img" />
<text v-else class="avatar-initials">{{ s.name ? s.name[0] : '?' }}</text>
</view>
<view class="staff-info">
<text class="staff-name">{{ s.name }}</text>
<text class="staff-phone">{{ s.phone }}</text>
<text class="staff-role">{{ s.role === 'boss' ? '🏠 店长' : '👤 员工' }}</text>
</view>
<text v-if="s.role !== 'boss'" class="delete-btn" @click="deleteStaff(s.id)">删除</text>
</view>
<view v-if="staffList.length === 0" class="empty-wrap">
<text class="empty-icon">👥</text>
<text class="empty-text">暂无员工</text>
</view>
</view>
<!-- 新增员工弹窗 -->
<view v-if="showAddStaff" class="dialog-mask" @click="showAddStaff = false">
<view class="dialog-content" @click.stop>
<view class="dialog-header">
<text class="dialog-title">新增员工</text>
<text class="dialog-close" @click="showAddStaff = false"></text>
</view>
<view class="dialog-body">
<view class="form-item">
<text class="form-label">员工姓名</text>
<input class="form-input" v-model="newStaff.name" placeholder="请输入" />
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input class="form-input" v-model="newStaff.phone" type="tel" placeholder="请输入" maxlength="11" />
</view>
</view>
<view class="dialog-footer">
<view class="btn-cancel" @click="showAddStaff = false">取消</view>
<view class="btn-confirm" @click="onAddStaffConfirm">确定</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getStaffList, createStaff, deleteStaff as delStaff } from '../../../utils/api.js'
const storeInfo = JSON.parse(uni.getStorageSync('petstore_store') || '{}')
const staffList = ref([])
const showAddStaff = ref(false)
const newStaff = ref({ name: '', phone: '' })
const COLORS = ['#ff7c43', '#07c160', '#8b6914', '#e06040', '#5090d0', '#9b59b6']
const avatarStyle = (s) => {
if (s.avatar) return {}
const idx = (s.name?.charCodeAt(0) || 0) % COLORS.length
return { background: COLORS[idx] }
}
const goBack = () => uni.navigateBack()
const loadStaff = async () => {
const res = await getStaffList(storeInfo.id)
if (res.code === 200) staffList.value = res.data
}
const copyCode = () => {
uni.setClipboardData({ data: storeInfo.inviteCode, success: () => uni.showToast({ title: '邀请码已复制', icon: 'success' }) })
}
const onAddStaffConfirm = async () => {
if (!newStaff.value.name) { uni.showToast({ title: '请输入员工姓名', icon: 'none' }); return }
if (!newStaff.value.phone || newStaff.value.phone.length !== 11) { uni.showToast({ title: '请输入正确的手机号', icon: 'none' }); return }
const res = await createStaff({ storeId: storeInfo.id, name: newStaff.value.name, phone: newStaff.value.phone })
if (res.code === 200) {
uni.showToast({ title: `添加成功,密码:${res.data.password}`, icon: 'none', duration: 3000 })
showAddStaff.value = false
newStaff.value = { name: '', phone: '' }
loadStaff()
} else {
uni.showToast({ title: res.message || '添加失败', icon: 'none' })
}
}
const deleteStaff = async (staffId) => {
uni.showModal({
title: '确认删除',
content: '确定删除该员工?',
success: async (res) => {
if (res.confirm) {
const r = await delStaff(staffId)
if (r.code === 200) {
uni.showToast({ title: '已删除', icon: 'success' })
loadStaff()
} else {
uni.showToast({ title: r.message || '删除失败', icon: 'none' })
}
}
}
})
}
onMounted(() => loadStaff())
</script>
<style scoped>
.staff-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 20px; }
.nav-bar {
background: #fff;
padding: 40px 16px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
.nav-back { font-size: 28px; color: #333; font-weight: 300; }
.nav-title { font-size: 16px; font-weight: 600; color: #333; }
.invite-code-row {
display: flex;
align-items: center;
background: #fff;
margin: 12px 16px;
padding: 14px 16px;
border-radius: 12px;
gap: 8px;
}
.invite-label { font-size: 14px; color: #666; }
.invite-code { flex: 1; font-size: 15px; color: #07c160; font-weight: 600; font-family: monospace; }
.copy-btn { font-size: 13px; color: #07c160; padding: 4px 12px; border: 1px solid #07c160; border-radius: 4px; }
.add-btn-wrap { padding: 0 16px 12px; }
.btn-add {
width: 100%;
height: 44px;
background: #07c160;
color: #fff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
}
.staff-list { padding: 0 16px; }
.staff-item {
display: flex;
align-items: center;
background: #fff;
border-radius: 12px;
padding: 14px 16px;
margin-bottom: 10px;
gap: 12px;
}
.staff-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #fff;
overflow: hidden;
flex-shrink: 0;
}
.avatar-img { width: 100%; height: 100%; object-fit: cover; }
.avatar-initials { font-weight: 600; }
.staff-info { flex: 1; display: flex; flex-direction: column; gap: 2px; }
.staff-name { font-size: 15px; font-weight: 500; color: #333; }
.staff-phone { font-size: 12px; color: #999; }
.staff-role { font-size: 11px; color: #999; }
.delete-btn { font-size: 13px; color: #ff4d4f; }
.empty-wrap { display: flex; flex-direction: column; align-items: center; padding: 60px 0; }
.empty-icon { font-size: 48px; }
.empty-text { font-size: 14px; color: #999; margin-top: 12px; }
/* 弹窗 */
.dialog-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
}
.dialog-content {
width: 80%;
max-width: 340px;
background: #fff;
border-radius: 12px;
overflow: hidden;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.dialog-title { font-size: 16px; font-weight: 600; color: #333; }
.dialog-close { font-size: 18px; color: #999; }
.dialog-body { padding: 16px 20px; }
.dialog-footer { display: flex; border-top: 1px solid #eee; }
.btn-cancel, .btn-confirm { flex: 1; text-align: center; padding: 14px 0; font-size: 15px; }
.btn-cancel { color: #666; border-right: 1px solid #eee; }
.btn-confirm { color: #07c160; font-weight: 600; }
.form-item { margin-bottom: 16px; }
.form-item:last-child { margin-bottom: 0; }
.form-label { font-size: 13px; color: #999; display: block; margin-bottom: 6px; }
.form-input {
width: 100%;
height: 40px;
border: 1px solid #eee;
border-radius: 8px;
padding: 0 12px;
font-size: 14px;
box-sizing: border-box;
}
</style>

View File

@ -1,124 +0,0 @@
<template>
<view class="store-page">
<!-- 导航栏 -->
<view class="nav-bar">
<text class="nav-back" @click="goBack"></text>
<text class="nav-title">店铺设置</text>
<view style="width:40px;"></view>
</view>
<view class="form-wrap">
<view class="form-card">
<view class="form-row">
<text class="row-label">店铺名称</text>
<input class="row-input" v-model="form.name" placeholder="请输入" />
</view>
<view class="form-row">
<text class="row-label">联系电话</text>
<input class="row-input" v-model="form.phone" placeholder="请输入" />
</view>
<view class="form-row">
<text class="row-label">地址</text>
<input class="row-input" v-model="form.address" placeholder="请输入" />
</view>
<view class="form-row" style="flex-direction:column; align-items:flex-start;">
<text class="row-label" style="margin-bottom:8px;">简介</text>
<textarea class="row-textarea" v-model="form.intro" placeholder="请输入店铺简介" rows="3" />
</view>
</view>
<view class="save-btn-wrap">
<view class="btn-save" :class="{ loading: saving }" @click="saveStore">保存设置</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { updateStore } from '../../../utils/api.js'
const storeInfo = JSON.parse(uni.getStorageSync('petstore_store') || '{}')
const saving = ref(false)
const form = ref({ name: '', phone: '', address: '', intro: '' })
const goBack = () => uni.navigateBack()
const saveStore = async () => {
if (!form.value.name) { uni.showToast({ title: '请输入店铺名称', icon: 'none' }); return }
saving.value = true
const res = await updateStore({ id: storeInfo.id, ...form.value })
saving.value = false
if (res.code === 200) {
uni.showToast({ title: '保存成功', icon: 'success' })
const updated = { ...storeInfo, ...form.value }
uni.setStorageSync('petstore_store', JSON.stringify(updated))
} else {
uni.showToast({ title: res.message || '保存失败', icon: 'none' })
}
}
onMounted(() => {
form.value = {
name: storeInfo.name || '',
phone: storeInfo.phone || '',
address: storeInfo.address || '',
intro: storeInfo.intro || ''
}
})
</script>
<style scoped>
.store-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 20px; }
.nav-bar {
background: #fff;
padding: 40px 16px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
.nav-back { font-size: 28px; color: #333; font-weight: 300; }
.nav-title { font-size: 16px; font-weight: 600; color: #333; }
.form-wrap { padding: 12px 16px 0; }
.form-card { background: #fff; border-radius: 12px; padding: 4px 16px; }
.form-row {
display: flex;
align-items: center;
padding: 14px 0;
border-bottom: 1px solid #f0f0f0;
}
.form-row:last-child { border-bottom: none; }
.row-label { font-size: 14px; color: #666; width: 72px; flex-shrink: 0; }
.row-input {
flex: 1;
font-size: 14px;
color: #333;
text-align: right;
}
.row-textarea {
width: 100%;
font-size: 14px;
color: #333;
border: none;
resize: none;
background: transparent;
box-sizing: border-box;
}
.save-btn-wrap { padding: 16px; }
.btn-save {
width: 100%;
height: 46px;
background: #07c160;
color: #fff;
border-radius: 23px;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.btn-save.loading { opacity: 0.7; }
</style>

View File

@ -1,416 +0,0 @@
<template>
<view class="report-view">
<!-- 加载中 -->
<view v-if="loading" class="loading-wrap">
<view class="loading-inner">
<view class="loading-spinner"></view>
<text class="loading-text">加载报告中...</text>
</view>
</view>
<!-- 未找到 -->
<view v-else-if="notFound" class="not-found">
<text class="not-found-icon">🔍</text>
<text class="not-found-text">报告不存在或链接已失效</text>
</view>
<!-- 报告内容 -->
<view v-else-if="reportData" class="report-content">
<!-- 品牌头部 -->
<view class="brand-header">
<view class="brand-logo">{{ reportData.store?.name || '宠伴生活馆' }}</view>
<view class="brand-sub">宠物服务让爱更专业</view>
<view class="brand-contact" v-if="reportData.store?.phone || reportData.store?.address">
<text v-if="reportData.store?.phone">📞 {{ reportData.store.phone }}</text>
<text v-if="reportData.store?.address">📍 {{ reportData.store.address }}</text>
</view>
</view>
<!-- 报告标题 -->
<view class="report-title-wrap">
<view class="report-title">服务报告</view>
<view class="report-time">{{ formatTime(reportData.appointmentTime) }}</view>
</view>
<!-- 服务信息 -->
<view class="service-info">
<view class="info-row">
<text class="info-label">宠物名字</text>
<text class="info-val">{{ reportData.petName || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">服务项目</text>
<text class="info-val">{{ reportData.serviceType || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">服务时间</text>
<text class="info-val">{{ formatTime(reportData.appointmentTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">服务技师</text>
<text class="info-val">{{ reportData.staffName || '-' }}</text>
</view>
</view>
<!-- 照片对比 -->
<view class="photo-section">
<view class="section-label">服务前后对比</view>
<view class="photo-grid">
<view class="photo-cell">
<text class="photo-label">服务前</text>
<image
v-if="reportData.beforePhoto"
:src="imgUrl(reportData.beforePhoto)"
class="photo-img"
mode="aspectFill"
/>
<view v-else class="photo-empty">暂无照片</view>
</view>
<view class="photo-cell">
<text class="photo-label">服务后</text>
<image
v-if="reportData.afterPhoto"
:src="imgUrl(reportData.afterPhoto)"
class="photo-img"
mode="aspectFill"
/>
<view v-else class="photo-empty">暂无照片</view>
</view>
</view>
</view>
<!-- 备注 -->
<view v-if="reportData.remark" class="remark-section">
<view class="section-label">备注</view>
<view class="remark-content">{{ reportData.remark }}</view>
</view>
<!-- 生成海报按钮 -->
<view class="action-section">
<view class="btn-share" @click="generatePoster">📸 生成图片分享朋友圈</view>
<view class="btn-book" @click="goHome">我要预约</view>
</view>
</view>
<!-- Canvas海报隐藏 -->
<canvas canvas-id="posterCanvas" id="posterCanvas" class="poster-canvas" />
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getReportByToken, imgUrl } from '../../utils/api.js'
const loading = ref(true)
const notFound = ref(false)
const reportData = ref(null)
const formatTime = (time) => {
if (!time) return ''
const d = new Date(time)
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
}
const loadReport = async () => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const token = currentPage?.options?.token || ''
if (!token) {
notFound.value = true
loading.value = false
return
}
const res = await getReportByToken(token)
loading.value = false
if (res.code === 200) {
reportData.value = res.data
} else {
notFound.value = true
}
}
const loadImage = (src) => {
return new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => resolve(img)
img.onerror = () => resolve(null)
img.src = src
})
}
const goHome = () => {
uni.switchTab({ url: '/pages/home/home' })
}
const generatePoster = async () => {
if (!reportData.value) return
uni.showLoading({ title: '生成中...' })
const r = reportData.value
const canvas = uni.createCanvasContext('posterCanvas')
canvas.width = 750
canvas.height = 1100
//
canvas.setFillStyle('#ffffff')
canvas.fillRect(0, 0, 750, 1100)
// 绿
canvas.setFillStyle('#07c160')
canvas.fillRect(0, 0, 750, 300)
const storeName = r.store?.name || '宠伴生活馆'
// Logo
canvas.setFillStyle('#ffffff')
canvas.setFontSize(36)
canvas.setTextAlign('center')
canvas.fillText(storeName, 375, 70)
canvas.setFontSize(20)
canvas.setGlobalAlpha(0.7)
canvas.fillText('宠物服务,让爱更专业', 375, 105)
canvas.setGlobalAlpha(1)
//
const storePhone = r.store?.phone || ''
const storeAddr = r.store?.address || ''
if (storePhone || storeAddr) {
canvas.setFontSize(18)
canvas.setGlobalAlpha(0.85)
const contactLine = [storePhone, storeAddr].filter(Boolean).join(' | ')
canvas.fillText(contactLine, 375, 138)
canvas.setGlobalAlpha(1)
}
//
canvas.setFillStyle('#333333')
canvas.setFontSize(36)
canvas.fillText('服务报告', 375, 220)
//
canvas.setFillStyle('#f8f6f3')
roundRect(canvas, 40, 260, 670, 220, 20)
canvas.fill()
const infoItems = [
['宠物名字', r.petName || '-'],
['服务项目', r.serviceType || '-'],
['服务时间', formatTime(r.appointmentTime) || '-'],
['服务技师', r.staffName || '-']
]
let y = 310
canvas.setTextAlign('left')
infoItems.forEach(([label, val]) => {
canvas.setFillStyle('#999999')
canvas.setFontSize(22)
canvas.fillText(label, 80, y)
canvas.setFillStyle('#333333')
canvas.setFontSize(24)
canvas.fillText(val, 220, y)
y += 48
})
//
canvas.setFillStyle('#f8f6f3')
roundRect(canvas, 40, 500, 670, 360, 20)
canvas.fill()
canvas.setFillStyle('#333333')
canvas.setFontSize(24)
canvas.setTextAlign('center')
canvas.fillText('服务前后对比', 375, 545)
const imgY = 575
const imgH = 260
const imgW = 300
//
canvas.setFillStyle('#e0e0e0')
roundRect(canvas, 60, imgY, imgW, imgH, 16)
canvas.fill()
canvas.setFillStyle('#999999')
canvas.setFontSize(20)
canvas.fillText('服务前', 210, imgY + imgH/2)
//
canvas.setFillStyle('#e0e0e0')
roundRect(canvas, 390, imgY, imgW, imgH, 16)
canvas.fill()
canvas.setFillStyle('#999999')
canvas.fillText('服务后', 540, imgY + imgH/2)
//
if (r.remark) {
canvas.setFillStyle('#f8f6f3')
roundRect(canvas, 40, 880, 670, 100, 20)
canvas.fill()
canvas.setFillStyle('#666666')
canvas.setFontSize(22)
canvas.setTextAlign('left')
const remark = r.remark
if (remark.length > 30) {
canvas.fillText(remark.substring(0, 30), 70, 920)
canvas.fillText(remark.substring(30), 70, 955)
} else {
canvas.fillText(remark, 70, 930)
}
}
// Logo
canvas.setFillStyle('#07c160')
canvas.setFontSize(22)
canvas.setTextAlign('center')
canvas.fillText(`${storeName}`, 375, 1050)
canvas.draw()
//
const beforeImgSrc = r.beforePhoto ? imgUrl(r.beforePhoto) : null
const afterImgSrc = r.afterPhoto ? imgUrl(r.afterPhoto) : null
const [beforeImg, afterImg] = await Promise.all([
beforeImgSrc ? loadImage(beforeImgSrc) : Promise.resolve(null),
afterImgSrc ? loadImage(afterImgSrc) : Promise.resolve(null)
])
if (beforeImg) {
canvas.drawImage(beforeImg, 60, imgY, imgW, imgH)
canvas.draw()
}
if (afterImg) {
canvas.drawImage(afterImg, 390, imgY, imgW, imgH)
canvas.draw()
}
uni.hideLoading()
uni.showToast({ title: '海报已生成,请截图分享', icon: 'none', duration: 3000 })
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath()
ctx.moveTo(x + r, y)
ctx.lineTo(x + w - r, y)
ctx.arcTo(x + w, y, x + w, y + r, r)
ctx.lineTo(x + w, y + h - r)
ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
ctx.lineTo(x + r, y + h)
ctx.arcTo(x, y + h, x, y + h - r, r)
ctx.lineTo(x, y + r)
ctx.arcTo(x, y, x + r, y, r)
ctx.closePath()
}
onMounted(() => loadReport())
</script>
<style scoped>
.report-view { background: #f8f6f3; min-height: 100vh; }
.loading-wrap {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.loading-inner { display: flex; flex-direction: column; align-items: center; gap: 16px; }
.loading-spinner {
width: 36px;
height: 36px;
border: 3px solid #07c160;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text { font-size: 14px; color: #999; }
.not-found { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; gap: 12px; }
.not-found-icon { font-size: 60px; }
.not-found-text { font-size: 15px; color: #999; }
.report-content { max-width: 430px; margin: 0 auto; background: #fff; min-height: 100vh; }
.brand-header {
background: linear-gradient(135deg, #07c160 0%, #10b76f 100%);
padding: 24px 24px 20px;
text-align: center;
color: #fff;
}
.brand-logo { font-size: 20px; font-weight: 700; letter-spacing: 2px; margin-bottom: 4px; }
.brand-sub { font-size: 12px; opacity: 0.7; margin-bottom: 12px; }
.brand-contact { font-size: 12px; opacity: 0.85; display: flex; justify-content: center; gap: 16px; flex-wrap: wrap; }
.report-title-wrap { text-align: center; padding: 24px 24px 16px; }
.report-title { font-size: 22px; font-weight: 700; color: #333; }
.report-time { font-size: 13px; color: #999; margin-top: 6px; }
.service-info {
margin: 0 16px 16px;
background: #f8f6f3;
border-radius: 12px;
padding: 4px 16px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-row:last-child { border-bottom: none; }
.info-label { font-size: 14px; color: #999; }
.info-val { font-size: 14px; color: #333; font-weight: 500; }
.photo-section { margin: 0 24px 24px; }
.section-label { font-size: 15px; font-weight: 600; color: #333; margin-bottom: 12px; }
.photo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.photo-cell { display: flex; flex-direction: column; gap: 8px; }
.photo-label { font-size: 13px; color: #999; text-align: center; }
.photo-img { width: 100%; height: 160px; border-radius: 12px; object-fit: cover; }
.photo-empty {
width: 100%;
height: 160px;
background: #f0f0f0;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #ccc;
font-size: 13px;
}
.remark-section { margin: 0 24px 24px; }
.remark-content { background: #f9f9f9; border-radius: 12px; padding: 16px; font-size: 14px; color: #666; line-height: 1.6; min-height: 60px; }
.action-section { margin: 0 24px 24px; }
.btn-share {
width: 100%;
height: 46px;
background: #07c160;
color: #fff;
border-radius: 23px;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.btn-book {
width: 100%;
height: 46px;
background: #fff;
color: #07c160;
border: 1px solid #07c160;
border-radius: 23px;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.poster-canvas {
position: fixed;
top: -9999px;
left: -9999px;
width: 750px;
height: 1100px;
}
</style>

View File

@ -1,333 +0,0 @@
<template>
<view class="report-page">
<!-- 提交结果 -->
<view v-if="reportResult" class="result-wrap">
<view class="notice-bar"> 提交成功可复制链接或扫描二维码分享给宠主</view>
<view class="result-content">
<view class="result-link-row">
<text class="result-link-label">报告链接</text>
<text class="result-link-val" selectable>{{ reportResult.reportUrl }}</text>
</view>
<view class="qr-wrap">
<image :src="qrUrl" mode="aspectFit" class="qr-img" />
</view>
<button class="btn-primary-full" @click="downloadQr">📥 保存二维码到相册</button>
<button class="btn-default-full" @click="goHome" style="margin-top:12px;">返回首页</button>
</view>
</view>
<!-- 填写表单 -->
<view v-else class="report-form">
<!-- 基本信息 -->
<view class="form-section">
<view class="form-title">基本信息</view>
<view class="form-card">
<view class="form-row">
<text class="row-label">宠物名字</text>
<input class="row-input" v-model="report.petName" placeholder="请输入宠物名字" />
</view>
<view class="form-row">
<text class="row-label">服务类型</text>
<picker :value="serviceTypeIdx" :range="serviceTypeOptions" :range-key="'name'" @change="onServiceTypeChange">
<view class="picker-val">{{ report.serviceType || '请选择' }} </view>
</picker>
</view>
<view class="form-row">
<text class="row-label">服务时间</text>
<picker mode="datetime" :value="report.appointmentTime" @change="onDateChange">
<view class="picker-val">{{ report.appointmentTime || '请选择' }} </view>
</picker>
</view>
</view>
</view>
<!-- 服务照片 -->
<view class="form-section">
<view class="form-title">服务照片</view>
<view class="form-card">
<view class="photo-row">
<text class="row-label">服务前</text>
<view class="photo-upload" @click="chooseImage('before')">
<image v-if="report.before" :src="report.before" class="photo-preview" mode="aspectFill" />
<view v-else class="photo-placeholder">
<text class="plus-icon">+</text>
<text class="upload-tip">上传照片</text>
</view>
</view>
</view>
<view class="photo-row">
<text class="row-label">服务后</text>
<view class="photo-upload" @click="chooseImage('after')">
<image v-if="report.after" :src="report.after" class="photo-preview" mode="aspectFill" />
<view v-else class="photo-placeholder">
<text class="plus-icon">+</text>
<text class="upload-tip">上传照片</text>
</view>
</view>
</view>
</view>
</view>
<!-- 备注 -->
<view class="form-section">
<view class="form-title">备注</view>
<view class="form-card">
<textarea class="remark-input" v-model="report.remark" placeholder="请输入备注信息..." rows="3" />
</view>
</view>
<view class="submit-wrap">
<button class="btn-submit" :class="{ loading: submitting }" @click="submitReport">提交报告</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { createReport, getServiceTypeList, uploadImage } from '../../utils/api.js'
import { imgUrl } from '../../utils/api.js'
const userInfo = JSON.parse(uni.getStorageSync('petstore_user') || '{}')
const storeInfo = JSON.parse(uni.getStorageSync('petstore_store') || '{}')
const report = ref({ petName: '', serviceType: '', appointmentTime: '', before: '', after: '', remark: '' })
const reportResult = ref(null)
const serviceTypes = ref([])
const serviceTypeIdx = ref(0)
const submitting = ref(false)
const currentAppointmentId = ref(null)
const serviceTypeOptions = computed(() =>
serviceTypes.value.map(s => ({
name: `${serviceEmoji(s.name)} ${s.name}`,
value: s.name
}))
)
const serviceEmoji = (name) => {
if (!name) return ''
if (name.includes('洗澡') && name.includes('美容')) return '🛁✂️'
if (name.includes('洗澡')) return '🛁'
if (name.includes('美容')) return '✂️'
if (name.includes('剪指甲')) return '💅'
if (name.includes('驱虫')) return '🐛'
return '✨'
}
const qrUrl = computed(() => reportResult.value
? `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(reportResult.value.reportUrl)}`
: '')
const loadServiceTypes = async () => {
if (!storeInfo.id) return
const res = await getServiceTypeList(storeInfo.id)
if (res.code === 200) serviceTypes.value = res.data
}
const onServiceTypeChange = (e) => {
serviceTypeIdx.value = e.detail.value
report.value.serviceType = serviceTypes.value[e.detail.value]?.name || ''
}
const onDateChange = (e) => {
report.value.appointmentTime = e.detail.value
}
const chooseImage = async (field) => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const filePath = res.tempFilePaths[0]
uni.showLoading({ title: '上传中...' })
const r = await uploadImage(filePath)
uni.hideLoading()
if (r.code === 200) {
report.value[field] = imgUrl(r.data.url)
uni.showToast({ title: '上传成功', icon: 'success' })
} else {
uni.showToast({ title: r.message || '上传失败', icon: 'none' })
}
}
})
}
const submitReport = async () => {
if (!report.value.petName) { uni.showToast({ title: '请输入宠物名字', icon: 'none' }); return }
if (!report.value.serviceType) { uni.showToast({ title: '请选择服务类型', icon: 'none' }); return }
submitting.value = true
const payload = {
appointmentId: currentAppointmentId.value || null,
userId: userInfo.id,
petName: report.value.petName,
serviceType: report.value.serviceType,
appointmentTime: report.value.appointmentTime || null,
beforePhoto: report.value.before,
afterPhoto: report.value.after,
remark: report.value.remark
}
const res = await createReport(payload)
submitting.value = false
if (res.code === 200) {
const token = res.data.reportToken
// #ifdef MP-WEIXIN
const reportUrl = `/pages/report-view/reportView?token=${token}`
// #endif
// #ifdef H5
const reportUrl = `${window.location.origin}/#/pages/report-view/reportView?token=${token}`
// #endif
reportResult.value = { token, reportUrl }
} else {
uni.showToast({ title: res.message || '提交失败', icon: 'none' })
}
}
const copyLink = () => {
uni.setClipboardData({ data: reportResult.value.reportUrl, success: () => uni.showToast({ title: '链接已复制', icon: 'success' }) })
}
const downloadQr = () => {
uni.showToast({ title: '请长按二维码图片保存', icon: 'none' })
}
const goHome = () => {
reportResult.value = null
report.value = { petName: '', serviceType: '', appointmentTime: '', before: '', after: '', remark: '' }
uni.switchTab({ url: '/pages/home/home' })
}
onMounted(async () => {
await loadServiceTypes()
const prefill = JSON.parse(uni.getStorageSync('petstore_report_prefill') || 'null')
if (prefill) {
currentAppointmentId.value = prefill.appointmentId
report.value.petName = prefill.petName || ''
report.value.serviceType = prefill.serviceType || ''
report.value.appointmentTime = prefill.appointmentTime ? prefill.appointmentTime.slice(0, 16) : ''
uni.removeStorageSync('petstore_report_prefill')
}
})
</script>
<style scoped>
.report-page { padding-bottom: 20px; background: #f5f5f5; min-height: 100vh; }
.notice-bar {
background: #f6ffed;
color: #52c41a;
font-size: 13px;
padding: 10px 16px;
border-bottom: 1px solid #b7eb8f;
}
.result-content { padding: 16px; }
.result-link-row { background: #f9f9f9; border-radius: 8px; padding: 12px; margin-bottom: 16px; }
.result-link-label { font-size: 12px; color: #999; display: block; margin-bottom: 4px; }
.result-link-val { font-size: 12px; color: #07c160; word-break: break-all; }
.qr-wrap { text-align: center; padding: 20px; background: #f9f9f9; border-radius: 12px; margin: 16px 0; }
.qr-img { width: 180px; height: 180px; }
.form-section { padding: 12px 16px 0; }
.form-title { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 8px; padding-left: 4px; }
.form-card { background: #fff; border-radius: 12px; padding: 4px 16px; }
.form-row {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.form-row:last-child { border-bottom: none; }
.row-label { font-size: 14px; color: #666; width: 80px; flex-shrink: 0; }
.row-input {
flex: 1;
font-size: 14px;
color: #333;
text-align: right;
}
.picker-val {
flex: 1;
font-size: 14px;
color: #333;
text-align: right;
}
.photo-row {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.photo-row:last-child { border-bottom: none; }
.photo-upload {
flex: 1;
display: flex;
justify-content: flex-end;
}
.photo-preview {
width: 80px;
height: 80px;
border-radius: 8px;
}
.photo-placeholder {
width: 80px;
height: 80px;
border: 1px dashed #ddd;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
}
.plus-icon { font-size: 24px; line-height: 1; }
.upload-tip { font-size: 11px; margin-top: 2px; }
.remark-input {
width: 100%;
padding: 8px 0;
font-size: 14px;
color: #333;
border: none;
resize: none;
box-sizing: border-box;
}
.submit-wrap { padding: 16px; }
.btn-submit {
width: 100%;
height: 46px;
background: #07c160;
color: #fff;
border: none;
border-radius: 23px;
font-size: 16px;
font-weight: 600;
}
.btn-submit.loading { opacity: 0.6; }
.btn-primary-full {
width: 100%;
height: 44px;
background: #07c160;
color: #fff;
border: none;
border-radius: 8px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-default-full {
width: 100%;
height: 44px;
background: #fff;
color: #333;
border: 1px solid #eee;
border-radius: 8px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -14,6 +14,7 @@ function scheduleAuthGuard() {
try { try {
if (isLoggedIn()) return if (isLoggedIn()) return
const pages = getCurrentPages() const pages = getCurrentPages()
if (pages.length === 0) return // login
const cur = pages[pages.length - 1] const cur = pages[pages.length - 1]
const route = cur && cur.route ? cur.route : '' const route = cur && cur.route ? cur.route : ''
if (isPublicRoute(route)) return if (isPublicRoute(route)) return

View File

@ -18,5 +18,5 @@
"navigationBarTitleText": "宠伴生活馆", "navigationBarTitleText": "宠伴生活馆",
"navigationBarBackgroundColor": "#07c160", "navigationBarBackgroundColor": "#07c160",
"backgroundColor": "#F8F8F8" "backgroundColor": "#F8F8F8"
}, }
} }

View File

@ -1,5 +1,49 @@
<template> <template>
<div class="page-shell home-page"> <div class="page-shell home-page">
<!-- C端视角微官网与预约 -->
<view v-if="userInfo.role === 'customer'" class="c-home">
<view class="home-nav nav-gradient" :style="navSafeStyle">
<text class="nav-title">宠伴生活馆</text>
</view>
<view class="c-header">
<view class="c-title">{{ storeInfo.name || '宠伴生活馆' }}</view>
<view class="c-sub">宠物服务让爱更专业</view>
</view>
<view class="c-booking-card">
<view class="c-booking-title">预约服务</view>
<view class="field-label">宠物名字</view>
<input v-model="newAppt.petName" class="van-field" placeholder="请输入宠物名字" />
<view class="field-label">宠物类型</view>
<picker mode="selector" :range="petTypes" range-key="label" @change="e => newAppt.petType = petTypes[e.detail.value].value">
<view class="van-field picker-field">{{ newAppt.petType || '请选择' }}</view>
</picker>
<view class="field-label">服务类型</view>
<picker mode="selector" :range="serviceTypes" range-key="label" @change="e => newAppt.serviceType = serviceTypes[e.detail.value].value">
<view class="van-field picker-field">{{ newAppt.serviceType || '请选择' }}</view>
</picker>
<view class="field-label">预约日期</view>
<picker mode="date" :value="appointmentDate" @change="onAppointmentDateChange">
<view class="van-field picker-field">{{ appointmentDate || '请选择日期' }}</view>
</picker>
<view class="field-label">预约时间</view>
<picker mode="time" :value="appointmentTime" @change="onAppointmentTimeOnlyChange">
<view class="van-field picker-field">{{ appointmentTime || '请选择时间' }}</view>
</picker>
<view class="field-label">备注可选</view>
<input v-model="newAppt.remark" class="van-field" placeholder="可选" />
<button class="btn-block" style="margin-top: 24px;" :loading="creatingAppt" @click="confirmNewAppt">提交预约</button>
</view>
</view>
<!-- B端视角订单管理 -->
<view v-else>
<!-- 顶部导航 --> <!-- 顶部导航 -->
<view class="home-nav nav-gradient" :style="navSafeStyle"> <view class="home-nav nav-gradient" :style="navSafeStyle">
<text class="nav-title">宠伴生活馆</text> <text class="nav-title">宠伴生活馆</text>
@ -107,6 +151,7 @@
</view> </view>
</view> </view>
</view> </view>
</view>
</div> </div>
<TabBar current-page="home" @change="goPage" /> <TabBar current-page="home" @change="goPage" />
@ -316,6 +361,24 @@ onMounted(() => {
.hero-sub { margin-top: 4px; font-size: 12px; color: #4b5563; line-height: 1.45; } .hero-sub { margin-top: 4px; font-size: 12px; color: #4b5563; line-height: 1.45; }
.list-content { padding: 0; } .list-content { padding: 0; }
.c-header {
background: linear-gradient(135deg, #07c160 0%, #10b76f 100%);
padding: 20px 20px 40px;
text-align: center;
color: #fff;
border-radius: 0 0 24px 24px;
}
.c-title { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
.c-sub { font-size: 14px; opacity: 0.8; }
.c-booking-card {
background: #fff;
border-radius: 16px;
margin: -20px 16px 0;
padding: 24px 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.c-booking-title { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 20px; text-align: center; }
/* 按钮 */ /* 按钮 */
.btn-primary { .btn-primary {
display: inline-flex; display: inline-flex;

View File

@ -10,7 +10,7 @@
<div class="user-info-content"> <div class="user-info-content">
<div class="user-name">{{ userInfo.name }}</div> <div class="user-name">{{ userInfo.name }}</div>
<div class="user-phone">{{ userInfo.phone }}</div> <div class="user-phone">{{ userInfo.phone }}</div>
<div class="role-pill">{{ userInfo.role === 'boss' ? '店长' : '员工' }}</div> <div class="role-pill">{{ userInfo.role === 'boss' ? '店长' : userInfo.role === 'customer' ? '客户' : '员工' }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -42,7 +42,7 @@
<div class="menu-left"><span class="menu-icon"><AppIcon name="profile" :size="15" /></span><span class="menu-text">个人信息</span></div> <div class="menu-left"><span class="menu-icon"><AppIcon name="profile" :size="15" /></span><span class="menu-text">个人信息</span></div>
<span class="menu-arrow"></span> <span class="menu-arrow"></span>
</div> </div>
<div class="menu-item" @click="navigateTo('myReports')"> <div v-if="userInfo.role !== 'customer'" class="menu-item" @click="navigateTo('myReports')">
<div class="menu-left"><span class="menu-icon"><AppIcon name="report" :size="15" /></span><span class="menu-text">我的报告</span></div> <div class="menu-left"><span class="menu-icon"><AppIcon name="report" :size="15" /></span><span class="menu-text">我的报告</span></div>
<span class="menu-arrow"></span> <span class="menu-arrow"></span>
</div> </div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<rect width="81" height="81" fill="#07c160"/>
<text x="40.5" y="45" text-anchor="middle" font-size="36">🏠</text>
</svg>

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<rect width="81" height="81" fill="#999"/>
<text x="40.5" y="45" text-anchor="middle" font-size="36">🏠</text>
</svg>

Before

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<rect width="81" height="81" fill="#07c160"/>
<text x="40.5" y="45" text-anchor="middle" font-size="36">👤</text>
</svg>

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<rect width="81" height="81" fill="#999"/>
<text x="40.5" y="45" text-anchor="middle" font-size="36">👤</text>
</svg>

Before

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<rect width="81" height="81" fill="#07c160"/>
<text x="40.5" y="45" text-anchor="middle" font-size="36">📋</text>
</svg>

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="81" height="81" viewBox="0 0 81 81">
<rect width="81" height="81" fill="#999"/>
<text x="40.5" y="45" text-anchor="middle" font-size="36">📋</text>
</svg>

Before

Width:  |  Height:  |  Size: 208 B

View File

@ -1,21 +0,0 @@
/* uni.scss - 全局样式变量 */
$uni-color-primary: #07c160;
$uni-color-success: #07c160;
$uni-color-warning: #ff9f00;
$uni-color-error: #ff4d4f;
$uni-color-white: #ffffff;
$uni-color-black: #000000;
$uni-bg-color: #f5f5f5;
$uni-text-color: #333333;
$uni-text-color-grey: #999999;
$uni-border-color: #eeeeee;
/* 主题色变量(兼容部分 vant 变量) */
:root {
--van-primary-color: #07c160;
--van-orange: #ff9f00;
--van-text: #333333;
--van-text-secondary: #999999;
--van-cream: #f8f6f3;
--van-brown: #8b6914;
}

View File

@ -1,124 +0,0 @@
// utils/api.js - uniapp API 封装
const BASE_URL = 'http://localhost:8080/api'
// 统一请求封装
const request = (options) => {
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: options.header || {
'Content-Type': 'application/json'
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(res)
}
},
fail: (err) => {
reject(err)
}
})
})
}
const get = (url, params = {}) => {
return request({ url, method: 'GET', data: params })
}
const post = (url, data = {}) => {
return request({ url, method: 'POST', data })
}
const put = (url, data = {}) => {
return request({ url, method: 'PUT', data })
}
const del = (url) => {
return request({ url, method: 'DELETE' })
}
// 图片完整URL
export const imgUrl = (path) => {
if (!path) return ''
if (path.startsWith('http')) return path
return `http://localhost:8080${path}`
}
// 发送验证码
export const sendSms = (phone) => post('/sms/send', { phone })
// 登录
export const login = (phone, code) => post('/user/login', { phone, code })
// 注册老板
export const registerBoss = (data) => post('/user/register-boss', data)
// 注册员工
export const registerStaff = (data) => post('/user/register-staff', data)
// 预约列表
export const getAppointmentList = (userId) => get('/appointment/list', { userId })
// 创建预约
export const createAppointment = (data) => post('/appointment/create', data)
// 开始服务
export const startAppointment = (appointmentId, staffUserId) =>
post('/appointment/start', { appointmentId, staffUserId })
// 取消预约
export const cancelAppointment = (id) => put(`/appointment/status?id=${id}&status=cancel`)
// 提交报告
export const createReport = (data) => post('/report/create', data)
// 获取报告通过token
export const getReportByToken = (token) => get('/report/get', { token })
// 报告列表
export const getReportList = (params) => get('/report/list', params)
// 服务类型列表
export const getServiceTypeList = (storeId) => get('/service-type/list', { storeId })
// 创建服务类型
export const createServiceType = (storeId, name) => post('/service-type/create', { storeId, name })
// 删除服务类型
export const deleteServiceType = (id) => del(`/service-type/delete?id=${id}`)
// 员工列表
export const getStaffList = (storeId) => get('/user/staff-list', { storeId })
// 添加员工
export const createStaff = (data) => post('/user/create-staff', data)
// 删除员工
export const deleteStaff = (staffId) => del(`/user/staff?staffId=${staffId}`)
// 更新店铺
export const updateStore = (data) => put('/store/update', data)
// 上传图片uniapp版
export const uploadImage = (filePath) => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: BASE_URL + '/upload/image',
filePath: filePath,
name: 'file',
success: (res) => {
try {
const data = JSON.parse(res.data)
resolve(data)
} catch (e) {
reject(e)
}
},
fail: (err) => reject(err)
})
})
}