20260816220132
This commit is contained in:
1 parent
b6f9a4c912
commit
72993ecdcd
179 files changed
+20050
-633
No files matched your search
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import { LayoutDashboard } from '@lucide/vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<div class="dashboard-icon" aria-hidden="true">
|
||||
<LayoutDashboard />
|
||||
</div>
|
||||
<h1 class="dashboard-title">工作台</h1>
|
||||
<p class="dashboard-desc">布局壳与业务页面正在搭建中,敬请期待。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
color: var(--fms-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dashboard-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--fms-primary) 10%, var(--fms-card));
|
||||
color: var(--fms-primary);
|
||||
|
||||
:deep(svg) {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-desc {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import ThemeToggle from '@/components/ui/ThemeToggle.vue'
|
||||
import ColorThemePicker from '@/components/ui/ColorThemePicker.vue'
|
||||
import ButtonDemo from '@/components/ui/demo/button.vue'
|
||||
import ButtonGroupDemo from '@/components/ui/demo/button-group.vue'
|
||||
import TabsDemo from '@/components/ui/demo/tabs.vue'
|
||||
import PaginationDemo from '@/components/ui/demo/pagination.vue'
|
||||
import InputDemo from '@/components/ui/demo/input.vue'
|
||||
import CheckboxDemo from '@/components/ui/demo/checkbox.vue'
|
||||
import RadioDemo from '@/components/ui/demo/radio.vue'
|
||||
import SwitchDemo from '@/components/ui/demo/switch.vue'
|
||||
import SelectDemo from '@/components/ui/demo/select.vue'
|
||||
import DateDemo from '@/components/ui/demo/date.vue'
|
||||
import FormDemo from '@/components/ui/demo/form.vue'
|
||||
import PopoverDemo from '@/components/ui/demo/popover.vue'
|
||||
import TooltipDemo from '@/components/ui/demo/tooltip.vue'
|
||||
import MessageDemo from '@/components/ui/demo/message.vue'
|
||||
import NotificationDemo from '@/components/ui/demo/notification.vue'
|
||||
import ModalDemo from '@/components/ui/demo/modal.vue'
|
||||
import DrawerDemo from '@/components/ui/demo/drawer.vue'
|
||||
import SpinDemo from '@/components/ui/demo/spin.vue'
|
||||
import GridDemo from '@/components/ui/demo/grid.vue'
|
||||
import SplitterDemo from '@/components/ui/demo/splitter.vue'
|
||||
import DropdownDemo from '@/components/ui/demo/dropdown.vue'
|
||||
import TagDemo from '@/components/ui/demo/tag.vue'
|
||||
import TreeDemo from '@/components/ui/demo/tree.vue'
|
||||
|
||||
/* ---------- 导航分类 ---------- */
|
||||
const activeKey = ref('button')
|
||||
|
||||
const categories = [
|
||||
{
|
||||
label: '通用',
|
||||
items: [
|
||||
{ key: 'button', label: 'Button 按钮' },
|
||||
{ key: 'button-group', label: 'ButtonGroup 按钮组' },
|
||||
{ key: 'tabs', label: 'Tabs 标签页' },
|
||||
{ key: 'pagination', label: 'Pagination 分页' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据录入',
|
||||
items: [
|
||||
{ key: 'input', label: 'Input 输入框' },
|
||||
{ key: 'checkbox', label: 'Checkbox 复选框' },
|
||||
{ key: 'radio', label: 'Radio 单选框' },
|
||||
{ key: 'switch', label: 'Switch 开关' },
|
||||
{ key: 'select', label: 'Select 下拉选择' },
|
||||
{ key: 'date', label: 'DatePicker 日期选择' },
|
||||
{ key: 'form', label: 'Form 表单' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据展示',
|
||||
items: [
|
||||
{ key: 'splitter', label: 'Splitter 分割面板' },
|
||||
{ key: 'dropdown', label: 'Dropdown 下拉菜单' },
|
||||
{ key: 'grid', label: 'Grid 栅格' },
|
||||
{ key: 'tag', label: 'Tag 标签' },
|
||||
{ key: 'tree', label: 'Tree 树形' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '反馈',
|
||||
items: [
|
||||
{ key: 'popover', label: 'Popover 气泡卡片' },
|
||||
{ key: 'tooltip', label: 'Tooltip 文字提示' },
|
||||
{ key: 'message', label: 'Message 轻提示' },
|
||||
{ key: 'notification', label: 'Notification 通知' },
|
||||
{ key: 'modal', label: 'Modal 模态框' },
|
||||
{ key: 'drawer', label: 'Drawer 抽屉' },
|
||||
{ key: 'spin', label: 'Spin 加载中' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// key -> demo 组件映射,页面只做按需切换渲染
|
||||
const demoMap = {
|
||||
button: ButtonDemo,
|
||||
'button-group': ButtonGroupDemo,
|
||||
tabs: TabsDemo,
|
||||
pagination: PaginationDemo,
|
||||
input: InputDemo,
|
||||
checkbox: CheckboxDemo,
|
||||
radio: RadioDemo,
|
||||
switch: SwitchDemo,
|
||||
select: SelectDemo,
|
||||
date: DateDemo,
|
||||
form: FormDemo,
|
||||
popover: PopoverDemo,
|
||||
tooltip: TooltipDemo,
|
||||
message: MessageDemo,
|
||||
notification: NotificationDemo,
|
||||
modal: ModalDemo,
|
||||
drawer: DrawerDemo,
|
||||
spin: SpinDemo,
|
||||
grid: GridDemo,
|
||||
splitter: SplitterDemo,
|
||||
dropdown: DropdownDemo,
|
||||
tag: TagDemo,
|
||||
tree: TreeDemo,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="app-shell">
|
||||
<header class="app-header">
|
||||
<div class="brand">
|
||||
<p class="eyebrow">FMS UI</p>
|
||||
<h1>基础组件</h1>
|
||||
</div>
|
||||
|
||||
<div class="theme-controls">
|
||||
<ColorThemePicker />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="app-body">
|
||||
<aside class="app-sidebar">
|
||||
<nav>
|
||||
<div v-for="cat in categories" :key="cat.label" class="nav-group">
|
||||
<p class="nav-group-title">{{ cat.label }}</p>
|
||||
<button
|
||||
v-for="item in cat.items"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item-active': activeKey === item.key }"
|
||||
@click="activeKey = item.key"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="app-content">
|
||||
<component :is="demoMap[activeKey]" :key="activeKey" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
background: var(--fms-background);
|
||||
color: var(--fms-text);
|
||||
transition:
|
||||
background-color 200ms ease,
|
||||
color 200ms ease;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
height: 64px;
|
||||
padding: 0 32px;
|
||||
background: var(--fms-card);
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
margin: 2px 0 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.theme-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-body {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
position: sticky;
|
||||
top: 64px;
|
||||
flex-shrink: 0;
|
||||
width: 200px;
|
||||
padding: 20px 12px 24px 16px;
|
||||
}
|
||||
|
||||
.nav-group + .nav-group {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.nav-group-title {
|
||||
margin: 0 0 8px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: transparent;
|
||||
color: var(--fms-text);
|
||||
font-size: 14px;
|
||||
line-height: var(--fms-line-height);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 150ms ease,
|
||||
color 150ms ease;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--fms-secondary-hover);
|
||||
}
|
||||
|
||||
.nav-item-active {
|
||||
background: var(--fms-primary);
|
||||
color: var(--fms-primary-text);
|
||||
}
|
||||
|
||||
.nav-item:focus-visible {
|
||||
outline: 2px solid var(--fms-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-header {
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.theme-controls {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.app-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
position: static;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.app-content {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.app-shell,
|
||||
.nav-item {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,235 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Building2, GalleryVerticalEnd, KeyRound, User } from '@lucide/vue'
|
||||
import Form from '@/components/ui/form/form.vue'
|
||||
import FormItem from '@/components/ui/form/form-item.vue'
|
||||
import Input from '@/components/ui/input/input.vue'
|
||||
import Button from '@/components/ui/button/button.vue'
|
||||
import Message from '@/components/ui/message/message-manager.js'
|
||||
import { loginApi } from '@/services/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
/* ---------- 登录提示(携带 reason 跳转时展示) ---------- */
|
||||
const loginHint = computed(() => {
|
||||
const reason = String(route.query.reason || '')
|
||||
if (reason === 'session-replaced') return '账号已在其他设备登录,请重新登录'
|
||||
if (reason === 'expired') return '登录已过期,请重新登录'
|
||||
return ''
|
||||
})
|
||||
|
||||
/* ---------- 表单 ---------- */
|
||||
const formModel = reactive({
|
||||
orgId: '',
|
||||
userId: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
orgId: [{ required: true, message: '请输入机构码' }],
|
||||
userId: [{ required: true, message: '请输入账号' }],
|
||||
}
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
formModel.orgId = authStore.orgId || ''
|
||||
formModel.userId = authStore.user?.account || ''
|
||||
formModel.password = ''
|
||||
// 聚焦机构码输入框(id 落在 Input 根元素上,需定位内部 input)
|
||||
document.querySelector('#orgId input')?.focus()
|
||||
})
|
||||
|
||||
/* ---------- 提交 ---------- */
|
||||
async function onSubmit(values) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await loginApi(values.orgId, values.userId, values.password ?? '')
|
||||
authStore.setSession(
|
||||
{ token: res.data.token, user: { ...res.data.user, account: values.userId } },
|
||||
res.data.orgid ?? values.orgId,
|
||||
)
|
||||
Message.success('登录成功')
|
||||
router.replace(String(route.query.redirect || '/dashboard'))
|
||||
} catch (e) {
|
||||
Message.error(e.message || '登录失败,请稍后重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onFailed() {
|
||||
Message.warning('请填写完整的登录信息')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<!-- 品牌区 -->
|
||||
<div class="login-brand">
|
||||
<div class="login-logo" aria-hidden="true">
|
||||
<GalleryVerticalEnd />
|
||||
</div>
|
||||
<h1 class="login-title">登录 FMS</h1>
|
||||
<p class="login-subtitle">企业管理系统</p>
|
||||
</div>
|
||||
|
||||
<!-- 会话提示条 -->
|
||||
<div v-if="loginHint" class="login-hint" role="alert">
|
||||
{{ loginHint }}
|
||||
</div>
|
||||
|
||||
<!-- 表单 -->
|
||||
<Form
|
||||
class="login-form"
|
||||
:model="formModel"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
@submit="onSubmit"
|
||||
@submit-failed="onFailed"
|
||||
>
|
||||
<FormItem name="orgId" label="机构码">
|
||||
<Input id="orgId" v-model="formModel.orgId" placeholder="请输入机构码" :disabled="submitting">
|
||||
<template #prefix><Building2 /></template>
|
||||
</Input>
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="userId" label="账号">
|
||||
<Input id="userId" v-model="formModel.userId" placeholder="请输入账号" :disabled="submitting">
|
||||
<template #prefix><User /></template>
|
||||
</Input>
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="password" label="密码">
|
||||
<Input
|
||||
id="password"
|
||||
v-model="formModel.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
:disabled="submitting"
|
||||
>
|
||||
<template #prefix><KeyRound /></template>
|
||||
</Input>
|
||||
</FormItem>
|
||||
|
||||
<Button type="default" block :loading="submitting" html-type="submit" class="login-submit">
|
||||
登录
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
<p class="login-footer">© 2026 FMS · 企业管理系统</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100svh;
|
||||
padding: 24px;
|
||||
background: var(--fms-background);
|
||||
color: var(--fms-text);
|
||||
transition:
|
||||
background-color 200ms ease,
|
||||
color 200ms ease;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 40px 36px 28px;
|
||||
background: var(--fms-card);
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
|
||||
transition:
|
||||
background-color 200ms ease,
|
||||
border-color 200ms ease;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 12px;
|
||||
background: var(--fms-primary);
|
||||
color: var(--fms-primary-text);
|
||||
|
||||
:deep(svg) {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: color-mix(in srgb, var(--fms-warning) 12%, var(--fms-card));
|
||||
border: 1px solid color-mix(in srgb, var(--fms-warning) 30%, transparent);
|
||||
color: var(--fms-warning);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-submit {
|
||||
margin-top: 8px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin: 24px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--fms-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-card {
|
||||
padding: 32px 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.login-page,
|
||||
.login-card {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user