使用routify路由

This commit is contained in:
zhengw
2025-09-08 09:14:31 +08:00
commit 2417f801ec
42 changed files with 3334 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,68 @@
<script context="module">
export const load = ({ route }) => ({
status: 404,
error: '[Routify] Page could not be found.',
props: { url: route.url },
})
const isDev = import.meta.env?.DEV
</script>
<script>
export let url
</script>
<!-- routify:meta inline=false -->
<!-- This file was created by Routify.
To customize the error page, create a catchall page
in the root of your project. Eg. [...404].svelte -->
<div class="four04">
<h1>404 - Page Not Found</h1>
<p>
The page <code>{url}</code> could not be found. Please check the URL or go back to
the <a href="/">homepage</a>.
</p>
{#if isDev}
<div>
<h5>Dev note:</h5>
<ul>
<li>
To customize this page, create a
<code>[...404].svelte</code> file in the root of your project.
</li>
<li>
You can copy this file from <code
>.routify/components/[...404].svelte</code
>.
</li>
<li>
Custom 404 files can be created at any level of your project. For
example, in <code>src/pages/blog/[...404].svelte</code>.
</li>
</ul>
</div>
{/if}
</div>
<style>
div {
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
}
div.four04 > * {
margin-top: 1em;
}
ul {
padding: 0;
}
ul li {
opacity: 75%;
list-style-type: none;
padding: 0;
margin: 0;
}
</style>

View File

@@ -0,0 +1,7 @@
import { Router, createRouter } from '@roxi/routify'
import routes from './routes.default.js'
// remove previous routers to avoid bumping router names (/path => /1/path)
globalThis.__routify.reset()
export const router = createRouter({routes})
export { Router, routes }

6
.routify/render.js Normal file
View File

@@ -0,0 +1,6 @@
import * as module from '../src/App.svelte'
import { renderModule } from '@roxi/routify/tools/ssr5.js'
import { map } from './route-map.js'
export const render = url => renderModule(module, { url, routesMap: map })

4
.routify/route-map.js Normal file
View File

@@ -0,0 +1,4 @@
export const map = {
'default': () => import('./routes.default.js').then(m => m.default)
}

144
.routify/routes.default.js Normal file
View File

@@ -0,0 +1,144 @@
// @ts-nocheck
export const routes = {
"meta": {},
"id": "_default",
"name": "",
"file": {
"path": "src/routes/_module.svelte",
"dir": "src/routes",
"base": "_module.svelte",
"ext": ".svelte",
"name": "_module"
},
"asyncModule": () => import('../src/routes/_module.svelte'),
"rootName": "default",
"routifyDir": import.meta.url,
"children": [
{
"meta": {
"dynamic": true,
"order": false,
"dynamicSpread": true,
"hideInMenu": true
},
"id": "_default_____404__svelte",
"name": "[...404]",
"file": {
"path": "src/routes/[...404].svelte",
"dir": "src/routes",
"base": "[...404].svelte",
"ext": ".svelte",
"name": "[...404]"
},
"asyncModule": () => import('../src/routes/[...404].svelte'),
"children": []
},
{
"meta": {
"isDefault": true,
"title": "主页",
"redirect": "/login"
},
"id": "_default_index_svelte",
"name": "index",
"file": {
"path": "src/routes/index.svelte",
"dir": "src/routes",
"base": "index.svelte",
"ext": ".svelte",
"name": "index"
},
"asyncModule": () => import('../src/routes/index.svelte'),
"children": []
},
{
"meta": {},
"id": "_default_login",
"name": "login",
"module": false,
"file": {
"path": "src/routes/login",
"dir": "src/routes",
"base": "login",
"ext": "",
"name": "login"
},
"children": [
{
"meta": {
"isDefault": true,
"title": "登录页面",
"hideInMenu": true,
"reset": true
},
"id": "_default_login_index_svelte",
"name": "index",
"file": {
"path": "src/routes/login/index.svelte",
"dir": "src/routes/login",
"base": "index.svelte",
"ext": ".svelte",
"name": "index"
},
"asyncModule": () => import('../src/routes/login/index.svelte'),
"children": []
}
]
},
{
"meta": {
"title": "订单管理"
},
"id": "_default_orders",
"name": "orders",
"file": {
"path": "src/routes/orders/_module.svelte",
"dir": "src/routes/orders",
"base": "_module.svelte",
"ext": ".svelte",
"name": "_module"
},
"asyncModule": () => import('../src/routes/orders/_module.svelte'),
"children": [
{
"meta": {
"isDefault": true,
"title": "订单管理",
"auth": "SF_XXX_2"
},
"id": "_default_orders_index_svelte",
"name": "index",
"file": {
"path": "src/routes/orders/index.svelte",
"dir": "src/routes/orders",
"base": "index.svelte",
"ext": ".svelte",
"name": "index"
},
"asyncModule": () => import('../src/routes/orders/index.svelte'),
"children": []
},
{
"meta": {
"title": "销售订单",
"auth": "SF_XXX_SAle"
},
"id": "_default_orders_sale_svelte",
"name": "sale",
"file": {
"path": "src/routes/orders/sale.svelte",
"dir": "src/routes/orders",
"base": "sale.svelte",
"ext": ".svelte",
"name": "sale"
},
"asyncModule": () => import('../src/routes/orders/sale.svelte'),
"children": []
}
]
}
]
}
export default routes

18
.routify/routify-init.js Normal file
View File

@@ -0,0 +1,18 @@
import { appInstance, preloadUrl } from '@roxi/routify'
import { map } from './route-map.js'
appInstance.routeMaps = map
// We need to import the App module since a router is likely declared here. This saves us pre-creating the router in the preload step below.
import * as module from '../src/App.svelte'
const preloadPromise = Promise.all([
module.load?.(),
// PreloadUrl parses the url and preloads each url chunk in a router that matches its name. So for '/hello;widget=/world',
// it will preload '/hello' in the default router and '/world' in the 'widget' router.
// If the respective routers don't exist, preloadUrl will use routesMap to pre-create a router and match it with the url chunk.
preloadUrl({ routesMap: map })
])
export const app = preloadPromise.then(() => import('../src/main.js'))

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

47
README.md Normal file
View File

@@ -0,0 +1,47 @@
# Svelte + Vite
This template should help get you started developing with Svelte in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `checkJs` in the JS template?**
It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/sveltejs/svelte-hmr/tree/master/packages/svelte-hmr#preservation-of-local-state).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```js
// store.js
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

36
jsconfig.json Normal file
View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
}

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "free_erp_admin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.1.4",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.12",
"@types/node": "^24.3.0",
"flowbite": "^3.1.2",
"flowbite-svelte": "^1.13.7",
"flowbite-svelte-icons": "^3.0.0",
"svelte": "^5.38.6",
"svelte-hot-french-toast": "^4.0.0",
"tailwindcss": "^4.1.12",
"vite": "^6.3.5"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
},
"dependencies": {
"@hvniel/svelte-router": "^0.0.2",
"@roxi/routify": "3.0.0-next.293",
"axios": "^1.11.0",
"qs": "^6.14.0"
}
}

