| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- import { computed, reactive, watch } from 'vue';
- import { Bell, LogOut, Store, Smartphone, ShieldCheck, LayoutDashboard, Package, ShoppingBag, Users, Megaphone, Headset, BookOpen, SlidersHorizontal, Rocket, Wallet, KeyRound, Radio, ClipboardCheck, History, ChartNoAxesCombined, MessageSquare, FileText, Settings, Activity, Layers, Cpu, Send, Gauge } from '@lucide/vue';
- import { useRoute } from 'vue-router';
- import { allowedPage } from './access.js';
- import CustomerPage, { customerPages } from './pages/customer.js';
- import MerchantPage, { merchantPages, merchantMenus } from './pages/merchant.js';
- import PlatformPage, { platformPages, platformMenus } from './pages/platform.js';
- export default {
- props: ['context'],
- components: { CustomerPage, MerchantPage, PlatformPage, Bell, LogOut, Store, Smartphone, ShieldCheck },
- setup(props) {
- const ctx = props.context;
- const route = useRoute();
- const login = reactive({ tenant: ctx.tenantId, account: '', password: '', pending: false, error: '' });
- const navigationIcons = { overview: LayoutDashboard, dashboard: LayoutDashboard, products: Package, orders: ShoppingBag, 'after-sales': ClipboardCheck, customers: Users, marketing: Megaphone, service: Headset, queue: Headset, knowledge: BookOpen, rules: SlidersHorizontal, release: Rocket, billing: Wallet, access: KeyRound, channels: Radio, notifications: Bell, approvals: ClipboardCheck, history: History, team: Users, statistics: ChartNoAxesCombined, reference: BookOpen, 'quick-replies': MessageSquare, tenants: Store, applications: Layers, capacity: Gauge, models: Cpu, deliveries: Send, health: Activity, audit: FileText, permissions: KeyRound, settings: Settings };
- function navigationIcon(name) { return navigationIcons[name] || FileText; }
- const product = computed(() => route.path.split('/')[1] || 'customer');
- const role = computed(() => product.value === 'merchant' ? ctx.session.merchant?.role || 'merchant' : product.value);
- const page = computed(() => route.path.split('/').filter(Boolean).at(-1) || 'home');
- const account = computed(() => ctx.session[product.value]);
- const productTitle = computed(() => ({ customer: '购物商城 H5', merchant: '商家后台', platform: '平台后台' })[product.value]);
- function allowed(name) {
- return allowedPage(product.value, account.value, name);
- }
- const menus = computed(() => (product.value === 'platform' ? platformMenus : merchantMenus[role.value] || []).map(([group, items]) => [group, items.filter(([name]) => allowed(name))]).filter(([, items]) => items.length));
- const current = computed(() => {
- if (product.value === 'customer') return customerPages.find(item => (typeof item === 'string' ? item : item.page) === page.value);
- if (product.value === 'platform') return platformPages.find(item => item.page === page.value);
- return merchantPages.find(item => item.role === role.value && item.page === page.value && allowed(item.page));
- });
- const title = computed(() => current.value?.title || (page.value === 'login' ? '员工登录' : '页面不可访问'));
- const blocked = computed(() => {
- const section = route.path.split('/')[2];
- const wrongSection = product.value === 'merchant' && account.value && (section === 'service' && route.path.split('/').length > 3 && role.value !== 'staff' || section === 'approval' && role.value !== 'supervisor');
- return !['customer', 'merchant', 'platform'].includes(product.value) || wrongSection || page.value !== 'login' && !current.value;
- });
- watch(() => route.path, () => {
- login.password = ''; login.error = '';
- if (route.path === '/') { ctx.navigate('customer', 'home'); return; }
- if (['merchant', 'platform'].includes(product.value) && page.value !== 'login' && !account.value) ctx.requireLogin(product.value, route.path);
- // 子路径仅描述业务栏目,员工身份始终来自已验证账户。
- if (product.value === 'merchant' && account.value && page.value !== 'login') {
- const segment = route.path.split('/')[2];
- if (segment === 'service' && route.path.split('/').length > 3 && role.value !== 'staff' || segment === 'approval' && role.value !== 'supervisor') ctx.notify('当前账户无权访问此功能。');
- }
- document.title = `${title.value} · 智服`;
- }, { immediate: true });
- async function submitLogin() {
- if (login.pending) return;
- login.pending = true; login.error = '';
- try { await ctx.login(product.value, login); }
- catch (error) { login.error = error.message; }
- finally { login.password = ''; login.pending = false; }
- }
- async function accept() {
- const state = ctx.confirmation;
- if (state.busy) return;
- state.busy = true;
- try { await state.callback(); state.open = false; }
- catch (error) { ctx.notify(error.message); state.open = false; }
- finally { state.busy = false; }
- }
- function logout() { ctx.confirm('退出登录', '确认退出当前后台?未提交内容将被清理。', () => ctx.logout(product.value)); }
- async function beforeCustomerClose(action) {
- // 异步业务结束前保持窗口,组件库负责确认按钮的处理中状态。
- if (ctx.confirmation.busy) return false;
- if (action === 'confirm') await accept();
- return true;
- }
- function home(target) { const r = ctx.session[target]?.role || target; ctx.navigate(r, { customer: 'home', merchant: 'overview', staff: 'queue', supervisor: 'approvals', platform: 'dashboard' }[r]); }
- return { ctx, product, role, page, account, productTitle, menus, title, blocked, login, submitLogin, accept, logout, home, navigationIcon, beforeCustomerClose };
- },
- template: `
- <header class="review-header"><div><strong>智服</strong><span>商城与管理中心</span></div><nav aria-label="产品入口"><el-button @click="home('customer')"><Smartphone :size="16" aria-hidden="true"/>购物商城 H5</el-button><el-button @click="home('merchant')"><Store :size="16" aria-hidden="true"/>商家后台</el-button><el-button @click="home('platform')"><ShieldCheck :size="16" aria-hidden="true"/>平台后台</el-button></nav></header>
- <el-alert v-if="ctx.notice.text" class="app-notice" :title="ctx.notice.text" type="info" show-icon @close="ctx.notice.text=''" role="status" />
- <main id="product">
- <section v-if="blocked" class="empty"><h1>页面不可访问</h1><el-empty description="页面不存在或当前账户未获得此功能权限。"><el-button type="primary" @click="home(product)">返回首页</el-button></el-empty></section>
- <CustomerPage v-else-if="product==='customer'" :key="ctx.session.customer?.id || 'guest'" :context="ctx" :page="page" />
- <div v-else-if="page==='login'" class="login-layout">
- <section class="login-intro"><h1>智服中台</h1><h2>{{productTitle}}</h2><p>{{product==='merchant'?'店铺经营 · 客户服务 · 售后管理':'商家接入 · 资源管理 · 运行监控'}}</p></section>
- <section class="surface login-panel"><h2>{{product==='merchant'?'商家员工登录':'平台员工登录'}}</h2><p>使用管理员分配的员工账户登录。</p>
- <el-form label-position="top" @submit.prevent="submitLogin"><el-form-item v-if="product==='merchant'" label="商家标识" for="login-tenant"><el-input id="login-tenant" name="tenant" v-model="login.tenant" required autocomplete="organization" /></el-form-item><el-form-item label="员工账户" for="login-account"><el-input id="login-account" name="account" v-model="login.account" required autocomplete="username" /></el-form-item><el-form-item label="密码" for="login-password"><el-input id="login-password" name="password" type="password" v-model="login.password" required autocomplete="current-password" show-password /></el-form-item><el-alert v-if="login.error" :title="login.error" type="error" :closable="false" show-icon role="alert"/><el-button type="primary" :loading="login.pending" :disabled="login.pending" native-type="submit">{{login.pending?'登录中…':'登录'}}</el-button></el-form>
- <p class="muted">账户与权限由对应管理员分配。</p><el-button text @click="ctx.notify('请联系对应管理员核实账户并重置密码。')">忘记密码 / 无法登录</el-button>
- </section>
- </div>
- <template v-else-if="account">
- <div class="admin-session"><el-button v-if="product==='merchant'" @click="ctx.navigate(role,'notifications')"><Bell :size="16" aria-hidden="true"/>消息通知({{ctx.data.notificationsUnread || 0}})</el-button><span>{{account.username}}</span><el-button text @click="logout"><LogOut :size="16" aria-hidden="true"/>退出登录</el-button></div>
- <div class="admin-shell"><aside class="admin-sidebar"><div class="admin-brand">智服中台<small>{{productTitle}}</small></div><nav aria-label="系统导航"><div v-for="group in menus" :key="group[0]" class="nav-group"><p>{{group[0]}}</p><el-button v-for="item in group[1]" :key="item[0]" :aria-current="page===item[0]?'page':undefined" @click="ctx.navigate(role,item[0])"><component :is="navigationIcon(item[0])" :size="18" aria-hidden="true"/><span>{{item[1]}}</span></el-button></div></nav></aside>
- <div class="admin-main"><header class="admin-topbar"><strong>{{product==='platform'?'平台运营中心':'商家店铺'}}</strong><div><span>{{account.username}} · {{ {merchant:'管理员',staff:'客服',supervisor:'主管',platform:'平台运营'}[role] }}</span></div></header><div class="admin-breadcrumb">{{productTitle}} / {{title}}</div><div class="admin-tabs"><span>{{title}}</span></div><div class="admin-content"><div class="admin-title"><h2>{{title}}</h2><span>当前范围:已授权业务</span></div><div class="surface"><MerchantPage v-if="product==='merchant'" :key="account.id" :context="ctx" :page="page" :role="role" /><PlatformPage v-else :key="account.id" :context="ctx" :page="page" /></div></div></div>
- </div>
- </template>
- </main>
- <van-dialog v-if="product==='customer'" v-model:show="ctx.confirmation.open" :title="ctx.confirmation.title" :aria-label="ctx.confirmation.title" :message="ctx.confirmation.text" show-cancel-button confirm-button-text="确认操作" cancel-button-text="返回检查" :cancel-button-disabled="ctx.confirmation.busy" :close-on-click-overlay="false" :before-close="beforeCustomerClose" class="customer-confirm" />
- <el-dialog v-else v-model="ctx.confirmation.open" :title="ctx.confirmation.title" width="460px" :close-on-click-modal="false" :close-on-press-escape="!ctx.confirmation.busy" :show-close="!ctx.confirmation.busy" align-center><p>{{ctx.confirmation.text}}</p><template #footer><el-button :disabled="ctx.confirmation.busy" @click="ctx.confirmation.open=false">返回检查</el-button><el-button type="primary" :loading="ctx.confirmation.busy" :disabled="ctx.confirmation.busy" @click="accept">确认操作</el-button></template></el-dialog>
- `,
- };
|