初始化询价智能体新仓:同步本地工程并排除密钥与 .env。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-13 14:30:44 +08:00
co-authored by Cursor
commit 2a14def882
2621 changed files with 393165 additions and 0 deletions
@@ -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>