适配气站工作人员与用户受保护头像
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 功能:平台总后台头像上传与鉴权读取客户端。
|
||||
* 版本:v1.1.0
|
||||
* 功能:气站管理端头像上传与气站范围内鉴权读取客户端。
|
||||
* 版本:v1.2.0
|
||||
*/
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
const platformApiBaseURL =
|
||||
const gasApiBaseURL =
|
||||
import.meta.env.VITE_API_BASE_URL ||
|
||||
'http://localhost:12426/heqi/platform/v1';
|
||||
'http://localhost:12426/heqi/gas/v1';
|
||||
|
||||
export type AvatarUploadReply = {
|
||||
uri: string;
|
||||
@@ -17,13 +17,13 @@ export type AvatarUploadReply = {
|
||||
|
||||
type ApiEnvelope<T> = { code?: number; message?: string; details?: T };
|
||||
|
||||
/** 生成服务根路径 URL,确保上传请求不会错误拼接平台 API 前缀。 */
|
||||
/** 生成服务根路径 URL,确保上传请求不会错误拼接气站 API 前缀。 */
|
||||
function serviceURL(path: string) {
|
||||
const platformURL = new URL(platformApiBaseURL, window.location.origin);
|
||||
return new URL(path, platformURL.origin).toString();
|
||||
const gasURL = new URL(gasApiBaseURL, window.location.origin);
|
||||
return new URL(path, gasURL.origin).toString();
|
||||
}
|
||||
|
||||
/** 返回与现有平台请求一致的 JWT 请求头。 */
|
||||
/** 返回与现有气站请求一致的 JWT 请求头。 */
|
||||
function authorizationHeaders(): Record<string, string> {
|
||||
const token = getToken();
|
||||
return token ? { Authorization: token } : {};
|
||||
@@ -52,7 +52,7 @@ async function load(
|
||||
signal?: AbortSignal,
|
||||
): Promise<Blob | undefined> {
|
||||
const response = await fetch(
|
||||
`${platformApiBaseURL}${resource}/${encodeURIComponent(identity)}/avatar`,
|
||||
`${gasApiBaseURL}${resource}/${encodeURIComponent(identity)}/avatar`,
|
||||
{ headers: authorizationHeaders(), signal },
|
||||
);
|
||||
if (response.status === 404) return undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!--
|
||||
功能:承载气站管理端全部标准资源的新建、详情和编辑独立页面。
|
||||
版本:v1.3.0
|
||||
版本:v1.4.0
|
||||
-->
|
||||
<template>
|
||||
<div class="record-page">
|
||||
@@ -37,9 +37,11 @@
|
||||
:mode="mode"
|
||||
:title="definition.title"
|
||||
:record="summaryRecord"
|
||||
avatar-url=""
|
||||
:avatar-enabled="false"
|
||||
:can-clear="false"
|
||||
:avatar-url="avatarUrl"
|
||||
:avatar-enabled="hasAvatarField"
|
||||
:can-clear="avatarCanClear"
|
||||
@select-avatar="selectAvatar"
|
||||
@clear-avatar="clearAvatar"
|
||||
/>
|
||||
|
||||
<template v-if="mode === 'detail'">
|
||||
@@ -196,6 +198,7 @@ import ResourceFieldForm from './ResourceFieldForm.vue';
|
||||
import ResourceWalletSummary from './ResourceWalletSummary.vue';
|
||||
import { createResourceRecordNavigation } from './resource-record-navigation';
|
||||
import { loadResourceRecordRelations } from './load-resource-record-relations';
|
||||
import { useResourceAvatar } from './use-resource-avatar';
|
||||
import { useResourceRelationLinkage } from './use-resource-relation-linkage';
|
||||
import { useStaffCredentialOwnerGuard } from './use-staff-credential-owner-guard';
|
||||
import { useUnsavedRecord } from './use-unsaved-record';
|
||||
@@ -253,6 +256,11 @@ const productOwnershipKeys = [
|
||||
];
|
||||
const actionVisible = ref(false);
|
||||
const activeAction = ref<DetailAction>();
|
||||
const avatar = useResourceAvatar();
|
||||
const avatarUrl = avatar.url;
|
||||
const avatarCanClear = avatar.canClear;
|
||||
const selectAvatar = avatar.select;
|
||||
const clearAvatar = avatar.clear;
|
||||
|
||||
/** 新建智能气阀选择归属时清空其他互斥归属,再执行字段原有联动。 */
|
||||
async function changeRelation(field: ResourceField, value: unknown) {
|
||||
@@ -273,7 +281,10 @@ async function changeRelation(field: ResourceField, value: unknown) {
|
||||
const accountSummary = computed(() => usesAccountSummary(definition.value));
|
||||
const accountSummaryVisible = computed(
|
||||
() =>
|
||||
accountSummary.value && mode.value !== 'create',
|
||||
accountSummary.value && (mode.value !== 'create' || hasAvatarField.value),
|
||||
);
|
||||
const hasAvatarField = computed(() =>
|
||||
definition.value.fields.some((field) => field.key === 'avatar'),
|
||||
);
|
||||
const formFields = computed(() =>
|
||||
mode.value === 'detail'
|
||||
@@ -283,11 +294,13 @@ const formFields = computed(() =>
|
||||
mode.value,
|
||||
record.value,
|
||||
context.value,
|
||||
),
|
||||
).filter((field) => field.key !== 'avatar'),
|
||||
);
|
||||
const payloadFields = computed(() =>
|
||||
mode.value === 'edit'
|
||||
? updatePayloadFields(definition.value, record.value, context.value)
|
||||
? updatePayloadFields(definition.value, record.value, context.value).filter(
|
||||
(field) => field.key !== 'avatar',
|
||||
)
|
||||
: formFields.value,
|
||||
);
|
||||
const editableKeySet = computed(
|
||||
@@ -364,7 +377,7 @@ const modeLabel = computed(() =>
|
||||
const errorTitle = computed(() => recordPageErrorTitle(errorStatus.value));
|
||||
|
||||
function snapshot() {
|
||||
return JSON.stringify({ form });
|
||||
return JSON.stringify({ form, avatar: avatar.marker() });
|
||||
}
|
||||
const unsaved = useUnsavedRecord(snapshot, () => mode.value !== 'detail');
|
||||
const { goEdit, viewWallet, goBack, requestBack } =
|
||||
@@ -429,8 +442,8 @@ async function initialize() {
|
||||
fieldOptions,
|
||||
roleOptions,
|
||||
});
|
||||
// 钱包属于附加信息,读取失败时不能阻断主详情或基础表单。
|
||||
await Promise.allSettled([loadWallet()]);
|
||||
// 钱包和头像属于附加信息,读取失败时不能阻断主详情或基础表单。
|
||||
await Promise.allSettled([loadWallet(), loadAvatar()]);
|
||||
unsaved.markInitialized();
|
||||
} catch (error) {
|
||||
const message = (error as Error).message;
|
||||
@@ -463,6 +476,13 @@ async function loadWallet() {
|
||||
).list[0];
|
||||
}
|
||||
|
||||
/** 读取当前气站范围内账户的受保护头像。 */
|
||||
async function loadAvatar() {
|
||||
if (!hasAvatarField.value || mode.value === 'create' || !recordIdentity.value)
|
||||
return;
|
||||
await avatar.load(definition.value.resource, recordIdentity.value);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const validationError = validateResourceRecordForm(
|
||||
form,
|
||||
@@ -485,6 +505,7 @@ async function save() {
|
||||
form,
|
||||
mode.value as 'create' | 'edit',
|
||||
);
|
||||
await avatar.applyToPayload(payload);
|
||||
if (mode.value === 'create') {
|
||||
const created = await resourceApi.create<ResourceRow>(
|
||||
definition.value.resource,
|
||||
@@ -539,7 +560,7 @@ async function reloadDetail() {
|
||||
definition.value.resource,
|
||||
identity.value,
|
||||
);
|
||||
await Promise.allSettled([loadWallet()]);
|
||||
await Promise.allSettled([loadWallet(), loadAvatar()]);
|
||||
}
|
||||
|
||||
async function updateGasStatus(enabled: string | number | boolean) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 功能:为受控账户列表提供头像请求限流、当前页缓存和取消能力。
|
||||
* 版本:v1.0.0
|
||||
* 版本:v1.1.0
|
||||
*/
|
||||
import { avatarApi } from '@/api/avatar';
|
||||
|
||||
const MAX_CONCURRENT_REQUESTS = 6;
|
||||
// 气站 API 暂无受控头像读取接口,头像继续按普通字段展示。
|
||||
const SUPPORTED_RESOURCES = new Set<string>();
|
||||
// 仅启用已具备气站范围鉴权读取接口的真实头像资源。
|
||||
const SUPPORTED_RESOURCES = new Set(['staff_account', 'user_account']);
|
||||
|
||||
type QueueTask = {
|
||||
run: () => void;
|
||||
|
||||
38
frontend/gas_admin/设计验收_工作人员头像适配.md
Normal file
38
frontend/gas_admin/设计验收_工作人员头像适配.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Design QA
|
||||
|
||||
- source visual truth path: `C:/Users/Lenovo/AppData/Local/Temp/codex-clipboard-1e3ee2da-a03c-4ade-af5d-81e88bacdc3e.png`
|
||||
- implementation screenshot path: unavailable
|
||||
- viewport: source image 2560 × 1159 px;实现页需保持同一浏览器窗口与登录态
|
||||
- source and implementation pixel dimensions: source 2560 × 1159 px;implementation unavailable
|
||||
- CSS size and density normalization: blocked because the authenticated implementation could not be captured
|
||||
- state: 5175 工作人员列表,安装人员菜单,单条带头像记录
|
||||
|
||||
## Full-view comparison evidence
|
||||
|
||||
源图显示头像列直接暴露 `/uploads/avatars/...` 存储路径。代码已按 5173 开启现有 `ProtectedAvatarThumbnail` 分支,目标表现为 32 像素圆形头像;但缺少登录后的实现截图,不能完成视觉对照。
|
||||
|
||||
## Focused region comparison evidence
|
||||
|
||||
头像列是唯一变更区域。组件沿用 5173 的圆形尺寸、默认头像、懒加载、失败提示和对象 URL 清理逻辑;仍需同一数据记录的刷新后截图作为最终证据。
|
||||
|
||||
## Findings
|
||||
|
||||
- [P1] 缺少登录态实现截图
|
||||
- Location: `/staff/installers` 头像列
|
||||
- Evidence: 只有修改前源图,没有刷新后的实现图
|
||||
- Impact: 无法确认运行中的后端已重启并返回头像二进制,也无法确认最终列对齐
|
||||
- Fix: 重启后端、刷新页面并捕获同一视口截图
|
||||
|
||||
## Comparison history
|
||||
|
||||
- Iteration 1: 定位为空的受保护资源集合,已加入 `staff_account` 与 `user_account`;自动化构建验证通过,视觉证据待补。
|
||||
|
||||
## Implementation checklist
|
||||
|
||||
- [x] 工作人员列表启用受保护头像缩略图
|
||||
- [x] 用户列表启用受保护头像缩略图
|
||||
- [x] 无头像时回退默认头像
|
||||
- [x] 类型检查、单文件 lint、页面覆盖检查与生产构建
|
||||
- [ ] 登录态同视口截图对照
|
||||
|
||||
final result: blocked
|
||||
Reference in New Issue
Block a user