refactor: 标准化项目结构 frontend/ + backend/ + SQLite + run.sh

- 目录重构: app/ -> frontend/,与 backend/ 并列
- 数据库: 默认 SQLite(零配置),修复 DSN() bug
- run.sh: 一键启动前后端,支持 start/stop/restart/status
- Makefile: install/dev/build/lint/clean 快捷命令
- vite.config.ts: 配置 dev server 端口 5173 + API 代理到 8080
- config.yaml: SQLite 默认配置,开箱即用
- docs/: 完整开发文档(项目结构/API/数据库/规范/部署)
- 修复: GORM foreign key 关联声明
- 修复: go get gorm.io/driver/sqlite 依赖
This commit is contained in:
attacker committed 2026-06-13 23:34:23 +08:00
1 parent 7d6674bddd
commit 97c4207ff8
132 files changed
+1572 -34

No files matched your search

+159
View File
@@ -0,0 +1,159 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { User } from '@/types';
// 主题状态
interface ThemeState {
sidebarCollapsed: boolean;
toggleSidebar: () => void;
}
export const useThemeStore = create<ThemeState>((set) => ({
sidebarCollapsed: false,
toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
}));
// 用户状态
interface UserState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
setUser: (user: User | null) => void;
setToken: (token: string | null) => void;
login: (user: User, token: string) => void;
logout: () => void;
}
export const useUserStore = create<UserState>()(
persist(
(set) => ({
user: null,
token: null,
isAuthenticated: false,
setUser: (user) => set({ user }),
setToken: (token) => set({ token }),
login: (user, token) => set({ user, token, isAuthenticated: true }),
logout: () => set({ user: null, token: null, isAuthenticated: false }),
}),
{
name: 'user-storage',
}
)
);
// 菜单状态
interface MenuState {
activeMenu: string;
openMenus: string[];
setActiveMenu: (menu: string) => void;
toggleMenu: (menu: string) => void;
}
export const useMenuStore = create<MenuState>((set) => ({
activeMenu: 'dashboard',
openMenus: [],
setActiveMenu: (menu) => set({ activeMenu: menu }),
toggleMenu: (menu) =>
set((state) => ({
openMenus: state.openMenus.includes(menu)
? state.openMenus.filter((m) => m !== menu)
: [...state.openMenus, menu],
})),
}));
// 标签页状态
interface TabItem {
id: string;
title: string;
path: string;
closable?: boolean;
}
interface TabState {
tabs: TabItem[];
activeTab: string;
addTab: (tab: TabItem) => void;
removeTab: (tabId: string) => void;
setActiveTab: (tabId: string) => void;
}
export const useTabStore = create<TabState>((set) => ({
tabs: [{ id: 'dashboard', title: '仪表盘', path: '/', closable: false }],
activeTab: 'dashboard',
addTab: (tab) =>
set((state) => {
const exists = state.tabs.find((t) => t.id === tab.id);
if (exists) {
return { activeTab: tab.id };
}
return { tabs: [...state.tabs, tab], activeTab: tab.id };
}),
removeTab: (tabId) =>
set((state) => {
const newTabs = state.tabs.filter((t) => t.id !== tabId);
if (state.activeTab === tabId && newTabs.length > 0) {
return { tabs: newTabs, activeTab: newTabs[newTabs.length - 1].id };
}
return { tabs: newTabs };
}),
setActiveTab: (tabId) => set({ activeTab: tabId }),
}));
// 通知状态
interface Notification {
id: string;
type: 'info' | 'success' | 'warning' | 'error';
title: string;
message: string;
timestamp: number;
read: boolean;
}
interface NotificationState {
notifications: Notification[];
unreadCount: number;
addNotification: (notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => void;
markAsRead: (id: string) => void;
markAllAsRead: () => void;
removeNotification: (id: string) => void;
}
export const useNotificationStore = create<NotificationState>((set) => ({
notifications: [],
unreadCount: 0,
addNotification: (notification) =>
set((state) => ({
notifications: [
{
...notification,
id: Date.now().toString(),
timestamp: Date.now(),
read: false,
},
...state.notifications,
].slice(0, 100),
unreadCount: state.unreadCount + 1,
})),
markAsRead: (id) =>
set((state) => {
const notification = state.notifications.find((n) => n.id === id);
if (notification && !notification.read) {
return {
notifications: state.notifications.map((n) =>
n.id === id ? { ...n, read: true } : n
),
unreadCount: Math.max(0, state.unreadCount - 1),
};
}
return state;
}),
markAllAsRead: () =>
set((state) => ({
notifications: state.notifications.map((n) => ({ ...n, read: true })),
unreadCount: 0,
})),
removeNotification: (id) =>
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
})),
}));