初始化询价智能体新仓:同步本地工程并排除密钥与 .env。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* 询价后台原型壳(1:1 对齐 admin/AdminLayout)。
|
||||
* 作为 Jeecg LAYOUT 使用:侧栏/顶栏完全按原型,不再套 Jeecg 默认 sider。
|
||||
* 鉴权用户信息来自 Jeecg userStore;退出走正式 logout。
|
||||
*/
|
||||
import { defineComponent, computed, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { useRoute, useRouter, RouterView } from 'vue-router';
|
||||
import { message, ConfigProvider } from 'ant-design-vue';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import ProtoIcon from '/@/views/inquiry/components/ProtoIcon.vue';
|
||||
import AdminModal from '/@/views/inquiry/components/AdminModal.vue';
|
||||
import '/@/views/inquiry/styles/admin-prototype.css';
|
||||
import '/@/views/inquiry/styles/admin-fidelity.css';
|
||||
import '/@/views/inquiry/styles/dashboard-prototype.css';
|
||||
|
||||
type NavIcon = 'dashboard' | 'tickets' | 'staff' | 'template' | 'botReply' | 'account' | 'role';
|
||||
|
||||
type NavItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: NavIcon;
|
||||
badge?: number;
|
||||
};
|
||||
|
||||
type NavGroup = {
|
||||
title: string;
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
/** 与原型侧栏一致;报价模板页保留(mock),暂不接主账接口 */
|
||||
const navGroups: NavGroup[] = [
|
||||
{
|
||||
title: '运营分析',
|
||||
items: [{ key: '/inquiry/dashboard', label: '分析看板', icon: 'dashboard' }],
|
||||
},
|
||||
{
|
||||
title: '业务管理',
|
||||
items: [{ key: '/inquiry/work-order', label: '询价工单', icon: 'tickets' }],
|
||||
},
|
||||
{
|
||||
title: '基础配置',
|
||||
items: [
|
||||
{ key: '/inquiry/staff-role', label: '员工管理', icon: 'staff' },
|
||||
{ key: '/inquiry/template', label: '报价模板', icon: 'template' },
|
||||
{ key: '/inquiry/bot-reply', label: '机器人回复语', icon: 'botReply' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '系统配置',
|
||||
items: [
|
||||
{ key: '/inquiry-sys/user', label: '账号管理', icon: 'account' },
|
||||
{ key: '/inquiry-sys/role', label: '角色管理', icon: 'role' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const themeToken = {
|
||||
colorPrimary: '#155eef',
|
||||
borderRadius: 8,
|
||||
fontFamily: 'Inter, PingFang SC, Microsoft YaHei, Noto Sans SC, system-ui, sans-serif',
|
||||
fontSize: 11,
|
||||
controlHeight: 34,
|
||||
controlHeightLG: 38,
|
||||
controlHeightSM: 28,
|
||||
colorText: '#172033',
|
||||
colorTextSecondary: '#667085',
|
||||
colorBorder: '#d9e2ef',
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InquiryLayout',
|
||||
components: { RouterView, ProtoIcon, AdminModal, ConfigProvider },
|
||||
setup() {
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userMenuOpen = ref(false);
|
||||
const profileOpen = ref(false);
|
||||
const passwordOpen = ref(false);
|
||||
|
||||
const pageTitle = computed(() => String(route.meta?.title || '询价机器人后台'));
|
||||
const crumb = computed(() => (route.meta?.crumb ? String(route.meta.crumb) : ''));
|
||||
|
||||
const username = computed(() => userStore.getUserInfo?.username || '');
|
||||
const displayName = computed(
|
||||
() => userStore.getUserInfo?.realname || userStore.getUserInfo?.username || '用户'
|
||||
);
|
||||
const avatarLetter = computed(() => displayName.value.slice(0, 1));
|
||||
const roleLabel = computed(() => {
|
||||
const u = username.value.trim().toLowerCase();
|
||||
return u === 'admin' ? '超级管理员' : '运营管理员';
|
||||
});
|
||||
|
||||
const profileForm = reactive({ displayName: '', phone: '' });
|
||||
const passwordForm = reactive({ oldPassword: '', newPassword: '', confirm: '' });
|
||||
|
||||
function go(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
function isActive(path: string) {
|
||||
return route.path === path || route.path.startsWith(path + '/');
|
||||
}
|
||||
|
||||
function toggleUserMenu() {
|
||||
userMenuOpen.value = !userMenuOpen.value;
|
||||
}
|
||||
function closeUserMenu() {
|
||||
userMenuOpen.value = false;
|
||||
}
|
||||
|
||||
function onAccountSettings() {
|
||||
closeUserMenu();
|
||||
profileForm.displayName = displayName.value;
|
||||
profileForm.phone = userStore.getUserInfo?.phone || '';
|
||||
profileOpen.value = true;
|
||||
}
|
||||
|
||||
function onChangePassword() {
|
||||
closeUserMenu();
|
||||
passwordForm.oldPassword = '';
|
||||
passwordForm.newPassword = '';
|
||||
passwordForm.confirm = '';
|
||||
passwordOpen.value = true;
|
||||
}
|
||||
|
||||
function saveProfile() {
|
||||
if (!profileForm.displayName.trim()) {
|
||||
message.warning('请填写显示姓名');
|
||||
return;
|
||||
}
|
||||
message.success('账户设置已保存(本地展示;正式改密请走系统用户接口)');
|
||||
profileOpen.value = false;
|
||||
}
|
||||
|
||||
function savePassword() {
|
||||
if (!passwordForm.oldPassword.trim() || !passwordForm.newPassword.trim()) {
|
||||
message.warning('请填写完整密码');
|
||||
return;
|
||||
}
|
||||
if (passwordForm.newPassword.length < 8) {
|
||||
message.warning('新密码至少 8 位');
|
||||
return;
|
||||
}
|
||||
if (passwordForm.newPassword !== passwordForm.confirm) {
|
||||
message.warning('两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
message.success('请在系统「账号管理」中完成正式改密');
|
||||
passwordOpen.value = false;
|
||||
}
|
||||
|
||||
function onRefreshCache() {
|
||||
closeUserMenu();
|
||||
message.success('已刷新本地缓存');
|
||||
}
|
||||
|
||||
async function onLogout() {
|
||||
closeUserMenu();
|
||||
await userStore.logout(true);
|
||||
}
|
||||
|
||||
function onDocClick(e: MouseEvent) {
|
||||
const t = e.target as HTMLElement;
|
||||
if (!t.closest('.admin-user-menu')) closeUserMenu();
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('click', onDocClick));
|
||||
onUnmounted(() => document.removeEventListener('click', onDocClick));
|
||||
|
||||
return {
|
||||
navGroups,
|
||||
themeToken,
|
||||
route,
|
||||
pageTitle,
|
||||
crumb,
|
||||
username,
|
||||
displayName,
|
||||
avatarLetter,
|
||||
roleLabel,
|
||||
userMenuOpen,
|
||||
profileOpen,
|
||||
passwordOpen,
|
||||
profileForm,
|
||||
passwordForm,
|
||||
go,
|
||||
isActive,
|
||||
toggleUserMenu,
|
||||
onAccountSettings,
|
||||
onChangePassword,
|
||||
saveProfile,
|
||||
savePassword,
|
||||
onRefreshCache,
|
||||
onLogout,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :theme="{ token: themeToken }">
|
||||
<div class="admin-shell">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="admin-brand">
|
||||
<div class="admin-brand-icon">
|
||||
<ProtoIcon name="bot" :size="16" />
|
||||
</div>
|
||||
<div class="admin-brand-title">询价机器人</div>
|
||||
</div>
|
||||
<nav class="admin-nav">
|
||||
<template v-for="group in navGroups" :key="group.title">
|
||||
<span class="admin-nav-group-title">{{ group.title }}</span>
|
||||
<button
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="admin-nav-link"
|
||||
:class="{ active: isActive(item.key) }"
|
||||
@click="go(item.key)"
|
||||
>
|
||||
<ProtoIcon class="admin-nav-icon" :name="item.icon" :size="15" />
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.badge != null" class="admin-nav-badge">{{ item.badge }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="main-stage">
|
||||
<header class="admin-topbar">
|
||||
<div class="admin-topbar-title">
|
||||
<div v-if="crumb" class="crumb" v-html="crumb" />
|
||||
<h1>{{ pageTitle }}</h1>
|
||||
</div>
|
||||
<div class="admin-topbar-actions">
|
||||
<div id="page-topbar-actions" class="page-topbar-actions-slot" />
|
||||
<div class="admin-user-menu" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="admin-user-trigger"
|
||||
:class="{ 'is-open': userMenuOpen }"
|
||||
:aria-expanded="userMenuOpen"
|
||||
aria-haspopup="menu"
|
||||
@click="toggleUserMenu"
|
||||
>
|
||||
<span class="admin-user-avatar">{{ avatarLetter }}</span>
|
||||
<span class="admin-user-name">{{ displayName }}</span>
|
||||
<ProtoIcon name="settings" :size="14" class="admin-user-gear" />
|
||||
</button>
|
||||
<div v-show="userMenuOpen" class="admin-user-dropdown" role="menu">
|
||||
<button type="button" class="admin-user-item" role="menuitem" @click="onAccountSettings">
|
||||
<ProtoIcon name="settings" :size="14" />
|
||||
<span>账户设置</span>
|
||||
</button>
|
||||
<button type="button" class="admin-user-item" role="menuitem" @click="onChangePassword">
|
||||
<ProtoIcon name="pencil" :size="14" />
|
||||
<span>密码修改</span>
|
||||
</button>
|
||||
<button type="button" class="admin-user-item" role="menuitem" @click="onRefreshCache">
|
||||
<ProtoIcon name="refresh" :size="14" />
|
||||
<span>刷新缓存</span>
|
||||
</button>
|
||||
<button type="button" class="admin-user-item is-danger" role="menuitem" @click="onLogout">
|
||||
<ProtoIcon name="power" :size="14" />
|
||||
<span>退出系统</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section class="admin-content">
|
||||
<RouterView />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<AdminModal
|
||||
v-model:open="profileOpen"
|
||||
title="账户设置"
|
||||
:subtitle="`登录账号:${username || '—'}`"
|
||||
size="sm"
|
||||
ok-text="保存"
|
||||
@ok="saveProfile"
|
||||
>
|
||||
<div class="form-row">
|
||||
<label class="lbl">登录账号</label>
|
||||
<input class="field field-mono is-readonly" type="text" :value="username || ''" readonly />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">显示姓名 <span class="req">*</span></label>
|
||||
<input v-model="profileForm.displayName" class="field" type="text" placeholder="顶栏与个人资料展示名" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">手机号</label>
|
||||
<input v-model="profileForm.phone" class="field" type="tel" placeholder="选填" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">当前角色</label>
|
||||
<input class="field is-readonly" type="text" :value="roleLabel" readonly />
|
||||
</div>
|
||||
</AdminModal>
|
||||
|
||||
<AdminModal
|
||||
v-model:open="passwordOpen"
|
||||
title="密码修改"
|
||||
:subtitle="`账号:${username || '—'}`"
|
||||
size="sm"
|
||||
ok-text="确认修改"
|
||||
@ok="savePassword"
|
||||
>
|
||||
<div class="form-row">
|
||||
<label class="lbl">当前密码 <span class="req">*</span></label>
|
||||
<input v-model="passwordForm.oldPassword" class="field" type="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">新密码 <span class="req">*</span></label>
|
||||
<input v-model="passwordForm.newPassword" class="field" type="password" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">确认新密码 <span class="req">*</span></label>
|
||||
<input v-model="passwordForm.confirm" class="field" type="password" autocomplete="new-password" />
|
||||
</div>
|
||||
</AdminModal>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-brand-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.admin-nav-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.page-topbar-actions-slot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
min-width: 0;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.admin-user-menu {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-user-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 34px;
|
||||
padding: 0 10px 0 6px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #344054;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.admin-user-trigger:hover,
|
||||
.admin-user-trigger.is-open {
|
||||
background: #f2f4f7;
|
||||
}
|
||||
|
||||
.admin-user-avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: linear-gradient(145deg, #1d4ed8, #0f172a);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-user-name {
|
||||
max-width: 96px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.admin-user-gear {
|
||||
color: #667085;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-user-dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 6px);
|
||||
z-index: 1200;
|
||||
min-width: 168px;
|
||||
padding: 6px;
|
||||
border: 1px solid #e8edf4;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.admin-user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #344054;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.admin-user-item:hover {
|
||||
background: #f2f4f7;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.admin-user-item.is-danger {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.admin-user-item.is-danger:hover {
|
||||
background: #fef3f2;
|
||||
color: #912018;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user