diff --git a/index.html b/index.html index 2897243..f232ecd 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,16 @@ - - - - - sundynix-micro-ui - - -
- - - + + + + + + Sundynix + + + +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e2a4fd8..97a00ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", @@ -26,9 +27,12 @@ "axios": "^1.15.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "framer-motion": "^12.38.0", "lucide-react": "^1.11.0", "react": "^19.2.5", "react-dom": "^19.2.5", + "react-parallax-tilt": "^1.7.324", "react-router-dom": "^7.14.2", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.4", @@ -925,6 +929,90 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", @@ -3615,6 +3703,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4127,6 +4231,33 @@ "node": ">= 6" } }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4782,6 +4913,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4987,6 +5133,16 @@ "react": "^19.2.5" } }, + "node_modules/react-parallax-tilt": { + "version": "1.7.324", + "resolved": "https://registry.npmjs.org/react-parallax-tilt/-/react-parallax-tilt-1.7.324.tgz", + "integrity": "sha512-iOmPMoObfRwozzf6Dfpp2k5irtjNhng8jgRX9RZFz40bTeuQT5ZRZW7o0u8vhtRqNvWwlEBkl9++qzoW+p6eFg==", + "license": "MIT", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", diff --git a/package.json b/package.json index b313b98..78fe4f3 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", @@ -28,9 +29,12 @@ "axios": "^1.15.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "framer-motion": "^12.38.0", "lucide-react": "^1.11.0", "react": "^19.2.5", "react-dom": "^19.2.5", + "react-parallax-tilt": "^1.7.324", "react-router-dom": "^7.14.2", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.4", diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..81a4108 Binary files /dev/null and b/public/logo.png differ diff --git a/src/App.tsx b/src/App.tsx index b361a4a..7a8c7f4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { useAuthStore } from '@/store/auth' +import { useAppStore } from '@/store/app' import AdminLayout from '@/layouts/AdminLayout' import LoginPage from '@/pages/LoginPage' import ErrorBoundary from '@/components/ErrorBoundary' @@ -77,9 +78,27 @@ function AppRoutes() { } export default function App() { + const themeHue = useAppStore(s => s.themeHue) + + useEffect(() => { + document.documentElement.style.setProperty('--theme-hue', themeHue) + if (themeHue === '45') { + document.documentElement.style.setProperty('--theme-l', '0.65') + document.documentElement.style.setProperty('--theme-c', '0.18') + document.documentElement.style.setProperty('--theme-l-dark', '0.70') + document.documentElement.style.setProperty('--theme-c-dark', '0.16') + } else { + document.documentElement.style.removeProperty('--theme-l') + document.documentElement.style.removeProperty('--theme-c') + document.documentElement.style.removeProperty('--theme-l-dark') + document.documentElement.style.removeProperty('--theme-c-dark') + } + }, [themeHue]) + return ( ) } + diff --git a/src/api/plant.ts b/src/api/plant.ts index 78359d3..29fc412 100644 --- a/src/api/plant.ts +++ b/src/api/plant.ts @@ -1,5 +1,5 @@ import { post, type PageResult, type PageParams } from '@/lib/request' -import { USE_MOCK, delay, paginate, mockId } from '@/mock' +import { USE_MOCK, delay, paginate, mockId, mockResponse } from '@/mock' import { mockWikis, mockWikiClasses, type PlantWiki, type PlantWikiClass } from '@/mock/plant/wiki' import { mockBanners, type Banner } from '@/mock/plant/banner' import { mockTopics, mockPosts, type Topic, type Post } from '@/mock/plant/community' @@ -7,94 +7,126 @@ import { mockExchangeItems, mockExchangeOrders, type ExchangeItem, type Exchange export type { PlantWiki, PlantWikiClass, Banner, Topic, Post, ExchangeItem, ExchangeOrder } +type R = { code: number; data: T; msg: string } + // ==================== Wiki ==================== export async function getWikiList(data: PageParams & { classId?: string }) { - if (USE_MOCK) { await delay(); let f = [...mockWikis]; if (data.classId) f = f.filter(w => w.classId === data.classId); if (data.keyword) f = f.filter(w => w.title.includes(data.keyword!)); return { data: paginate(f, data.current, data.pageSize) } } - return post<{ data: PageResult }>('/plant/wiki/list', data) + if (USE_MOCK) { + await delay(); let f = [...mockWikis] + if (data.classId) f = f.filter(w => w.classId === data.classId) + if (data.keyword) f = f.filter(w => w.title.includes(data.keyword!)) + return mockResponse(paginate(f, data.current, data.pageSize)) + } + return post>>('/plant/wiki/list', data) } export async function saveWiki(data: Partial) { - if (USE_MOCK) { await delay(); mockWikis.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as PlantWiki); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/wiki/save', data) + if (USE_MOCK) { + await delay() + mockWikis.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as PlantWiki) + return mockResponse(null) + } + return post>('/plant/wiki/save', data) } export async function updateWiki(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockWikis.findIndex(w => w.id === data.id); if (i >= 0) Object.assign(mockWikis[i], data); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/wiki/update', data) + if (USE_MOCK) { await delay(); const i = mockWikis.findIndex(w => w.id === data.id); if (i >= 0) Object.assign(mockWikis[i], data); return mockResponse(null) } + return post>('/plant/wiki/update', data) } export async function deleteWiki(id: string) { - if (USE_MOCK) { await delay(); const i = mockWikis.findIndex(w => w.id === id); if (i >= 0) mockWikis.splice(i, 1); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/wiki/delete', { id }) + if (USE_MOCK) { await delay(); const i = mockWikis.findIndex(w => w.id === id); if (i >= 0) mockWikis.splice(i, 1); return mockResponse(null) } + return post>('/plant/wiki/delete', { id }) } // ==================== Wiki Class ==================== export async function getWikiClassList() { - if (USE_MOCK) { await delay(); return { data: mockWikiClasses } } - return post<{ data: PlantWikiClass[] }>('/plant/wikiClass/list', {}) + if (USE_MOCK) { await delay(); return mockResponse(mockWikiClasses) } + return post>('/plant/wikiClass/list', {}) } export async function saveWikiClass(data: Partial) { - if (USE_MOCK) { await delay(); mockWikiClasses.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as PlantWikiClass); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/wikiClass/save', data) + if (USE_MOCK) { + await delay() + mockWikiClasses.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as PlantWikiClass) + return mockResponse(null) + } + return post>('/plant/wikiClass/save', data) } export async function deleteWikiClass(id: string) { - if (USE_MOCK) { await delay(); const i = mockWikiClasses.findIndex(c => c.id === id); if (i >= 0) mockWikiClasses.splice(i, 1); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/wikiClass/delete', { id }) + if (USE_MOCK) { await delay(); const i = mockWikiClasses.findIndex(c => c.id === id); if (i >= 0) mockWikiClasses.splice(i, 1); return mockResponse(null) } + return post>('/plant/wikiClass/delete', { id }) } // ==================== Banner ==================== export async function getBannerList(data: PageParams & { title?: string; isActive?: number }) { - if (USE_MOCK) { await delay(); let f = [...mockBanners]; if (data.keyword) f = f.filter(b => b.title.includes(data.keyword!)); return { data: paginate(f, data.current, data.pageSize) } } - return post<{ data: PageResult }>('/plant/banner/list', data) + if (USE_MOCK) { + await delay(); let f = [...mockBanners] + if (data.keyword) f = f.filter(b => b.title.includes(data.keyword!)) + return mockResponse(paginate(f, data.current, data.pageSize)) + } + return post>>('/plant/banner/list', data) } export async function saveBanner(data: Partial) { - if (USE_MOCK) { await delay(); mockBanners.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as Banner); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/banner/save', data) + if (USE_MOCK) { await delay(); mockBanners.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as Banner); return mockResponse(null) } + return post>('/plant/banner/save', data) } export async function updateBanner(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockBanners.findIndex(b => b.id === data.id); if (i >= 0) Object.assign(mockBanners[i], data); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/banner/update', data) + if (USE_MOCK) { await delay(); const i = mockBanners.findIndex(b => b.id === data.id); if (i >= 0) Object.assign(mockBanners[i], data); return mockResponse(null) } + return post>('/plant/banner/update', data) } export async function deleteBanner(id: string) { - if (USE_MOCK) { await delay(); const i = mockBanners.findIndex(b => b.id === id); if (i >= 0) mockBanners.splice(i, 1); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/banner/delete', { id }) + if (USE_MOCK) { await delay(); const i = mockBanners.findIndex(b => b.id === id); if (i >= 0) mockBanners.splice(i, 1); return mockResponse(null) } + return post>('/plant/banner/delete', { id }) } // ==================== Topic ==================== export async function getTopicList(data: PageParams) { - if (USE_MOCK) { await delay(); let f = [...mockTopics]; if (data.keyword) f = f.filter(t => t.title.includes(data.keyword!)); return { data: paginate(f, data.current, data.pageSize) } } - return post<{ data: PageResult }>('/plant/topic/list', data) + if (USE_MOCK) { + await delay(); let f = [...mockTopics] + if (data.keyword) f = f.filter(t => t.title.includes(data.keyword!)) + return mockResponse(paginate(f, data.current, data.pageSize)) + } + return post>>('/plant/topic/list', data) } export async function saveTopic(data: Partial) { - if (USE_MOCK) { await delay(); mockTopics.push({ ...data, id: mockId(), postCount: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as Topic); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/topic/save', data) + if (USE_MOCK) { await delay(); mockTopics.push({ ...data, id: mockId(), postCount: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as Topic); return mockResponse(null) } + return post>('/plant/topic/save', data) } export async function deleteTopic(id: string) { - if (USE_MOCK) { await delay(); const i = mockTopics.findIndex(t => t.id === id); if (i >= 0) mockTopics.splice(i, 1); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/topic/delete', { id }) + if (USE_MOCK) { await delay(); const i = mockTopics.findIndex(t => t.id === id); if (i >= 0) mockTopics.splice(i, 1); return mockResponse(null) } + return post>('/plant/topic/delete', { id }) } // ==================== Post ==================== export async function getPostList(data: PageParams & { topicId?: string }) { - if (USE_MOCK) { await delay(); let f = [...mockPosts]; if (data.topicId) f = f.filter(p => p.topicId === data.topicId); if (data.keyword) f = f.filter(p => p.title.includes(data.keyword!)); return { data: paginate(f, data.current, data.pageSize) } } - return post<{ data: PageResult }>('/plant/post/list', data) + if (USE_MOCK) { + await delay(); let f = [...mockPosts] + if (data.topicId) f = f.filter(p => p.topicId === data.topicId) + if (data.keyword) f = f.filter(p => p.title.includes(data.keyword!)) + return mockResponse(paginate(f, data.current, data.pageSize)) + } + return post>>('/plant/post/list', data) } export async function deletePost(id: string) { - if (USE_MOCK) { await delay(); const i = mockPosts.findIndex(p => p.id === id); if (i >= 0) mockPosts.splice(i, 1); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/post/delete', { id }) + if (USE_MOCK) { await delay(); const i = mockPosts.findIndex(p => p.id === id); if (i >= 0) mockPosts.splice(i, 1); return mockResponse(null) } + return post>('/plant/post/delete', { id }) } // ==================== Exchange ==================== export async function getExchangeItemList(data: PageParams) { - if (USE_MOCK) { await delay(); let f = [...mockExchangeItems]; if (data.keyword) f = f.filter(e => e.name.includes(data.keyword!)); return { data: paginate(f, data.current, data.pageSize) } } - return post<{ data: PageResult }>('/plant/exchange/itemList', data) + if (USE_MOCK) { + await delay(); let f = [...mockExchangeItems] + if (data.keyword) f = f.filter(e => e.name.includes(data.keyword!)) + return mockResponse(paginate(f, data.current, data.pageSize)) + } + return post>>('/plant/exchange/itemList', data) } export async function saveExchangeItem(data: Partial) { - if (USE_MOCK) { await delay(); mockExchangeItems.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as ExchangeItem); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/exchange/saveItem', data) + if (USE_MOCK) { await delay(); mockExchangeItems.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as ExchangeItem); return mockResponse(null) } + return post>('/plant/exchange/saveItem', data) } export async function deleteExchangeItem(id: string) { - if (USE_MOCK) { await delay(); const i = mockExchangeItems.findIndex(e => e.id === id); if (i >= 0) mockExchangeItems.splice(i, 1); return { msg: 'ok' } } - return post<{ msg: string }>('/plant/exchange/deleteItem', { id }) + if (USE_MOCK) { await delay(); const i = mockExchangeItems.findIndex(e => e.id === id); if (i >= 0) mockExchangeItems.splice(i, 1); return mockResponse(null) } + return post>('/plant/exchange/deleteItem', { id }) } export async function getExchangeOrderList(data: PageParams) { - if (USE_MOCK) { await delay(); return { data: paginate([...mockExchangeOrders], data.current, data.pageSize) } } - return post<{ data: PageResult }>('/plant/exchange/orderList', data) + if (USE_MOCK) { await delay(); return mockResponse(paginate([...mockExchangeOrders], data.current, data.pageSize)) } + return post>>('/plant/exchange/orderList', data) } diff --git a/src/api/radio.ts b/src/api/radio.ts index d23a2b2..e60be87 100644 --- a/src/api/radio.ts +++ b/src/api/radio.ts @@ -1,5 +1,5 @@ import { post, type PageResult, type PageParams } from '@/lib/request' -import { USE_MOCK, delay, paginate, mockId } from '@/mock' +import { USE_MOCK, delay, paginate, mockId, mockResponse } from '@/mock' import { mockRadioCategories, mockRadioChannels, mockRadioPrograms } from '@/mock/radio/channels' import { mockSubscriptions } from '@/mock/radio/subscriptions' import type { RadioCategory, RadioChannel, RadioProgram, RadioSubscription } from '@/mock/radio/channels' @@ -8,56 +8,56 @@ export type { RadioCategory, RadioChannel, RadioProgram, RadioSubscription } // ==================== Category ==================== export async function getRadioCategoryList() { - if (USE_MOCK) { await delay(); return { data: [...mockRadioCategories] } } - return post<{ data: RadioCategory[] }>('/radio/category/list', {}) + if (USE_MOCK) { await delay(); return mockResponse([...mockRadioCategories]) } + return post<{ code: number; data: RadioCategory[]; msg: string }>('/radio/category/list', {}) } export async function saveRadioCategory(data: Partial) { - if (USE_MOCK) { await delay(); mockRadioCategories.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as RadioCategory); return { msg: 'ok' } } - return post<{ msg: string }>('/radio/category/save', data) + if (USE_MOCK) { await delay(); mockRadioCategories.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as RadioCategory); return mockResponse(null) } + return post<{ code: number; data: null; msg: string }>('/radio/category/save', data) } export async function deleteRadioCategory(id: string) { - if (USE_MOCK) { await delay(); const i = mockRadioCategories.findIndex(c => c.id === id); if (i >= 0) mockRadioCategories.splice(i, 1); return { msg: 'ok' } } - return post<{ msg: string }>('/radio/category/delete', { id }) + if (USE_MOCK) { await delay(); const i = mockRadioCategories.findIndex(c => c.id === id); if (i >= 0) mockRadioCategories.splice(i, 1); return mockResponse(null) } + return post<{ code: number; data: null; msg: string }>('/radio/category/delete', { id }) } // ==================== Channel ==================== export async function getRadioChannelList(data: PageParams & { categoryId?: string }) { - if (USE_MOCK) { await delay(); let f = [...mockRadioChannels]; if (data.categoryId) f = f.filter(c => c.categoryId === data.categoryId); if (data.keyword) f = f.filter(c => c.name.includes(data.keyword!)); return { data: paginate(f, data.current, data.pageSize) } } - return post<{ data: PageResult }>('/radio/channel/list', data) + if (USE_MOCK) { await delay(); let f = [...mockRadioChannels]; if (data.categoryId) f = f.filter(c => c.categoryId === data.categoryId); if (data.keyword) f = f.filter(c => c.name.includes(data.keyword!)); return mockResponse(paginate(f, data.current, data.pageSize)) } + return post<{ code: number; data: PageResult; msg: string }>('/radio/channel/list', data) } export async function saveRadioChannel(data: Partial) { - if (USE_MOCK) { await delay(); mockRadioChannels.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as RadioChannel); return { msg: 'ok' } } - return post<{ msg: string }>('/radio/channel/save', data) + if (USE_MOCK) { await delay(); mockRadioChannels.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as RadioChannel); return mockResponse(null) } + return post<{ code: number; data: null; msg: string }>('/radio/channel/save', data) } export async function updateRadioChannel(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockRadioChannels.findIndex(c => c.id === data.id); if (i >= 0) Object.assign(mockRadioChannels[i], data); return { msg: 'ok' } } - return post<{ msg: string }>('/radio/channel/update', data) + if (USE_MOCK) { await delay(); const i = mockRadioChannels.findIndex(c => c.id === data.id); if (i >= 0) Object.assign(mockRadioChannels[i], data); return mockResponse(null) } + return post<{ code: number; data: null; msg: string }>('/radio/channel/update', data) } export async function deleteRadioChannel(id: string) { - if (USE_MOCK) { await delay(); const i = mockRadioChannels.findIndex(c => c.id === id); if (i >= 0) mockRadioChannels.splice(i, 1); return { msg: 'ok' } } - return post<{ msg: string }>('/radio/channel/delete', { id }) + if (USE_MOCK) { await delay(); const i = mockRadioChannels.findIndex(c => c.id === id); if (i >= 0) mockRadioChannels.splice(i, 1); return mockResponse(null) } + return post<{ code: number; data: null; msg: string }>('/radio/channel/delete', { id }) } // ==================== Program ==================== export async function getRadioProgramList(data: PageParams & { channelId?: string }) { - if (USE_MOCK) { await delay(); let f = [...mockRadioPrograms]; if (data.channelId) f = f.filter(p => p.channelId === data.channelId); if (data.keyword) f = f.filter(p => p.title.includes(data.keyword!)); return { data: paginate(f, data.current, data.pageSize) } } - return post<{ data: PageResult }>('/radio/program/list', data) + if (USE_MOCK) { await delay(); let f = [...mockRadioPrograms]; if (data.channelId) f = f.filter(p => p.channelId === data.channelId); if (data.keyword) f = f.filter(p => p.title.includes(data.keyword!)); return mockResponse(paginate(f, data.current, data.pageSize)) } + return post<{ code: number; data: PageResult; msg: string }>('/radio/program/list', data) } export async function saveRadioProgram(data: Partial) { - if (USE_MOCK) { await delay(); mockRadioPrograms.push({ ...data, id: mockId(), audioStatus: 0, duration: 0, playCount: 0, likeCount: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as RadioProgram); return { msg: 'ok' } } - return post<{ msg: string }>('/radio/program/save', data) + if (USE_MOCK) { await delay(); mockRadioPrograms.push({ ...data, id: mockId(), audioStatus: 0, duration: 0, playCount: 0, likeCount: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as RadioProgram); return mockResponse(null) } + return post<{ code: number; data: null; msg: string }>('/radio/program/save', data) } export async function updateRadioProgram(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockRadioPrograms.findIndex(p => p.id === data.id); if (i >= 0) Object.assign(mockRadioPrograms[i], data); return { msg: 'ok' } } - return post<{ msg: string }>('/radio/program/update', data) + if (USE_MOCK) { await delay(); const i = mockRadioPrograms.findIndex(p => p.id === data.id); if (i >= 0) Object.assign(mockRadioPrograms[i], data); return mockResponse(null) } + return post<{ code: number; data: null; msg: string }>('/radio/program/update', data) } export async function deleteRadioProgram(id: string) { - if (USE_MOCK) { await delay(); const i = mockRadioPrograms.findIndex(p => p.id === id); if (i >= 0) mockRadioPrograms.splice(i, 1); return { msg: 'ok' } } - return post<{ msg: string }>('/radio/program/delete', { id }) + if (USE_MOCK) { await delay(); const i = mockRadioPrograms.findIndex(p => p.id === id); if (i >= 0) mockRadioPrograms.splice(i, 1); return mockResponse(null) } + return post<{ code: number; data: null; msg: string }>('/radio/program/delete', { id }) } // ==================== Subscription ==================== export async function getSubscriptionList(data: PageParams & { channelId?: string; status?: number }) { - if (USE_MOCK) { await delay(); let f = [...mockSubscriptions]; if (data.channelId) f = f.filter(s => s.channelId === data.channelId); if (data.status !== undefined) f = f.filter(s => s.status === data.status); return { data: paginate(f, data.current, data.pageSize) } } - return post<{ data: PageResult }>('/radio/subscription/list', data) + if (USE_MOCK) { await delay(); let f = [...mockSubscriptions]; if (data.channelId) f = f.filter(s => s.channelId === data.channelId); if (data.status !== undefined) f = f.filter(s => s.status === data.status); return mockResponse(paginate(f, data.current, data.pageSize)) } + return post<{ code: number; data: PageResult; msg: string }>('/radio/subscription/list', data) } diff --git a/src/api/system.ts b/src/api/system.ts index 3c48eb9..adb1fbf 100644 --- a/src/api/system.ts +++ b/src/api/system.ts @@ -1,5 +1,5 @@ import { get, post } from '@/lib/request' -import { USE_MOCK, delay } from '@/mock' +import { USE_MOCK, delay, mockResponse } from '@/mock' import { mockUsers } from '@/mock/system/users' // ==================== Types ==================== @@ -51,18 +51,18 @@ export interface LoginResponse { token: string; expiresAt: number; user: SystemU // ==================== Auth ==================== export async function getCaptcha() { - if (USE_MOCK) { await delay(200); return { data: { captcha: 'data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=', captchaId: 'mock-captcha-id' } } } - return get<{ data: CaptchaRes }>('/auth/captcha') + if (USE_MOCK) { await delay(200); return mockResponse({ captcha: 'data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=', captchaId: 'mock-captcha-id' }) } + return get<{ code: number; data: CaptchaRes; msg: string }>('/auth/captcha') } export async function login(data: LoginParams) { - if (USE_MOCK) { await delay(500); return { data: { token: 'mock-token-xxx', expiresAt: Date.now() + 7200000, user: mockUsers[0] }, msg: '登录成功' } } - return post<{ data: LoginResponse; msg: string }>('/auth/login', data) + if (USE_MOCK) { await delay(500); return mockResponse({ token: 'mock-token-xxx', expiresAt: Date.now() + 7200000, user: mockUsers[0] }, '登录成功') } + return post<{ code: number; data: LoginResponse; msg: string }>('/auth/login', data) } export async function logout() { - if (USE_MOCK) { await delay(100); return { msg: '已登出' } } - return get<{ msg: string }>('/auth/logout') + if (USE_MOCK) { await delay(100); return mockResponse(null, '已登出') } + return get<{ code: number; data: null; msg: string }>('/auth/logout') } // ==================== 以下在各自文件中实现 ==================== diff --git a/src/api/system/log.ts b/src/api/system/log.ts index da36b89..14d4487 100644 --- a/src/api/system/log.ts +++ b/src/api/system/log.ts @@ -1,5 +1,5 @@ import { get } from '@/lib/request' -import { USE_MOCK, delay, paginate } from '@/mock' +import { USE_MOCK, delay, paginate, mockResponse } from '@/mock' import { mockLogs } from '@/mock/system/logs' import type { OperationLog } from '../system' @@ -19,7 +19,7 @@ export async function getLogList(params: { if (params.path) list = list.filter(l => l.path.includes(params.path!)) if (params.status) list = list.filter(l => l.statusCode === params.status) - return { data: paginate(list, params.current, params.pageSize) } + return mockResponse(paginate(list, params.current, params.pageSize)) } - return get<{ data: { list: OperationLog[]; total: number } }>('/system/logs', params) + return get<{ code: number; data: { list: OperationLog[]; total: number }; msg: string }>('/system/logs', params) } diff --git a/src/api/system/menu.ts b/src/api/system/menu.ts index 58202af..3f5c6d5 100644 --- a/src/api/system/menu.ts +++ b/src/api/system/menu.ts @@ -1,27 +1,24 @@ import { get, post, put, del } from '@/lib/request' -import { USE_MOCK, delay } from '@/mock' +import { USE_MOCK, delay, mockResponse } from '@/mock' import { mockMenuTree } from '@/mock/system/menus' import type { SystemMenu } from '../system' export async function getMenuTree() { - if (USE_MOCK) { - await delay() - return { data: mockMenuTree } - } - return get<{ data: SystemMenu[] }>('/system/menus/tree') + if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) } + return get<{ code: number; data: SystemMenu[]; msg: string }>('/system/menus/tree') } export async function createMenu(data: Partial) { - if (USE_MOCK) { await delay(); return { msg: '创建成功' } } - return post<{ msg: string }>('/system/menus', data) + if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } + return post<{ code: number; data: null; msg: string }>('/system/menus', data) } export async function updateMenu(id: string, data: Partial) { - if (USE_MOCK) { await delay(); return { msg: '更新成功' } } - return put<{ msg: string }>(`/system/menus/${id}`, data) + if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } + return put<{ code: number; data: null; msg: string }>(`/system/menus/${id}`, data) } export async function deleteMenu(id: string) { - if (USE_MOCK) { await delay(); return { msg: '删除成功' } } - return del<{ msg: string }>(`/system/menus/${id}`) + if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } + return del<{ code: number; data: null; msg: string }>(`/system/menus/${id}`) } diff --git a/src/api/system/role.ts b/src/api/system/role.ts index b3b092d..31ef069 100644 --- a/src/api/system/role.ts +++ b/src/api/system/role.ts @@ -1,5 +1,5 @@ import { get, post, put, del } from '@/lib/request' -import { USE_MOCK, delay, paginate, mockDate } from '@/mock' +import { USE_MOCK, delay, paginate, mockDate, mockResponse } from '@/mock' import type { SystemRole } from '../system' const mockRoles: SystemRole[] = [ @@ -15,22 +15,22 @@ export async function getRoleList(params: { current: number; pageSize: number; n await delay() let list = [...mockRoles] if (params.name) list = list.filter(r => r.name.includes(params.name!)) - return { data: paginate(list, params.current, params.pageSize) } + return mockResponse(paginate(list, params.current, params.pageSize)) } - return get<{ data: { list: SystemRole[]; total: number } }>('/system/roles', params) + return get<{ code: number; data: { list: SystemRole[]; total: number }; msg: string }>('/system/roles', params) } export async function createRole(data: Partial) { - if (USE_MOCK) { await delay(); return { msg: '创建成功' } } - return post<{ msg: string }>('/system/roles', data) + if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } + return post<{ code: number; data: null; msg: string }>('/system/roles', data) } export async function updateRole(id: string, data: Partial) { - if (USE_MOCK) { await delay(); return { msg: '更新成功' } } - return put<{ msg: string }>(`/system/roles/${id}`, data) + if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } + return put<{ code: number; data: null; msg: string }>(`/system/roles/${id}`, data) } export async function deleteRole(id: string) { - if (USE_MOCK) { await delay(); return { msg: '删除成功' } } - return del<{ msg: string }>(`/system/roles/${id}`) + if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } + return del<{ code: number; data: null; msg: string }>(`/system/roles/${id}`) } diff --git a/src/api/system/user.ts b/src/api/system/user.ts index e423062..ce9a97b 100644 --- a/src/api/system/user.ts +++ b/src/api/system/user.ts @@ -1,5 +1,5 @@ import { get, post, put, del } from '@/lib/request' -import { USE_MOCK, delay, paginate } from '@/mock' +import { USE_MOCK, delay, paginate, mockResponse } from '@/mock' import { mockUsers } from '@/mock/system/users' import type { SystemUser } from '../system' @@ -9,22 +9,22 @@ export async function getUserList(params: { current: number; pageSize: number; a let list = [...mockUsers] if (params.account) list = list.filter(u => u.account.includes(params.account!)) if (params.name) list = list.filter(u => u.name.includes(params.name!)) - return { data: paginate(list, params.current, params.pageSize) } + return mockResponse(paginate(list, params.current, params.pageSize)) } - return get<{ data: { list: SystemUser[]; total: number } }>('/system/users', params) + return get<{ code: number; data: { list: SystemUser[]; total: number }; msg: string }>('/system/users', params) } export async function createUser(data: Partial) { - if (USE_MOCK) { await delay(); return { msg: '创建成功' } } - return post<{ msg: string }>('/system/users', data) + if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } + return post<{ code: number; data: null; msg: string }>('/system/users', data) } export async function updateUser(id: string, data: Partial) { - if (USE_MOCK) { await delay(); return { msg: '更新成功' } } - return put<{ msg: string }>(`/system/users/${id}`, data) + if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } + return put<{ code: number; data: null; msg: string }>(`/system/users/${id}`, data) } export async function deleteUser(id: string) { - if (USE_MOCK) { await delay(); return { msg: '删除成功' } } - return del<{ msg: string }>(`/system/users/${id}`) + if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } + return del<{ code: number; data: null; msg: string }>(`/system/users/${id}`) } diff --git a/src/api/systemCrud.ts b/src/api/systemCrud.ts index baa9d11..2e4e2d3 100644 --- a/src/api/systemCrud.ts +++ b/src/api/systemCrud.ts @@ -1,5 +1,5 @@ import { get, post, type PageResult, type PageParams } from '@/lib/request' -import { USE_MOCK, delay, paginate, mockId } from '@/mock' +import { USE_MOCK, delay, paginate, mockId, mockResponse } from '@/mock' import { mockUsers } from '@/mock/system/users' import { mockRoles } from '@/mock/system/roles' import { mockMenuTree } from '@/mock/system/menus' @@ -15,28 +15,28 @@ export async function getUserList(data: PageParams & { account?: string; phone?: await delay() let filtered = [...mockUsers] if (data.keyword) filtered = filtered.filter(u => u.name.includes(data.keyword!) || u.account.includes(data.keyword!)) - return { data: paginate(filtered, data.current, data.pageSize) } + return mockResponse(paginate(filtered, data.current, data.pageSize)) } return post<{ data: PageResult }>('/user/getUserList', data) } export async function saveUser(data: Partial) { - if (USE_MOCK) { await delay(); mockUsers.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemUser); return { msg: '创建成功' } } + if (USE_MOCK) { await delay(); mockUsers.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemUser); return mockResponse(null, '创建成功') } return post<{ msg: string }>('/user/save', data) } export async function updateUser(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockUsers.findIndex(u => u.id === data.id); if (i >= 0) Object.assign(mockUsers[i], data); return { msg: '更新成功' } } + if (USE_MOCK) { await delay(); const i = mockUsers.findIndex(u => u.id === data.id); if (i >= 0) Object.assign(mockUsers[i], data); return mockResponse(null, '更新成功') } return post<{ msg: string }>('/user/update', data) } export async function deleteUser(ids: string[]) { - if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockUsers.findIndex(u => u.id === id); if (i >= 0) mockUsers.splice(i, 1) }); return { msg: '删除成功' } } + if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockUsers.findIndex(u => u.id === id); if (i >= 0) mockUsers.splice(i, 1) }); return mockResponse(null, '删除成功') } return post<{ msg: string }>('/user/delete', { ids }) } export async function changePassword(data: { id: string; newPwd: string }) { - if (USE_MOCK) { await delay(); return { msg: '密码修改成功' } } + if (USE_MOCK) { await delay(); return mockResponse(null, '密码修改成功') } return post<{ msg: string }>('/user/changePassword', data) } @@ -45,7 +45,7 @@ export async function grantRole(data: { userId: string; roleIds: string[] }) { await delay() const user = mockUsers.find(u => u.id === data.userId) if (user) user.roles = mockRoles.filter(r => data.roleIds.includes(r.id)).map(r => ({ ...r })) - return { msg: '角色分配成功' } + return mockResponse(null, '角色分配成功') } return post<{ msg: string }>('/user/grantRole', data) } @@ -57,28 +57,28 @@ export async function getRoleList(data: PageParams & { name?: string }) { await delay() let filtered = [...mockRoles] if (data.keyword) filtered = filtered.filter(r => r.name.includes(data.keyword!)) - return { data: paginate(filtered, data.current, data.pageSize) } + return mockResponse(paginate(filtered, data.current, data.pageSize)) } return post<{ data: PageResult }>('/role/getRoleList', data) } export async function getAllRoles() { - if (USE_MOCK) { await delay(100); return { data: [...mockRoles] } } + if (USE_MOCK) { await delay(100); return mockResponse([...mockRoles]) } return post<{ data: SystemRole[] }>('/role/getAllRoles', {}) } export async function saveRole(data: Partial) { - if (USE_MOCK) { await delay(); mockRoles.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemRole); return { msg: '创建成功' } } + if (USE_MOCK) { await delay(); mockRoles.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemRole); return mockResponse(null, '创建成功') } return post<{ msg: string }>('/role/save', data) } export async function updateRole(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockRoles.findIndex(r => r.id === data.id); if (i >= 0) Object.assign(mockRoles[i], data); return { msg: '更新成功' } } + if (USE_MOCK) { await delay(); const i = mockRoles.findIndex(r => r.id === data.id); if (i >= 0) Object.assign(mockRoles[i], data); return mockResponse(null, '更新成功') } return post<{ msg: string }>('/role/update', data) } export async function deleteRole(ids: string[]) { - if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockRoles.findIndex(r => r.id === id); if (i >= 0) mockRoles.splice(i, 1) }); return { msg: '删除成功' } } + if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockRoles.findIndex(r => r.id === id); if (i >= 0) mockRoles.splice(i, 1) }); return mockResponse(null, '删除成功') } return post<{ msg: string }>('/role/delete', { ids }) } @@ -90,7 +90,7 @@ export async function grantMenu(data: { roleId: string; menuIds: string[] }) { const flatMenus = (items: SystemMenu[]): SystemMenu[] => items.flatMap(m => [m, ...(m.children ? flatMenus(m.children) : [])]) role.menus = flatMenus(mockMenuTree).filter(m => data.menuIds.includes(m.id)) } - return { msg: '菜单授权成功' } + return mockResponse(null, '菜单授权成功') } return post<{ msg: string }>('/role/grantMenu', data) } @@ -98,27 +98,27 @@ export async function grantMenu(data: { roleId: string; menuIds: string[] }) { // ==================== Menu ==================== export async function getAllMenuTree() { - if (USE_MOCK) { await delay(); return { data: mockMenuTree } } + if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) } return post<{ data: SystemMenu[] }>('/menu/getAllMenuTree', {}) } export async function getUserMenuTree() { - if (USE_MOCK) { await delay(); return { data: mockMenuTree } } + if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) } return get<{ data: SystemMenu[] }>('/menu/getUserMenuTree') } export async function saveMenu(data: Partial) { - if (USE_MOCK) { await delay(); return { msg: '创建成功' } } + if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') } return post<{ msg: string }>('/menu/save', data) } export async function updateMenu(data: Partial) { - if (USE_MOCK) { await delay(); return { msg: '更新成功' } } + if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') } return post<{ msg: string }>('/menu/update', data) } export async function deleteMenu(id: string) { - if (USE_MOCK) { await delay(); return { msg: '删除成功' } } + if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') } return get<{ msg: string }>('/menu/delete', { id }) } @@ -129,23 +129,23 @@ export async function getClientList(data: PageParams & { clientId?: string; name await delay() let filtered = [...mockClients] if (data.keyword) filtered = filtered.filter(c => c.name.includes(data.keyword!) || c.clientId.includes(data.keyword!)) - return { data: paginate(filtered, data.current, data.pageSize) } + return mockResponse(paginate(filtered, data.current, data.pageSize)) } return post<{ data: PageResult }>('/client/getClientList', data) } export async function saveClient(data: Partial) { - if (USE_MOCK) { await delay(); mockClients.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemClient); return { msg: '创建成功' } } + if (USE_MOCK) { await delay(); mockClients.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemClient); return mockResponse(null, '创建成功') } return post<{ msg: string }>('/client/save', data) } export async function updateClient(data: Partial) { - if (USE_MOCK) { await delay(); const i = mockClients.findIndex(c => c.id === data.id); if (i >= 0) Object.assign(mockClients[i], data); return { msg: '更新成功' } } + if (USE_MOCK) { await delay(); const i = mockClients.findIndex(c => c.id === data.id); if (i >= 0) Object.assign(mockClients[i], data); return mockResponse(null, '更新成功') } return post<{ msg: string }>('/client/update', data) } export async function deleteClient(ids: string[]) { - if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockClients.findIndex(c => c.id === id); if (i >= 0) mockClients.splice(i, 1) }); return { msg: '删除成功' } } + if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockClients.findIndex(c => c.id === id); if (i >= 0) mockClients.splice(i, 1) }); return mockResponse(null, '删除成功') } return post<{ msg: string }>('/client/delete', { ids }) } @@ -156,19 +156,19 @@ export async function getFileList(data: PageParams & { name?: string }) { await delay() let filtered = [...mockFiles] if (data.keyword) filtered = filtered.filter(f => f.name.includes(data.keyword!)) - return { data: paginate(filtered, data.current, data.pageSize) } + return mockResponse(paginate(filtered, data.current, data.pageSize)) } return post<{ data: PageResult }>('/oss/getFileList', data) } export async function uploadFile(_file: File) { - if (USE_MOCK) { await delay(800); const f: SystemOss = { id: mockId(), name: _file.name, key: `uploads/${_file.name}`, url: `https://picsum.photos/seed/${Date.now()}/400/300`, suffix: _file.name.split('.').pop() || '', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; mockFiles.unshift(f); return { data: { file: f }, msg: '上传成功' } } + if (USE_MOCK) { await delay(800); const f: SystemOss = { id: mockId(), name: _file.name, key: `uploads/${_file.name}`, url: `https://picsum.photos/seed/${Date.now()}/400/300`, suffix: _file.name.split('.').pop() || '', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; mockFiles.unshift(f); return mockResponse({ file: f }, '上传成功') } const formData = new FormData(); formData.append('file', _file) return post<{ data: { file: SystemOss }; msg: string }>('/oss/upload', formData) } export async function deleteFile(ids: string[]) { - if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockFiles.findIndex(f => f.id === id); if (i >= 0) mockFiles.splice(i, 1) }); return { msg: '删除成功' } } + if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockFiles.findIndex(f => f.id === id); if (i >= 0) mockFiles.splice(i, 1) }); return mockResponse(null, '删除成功') } return post<{ msg: string }>('/oss/delete', { ids }) } @@ -182,7 +182,7 @@ export async function getOperationLogList(data: PageParams & { clientId?: string if (data.method) filtered = filtered.filter(l => l.method === data.method) if (data.statusCode !== undefined) filtered = filtered.filter(l => l.statusCode === data.statusCode) if (data.keyword) filtered = filtered.filter(l => l.path.includes(data.keyword!) || l.title.includes(data.keyword!) || l.operatorName.includes(data.keyword!)) - return { data: paginate(filtered, data.current, data.pageSize) } + return mockResponse(paginate(filtered, data.current, data.pageSize)) } return post<{ data: PageResult }>('/log/getOperationLogList', data) } diff --git a/src/components/AIBanner.tsx b/src/components/AIBanner.tsx new file mode 100644 index 0000000..fd80b46 --- /dev/null +++ b/src/components/AIBanner.tsx @@ -0,0 +1,162 @@ +import React from 'react' +import { motion } from 'framer-motion' + +export default function AIBanner() { + // Generate Data Stream particles for the left side (Cyan) + const dataParticles = Array.from({ length: 100 }).map((_, i) => ({ + id: `dp-${i}`, + y: 10 + Math.random() * 80, // spread vertically between 10% and 90% + size: Math.random() > 0.8 ? 6 : Math.random() * 3 + 1.5, // slightly larger + isSquare: Math.random() > 0.6, + duration: Math.random() * 2 + 1.5, // faster + delay: Math.random() * 3, + opacity: Math.random() * 0.7 + 0.3 + })) + + // Generate Neural Network nodes for the right side (Purple/Fuchsia) + const neuralNodes = [ + { x: 10, y: 50 }, { x: 30, y: 25 }, { x: 35, y: 75 }, + { x: 55, y: 45 }, { x: 75, y: 15 }, { x: 70, y: 85 }, + { x: 90, y: 55 }, { x: 80, y: 40 }, { x: 50, y: 90 }, + { x: 20, y: 95 }, { x: 95, y: 20 }, { x: 45, y: 10 }, + { x: 85, y: 75 }, { x: 15, y: 20 } + ] + const neuralLines = [ + [0, 1], [0, 2], [1, 3], [2, 3], [1, 4], [3, 4], + [3, 6], [2, 5], [3, 5], [5, 6], [4, 7], [6, 7], + [2, 8], [5, 8], [0, 9], [2, 9], [4, 10], [7, 10], + [1, 11], [4, 11], [6, 12], [5, 12], [0, 13], [1, 13] + ] + + return ( +
+ {/* Background glow matching the logo's dual tone */} +
+
+ + {/* Background dark stone texture (simulated) */} +
+ + {/* DYNAMIC DATA STREAM: Cyan Data Stream & Pixels (Flowing right and naturally fading out via Mask) */} +
+ {dataParticles.map(p => ( + + ))} + {/* Horizontal glowing fiber streaks shooting from left to right */} +
+ {Array.from({length: 20}).map((_, i) => ( + + ))} +
+
+ + {/* RIGHT SIDE: Fuchsia/Purple Neural Network */} +
+ + {/* Neural Lines */} + {neuralLines.map((line, i) => { + const n1 = neuralNodes[line[0]] + const n2 = neuralNodes[line[1]] + return ( + + ) + })} + {/* Neural Nodes */} + {neuralNodes.map((node, i) => ( + + ))} + +
+ + {/* CENTRAL ELEGANT CONNECTION (Subtle, thin flowing lines connecting left and right) */} +
+ + + + + + + + + + + {/* Draw a few extremely elegant, thin sweeping curves spanning the entire width */} + {Array.from({length: 4}).map((_, i) => ( + + ))} + +
+
+ ) +} diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx new file mode 100644 index 0000000..37c6002 --- /dev/null +++ b/src/components/CommandPalette.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from 'react' +import { Command } from 'cmdk' +import { useNavigate } from 'react-router-dom' +import { Search, Monitor, Moon, Sun, Laptop, Palette } from 'lucide-react' +import { useAppStore } from '@/store/app' +import { useAuthStore } from '@/store/auth' +import './cmdk.css' // We'll add some styles for cmdk + +export default function CommandPalette() { + const { cmdKOpen, setCmdKOpen, setThemeHue } = useAppStore() + const menus = useAuthStore(s => s.menus) + const navigate = useNavigate() + const [theme, setTheme] = useState(document.documentElement.classList.contains('dark') ? 'dark' : 'light') + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault() + setCmdKOpen(!cmdKOpen) + } + } + document.addEventListener('keydown', down) + return () => document.removeEventListener('keydown', down) + }, [cmdKOpen, setCmdKOpen]) + + const runCommand = (command: () => void) => { + setCmdKOpen(false) + command() + } + + const toggleTheme = (mode: 'light' | 'dark' | 'system') => { + if (mode === 'dark') { + document.documentElement.classList.add('dark') + setTheme('dark') + } else if (mode === 'light') { + document.documentElement.classList.remove('dark') + setTheme('light') + } else { + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + document.documentElement.classList.add('dark') + setTheme('dark') + } else { + document.documentElement.classList.remove('dark') + setTheme('light') + } + } + } + + // Flatten menus for search + const flatMenus: { title: string, path: string }[] = [] + const flatten = (items: any[]) => { + items?.forEach(i => { + if (i.path && !i.children?.length && i.path !== '/dashboard') { + flatMenus.push({ title: i.title || i.name, path: i.path }) + } + if (i.children) flatten(i.children) + }) + } + if (menus) flatten(menus) + + return ( + +
+ + +
+ + + 没找到相关内容. + + + runCommand(() => navigate('/dashboard'))} className="flex items-center px-3 py-2.5 text-sm rounded-lg cursor-pointer transition-colors aria-selected:bg-primary/10 aria-selected:text-primary text-foreground mt-1"> + 仪表盘 + + {flatMenus.map(m => ( + runCommand(() => navigate(m.path))} className="flex items-center px-3 py-2.5 text-sm rounded-lg cursor-pointer transition-colors aria-selected:bg-primary/10 aria-selected:text-primary text-foreground mt-1"> + {m.title} + + ))} + + + + runCommand(() => toggleTheme('light'))} className="flex items-center px-3 py-2.5 text-sm rounded-lg cursor-pointer transition-colors aria-selected:bg-primary/10 aria-selected:text-primary text-foreground mt-1"> + 浅色模式 + + runCommand(() => toggleTheme('dark'))} className="flex items-center px-3 py-2.5 text-sm rounded-lg cursor-pointer transition-colors aria-selected:bg-primary/10 aria-selected:text-primary text-foreground mt-1"> + 深色模式 + + runCommand(() => toggleTheme('system'))} className="flex items-center px-3 py-2.5 text-sm rounded-lg cursor-pointer transition-colors aria-selected:bg-primary/10 aria-selected:text-primary text-foreground mt-1"> + 跟随系统 + + + + +
+ + + + + +
+
+
+
+ ) +} diff --git a/src/components/ParticleBackground.tsx b/src/components/ParticleBackground.tsx new file mode 100644 index 0000000..ee5252e --- /dev/null +++ b/src/components/ParticleBackground.tsx @@ -0,0 +1,133 @@ +import React, { useEffect, useRef } from 'react'; + +interface Particle { + x: number; + y: number; + size: number; + speedX: number; + speedY: number; + opacity: number; +} + +export default function ParticleBackground() { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + let animationFrameId: number; + let particles: Particle[] = []; + + // Config + const particleCount = 60; + const connectionDistance = 150; + const mouseRadius = 150; + + const mouse = { x: -1000, y: -1000 }; + + const resize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + + window.addEventListener('resize', resize); + resize(); + + const handleMouseMove = (e: MouseEvent) => { + mouse.x = e.clientX; + mouse.y = e.clientY; + }; + window.addEventListener('mousemove', handleMouseMove); + + const handleMouseLeave = () => { + mouse.x = -1000; + mouse.y = -1000; + }; + window.addEventListener('mouseleave', handleMouseLeave); + + // Initialize particles + for (let i = 0; i < particleCount; i++) { + particles.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + size: Math.random() * 2 + 0.5, + speedX: (Math.random() - 0.5) * 0.8, + speedY: (Math.random() - 0.5) * 0.8, + opacity: Math.random() * 0.5 + 0.2, + }); + } + + const draw = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const themeHue = getComputedStyle(document.documentElement).getPropertyValue('--theme-hue') || '145'; + const hue = themeHue.trim() ? themeHue : '145'; + + // Update and draw particles + particles.forEach((p, index) => { + p.x += p.speedX; + p.y += p.speedY; + + // Bounce off edges + if (p.x < 0 || p.x > canvas.width) p.speedX *= -1; + if (p.y < 0 || p.y > canvas.height) p.speedY *= -1; + + // Interaction with mouse + const dx = mouse.x - p.x; + const dy = mouse.y - p.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < mouseRadius) { + const forceDirectionX = dx / distance; + const forceDirectionY = dy / distance; + const force = (mouseRadius - distance) / mouseRadius; + // push away slightly + p.x -= forceDirectionX * force * 2; + p.y -= forceDirectionY * force * 2; + } + + ctx.beginPath(); + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + ctx.fillStyle = `oklch(0.65 0.15 ${hue} / ${p.opacity})`; + ctx.fill(); + + // Connect particles + for (let j = index + 1; j < particles.length; j++) { + const p2 = particles[j]; + const pdx = p.x - p2.x; + const pdy = p.y - p2.y; + const pDistance = Math.sqrt(pdx * pdx + pdy * pdy); + + if (pDistance < connectionDistance) { + ctx.beginPath(); + ctx.strokeStyle = `oklch(0.65 0.15 ${hue} / ${0.15 * (1 - pDistance / connectionDistance)})`; + ctx.lineWidth = 1; + ctx.moveTo(p.x, p.y); + ctx.lineTo(p2.x, p2.y); + ctx.stroke(); + } + } + }); + + animationFrameId = requestAnimationFrame(draw); + }; + + draw(); + + return () => { + window.removeEventListener('resize', resize); + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseleave', handleMouseLeave); + cancelAnimationFrame(animationFrameId); + }; + }, []); + + return ( + + ); +} diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx new file mode 100644 index 0000000..ceb4df4 --- /dev/null +++ b/src/components/TabBar.tsx @@ -0,0 +1,139 @@ +import { useRef, useEffect } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' +import { X, ChevronLeft, ChevronRight } from 'lucide-react' +import { useTabsStore } from '@/store/tabs' +import { useAuthStore } from '@/store/auth' +import { cn } from '@/lib/utils' +import { + ContextMenu, ContextMenuContent, ContextMenuItem, + ContextMenuSeparator, ContextMenuTrigger, +} from '@/components/ui/context-menu' +import type { SystemMenu } from '@/api/system' + +/** Resolve page title from menu tree by path */ +function resolveTitle(menus: SystemMenu[], path: string): string { + for (const m of menus) { + if (m.path === path) return m.title || m.name + if (m.children?.length) { + const found = resolveTitle(m.children, path) + if (found) return found + } + } + return '' +} + +export default function TabBar() { + const navigate = useNavigate() + const location = useLocation() + const menus = useAuthStore(s => s.menus) + const { tabs, activeTab, addTab, removeTab, setActiveTab, closeOthers, closeAll, closeRight, closeLeft } = useTabsStore() + const scrollRef = useRef(null) + + // Auto-register tab on route change + useEffect(() => { + const path = location.pathname + if (path === '/login') return + const title = resolveTitle(menus || [], path) + || path.split('/').filter(Boolean).pop()?.replace(/^\w/, c => c.toUpperCase()) || 'Page' + addTab({ path, title, closable: path !== '/dashboard' }) + }, [location.pathname, menus]) + + const handleClick = (path: string) => { + setActiveTab(path) + navigate(path) + } + + const handleClose = (e: React.MouseEvent, path: string) => { + e.stopPropagation() + const next = removeTab(path) + navigate(next) + } + + const handleCloseOthers = (path: string) => { + closeOthers(path) + navigate(path) + } + + const handleCloseAll = () => { + const home = closeAll() + navigate(home) + } + + const scroll = (dir: 'left' | 'right') => { + scrollRef.current?.scrollBy({ left: dir === 'left' ? -200 : 200, behavior: 'smooth' }) + } + + const showArrows = tabs.length > 8 + + return ( +
+ {showArrows && ( + + )} + +
+ {tabs.map(tab => ( + + + + + + {tab.closable && ( + { const next = removeTab(tab.path); navigate(next) }}> + 关闭当前 + + )} + handleCloseOthers(tab.path)}> + 关闭其他 + + { closeRight(tab.path); navigate(tab.path) }}> + 关闭右侧 + + { closeLeft(tab.path); navigate(tab.path) }}> + 关闭左侧 + + + + 关闭所有 + + + + ))} +
+ + {showArrows && ( + + )} +
+ ) +} diff --git a/src/components/cmdk.css b/src/components/cmdk.css new file mode 100644 index 0000000..87f5519 --- /dev/null +++ b/src/components/cmdk.css @@ -0,0 +1,71 @@ +.cmdk-dialog { + position: fixed; + top: 20%; + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: 640px; + background: var(--color-background); + border-radius: 1rem; + box-shadow: var(--shadow-soft-lg); + border: 1px solid oklch(0.9 0.01 110 / 0.2); + z-index: 100; + overflow: hidden; + animation: dialogIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +:root.dark .cmdk-dialog { + background: oklch(0.16 0.02 145 / 0.85); + backdrop-filter: blur(16px); + border: 1px solid oklch(1 0 0 / 0.1); +} + +@keyframes dialogIn { + from { + opacity: 0; + transform: translate(-50%, -40%) scale(0.96); + } + to { + opacity: 1; + transform: translate(-50%, 0) scale(1); + } +} + +[data-cmdk-overlay] { + background: rgba(0, 0, 0, 0.4); + position: fixed; + inset: 0; + z-index: 99; + backdrop-filter: blur(4px); + animation: overlayIn 0.2s ease-out forwards; +} + +@keyframes overlayIn { + from { opacity: 0; } + to { opacity: 1; } +} + +[data-cmdk-group-heading] { + user-select: none; + font-size: 12px; + color: oklch(0.55 0.02 145); + padding: 0 8px; + margin-bottom: 8px; + font-weight: 500; +} + +[cmdk-input], +[data-cmdk-input] { + outline: none !important; + box-shadow: none !important; + border: none !important; +} + +[cmdk-input]:focus, +[data-cmdk-input]:focus, +[cmdk-input]:focus-visible, +[data-cmdk-input]:focus-visible { + outline: none !important; + box-shadow: none !important; + border: none !important; +} diff --git a/src/components/login-effects/AICoreEffect.tsx b/src/components/login-effects/AICoreEffect.tsx new file mode 100644 index 0000000..cddce22 --- /dev/null +++ b/src/components/login-effects/AICoreEffect.tsx @@ -0,0 +1,223 @@ +import { useState } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import AIBanner from '../AIBanner' +import { Cpu, ArrowRight } from 'lucide-react' + +export default function AICoreEffect({ onUnlock }: { onUnlock: () => void }) { + const [synced, setSynced] = useState(false) + const [progress, setProgress] = useState(0) + + const handleSync = () => { + if (synced || progress > 0) return + + // Auto progress over 2.5 seconds + let p = 0 + const interval = setInterval(() => { + p += 1 + setProgress(p) + if (p >= 100) { + clearInterval(interval) + setSynced(true) + setTimeout(() => onUnlock(), 800) + } + }, 25) + } + + return ( + + {!synced && ( + + {/* Base Background matching the main page */} +
+ +
+ + {/* Overlay to dim the background, focusing attention on the core */} +
+ + {/* MASSIVE GLOWING LOGO (Faithful reproduction of the user's uploaded logo) */} +
0 ? 'opacity-100 scale-[1.05]' : 'opacity-90 scale-100'}`}> + + + + + + + + + + + + + + + + + + + + + + + + + + {/* LEFT DATA HALF (Cyan Hexagon & Particles) */} + + {/* Hexagon Left Outline */} + + {/* Grid of cyan dots inside the left hexagon */} + {Array.from({length: 60}).map((_, i) => { + const x = 22 + Math.random() * 25; + const y = 25 + Math.random() * 50; + return ( + 0.7 ? 1 : 0.5} fill="#06b6d4" + animate={{ opacity: [0.2, 1, 0.2] }} + transition={{ duration: 1 + Math.random() * 2, repeat: Infinity, delay: Math.random() * 2 }} + /> + ) + })} + {/* Incoming Cyan Stream from outside */} + {Array.from({length: 40}).map((_, i) => ( + 0.5 ? 1.5 : 0.8} fill="#06b6d4" + animate={{ x: [0, 45], opacity: [0, 1, 0] }} + transition={{ duration: 1.5 + Math.random() * 1.5, repeat: Infinity, ease: "linear", delay: Math.random() * 2 }} + /> + ))} + + + {/* RIGHT NEURAL HALF (Purple Hexagon & Network) */} + + {/* Hexagon Right Outline */} + + {/* Neural Net nodes inside the right hexagon */} + {Array.from({length: 20}).map((_, i) => { + const x = 52 + Math.random() * 25; + const y = 25 + Math.random() * 50; + return ( + + + + + ) + })} + {/* Outgoing Purple Network lines to outside */} + {Array.from({length: 20}).map((_, i) => { + const startY = 30 + Math.random() * 40; + const endY = 20 + Math.random() * 60; + return ( + + + + + ) + })} + + + {/* THE GLOWING S-CORE (The thick central energy paths) */} + + {Array.from({length: 80}).map((_, i) => { + const offset = (i - 40) * 0.25; // 80 lines packed tightly + const isCenter = Math.abs(offset) < 3; // Make the very center lines extremely bright and white + return ( + 0 ? 0.6 + Math.random() * 0.5 : 2.5 + Math.random() * 2, // Hyper-speed when syncing! + repeat: Infinity, delay: Math.random() * 3, ease: "linear" + }} + /> + ) + })} + + +
+ + {/* Glitchy Scanline Overlay */} +
+ + {/* Sync Interface */} +
+
+ {/* Circular progress SVG */} + + + + + + {/* Hexagon framing the CPU */} + 0 ? 'text-fuchsia-500' : 'text-cyan-500/30'}`} + animate={{ rotate: progress > 0 ? 360 : 0 }} + transition={{ duration: 10, repeat: Infinity, ease: "linear" }} + > + + + + +
+ +
+
+ {progress === 0 ? 'Core Synchronization Required' : progress < 100 ? 'Establishing Neural Link...' : 'Link Established'} +
+ + {progress === 0 && ( + + INITIATE BOOT SEQUENCE + + )} + + {progress > 0 && ( +
+ {progress}% +
+ )} +
+
+ + )} + + {/* Screen flash effect on sync completion */} + {synced && ( + + )} + + ) +} diff --git a/src/components/login-effects/BiometricEffect.tsx b/src/components/login-effects/BiometricEffect.tsx new file mode 100644 index 0000000..fcf6eb1 --- /dev/null +++ b/src/components/login-effects/BiometricEffect.tsx @@ -0,0 +1,105 @@ +import { useState, useRef, useEffect } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { Fingerprint } from 'lucide-react' + +export default function BiometricEffect({ onUnlock }: { onUnlock: () => void }) { + const [progress, setProgress] = useState(0) + const [unlocked, setUnlocked] = useState(false) + const intervalRef = useRef(null) + + const handleStart = () => { + if (unlocked) return + if (intervalRef.current) clearInterval(intervalRef.current) + intervalRef.current = setInterval(() => { + setProgress(p => Math.min(p + 2, 100)) + }, 30) // takes about 1.5 seconds to fill + } + + const handleStop = () => { + if (unlocked) return + if (intervalRef.current) clearInterval(intervalRef.current) + setProgress(p => { + if (p >= 100) return p + return 0 + }) + } + + useEffect(() => { + if (progress >= 100 && !unlocked) { + if (intervalRef.current) clearInterval(intervalRef.current) + setUnlocked(true) + setTimeout(() => onUnlock(), 800) + } + }, [progress, unlocked, onUnlock]) + + useEffect(() => { + return () => { if (intervalRef.current) clearInterval(intervalRef.current) } + }, []) + + return ( + + {!unlocked && ( + +
+ {/* The Fingerprint Button */} + 0 ? 'text-cyan-400 scale-105' : 'text-slate-700' + }`} + style={{ + boxShadow: progress > 0 ? `0 0 ${progress}px rgba(34,211,238,0.4)` : 'none' + }} + > + + + + {/* Circular Progress SVG */} + + + + + + {/* Scanning Laser Line */} + {progress > 0 && progress < 100 && ( + + )} +
+ +

+ {progress > 0 ? (progress === 100 ? 'Access Granted' : 'Scanning...') : 'Hold to Authenticate'} +

+
+ )} + + {/* Iris Wipe Effect */} + {unlocked && ( + + + + )} +
+ ) +} diff --git a/src/components/login-effects/HackerEffect.tsx b/src/components/login-effects/HackerEffect.tsx new file mode 100644 index 0000000..7d4b73f --- /dev/null +++ b/src/components/login-effects/HackerEffect.tsx @@ -0,0 +1,68 @@ +import { useState, useEffect } from 'react' +import { motion, AnimatePresence } from 'framer-motion' + +export default function HackerEffect({ onUnlock }: { onUnlock: () => void }) { + const [hacked, setHacked] = useState(false) + const [text, setText] = useState('') + const fullText = "SYSTEM LOCKED.\nAWAITING OVERRIDE...\n\n> " + + useEffect(() => { + let i = 0 + const timer = setInterval(() => { + setText(fullText.slice(0, i)) + i++ + if (i > fullText.length) clearInterval(timer) + }, 40) + return () => clearInterval(timer) + }, []) + + const handleHack = () => { + setHacked(true) + setTimeout(() => onUnlock(), 1200) + } + + return ( + + {!hacked ? ( + + {/* CRT Scanline Effect */} +
+ +
+ {text}_ +
+ + {text.length >= fullText.length && ( + + [ Initiate Hack ] + + )} + + ) : ( + + {/* Glitch visuals */} + + SYSTEM OVERRIDE + + + )} + + ) +} diff --git a/src/components/login-effects/LampEffect.tsx b/src/components/login-effects/LampEffect.tsx new file mode 100644 index 0000000..6a83b74 --- /dev/null +++ b/src/components/login-effects/LampEffect.tsx @@ -0,0 +1,79 @@ +import { useState, useEffect } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { Power } from 'lucide-react' + +export default function LampEffect({ onUnlock }: { onUnlock: () => void }) { + const [mousePos, setMousePos] = useState({ x: -1000, y: -1000 }) + const [isHoveringSwitch, setIsHoveringSwitch] = useState(false) + const [unlocked, setUnlocked] = useState(false) + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + setMousePos({ x: e.clientX, y: e.clientY }) + } + window.addEventListener('mousemove', handleMouseMove) + return () => window.removeEventListener('mousemove', handleMouseMove) + }, []) + + const handleTurnOn = () => { + setUnlocked(true) + setTimeout(() => onUnlock(), 800) // Give time for flicker + } + + return ( + + {!unlocked && ( + + {/* Flashlight mask */} +
+ + {/* The switch */} +
+ +

+ Main Power +

+
+ + {/* Custom Cursor */} +
+ + )} + + {/* Power On Flicker Effect */} + {unlocked && ( + + )} + + ) +} diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..b4c30cc --- /dev/null +++ b/src/components/ui/context-menu.tsx @@ -0,0 +1,82 @@ +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" + +import { cn } from "@/lib/utils" + +const ContextMenu = ContextMenuPrimitive.Root +const ContextMenuTrigger = ContextMenuPrimitive.Trigger +const ContextMenuGroup = ContextMenuPrimitive.Group +const ContextMenuPortal = ContextMenuPrimitive.Portal +const ContextMenuSub = ContextMenuPrimitive.Sub +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef & { inset?: boolean } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuLabel = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef & { inset?: boolean } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuLabel, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuRadioGroup, +} diff --git a/src/index.css b/src/index.css index 00188b9..34a7a4a 100644 --- a/src/index.css +++ b/src/index.css @@ -3,34 +3,34 @@ @custom-variant dark (&:is(.dark *)); @theme { - --color-background: oklch(0.98 0.01 145); - --color-foreground: oklch(0.15 0.02 145); + --color-background: oklch(0.98 0.01 var(--theme-hue, 145)); + --color-foreground: oklch(0.15 0.02 var(--theme-hue, 145)); --color-card: oklch(1 0 0); - --color-card-foreground: oklch(0.15 0.02 145); + --color-card-foreground: oklch(0.15 0.02 var(--theme-hue, 145)); --color-popover: oklch(1 0 0); - --color-popover-foreground: oklch(0.15 0.02 145); - --color-primary: oklch(0.55 0.12 145); + --color-popover-foreground: oklch(0.15 0.02 var(--theme-hue, 145)); + --color-primary: oklch(var(--theme-l, 0.55) var(--theme-c, 0.12) var(--theme-hue, 145)); --color-primary-foreground: oklch(1 0 0); - --color-secondary: oklch(0.96 0.01 145); - --color-secondary-foreground: oklch(0.35 0.08 145); - --color-muted: oklch(0.96 0.01 145); - --color-muted-foreground: oklch(0.55 0.02 145); - --color-accent: oklch(0.96 0.01 145); - --color-accent-foreground: oklch(0.35 0.08 145); + --color-secondary: oklch(0.96 0.01 var(--theme-hue, 145)); + --color-secondary-foreground: oklch(0.35 0.08 var(--theme-hue, 145)); + --color-muted: oklch(0.96 0.01 var(--theme-hue, 145)); + --color-muted-foreground: oklch(0.55 0.02 var(--theme-hue, 145)); + --color-accent: oklch(0.96 0.01 var(--theme-hue, 145)); + --color-accent-foreground: oklch(0.35 0.08 var(--theme-hue, 145)); --color-destructive: oklch(0.58 0.18 25); --color-destructive-foreground: oklch(1 0 0); - --color-border: oklch(0.92 0.01 145); - --color-input: oklch(0.92 0.01 145); - --color-ring: oklch(0.55 0.12 145); + --color-border: oklch(0.92 0.01 var(--theme-hue, 145)); + --color-input: oklch(0.92 0.01 var(--theme-hue, 145)); + --color-ring: oklch(var(--theme-l, 0.55) var(--theme-c, 0.12) var(--theme-hue, 145)); - --color-sidebar-background: oklch(0.985 0.005 100 / 0.8); - --color-sidebar-foreground: oklch(0.30 0.02 140); - --color-sidebar-primary: oklch(0.55 0.12 145); + --color-sidebar-background: oklch(0.985 0.005 var(--theme-hue, 145) / 0.8); + --color-sidebar-foreground: oklch(0.30 0.02 var(--theme-hue, 145)); + --color-sidebar-primary: oklch(var(--theme-l, 0.55) var(--theme-c, 0.12) var(--theme-hue, 145)); --color-sidebar-primary-foreground: oklch(0.99 0 0); - --color-sidebar-accent: oklch(0.95 0.02 135); - --color-sidebar-accent-foreground: oklch(0.35 0.08 145); - --color-sidebar-border: oklch(0.90 0.01 110 / 0.3); - --color-sidebar-ring: oklch(0.55 0.12 145); + --color-sidebar-accent: oklch(0.95 0.02 var(--theme-hue, 145)); + --color-sidebar-accent-foreground: oklch(0.35 0.08 var(--theme-hue, 145)); + --color-sidebar-border: oklch(0.90 0.01 var(--theme-hue, 145) / 0.3); + --color-sidebar-ring: oklch(var(--theme-l, 0.55) var(--theme-c, 0.12) var(--theme-hue, 145)); --radius-lg: 1.5rem; --radius-md: 1rem; @@ -41,34 +41,34 @@ } :root.dark { - --color-background: oklch(0.18 0.02 145); - --color-foreground: oklch(0.96 0.01 130); - --color-card: oklch(0.22 0.02 145); - --color-card-foreground: oklch(0.96 0.01 130); - --color-popover: oklch(0.22 0.02 145); - --color-popover-foreground: oklch(0.96 0.01 130); - --color-primary: oklch(0.65 0.15 145); - --color-primary-foreground: oklch(0.12 0.02 145); - --color-secondary: oklch(0.28 0.03 145); - --color-secondary-foreground: oklch(0.92 0.02 145); - --color-muted: oklch(0.25 0.02 145); - --color-muted-foreground: oklch(0.70 0.02 145); - --color-accent: oklch(0.28 0.03 145); - --color-accent-foreground: oklch(0.92 0.02 145); + --color-background: oklch(0.18 0.02 var(--theme-hue, 145)); + --color-foreground: oklch(0.96 0.01 var(--theme-hue, 145)); + --color-card: oklch(0.22 0.02 var(--theme-hue, 145)); + --color-card-foreground: oklch(0.96 0.01 var(--theme-hue, 145)); + --color-popover: oklch(0.22 0.02 var(--theme-hue, 145)); + --color-popover-foreground: oklch(0.96 0.01 var(--theme-hue, 145)); + --color-primary: oklch(var(--theme-l-dark, 0.65) var(--theme-c-dark, 0.15) var(--theme-hue, 145)); + --color-primary-foreground: oklch(0.12 0.02 var(--theme-hue, 145)); + --color-secondary: oklch(0.28 0.03 var(--theme-hue, 145)); + --color-secondary-foreground: oklch(0.92 0.02 var(--theme-hue, 145)); + --color-muted: oklch(0.25 0.02 var(--theme-hue, 145)); + --color-muted-foreground: oklch(0.70 0.02 var(--theme-hue, 145)); + --color-accent: oklch(0.28 0.03 var(--theme-hue, 145)); + --color-accent-foreground: oklch(0.92 0.02 var(--theme-hue, 145)); --color-destructive: oklch(0.58 0.18 25); --color-destructive-foreground: oklch(0.99 0 0); - --color-border: oklch(0.32 0.03 145); - --color-input: oklch(0.32 0.03 145); - --color-ring: oklch(0.65 0.15 145); + --color-border: oklch(0.32 0.03 var(--theme-hue, 145)); + --color-input: oklch(0.32 0.03 var(--theme-hue, 145)); + --color-ring: oklch(var(--theme-l-dark, 0.65) var(--theme-c-dark, 0.15) var(--theme-hue, 145)); - --color-sidebar-background: oklch(0.16 0.02 145 / 0.8); - --color-sidebar-foreground: oklch(0.85 0.02 145); - --color-sidebar-primary: oklch(0.65 0.15 145); - --color-sidebar-primary-foreground: oklch(0.12 0.02 145); - --color-sidebar-accent: oklch(0.28 0.03 145); - --color-sidebar-accent-foreground: oklch(0.92 0.02 145); - --color-sidebar-border: oklch(0.32 0.03 145 / 0.3); - --color-sidebar-ring: oklch(0.65 0.15 145); + --color-sidebar-background: oklch(0.16 0.02 var(--theme-hue, 145) / 0.8); + --color-sidebar-foreground: oklch(0.85 0.02 var(--theme-hue, 145)); + --color-sidebar-primary: oklch(var(--theme-l-dark, 0.65) var(--theme-c-dark, 0.15) var(--theme-hue, 145)); + --color-sidebar-primary-foreground: oklch(0.12 0.02 var(--theme-hue, 145)); + --color-sidebar-accent: oklch(0.28 0.03 var(--theme-hue, 145)); + --color-sidebar-accent-foreground: oklch(0.92 0.02 var(--theme-hue, 145)); + --color-sidebar-border: oklch(0.32 0.03 var(--theme-hue, 145) / 0.3); + --color-sidebar-ring: oklch(var(--theme-l-dark, 0.65) var(--theme-c-dark, 0.15) var(--theme-hue, 145)); } * { @@ -88,15 +88,15 @@ body { line-height: 1.6; letter-spacing: -0.015em; background-image: - radial-gradient(circle at 15% 50%, oklch(0.55 0.12 145 / 0.05), transparent 25%), - radial-gradient(circle at 85% 30%, oklch(0.65 0.15 170 / 0.06), transparent 25%); + radial-gradient(circle at 15% 50%, oklch(0.55 0.12 var(--theme-hue, 145) / 0.05), transparent 25%), + radial-gradient(circle at 85% 30%, oklch(0.65 0.15 var(--theme-hue, 145) / 0.06), transparent 25%); background-attachment: fixed; } :root.dark body { background-image: - radial-gradient(circle at 15% 50%, oklch(0.55 0.12 145 / 0.15), transparent 25%), - radial-gradient(circle at 85% 30%, oklch(0.65 0.15 170 / 0.15), transparent 25%); + radial-gradient(circle at 15% 50%, oklch(0.55 0.12 var(--theme-hue, 145) / 0.15), transparent 25%), + radial-gradient(circle at 85% 30%, oklch(0.65 0.15 var(--theme-hue, 145) / 0.15), transparent 25%); } /* Scrollbar */ @@ -142,5 +142,8 @@ input:focus, textarea:focus, select:focus { @apply transition-shadow duration-15 .glass-card:hover { @apply shadow-soft-lg -translate-y-1 bg-card/90 border-white/80 dark:border-white/20; } /* Skeleton */ -.skeleton { @apply bg-muted animate-pulse rounded; -} +.skeleton { @apply bg-muted animate-pulse rounded; } + +/* Scrollbar hide */ +.scrollbar-none { scrollbar-width: none; -ms-overflow-style: none; } +.scrollbar-none::-webkit-scrollbar { display: none; } diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index d95cec7..8beb2ff 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -1,9 +1,11 @@ import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom' +import { AnimatePresence, motion } from 'framer-motion' +import CommandPalette from '@/components/CommandPalette' import { LayoutDashboard, Users, Shield, MessageSquare, FolderTree, Leaf, LogOut, ChevronDown, Menu, FileText, Settings, Book, Home, Monitor, Hash, Folder, ChevronRight, Search, Bell, ChevronLeft, Bot, Radio, - Image, Music, ScrollText, Sun, Moon, + Image, Music, ScrollText, Sun, Moon, Trophy, Award, Star, Gift, List, } from 'lucide-react' import { useState, useMemo, useEffect } from 'react' import { useAuthStore } from '@/store/auth' @@ -12,11 +14,11 @@ import type { SystemMenu } from '@/api/system' import { Button } from '@/components/ui/button' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { ScrollArea } from '@/components/ui/scroll-area' +import TabBar from '@/components/TabBar' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' // Icon mapping @@ -51,6 +53,11 @@ const iconMap: Record = { music: , scroll: , log: , + trophy: , + award: , + star: , + gift: , + list: , } function getIcon(iconName?: string): React.ReactNode { @@ -195,25 +202,62 @@ export default function AdminLayout() { // ==================== Shell Render ==================== +import { useRef } from 'react' + function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu, navItems, user, onLogout }: { sidebarOpen: boolean; mobileMenuOpen: boolean; toggleSidebar: () => void; setMobileMenu: (v: boolean) => void navItems: NavItem[]; user: import('@/api/system').SystemUser | null; onLogout: () => void }) { + const { setCmdKOpen } = useAppStore() + const location = useLocation() + const spotlightRef = useRef(null) + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (spotlightRef.current) { + spotlightRef.current.style.background = `radial-gradient(600px circle at ${e.clientX}px ${e.clientY}px, rgba(6,182,212,0.25), transparent 40%)` + } + } + window.addEventListener('mousemove', handleMouseMove) + return () => window.removeEventListener('mousemove', handleMouseMove) + }, []) + return ( -
+
+ + {/* Interactive Cursor Spotlight (Illuminates the glass as you move the mouse) */} +
+ + {/* Global Ambient Background */} +
+ {/* Cyan Glow Top Left */} +
+ {/* Purple Glow Bottom Right */} +
+ {/* Subtle Tech Grid */} +
+
+ + {/* Sidebar */}