20260816220132

This commit is contained in:
oneao committed 2026-08-16 22:01:32 +08:00
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>
+283
View File
@@ -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>
+235
View File
@@ -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>