feat: 完善平台总后台认证与管理入口
This commit is contained in:
@@ -1,19 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import {
|
||||
platformAPI,
|
||||
type DashboardOverview,
|
||||
type IdnAccount,
|
||||
type OrgDeliveryPoint,
|
||||
type OrgGasStation,
|
||||
type OrgServicePerson,
|
||||
type SafEvent,
|
||||
} from './api/platform';
|
||||
import { platformAPI, type DashboardOverview, type IdnAccount, type OrgDeliveryPoint, type OrgGasStation, type OrgServicePerson, type Profile, type SafEvent } from './api/platform';
|
||||
|
||||
type Tab = 'dashboard' | 'station' | 'delivery' | 'person' | 'user' | 'safety';
|
||||
type Tab = 'dashboard' | 'station' | 'delivery' | 'person' | 'user' | 'safety' | 'trade' | 'finance' | 'content' | 'track' | 'operation' | 'audit';
|
||||
type CatalogItem = { title: string; description: string; operations: string[] };
|
||||
|
||||
const currentTab = ref<Tab>('dashboard');
|
||||
const loading = ref(false);
|
||||
const loggedIn = ref(platformAPI.hasSession());
|
||||
const errorMessage = ref('');
|
||||
const overview = ref<DashboardOverview>({});
|
||||
const stations = ref<OrgGasStation[]>([]);
|
||||
@@ -21,124 +15,112 @@ const deliveryPoints = ref<OrgDeliveryPoint[]>([]);
|
||||
const servicePeople = ref<OrgServicePerson[]>([]);
|
||||
const users = ref<IdnAccount[]>([]);
|
||||
const safetyEvents = ref<SafEvent[]>([]);
|
||||
const form = reactive({ stationCode: '', name: '', principal: '', serviceArea: '' });
|
||||
const profile = ref<Profile>();
|
||||
const loginForm = reactive({ username: 'root', password: '' });
|
||||
const stationForm = reactive({ stationCode: '', name: '', principal: '', serviceArea: '' });
|
||||
const passwordForm = reactive({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
|
||||
const tabs: Array<{ key: Tab; label: string }> = [
|
||||
{ key: 'dashboard', label: '运营概览' },
|
||||
{ key: 'station', label: '气站管理' },
|
||||
{ key: 'delivery', label: '配送点管理' },
|
||||
{ key: 'person', label: '服务人员' },
|
||||
{ key: 'user', label: '用户管理' },
|
||||
{ key: 'safety', label: '安全运营' },
|
||||
{ key: 'dashboard', label: '运营总览' }, { key: 'station', label: '气站管理' }, { key: 'delivery', label: '配送点管理' },
|
||||
{ key: 'person', label: '服务人员' }, { key: 'user', label: '用户管理' }, { key: 'safety', label: '安全运营' },
|
||||
{ key: 'trade', label: '商品交易' }, { key: 'finance', label: '资金结算' }, { key: 'content', label: '内容客服' },
|
||||
{ key: 'track', label: '邀请与轨迹' }, { key: 'operation', label: '全局运营' }, { key: 'audit', label: '审计合规' },
|
||||
];
|
||||
|
||||
const catalog: Partial<Record<Tab, CatalogItem[]>> = {
|
||||
trade: [{ title: '订单中心', description: '统一检索预约、配送、安装维修、安检及售后订单,支持分单、改派、取消和异常升级。', operations: ['订单查询', '异常升级', '退款审核'] }, { title: '商品与价格', description: '维护可燃气体商品、服务项目、区域价目及促销规则。', operations: ['商品上架', '价格审批', '活动配置'] }],
|
||||
finance: [{ title: '结算中心', description: '按气站、配送点、服务人员和订单维度出具结算单并保留审批链路。', operations: ['结算单', '对账差异', '付款审批'] }, { title: '资金风控', description: '识别退款、补贴、佣金和余额异常,重大风险须人工复核。', operations: ['风险规则', '冻结处置', '凭证归档'] }],
|
||||
content: [{ title: '内容运营', description: '管理公告、可燃气体安全知识、消息模板与区域投放策略。', operations: ['内容发布', '模板审核', '投放记录'] }, { title: '客服工单', description: '统一受理用户咨询、投诉、回访和升级闭环。', operations: ['工单分派', '服务质检', '满意度'] }],
|
||||
track: [{ title: '邀请注册二维码', description: '管理气站、配送点邀请二维码的归属、有效期、扫描转化与失效处置。', operations: ['生成二维码', '归属迁移', '转化分析'] }, { title: '订单配送轨迹', description: '按订单回放接单、出库、到达、交付和异常节点,保留不可抵赖审计记录。', operations: ['轨迹查询', '异常告警', '轨迹导出'] }],
|
||||
operation: [{ title: '规则与策略', description: '维护服务范围、配送时段、调度、补贴和风控策略,并按区域灰度发布。', operations: ['规则发布', '灰度控制', '回滚记录'] }, { title: '消息与指标', description: '管理消息触达渠道、运营指标、预警阈值与日报。', operations: ['消息任务', '指标看板', '预警订阅'] }],
|
||||
audit: [{ title: '审计日志', description: '记录组织、人员、用户、设备、订单、结算及权限等关键操作,支持按对象追溯。', operations: ['日志检索', '操作追溯', '导出留档'] }, { title: '合规审批', description: '对高风险安全事件、敏感资料导出、资质过期和重大组织变更实施双人复核。', operations: ['审批队列', '证据附件', '合规报表'] }],
|
||||
};
|
||||
|
||||
const overviewCards = computed(() => [
|
||||
['启用气站', overview.value.gasStationCount ?? 0],
|
||||
['启用配送点', overview.value.deliveryPointCount ?? 0],
|
||||
['在岗服务人员', overview.value.servicePersonCount ?? 0],
|
||||
['服务用户', overview.value.userCount ?? 0],
|
||||
['待处理安全事件', overview.value.pendingSafetyCount ?? 0],
|
||||
['启用气站', overview.value.gasStationCount ?? 0], ['启用配送点', overview.value.deliveryPointCount ?? 0],
|
||||
['在岗服务人员', overview.value.servicePersonCount ?? 0], ['服务用户', overview.value.userCount ?? 0], ['待处理安全事件', overview.value.pendingSafetyCount ?? 0],
|
||||
]);
|
||||
const activeLabel = computed(() => tabs.find((tab) => tab.key === currentTab.value)?.label ?? '平台总后台');
|
||||
|
||||
async function loadData() {
|
||||
if (!loggedIn.value) return;
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
try {
|
||||
[overview.value, stations.value, deliveryPoints.value, servicePeople.value, users.value, safetyEvents.value] = await Promise.all([
|
||||
platformAPI.getDashboard(),
|
||||
platformAPI.listOrgGasStation(),
|
||||
platformAPI.listOrgDeliveryPoint(),
|
||||
platformAPI.listOrgServicePerson(),
|
||||
platformAPI.listIdnAccount(),
|
||||
platformAPI.listSafEvent(),
|
||||
[profile.value, overview.value, stations.value, deliveryPoints.value, servicePeople.value, users.value, safetyEvents.value] = await Promise.all([
|
||||
platformAPI.getProfile(), platformAPI.getDashboard(), platformAPI.listOrgGasStation(), platformAPI.listOrgDeliveryPoint(),
|
||||
platformAPI.listOrgServicePerson(), platformAPI.listIdnAccount(), platformAPI.listSafEvent(),
|
||||
]);
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '加载数据失败';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
if (errorMessage.value.includes('Token') || errorMessage.value.includes('认证')) logout();
|
||||
} finally { loading.value = false; }
|
||||
}
|
||||
|
||||
async function createStation() {
|
||||
if (!form.stationCode || !form.name || !form.principal || !form.serviceArea) {
|
||||
errorMessage.value = '请完整填写气站资料';
|
||||
return;
|
||||
}
|
||||
async function login() {
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
try {
|
||||
await platformAPI.createOrgGasStation({ ...form });
|
||||
Object.assign(form, { stationCode: '', name: '', principal: '', serviceArea: '' });
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '创建气站失败';
|
||||
loading.value = false;
|
||||
}
|
||||
try { await platformAPI.login(loginForm.username, loginForm.password); loggedIn.value = true; loginForm.password = ''; await loadData(); }
|
||||
catch (error) { errorMessage.value = error instanceof Error ? error.message : '登录失败'; }
|
||||
finally { loading.value = false; }
|
||||
}
|
||||
|
||||
function logout() { platformAPI.clearSession(); loggedIn.value = false; profile.value = undefined; }
|
||||
|
||||
async function createStation() {
|
||||
if (!stationForm.stationCode || !stationForm.name || !stationForm.principal || !stationForm.serviceArea) { errorMessage.value = '请完整填写气站资料'; return; }
|
||||
loading.value = true;
|
||||
try { await platformAPI.createOrgGasStation({ ...stationForm }); Object.assign(stationForm, { stationCode: '', name: '', principal: '', serviceArea: '' }); await loadData(); }
|
||||
catch (error) { errorMessage.value = error instanceof Error ? error.message : '创建气站失败'; }
|
||||
finally { loading.value = false; }
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) { errorMessage.value = '两次输入的新密码不一致'; return; }
|
||||
loading.value = true;
|
||||
try { await platformAPI.changePassword(passwordForm.currentPassword, passwordForm.newPassword); Object.assign(passwordForm, { currentPassword: '', newPassword: '', confirmPassword: '' }); await loadData(); }
|
||||
catch (error) { errorMessage.value = error instanceof Error ? error.message : '修改密码失败'; }
|
||||
finally { loading.value = false; }
|
||||
}
|
||||
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="shell">
|
||||
<main v-if="!loggedIn" class="login-page">
|
||||
<form class="login-card" @submit.prevent="login">
|
||||
<span class="brand-mark">阀</span><p class="eyebrow">HEQI PLATFORM</p><h1>可燃气体平台总后台</h1>
|
||||
<p>使用平台管理员账号登录。首次登录后必须修改初始密码。</p>
|
||||
<input v-model.trim="loginForm.username" autocomplete="username" placeholder="账号" required />
|
||||
<input v-model="loginForm.password" type="password" autocomplete="current-password" placeholder="密码" required />
|
||||
<p v-if="errorMessage" class="error">{{ errorMessage }}</p><button :disabled="loading" type="submit">{{ loading ? '登录中…' : '登录' }}</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<main v-else class="shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<span class="brand-mark">阀</span>
|
||||
<div><strong>可燃气体平台</strong><small>平台总后台</small></div>
|
||||
</div>
|
||||
<nav>
|
||||
<button v-for="tab in tabs" :key="tab.key" :class="{ active: currentTab === tab.key }" @click="currentTab = tab.key">
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</nav>
|
||||
<div class="mock-note">支付、地图、IoT 与消息服务均使用 Mock 边界。</div>
|
||||
<div class="brand"><span class="brand-mark">阀</span><div><strong>可燃气体平台</strong><small>平台总后台</small></div></div>
|
||||
<nav><button v-for="tab in tabs" :key="tab.key" :class="{ active: currentTab === tab.key }" @click="currentTab = tab.key">{{ tab.label }}</button></nav>
|
||||
<div class="account"><strong>{{ profile?.displayName || '平台管理员' }}</strong><small>{{ profile?.roleCode || '加载中' }}</small><button class="link-button" @click="logout">退出登录</button></div>
|
||||
</aside>
|
||||
|
||||
<section class="content">
|
||||
<header>
|
||||
<div><p class="eyebrow">M0 + M1</p><h1>{{ tabs.find((tab) => tab.key === currentTab)?.label }}</h1></div>
|
||||
<button class="secondary" :disabled="loading" @click="loadData">{{ loading ? '同步中…' : '刷新数据' }}</button>
|
||||
</header>
|
||||
<header><div><p class="eyebrow">PLATFORM ADMIN</p><h1>{{ activeLabel }}</h1></div><button class="secondary" :disabled="loading" @click="loadData">{{ loading ? '同步中…' : '刷新数据' }}</button></header>
|
||||
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
|
||||
<article v-if="profile?.mustChangePassword" class="panel warning"><h2>首次登录安全设置</h2><p>root 初始密码仅可用于首次进入,请立即设置不少于 12 位的新密码。</p><form class="password-form" @submit.prevent="changePassword"><input v-model="passwordForm.currentPassword" type="password" placeholder="当前密码" required /><input v-model="passwordForm.newPassword" type="password" placeholder="新密码(至少 12 位)" required /><input v-model="passwordForm.confirmPassword" type="password" placeholder="确认新密码" required /><button :disabled="loading">修改密码</button></form></article>
|
||||
|
||||
<template v-if="currentTab === 'dashboard'">
|
||||
<div class="cards"><article v-for="([label, value]) in overviewCards" :key="label"><span>{{ label }}</span><strong>{{ value }}</strong></article></div>
|
||||
<article class="panel"><h2>安全处置提示</h2><p>高风险安全事件由安全运营中心统一升级、审计和派发。当前待处理事件:{{ overview.pendingSafetyCount ?? 0 }} 项。</p></article>
|
||||
</template>
|
||||
|
||||
<template v-if="currentTab === 'station'">
|
||||
<article class="panel"><h2>创建气站</h2><form @submit.prevent="createStation"><input v-model.trim="form.stationCode" placeholder="站点编码,例如 GS-1002" /><input v-model.trim="form.name" placeholder="气站名称" /><input v-model.trim="form.principal" placeholder="负责人" /><input v-model.trim="form.serviceArea" placeholder="服务区域" /><button :disabled="loading" type="submit">提交审核</button></form></article>
|
||||
<DataTable :headers="['编码', '名称', '负责人', '服务区域', '状态']" :rows="stations.map((item) => [item.stationCode, item.name, item.principal, item.serviceArea, item.status])" />
|
||||
</template>
|
||||
|
||||
<template v-if="currentTab === 'delivery'">
|
||||
<DataTable :headers="['编码', '配送点', '归属气站', '服务区域', '状态']" :rows="deliveryPoints.map((item) => [item.deliveryCode, item.name, item.gasStationName, item.serviceArea, item.status])" />
|
||||
</template>
|
||||
|
||||
<template v-if="currentTab === 'person'">
|
||||
<DataTable :headers="['姓名', '手机号', '角色', '工作状态', '资质状态']" :rows="servicePeople.map((item) => [item.name, item.phoneMasked, item.roles, item.workStatus, item.credentialStatus])" />
|
||||
</template>
|
||||
|
||||
<template v-if="currentTab === 'user'">
|
||||
<DataTable :headers="['手机号', '账户类型', '状态', '服务区域']" :rows="users.map((item) => [item.phoneMasked, item.accountType, item.status, item.serviceArea])" />
|
||||
</template>
|
||||
|
||||
<template v-if="currentTab === 'safety'">
|
||||
<DataTable :headers="['事件编码', '等级', '事件说明', '状态', '创建时间']" :rows="safetyEvents.map((item) => [item.eventCode, `${item.level} 级`, item.title, item.status, new Date(item.createdAt).toLocaleString()])" />
|
||||
</template>
|
||||
<template v-if="currentTab === 'dashboard'"><div class="cards"><article v-for="([label, value]) in overviewCards" :key="label"><span>{{ label }}</span><strong>{{ value }}</strong></article></div><article class="panel"><h2>平台职责边界</h2><p>统一管理所有气站、配送点、服务人员和用户,并对可燃气体业务的安全、订单配送轨迹、资金结算、邀请二维码和高风险操作保留审计闭环。</p></article></template>
|
||||
<template v-if="currentTab === 'station'"><article class="panel"><h2>创建气站</h2><form @submit.prevent="createStation"><input v-model.trim="stationForm.stationCode" placeholder="气站编码,例如 GS-1002" /><input v-model.trim="stationForm.name" placeholder="气站名称" /><input v-model.trim="stationForm.principal" placeholder="负责人" /><input v-model.trim="stationForm.serviceArea" placeholder="服务区域" /><button :disabled="loading">提交审核</button></form><p>气站可继续管理其配送点、服务人员、用户及专属邀请注册二维码。</p></article><DataTable :headers="['编码', '名称', '负责人', '服务区域', '状态']" :rows="stations.map((item) => [item.stationCode, item.name, item.principal, item.serviceArea, item.status])" /></template>
|
||||
<template v-if="currentTab === 'delivery'"><article class="panel"><h2>配送点管理边界</h2><p>配送点归属气站,负责管理服务人员、用户、配送班次、订单交付与配送轨迹;平台可跨组织查看、审核、冻结和迁移。</p></article><DataTable :headers="['编码', '配送点', '归属气站', '服务区域', '状态']" :rows="deliveryPoints.map((item) => [item.deliveryCode, item.name, item.gasStationName, item.serviceArea, item.status])" /></template>
|
||||
<template v-if="currentTab === 'person'"><article class="panel"><h2>服务人员生命周期</h2><p>覆盖安装维修、安检、配送角色、资质、登录设备、接单状态、任务绩效及停用留档。</p></article><DataTable :headers="['姓名', '手机号', '角色', '工作状态', '资质状态']" :rows="servicePeople.map((item) => [item.name, item.phoneMasked, item.roles, item.workStatus, item.credentialStatus])" /></template>
|
||||
<template v-if="currentTab === 'user'"><article class="panel"><h2>用户 360° 管理</h2><p>统一查看用户资料、地址、智能瓶阀、订单、权益、投诉与风险处置;敏感信息按最小化原则展示。</p></article><DataTable :headers="['手机号', '账户类型', '状态', '服务区域']" :rows="users.map((item) => [item.phoneMasked, item.accountType, item.status, item.serviceArea])" /></template>
|
||||
<template v-if="currentTab === 'safety'"><article class="panel"><h2>安全运营中心</h2><p>对智能瓶阀告警、安检异常、可燃气体风险、资质临期与订单异常进行分级、派发、升级和复盘。</p></article><DataTable :headers="['事件编码', '等级', '事件说明', '状态', '创建时间']" :rows="safetyEvents.map((item) => [item.eventCode, `${item.level} 级`, item.title, item.status, new Date(item.createdAt).toLocaleString()])" /></template>
|
||||
<template v-if="catalog[currentTab]"><div class="catalog"><article v-for="item in catalog[currentTab]" :key="item.title" class="panel"><h2>{{ item.title }}</h2><p>{{ item.description }}</p><div class="tags"><span v-for="operation in item.operations" :key="operation">{{ operation }}</span></div></article></div></template>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
DataTable: defineComponent({
|
||||
props: { headers: { type: Array as PropType<string[]>, required: true }, rows: { type: Array as PropType<string[][]>, required: true } },
|
||||
template: `<article class="panel table-panel"><table><thead><tr><th v-for="header in headers" :key="header">{{ header }}</th></tr></thead><tbody><tr v-for="(row, index) in rows" :key="index"><td v-for="(cell, cellIndex) in row" :key="cellIndex">{{ cell }}</td></tr><tr v-if="rows.length === 0"><td :colspan="headers.length">暂无数据</td></tr></tbody></table></article>`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
export default defineComponent({ components: { DataTable: defineComponent({ props: { headers: { type: Array as PropType<string[]>, required: true }, rows: { type: Array as PropType<string[][]>, required: true } }, template: `<article class="panel table-panel"><table><thead><tr><th v-for="header in headers" :key="header">{{ header }}</th></tr></thead><tbody><tr v-for="(row, index) in rows" :key="index"><td v-for="(cell, cellIndex) in row" :key="cellIndex">{{ cell }}</td></tr><tr v-if="rows.length === 0"><td :colspan="headers.length">暂无数据</td></tr></tbody></table></article>` }) } });
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user