2210
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

73
src/App.svelte Normal file
View File

@@ -0,0 +1,73 @@
<!-- <script lang="ts">
import "./app.css";
import Error from "./layouts/Error.svelte";
import About from "./pages/About.svelte";
import Home from "./pages/Home.svelte";
import { createHashRouter, RouterProvider } from "@hvniel/svelte-router";
import AppLayout from "./layouts/AppLayout.svelte";
import User, { userLoader } from "./pages/User.svelte";
const router = createHashRouter([
{
// path: "/home",
Component: AppLayout,
children: [
{
index: true,
Component: Home,
},
// {
// path: "/home",
// index: true,
// Component: Home,
// },
{
path: "/user",
index: true,
Component: User,
loader: userLoader,
},
{
path: "/about",
index: true,
Component: About,
},
{
path: "*",
Component: Error,
},
],
},
{
path: "*",
Component: Error,
},
]);
</script>
<RouterProvider {router} /> -->
<script context="module">
import { Router, createRouter } from "@roxi/routify";
import { Toaster } from "svelte-hot-french-toast";
import routes from "../.routify/routes.default.js";
export const router = createRouter({
routes,
urlRewrite: {
toExternal: (url) => `#${url}`, // prepend URLs with #
toInternal: (url) => url.replace(/^.+#/, ""), // remove leading #
},
beforeUrlChange({ route }) {
// console.log(route);
const { meta } = route;
// console.log(meta);
document.title = meta["title"] || "";
return true;
},
});
</script>
<Router {router} />
<Toaster position={"top-end"} />

43
src/app.css Normal file
View File

@@ -0,0 +1,43 @@
@import "tailwindcss";
@plugin 'flowbite/plugin';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-primary-50: #fff5f2;
--color-primary-100: #fff1ee;
--color-primary-200: #ffe4de;
--color-primary-300: #ffd5cc;
--color-primary-400: #ffbcad;
--color-primary-500: #fe795d;
--color-primary-600: #ef562f;
--color-primary-700: #eb4f27;
--color-primary-800: #cc4522;
--color-primary-900: #a5371b;
--color-secondary-50: #f0f9ff;
--color-secondary-100: #e0f2fe;
--color-secondary-200: #bae6fd;
--color-secondary-300: #7dd3fc;
--color-secondary-400: #38bdf8;
--color-secondary-500: #0ea5e9;
--color-secondary-600: #0284c7;
--color-secondary-700: #0369a1;
--color-secondary-800: #075985;
--color-secondary-900: #0c4a6e;
}
@source "../node_modules/flowbite-svelte/dist";
@source "../node_modules/flowbite-svelte-icons/dist";
a {
color: blue;
}
@layer base {
/* disable chrome cancel button */
input[type="search"]::-webkit-search-cancel-button {
display: none;
}
}

