Files
platforms/frontend/platform_admin/src/App.vue

116 lines
12 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
2026-07-27 00:18:42 +08:00
import { platformAPI, type DashboardOverview, type OrgDeliveryPoint, type OrgGasStation, type OrgServicePerson, type PlatfromAccount, type Profile, type SafEvent } from './api/platform';
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[]>([]);
const deliveryPoints = ref<OrgDeliveryPoint[]>([]);
const servicePeople = ref<OrgServicePerson[]>([]);
2026-07-27 00:18:42 +08:00
const users = ref<PlatfromAccount[]>([]);
const safetyEvents = ref<SafEvent[]>([]);
const profile = ref<Profile>();
const loginForm = reactive({ username: 'root', password: '' });
const stationForm = reactive({ stationCode: '', name: '', principal: '', serviceArea: '' });
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: '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],
]);
const activeLabel = computed(() => tabs.find((tab) => tab.key === currentTab.value)?.label ?? '平台总后台');
async function loadData() {
if (!loggedIn.value) return;
loading.value = true;
errorMessage.value = '';
try {
[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(),
2026-07-27 00:18:42 +08:00
platformAPI.listOrgServicePerson(), platformAPI.listPlatfromAccount(), platformAPI.listSafEvent(),
]);
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '加载数据失败';
if (errorMessage.value.includes('Token') || errorMessage.value.includes('认证')) logout();
} finally { loading.value = false; }
}
async function login() {
loading.value = true;
errorMessage.value = '';
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; }
}
onMounted(loadData);
</script>
<template>
<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>
2026-07-27 00:18:42 +08:00
<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">
2026-07-27 00:18:42 +08:00
<div class="brand"><img v-if="profile?.avatar" class="avatar" :src="profile.avatar" :alt="profile.displayName" /><span v-else 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">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>
<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>
2026-07-27 00:18:42 +08:00
<template v-if="currentTab === 'user'"><article class="panel"><h2>平台账户管理</h2><p>统一查看平台账户资料、角色、头像、手机号、状态与操作记录;敏感信息按最小化原则展示。</p></article><DataTable :headers="['账号', '名称', '头像', '手机号', '角色', '状态']" :rows="users.map((item) => [item.username, item.displayName, item.avatar || '未设置', item.phoneMasked, item.roleCode, item.status])" /></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>` }) } });
</script>