适配气站工作人员与用户受保护头像

This commit is contained in:
czl231
2026-08-18 20:28:01 +08:00
parent 084eccc3ec
commit fd2a2b38e3
11 changed files with 228 additions and 49 deletions

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;