1
src/assets/svelte.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { Outlet, useNavigate } from "@hvniel/svelte-router";
import { Button } from "flowbite-svelte";
const navigate = useNavigate();
</script>
<div class="layout">
<header>header</header>
<main>
<!-- <Link to="/home">Home</Link>
<Link to="/about">About</Link> -->
<Button onclick={() => navigate("/user")}>User</Button>
<Button onclick={() => navigate("/about")}>About</Button>
<Outlet />
</main>
<footer>© 2024</footer>
</div>

1
src/layouts/Error.svelte Normal file
View File

@@ -0,0 +1 @@
<main>Error</main>

34
src/lib/Counter.svelte Normal file
View File

@@ -0,0 +1,34 @@
<script>
import { Button, Modal, P } from "flowbite-svelte";
let count = $state(0);
let open = $state(false);
const increment = () => {
count += 1;
open = !open;
};
</script>
<Button onclick={increment} color="blue" size="xs">Default {count}</Button>
<Modal
title="Terms of Service"
form
bind:open
onaction={({ action }) => alert(`Handle "${action}"`)}
>
<P
>With less than a month to go before the European Union enacts new consumer
privacy laws for its citizens, companies around the world are updating their
terms of service agreements to comply.</P
>
<P
>The European Unions General Data Protection Regulation (G.D.P.R.) goes
into effect on May 25 and is meant to ensure a common set of data rights in
the European Union. It requires organizations to notify users as soon as
possible of high-risk data breaches that could personally affect them.</P
>
{#snippet footer()}
<Button type="submit" value="success">I accept</Button>
<Button type="submit" value="decline" color="alternative">Decline</Button>
{/snippet}
</Modal>

9
src/main.js Normal file
View File

@@ -0,0 +1,9 @@
import { mount } from "svelte";
import "./app.css";
import App from "./App.svelte";
const app = mount(App, {
target: document.getElementById("app"),
});
export default app;

7
src/pages/About.svelte Normal file
View File

@@ -0,0 +1,7 @@
<script>
import { Link } from "@hvniel/svelte-router";
console.log("About");
</script>
<main>About Page</main>
<Link to="/home/index">to Home</Link>

39
src/pages/Home.svelte Normal file
View File

@@ -0,0 +1,39 @@
<script lang="ts">
import { Link } from "@hvniel/svelte-router";
let count = $state(0);
let open = $state(false);
const increment = () => {
count += 1;
open = !open;
};
console.log("Home");
</script>
<main>Home Page</main>
<Link to="/home/about">to About</Link>
<!-- <Button onclick={increment} color="blue" size="xs">Default {count}</Button>
<Modal
title="Terms of Service"
form
bind:open
onaction={({ action }) => alert(`Handle "${action}"`)}
>
<P
>With less than a month to go before the European Union enacts new consumer
privacy laws for its citizens, companies around the world are updating their
terms of service agreements to comply.</P
>
<P
>The European Unions General Data Protection Regulation (G.D.P.R.) goes
into effect on May 25 and is meant to ensure a common set of data rights in
the European Union. It requires organizations to notify users as soon as
possible of high-risk data breaches that could personally affect them.</P
>
{#snippet footer()}
<Button type="submit" value="success">I accept</Button>
<Button type="submit" value="decline" color="alternative">Decline</Button>
{/snippet}
</Modal> -->

19
src/pages/User.svelte Normal file
View File

@@ -0,0 +1,19 @@
<script module>
export const userLoader = async () => {
return {
name: "data.name",
};
};
</script>
<script lang="ts">
import { useLoaderData, useSearchParams } from "@hvniel/svelte-router";
let a = $state(0);
const params = useSearchParams();
console.log(JSON.stringify(params));
const data = useLoaderData<typeof userLoader>();
console.log("data", data);
</script>
<p>User {a} {data?.name}</p>

View File

@@ -0,0 +1,5 @@
export default (ctx) => {
return {
hideInMenu: true,
};
};

View File

@@ -0,0 +1 @@
<main>找不到页面</main>

167
src/routes/_module.svelte Normal file
View File

@@ -0,0 +1,167 @@
<!-- 布局 -->
<script lang="ts">
import { afterUrlChange } from "@roxi/routify";
import {
Sidebar,
SidebarDropdownWrapper,
SidebarGroup,
SidebarItem,
} from "flowbite-svelte";
import { ShoppingBagSolid } from "flowbite-svelte-icons";
import routes from "../../.routify/routes.default.js";
const menu = [];
/** 选择菜单 */
let activeHref = "";
// 展开菜单
let openTitle = "";
export let context;
const { route } = context;
menu.length = 0;
routes.children.forEach((el) => {
const menu2: any[] = [];
if (!el.meta.hideInMenu && el.children.length) {
el.children.forEach((ell) => {
if (!ell.meta.hideInMenu) {
const href =
ell.file.dir.replace("src/routes", "") + "/" + ell.file.name;
menu2.push({
...ell,
href: href,
});
if (href == route.url || href == `${route.url}/index`) {
activeHref = href;
openTitle = el.meta?.title;
}
}
});
}
if (menu2.length) {
menu.push({ ...el, children: menu2 });
}
});
$afterUrlChange;
$afterUrlChange((route) => {
activeHref = "";
const url = route.route.url;
a: for (const element of menu) {
for (const ell of element.children) {
if (ell.href == url || ell.href == `${url}/index`) {
activeHref = ell.href;
openTitle = element.meta?.title;
break a;
}
}
}
});
</script>
<header>
<a href="/">Home</a>
<a href="/user">user</a>
<a href="/about">About</a>
<a href="/orders/sale">orders/sale</a>
<a href="/login">Login</a>
</header>
<section>
<aside>
<Sidebar
isSingle={false}
backdrop={false}
params={{ x: -50, duration: 50 }}
position="absolute"
classes={{ nonactive: "p-2", active: "p-2" }}
class="z-50 h-full aside"
style="width: 100%;"
>
<SidebarGroup>
{#each menu as el}
<SidebarDropdownWrapper
isOpen={openTitle == el.meta?.title}
label={el.meta?.title}
classes={{ btn: "p-2" }}
>
{#snippet icon()}
<ShoppingBagSolid
class="h-5 w-5 text-gray-500 transition duration-75 group-hover:text-gray-900 dark:text-gray-400 dark:group-hover:text-white"
/>
{/snippet}
{#each el.children as ell}
<SidebarItem
label={ell?.meta?.title}
href={ell.href}
active={activeHref == ell.href}
/>
{/each}
</SidebarDropdownWrapper>
{/each}
</SidebarGroup>
</Sidebar>
</aside>
<main>
<div class="main-content">
<!-- {@render children?.()} -->
<slot />
</div>
<footer>footer</footer>
</main>
</section>
<style>
:root {
--header-height: 40px;
--footer-height: 34px;
}
header {
height: var(--header-height);
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
}
section {
display: flex;
flex: 1;
margin: 0;
padding: 0;
}
aside {
width: 200px;
background: rgb(255, 255, 255);
overflow: auto;
height: calc(100vh - var(--header-height));
position: sticky;
left: 0px;
top: var(--header-height);
}
main {
overflow: inherit;
background: rgb(244, 244, 244);
box-sizing: border-box;
min-width: 0px;
flex: 1;
padding: 12px 12px 0px;
}
.main-content {
min-height: calc(
100vh - var(--header-height) - var(--footer-height) - 12px
);
padding: 12px 12px 0px;
background: #fff;
}
footer {
display: flex;
height: 34px;
align-items: center;
justify-content: center;
}
</style>

6
src/routes/index.meta.js Normal file
View File

@@ -0,0 +1,6 @@
export default ctx => {
return {
title: "主页",
redirect: '/login'
}
}

48
src/routes/index.svelte Normal file
View File

@@ -0,0 +1,48 @@
<script context="module">
// 重定向页面
export const load = (ctx) => {
return {
redirect: "/orders",
};
};
</script>
<script lang="ts">
import { goto } from "@roxi/routify";
import { onMount } from "svelte";
$goto;
onMount(() => {
// console.log('"xxxxxxxxxxxxxxxx');
// $goto("/orders");
});
</script>
<main>Home Page</main>
<!-- <Link to="/home/about">to About</Link> -->
<!-- <Button onclick={increment} color="blue" size="xs">Default {count}</Button>
<Modal
title="Terms of Service"
form
bind:open
onaction={({ action }) => alert(`Handle "${action}"`)}
>
<P
>With less than a month to go before the European Union enacts new consumer
privacy laws for its citizens, companies around the world are updating their
terms of service agreements to comply.</P
>
<P
>The European Unions General Data Protection Regulation (G.D.P.R.) goes
into effect on May 25 and is meant to ensure a common set of data rights in
the European Union. It requires organizations to notify users as soon as
possible of high-risk data breaches that could personally affect them.</P
>
{#snippet footer()}
<Button type="submit" value="success">I accept</Button>
<Button type="submit" value="decline" color="alternative">Decline</Button>
{/snippet}
</Modal> -->

View File

@@ -0,0 +1,6 @@
export default ctx => {
return {
title: "登录页面",
hideInMenu: true
}
}

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import { post } from "@/utils/http";
import { goto } from "@roxi/routify";
import { Button, Input } from "flowbite-svelte";
import toast from "svelte-hot-french-toast";
$goto; // manually initialize the helper
const userInfo = $state({
login_name: "zhengw",
password: "123456",
login_type: 1,
});
const login = () => {
post("/Users/login", userInfo).then((res) => {
if (res.err_code == 0) {
toast.success("登录成功");
$goto("/");
} else {
toast.error(`${res.err_code}: ${res.err_code}`);
}
});
};
</script>
<!-- 禁用父模块 -->
<!-- routify:meta reset -->
<main class="login-container">
<div class="login-box">
<div>后台登录</div>
<div>
<Input
size="lg"
placeholder="Large input"
bind:value={userInfo.login_name}
/>
</div>
<div>
<Input
size="lg"
type="password"
placeholder="Large input"
bind:value={userInfo.password}
/>
</div>
<div>
<Button size="lg" style="display: block;width: 100%;" onclick={login}
>登录</Button
>
</div>
</div>
</main>
<style>
.login-container {
display: flex;
width: 100%;
height: 100vh;
height: 100dvh;
align-items: center;
justify-content: center;
}
.login-box {
width: 400px;
border: 1px solid #ddd;
padding: 12px;
display: flex;
flex-direction: column;
row-gap: 12px;
}
</style>

View File

@@ -0,0 +1,5 @@
export default ctx => {
return {
title: "订单管理",
}
}

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import { goto } from "@roxi/routify";
$goto;
// onMount(() => {
// $goto("/orders");
// });
</script>
<slot />

View File

@@ -0,0 +1,6 @@
export default ctx => {
return {
title: "订单管理",
auth: "SF_XXX_2"
}
}

View File

@@ -0,0 +1 @@
<main>Orders - index</main>

View File

@@ -0,0 +1,6 @@
export default ctx => {
return {
title: '销售订单',
auth: "SF_XXX_SAle"
}
}

View File

@@ -0,0 +1 @@
<main>Orders - sale</main>

25
src/utils/common.ts Normal file
View File

@@ -0,0 +1,25 @@
/** 路径前面添加 api */
export const pathAddApiString = (path: any): string => {
/** 生产环境不加 api */
// if (import.meta.env.PROD) {
// return path;
// }
if (typeof path == 'string') {
if (`${path}`.startsWith('http') || `${path}`.startsWith('blob:http')) {
return path;
}
if (`${path}`.startsWith('/')) {
return `/api${path}`;
}
return `/api/${path}`;
}
return path;
};
/** ajax返回的基础类型 */
export type IAjaxDataBase = {
count: number | undefined;
err_msg?: string;
err_code?: number;
[key: string]: any;
};

62
src/utils/http.ts Normal file
View File

@@ -0,0 +1,62 @@
import axios, { type AxiosRequestConfig } from "axios";
import { type IAjaxDataBase, pathAddApiString } from "./common";
// 添加请求拦截器
axios.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
return config;
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 添加响应拦截器
axios.interceptors.response.use(
(response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data;
},
(error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
}
);
const ajax = (
method: string,
url: string,
data: any,
config?: AxiosRequestConfig<any>
) => {
return axios.request<any, IAjaxDataBase>({
headers: {
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
},
method: method,
url: pathAddApiString(url),
data: data,
...config,
});
};
export const post = (
url: string,
data?: any,
config?: AxiosRequestConfig<any>
) => {
return ajax("POST", url, data, config);
};
export const get = (
url: string,
data: any,
config?: AxiosRequestConfig<any>
) => {
return ajax("GET", url, data, config);
};

2
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

8
svelte.config.js Normal file
View File

@@ -0,0 +1,8 @@
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess()
};

40
vite.config.js Normal file
View File

@@ -0,0 +1,40 @@
import routify from "@roxi/routify/vite-plugin";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import tailwindcss from "@tailwindcss/vite";
import path from "node:path";
import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [
tailwindcss(),
svelte(),
routify({
singlePage: true,
render: {
csr: true,
ssg: false,
ssr: false,
},
}),
],
base: "./",
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
hmr: true,
port: 4010,
host: "0.0.0.0",
proxy: {
"/api/": {
target: "http://192.168.1.138:93",
// target: 'http://192.168.1.148:8089',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});