新增配送点资料只读卡片
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"type:check": "vue-tsc -p tsconfig.build.json --noEmit --skipLibCheck",
|
||||
"contract:sync": "node scripts/sync-backend-contract.mjs",
|
||||
"contract:check": "node scripts/check-backend-contract.mjs",
|
||||
"profile:check": "node scripts/check-delivery-profile-page.mjs",
|
||||
"audit:platform": "node scripts/check-backend-contract.mjs",
|
||||
"lint": "biome lint .",
|
||||
"lint:fix": "biome lint --write .",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/** 功能描述:静态检查本点资料专用卡片和非 JSON 错误保护。版本:v1.0.0。 */
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const root = resolve(import.meta.dirname, '..');
|
||||
const profilePage = readFileSync(resolve(root, 'src/views/shared/DeliveryProfilePage.vue'), 'utf8');
|
||||
const resourcePage = readFileSync(resolve(root, 'src/views/shared/ResourcePage.vue'), 'utf8');
|
||||
const httpClient = readFileSync(resolve(root, 'src/api/http.ts'), 'utf8');
|
||||
|
||||
assert.match(resourcePage, /definition\.name === 'delivery_profile'/, '本点资料未使用专用页面');
|
||||
for (const field of ['配送点编码', '负责人', '配送点地址', '所属气站', '配送点标识']) {
|
||||
assert.ok(profilePage.includes(field), `资料卡缺少字段:${field}`);
|
||||
}
|
||||
assert.doesNotMatch(profilePage, /详情|分页|关键字/, '资料卡不应保留通用列表交互');
|
||||
assert.match(httpClient, /response\.text\(\)/, 'HTTP 客户端未兼容非 JSON 响应');
|
||||
assert.match(httpClient, /HTTP \$\{response\.status\}/, 'HTTP 客户端未提供状态码错误');
|
||||
|
||||
console.log('配送点资料页检查通过:专用只读卡片与非 JSON 错误保护已启用。');
|
||||
@@ -5,6 +5,11 @@ const apiBaseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:12426/
|
||||
|
||||
export type PageResult<T> = { total: number; list: T[] };
|
||||
|
||||
/**
|
||||
* 发送配送点后台请求。
|
||||
* 参数:path 为 API 相对路径,init 为标准 Fetch 请求选项。
|
||||
* 返回值:统一响应中的 details;网络、协议或业务失败时抛出可读错误。
|
||||
*/
|
||||
export async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const token = getToken();
|
||||
let response: Response;
|
||||
@@ -20,7 +25,15 @@ export async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
} catch {
|
||||
throw new Error('无法连接服务器,请确认服务已启动');
|
||||
}
|
||||
const payload = (await response.json()) as { code?: number; message?: string; details?: T };
|
||||
const responseText = await response.text();
|
||||
let payload: { code?: number; message?: string; details?: T };
|
||||
try {
|
||||
payload = responseText ? JSON.parse(responseText) : {};
|
||||
} catch {
|
||||
// 网关、代理和不存在的路由可能返回纯文本,不能把底层 JSON 异常暴露给用户。
|
||||
if (!response.ok) throw new Error(`请求失败(HTTP ${response.status})`);
|
||||
throw new Error('服务器返回了无法识别的数据格式');
|
||||
}
|
||||
if (!response.ok || payload.code !== 0) throw new Error(payload.message || '请求失败');
|
||||
return payload.details as T;
|
||||
}
|
||||
|
||||
169
frontend/delivery_admin/src/views/shared/DeliveryProfilePage.vue
Normal file
169
frontend/delivery_admin/src/views/shared/DeliveryProfilePage.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<!-- 功能描述:展示当前登录管理员所属配送点的只读资料卡。版本:v1.0.0。 -->
|
||||
<template>
|
||||
<a-card class="profile-card" :bordered="false">
|
||||
<template #title>配送点资料</template>
|
||||
<template #extra>
|
||||
<a-button :loading="loading" @click="loadProfile">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-spin :loading="loading" class="profile-loading">
|
||||
<a-result v-if="errorMessage" status="error" title="配送点资料加载失败" :subtitle="errorMessage">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="loadProfile">重新加载</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
<a-empty v-else-if="!profile" description="未找到当前账号关联的配送点,请联系平台管理员" />
|
||||
<template v-else>
|
||||
<div class="profile-heading">
|
||||
<div>
|
||||
<div class="profile-name">{{ profile.name || '未命名配送点' }}</div>
|
||||
<div class="profile-code">配送点编码:{{ displayText(profile.delivery_code) }}</div>
|
||||
</div>
|
||||
<a-tag :color="statusColor(profile.status)" size="large">
|
||||
{{ statusLabel(profile.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
||||
<a-descriptions :column="2" bordered size="large" class="profile-details">
|
||||
<a-descriptions-item label="配送点编码">{{ displayText(profile.delivery_code) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="负责人">{{ displayText(profile.principal) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="配送点地址" :span="2">{{ displayText(profile.address) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="所属气站">
|
||||
<div class="identity-value">
|
||||
<span>{{ displayText(profile.gas_basic_name, '关联气站已失效') }}</span>
|
||||
<IdentityText v-if="profile.gas_basic_identity" :value="profile.gas_basic_identity" />
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="配送点标识">
|
||||
<IdentityText :value="profile.identity" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{ formatTime(profile.created_at) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">{{ formatTime(profile.updated_at) }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { resourceApi } from '@/api/resource';
|
||||
import IdentityText from '@/components/IdentityText.vue';
|
||||
|
||||
/** 配送点资料接口返回的只读展示模型。 */
|
||||
type DeliveryProfile = {
|
||||
id: number;
|
||||
identity: string;
|
||||
delivery_code: string;
|
||||
name: string;
|
||||
principal: string;
|
||||
address: string;
|
||||
gas_basic_identity: string;
|
||||
gas_basic_name: string;
|
||||
status: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const profile = ref<DeliveryProfile>();
|
||||
|
||||
/** 加载 JWT 数据范围内唯一配送点的资料。 */
|
||||
async function loadProfile() {
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
try {
|
||||
const result = await resourceApi.list<DeliveryProfile>('/delivery_profile', 1, 1);
|
||||
profile.value = result.list[0];
|
||||
} catch (error) {
|
||||
profile.value = undefined;
|
||||
errorMessage.value = (error as Error).message;
|
||||
Message.error(errorMessage.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 将空白资料字段转换为明确占位文案。 */
|
||||
function displayText(value: unknown, emptyText = '暂未填写') {
|
||||
const text = String(value ?? '').trim();
|
||||
return text || emptyText;
|
||||
}
|
||||
|
||||
/** 将接口时间转换为本地可读格式。 */
|
||||
function formatTime(value: string) {
|
||||
const time = dayjs(value);
|
||||
return time.isValid() ? time.format('YYYY-MM-DD HH:mm:ss') : '暂无记录';
|
||||
}
|
||||
|
||||
/** 返回通用记录状态的中文名称。 */
|
||||
function statusLabel(status: number) {
|
||||
return { 0: '待审核', 1: '启用', 2: '停用', 3: '已归档', 4: '已冻结' }[status] ?? '未知状态';
|
||||
}
|
||||
|
||||
/** 返回通用记录状态对应的标签颜色。 */
|
||||
function statusColor(status: number) {
|
||||
return { 0: 'orange', 1: 'green', 2: 'red', 3: 'gray', 4: 'purple' }[status] ?? 'gray';
|
||||
}
|
||||
|
||||
onMounted(loadProfile);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-card {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.profile-loading {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.profile-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0 24px;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
color: var(--color-text-1);
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.profile-code {
|
||||
margin-top: 6px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.profile-details {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.identity-value {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-heading {
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,11 @@
|
||||
<!-- 功能描述:按资源类型分发配送点后台页面,配送点资料使用专用只读卡片。版本:v1.1.0。 -->
|
||||
<template>
|
||||
<DeliveryProfilePage
|
||||
v-if="definition.name === 'delivery_profile'"
|
||||
:key="String(route.name)"
|
||||
/>
|
||||
<TreePage
|
||||
v-if="definition.pageKind === 'tree'"
|
||||
v-else-if="definition.pageKind === 'tree'"
|
||||
:key="String(route.name)"
|
||||
:definition="definition"
|
||||
/>
|
||||
@@ -16,6 +21,7 @@ import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getResource } from '@/api/resources';
|
||||
import CrudListPage from './CrudListPage.vue';
|
||||
import DeliveryProfilePage from './DeliveryProfilePage.vue';
|
||||
import TreePage from './TreePage.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
Reference in New Issue
Block a user