style: 优化个人中心和偏好设置弹窗样式,重构工作入口为精致分屏布局并移除首字徽标
This commit is contained in:
@@ -1,91 +1,73 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="profile-layout">
|
||||
<aside class="profile-aside">
|
||||
<div class="avatar-panel">
|
||||
<el-avatar :size="88" :src="avatarPreview" :alt="form.full_name || form.email" class="profile-avatar">
|
||||
{{ profileInitial }}
|
||||
</el-avatar>
|
||||
<div class="avatar-meta">
|
||||
<div class="avatar-name">{{ form.full_name || TEXT.modules.profile.title }}</div>
|
||||
<div class="avatar-email">{{ form.email }}</div>
|
||||
</div>
|
||||
<el-button :icon="Upload" class="upload-button avatar-uploader" @click="selectAndUploadAvatar">
|
||||
{{ TEXT.modules.profile.uploadAvatar }}
|
||||
</el-button>
|
||||
<div class="profile-layout">
|
||||
<aside class="profile-aside">
|
||||
<div class="avatar-panel">
|
||||
<el-avatar :size="88" :src="avatarPreview" :alt="form.full_name || form.email" class="profile-avatar">
|
||||
{{ profileInitial }}
|
||||
</el-avatar>
|
||||
<div class="avatar-meta">
|
||||
<div class="avatar-name">{{ form.full_name || TEXT.modules.profile.title }}</div>
|
||||
<div class="avatar-email">{{ form.email }}</div>
|
||||
</div>
|
||||
</aside>
|
||||
<el-button :icon="Upload" class="upload-button avatar-uploader" @click="selectAndUploadAvatar">
|
||||
{{ TEXT.modules.profile.uploadAvatar }}
|
||||
</el-button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="profile-main">
|
||||
<header class="profile-header">
|
||||
<div>
|
||||
<h3 class="title">{{ TEXT.modules.profile.title }}</h3>
|
||||
<main class="profile-main">
|
||||
<header class="profile-header">
|
||||
<div>
|
||||
<h3 class="title">{{ TEXT.modules.profile.title }}</h3>
|
||||
</div>
|
||||
<el-button class="dialog-close" text :icon="Close" aria-label="关闭个人中心" @click="emit('close-request')" />
|
||||
</header>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="112px" class="form" autocomplete="off">
|
||||
<section class="form-section">
|
||||
<div class="section-heading">
|
||||
<span class="section-kicker">Account</span>
|
||||
<h4>基本信息</h4>
|
||||
</div>
|
||||
<el-button class="dialog-close" text :icon="Close" aria-label="关闭个人中心" @click="emit('close-request')" />
|
||||
</header>
|
||||
<el-form-item :label="TEXT.common.fields.email">
|
||||
<el-input v-model="form.email" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.name" prop="full_name">
|
||||
<el-input v-model="form.full_name" autocomplete="off" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.clinicalDepartment" prop="clinical_department">
|
||||
<el-input v-model="form.clinical_department" autocomplete="off" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.clinicalDepartment" />
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="112px" class="form" autocomplete="off">
|
||||
<section class="form-section">
|
||||
<div class="section-heading">
|
||||
<span class="section-kicker">Account</span>
|
||||
<h4>基本信息</h4>
|
||||
</div>
|
||||
<el-form-item :label="TEXT.common.fields.email">
|
||||
<el-input v-model="form.email" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.name" prop="full_name">
|
||||
<el-input v-model="form.full_name" autocomplete="off" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.clinicalDepartment" prop="clinical_department">
|
||||
<el-input v-model="form.clinical_department" autocomplete="off" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.clinicalDepartment" />
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<section class="form-section form-section--password">
|
||||
<div class="section-heading">
|
||||
<span class="section-kicker">Security</span>
|
||||
<h4>修改密码</h4>
|
||||
</div>
|
||||
<el-form-item :label="TEXT.modules.profile.currentPassword" prop="current_password">
|
||||
<el-input
|
||||
v-model="form.current_password"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
name="ctms-profile-current-password"
|
||||
:placeholder="TEXT.modules.profile.currentPasswordHint"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.profile.newPassword" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="TEXT.modules.profile.newPasswordHint" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.profile.confirmPassword" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" show-password :placeholder="TEXT.modules.profile.confirmPasswordHint" />
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<section class="form-section form-section--diagnostics">
|
||||
<div class="section-heading">
|
||||
<span class="section-kicker">Diagnostics</span>
|
||||
<h4>客户端诊断</h4>
|
||||
</div>
|
||||
<div class="diagnostics-panel">
|
||||
<dl>
|
||||
<template v-for="row in clientDiagnosticRows" :key="row.label">
|
||||
<dt>{{ row.label }}</dt>
|
||||
<dd>{{ row.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<el-button size="small" :icon="DocumentCopy" @click="copyClientDiagnostics">复制诊断信息</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="actions">
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<section class="form-section form-section--password">
|
||||
<div class="section-heading">
|
||||
<span class="section-kicker">Security</span>
|
||||
<h4>修改密码</h4>
|
||||
</div>
|
||||
</el-form>
|
||||
</main>
|
||||
</div>
|
||||
<el-form-item :label="TEXT.modules.profile.currentPassword" prop="current_password">
|
||||
<el-input
|
||||
v-model="form.current_password"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
name="ctms-profile-current-password"
|
||||
:placeholder="TEXT.modules.profile.currentPasswordHint"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.profile.newPassword" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="TEXT.modules.profile.newPasswordHint" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.profile.confirmPassword" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" show-password :placeholder="TEXT.modules.profile.confirmPasswordHint" />
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<div class="actions">
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -93,12 +75,11 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Close, DocumentCopy, Upload } from "@element-plus/icons-vue";
|
||||
import { Close, Upload } from "@element-plus/icons-vue";
|
||||
import { updateProfile, fetchMe, uploadAvatar } from "../api/auth";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { TEXT, requiredMessage } from "../locales";
|
||||
import { pickFilesWithFeedback } from "../utils/fileTaskFeedback";
|
||||
import { clientRuntime, getAppMetadata, getDesktopServerUrl } from "../runtime";
|
||||
|
||||
const emit = defineEmits<{
|
||||
"close-request": [];
|
||||
@@ -123,37 +104,12 @@ const savedProfile = ref({
|
||||
const avatarPreview = ref<string | undefined>();
|
||||
const profileInitial = computed(() => (form.full_name?.charAt(0) || form.email?.charAt(0) || "?").toUpperCase());
|
||||
const avatarAcceptedTypes = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
||||
const clientMetadata = getAppMetadata();
|
||||
const hasUnsavedChanges = computed(
|
||||
() =>
|
||||
form.full_name !== savedProfile.value.full_name ||
|
||||
form.clinical_department !== savedProfile.value.clinical_department ||
|
||||
Boolean(form.current_password || form.password || form.confirmPassword)
|
||||
);
|
||||
const clientDiagnosticRows = computed(() => {
|
||||
const capabilities = clientRuntime.capabilities();
|
||||
return [
|
||||
{ label: "客户端", value: `${clientMetadata.clientType} ${clientMetadata.version}` },
|
||||
{ label: "平台", value: clientMetadata.platform },
|
||||
{ label: "构建通道", value: clientMetadata.channel },
|
||||
{ label: "提交", value: clientMetadata.commit },
|
||||
{
|
||||
label: "服务器",
|
||||
value: clientMetadata.clientType === "desktop" ? getDesktopServerUrl() || "未配置" : clientRuntime.apiBaseUrl(),
|
||||
},
|
||||
{
|
||||
label: "能力",
|
||||
value: [
|
||||
capabilities.serverConfiguration ? "服务器配置" : "固定入口",
|
||||
capabilities.secureSessionStorage ? "安全会话" : "浏览器会话",
|
||||
capabilities.nativeFiles ? "原生文件" : "浏览器文件",
|
||||
capabilities.systemNotifications ? "系统通知" : "无系统通知",
|
||||
capabilities.automaticUpdates ? "自动更新" : "无自动更新",
|
||||
].join(" / "),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
full_name: [{ required: true, message: requiredMessage(TEXT.common.fields.name), trigger: "blur" }],
|
||||
clinical_department: [{ required: true, message: requiredMessage(TEXT.common.fields.clinicalDepartment), trigger: "blur" }],
|
||||
@@ -261,16 +217,6 @@ const selectAndUploadAvatar = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const copyClientDiagnostics = async () => {
|
||||
const text = clientDiagnosticRows.value.map((row) => `${row.label}: ${row.value}`).join("\n");
|
||||
if (!navigator.clipboard?.writeText) {
|
||||
ElMessage.warning("当前环境无法访问剪贴板");
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(text);
|
||||
ElMessage.success("诊断信息已复制");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadProfile();
|
||||
});
|
||||
@@ -279,15 +225,16 @@ watch(hasUnsavedChanges, (dirty) => emit("dirty-change", dirty), { immediate: tr
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.profile-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
height: min(720px, calc(100vh - 64px));
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.26);
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.profile-aside {
|
||||
@@ -296,6 +243,8 @@ watch(hasUnsavedChanges, (dirty) => emit("dirty-change", dirty), { immediate: tr
|
||||
overflow: hidden;
|
||||
border-right: 1px solid #e5ebf2;
|
||||
background: linear-gradient(180deg, #f8fbfd 0%, #f1f5f9 100%);
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.avatar-panel {
|
||||
@@ -351,8 +300,11 @@ watch(hasUnsavedChanges, (dirty) => emit("dirty-change", dirty), { immediate: tr
|
||||
.profile-main {
|
||||
min-height: 0;
|
||||
padding: 42px 48px 36px;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
background: #fff;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
@@ -392,12 +344,6 @@ watch(hasUnsavedChanges, (dirty) => emit("dirty-change", dirty), { immediate: tr
|
||||
border-top: 1px solid #e5ebf2;
|
||||
}
|
||||
|
||||
.form-section--diagnostics {
|
||||
margin-top: 26px;
|
||||
padding-top: 28px;
|
||||
border-top: 1px solid #e5ebf2;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
margin-bottom: 18px;
|
||||
padding-left: 112px;
|
||||
@@ -440,42 +386,6 @@ watch(hasUnsavedChanges, (dirty) => emit("dirty-change", dirty), { immediate: tr
|
||||
box-shadow: 0 0 0 1px #3f8f6b inset, 0 0 0 3px rgba(63, 143, 107, 0.12);
|
||||
}
|
||||
|
||||
.diagnostics-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
margin-left: 112px;
|
||||
padding: 16px;
|
||||
border: 1px solid #dfe6ee;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.diagnostics-panel dl {
|
||||
display: grid;
|
||||
grid-template-columns: 76px minmax(0, 1fr);
|
||||
gap: 8px 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.diagnostics-panel dt {
|
||||
color: #7f92ad;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.diagnostics-panel dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: #1f2a3d;
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.diagnostics-panel :deep(.el-button) {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 28px;
|
||||
padding-left: 112px;
|
||||
@@ -518,10 +428,6 @@ watch(hasUnsavedChanges, (dirty) => emit("dirty-change", dirty), { immediate: tr
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.diagnostics-panel {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user