Commit 19339211 authored by qd01's avatar qd01

初始化提交

parents
Pipeline #44 failed with stages
module.exports = {
ignores: [commit => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'feat', // 新增功能
'fix', // 修复问题 or bug
'style', // 代码风格相关(不影响运行结果)
'perf', // 优化 or 性能提升
'chore', // 依赖更新 or 脚手架配置更新等
'refactor', // 重构
'docs', // 文档 or 注释
'test', // 测试相关
'build', // 打包构建
'ci', // 持续集成
'revert', // 撤销修改(回滚)
'wip', // 开发中
'workflow', // 工作流更新
'types', // 类型定义文件更新
'release', // 发布
],
],
},
}
# 自定义环境变量的 TS 类型在 /types/env.d.ts 文件中定义
# 这个文件定义的配置在所有环境中都会加载,可作为默认配置使用
# 如果在特定环境中定义了相同的配置,可以对其进行覆盖
# 官方文档:https://cn.vitejs.dev/guide/env-and-mode.html
# 环境变量
VITE_APP_ENV=production
# 开发调试
VITE_DEV_PORT=2023
VITE_DEV_TOOLS=true
# 打包构建
VITE_BUILD_OUTPUT_DIR=dist
# 接口域名
VITE_API_BASE_URL=http://xxx.xxx.com
\ No newline at end of file
# 环境变量
VITE_APP_ENV=development
# 开发代理
# VITE_DEV_PROXY_PATH=/api
# VITE_DEV_PROXY_TARGET=http://xxx.xxx.com
\ No newline at end of file
# 环境变量
VITE_APP_ENV=production
# 开发调试
VITE_DEV_TOOLS=false
# 打包构建
VITE_BUILD_OUTPUT_DIR=dist
# 接口域名
VITE_API_BASE_URL=http://xxx.xxx.com
\ No newline at end of file
# 环境变量
VITE_APP_ENV=test
# 打包构建
VITE_BUILD_OUTPUT_DIR=dist-test
# 接口域名
# VITE_API_BASE_URL=http://xxx.xxx.com
\ No newline at end of file
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
dist-test
*.local
# Editor directories and files
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.history
\ No newline at end of file
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
ls:
.dir: kebab-case
src:
.dir: kebab-case
.html: kebab-case
.js: kebab-case
.ts: kebab-case
.d.ts: kebab-case
.css: kebab-case
.scss: kebab-case
.less: kebab-case
.vue: kebab-case | regex:^App$
.jsx: kebab-case
.tsx: kebab-case
ignore:
- .git
- .husky
- .vscode
- .history
- .npm
- .github
- node_modules
- pre
- dist
- public
# 依赖提升,访问node_modules 之外的模块未声明的依赖项
shamefully-hoist=true
# 如果启用了此选项,那么在依赖树中存在缺失或无效的 peer 依赖关系时,命令将执行失败
strict-peer-dependencies=false
# 淘宝源
registry=https://registry.npmmirror.com
# 禁用 npm 锁文件
# package-lock=false
\ No newline at end of file
v18
\ No newline at end of file
module.exports = {
plugins: {
// 这里设置设计稿宽度375,是为了与vant统一,其他尺寸的设计稿可能需要进行调整
// 如果你使用蓝湖,可自定义尺寸为375:https://support.lanhuapp.com/5612/2a6d/6949
// 插件文档:https://github.com/lkxian888/postcss-px-to-viewport-8-plugin
'postcss-px-to-viewport-8-plugin': {
viewportWidth: 375,
mediaQuery: true,
},
},
}
node_modules
public
dist
dist-*
.history
\ No newline at end of file
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-rational-order',
'stylelint-config-recommended-vue',
],
rules: {
'no-empty-source': null,
'alpha-value-notation': null,
'color-function-notation': null,
'color-hex-length': 'short',
'custom-property-pattern': null,
'font-family-no-missing-generic-family-keyword': null,
'function-url-quotes': null,
'no-descending-specificity': null,
'property-no-vendor-prefix': null,
'selector-class-pattern': [
'^([a-z][a-z0-9]*)(((-{1,2})|(_{2}))[a-z0-9]+)*$',
{
message: 'Expected class selector to be BEM, more: http://getbem.com/naming/',
},
],
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['function', 'if', 'for', 'else', 'each', 'mixin', 'apply'],
},
],
'function-no-unknown': [
true,
{
ignoreFunctions: ['fade'],
},
],
'value-no-vendor-prefix': null,
'declaration-empty-line-before': null,
'import-notation': 'string',
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['deep', 'global'],
},
],
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'],
},
],
},
overrides: [
{
files: '**/*.scss',
customSyntax: 'postcss-scss',
},
{
files: '**/*.html',
customSyntax: 'postcss-html',
},
],
}
{
"recommendations": [
"antfu.iconify",
"antfu.goto-alias",
"antfu.unocss",
"bradlc.vscode-tailwindcss",
"csstools.postcss",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"streetsidesoftware.code-spell-checker",
"vue.volar"
]
}
{
// Enable the ESlint flat config support
"eslint.experimental.useFlatConfig": true,
// Disable the default formatter
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "format/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
],
// markdown
"markdownlint.config": {
"single-h1": false,
"no-inline-html": false,
"first-line-h1": false,
"fenced-code-language": false,
"no-duplicate-header": false
},
// extensions
"local-history.enabled": 0,
"cSpell.words": [
"Attributify",
"bumpp",
"Customizer",
"eruda",
"iconify",
"kolorist",
"taze",
"unocss",
"vant",
"Windi",
"zhangsanplus"
],
"npm.packageManager": "pnpm",
"typescript.tsdk": "./node_modules/typescript/lib",
"editor.quickSuggestions": {
"strings": true
},
// css
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore",
"files.associations": {
"*.css": "tailwindcss"
}
}
MIT License
Copyright (c) 2023 zhangsanplus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<div align="center">
<img src="https://github.com/zhangsanplus/ares-admin/blob/main/screenshot/logo.png?raw=true" height="120" />
[![License](https://img.shields.io/npm/l/package.json.svg?style=flat)](https://github.com/zhangsanplus/ares-mobile/blob/main/LICENSE) [![release](https://img.shields.io/github/release/zhangsanplus/ares-mobile.svg)](https://github.com/zhangsanplus/ares-mobile/releases)
[Ares Mobile](https://github.com/zhangsanplus/ares-mobile/tree/mpa) - 基于 Vant4 和 Vue3 的 H5 前端**多页面**模板
</div>
## 🔥 介绍
**Ares Mobile** 是一个基于 Vant4 和 Vite 搭建的 H5 前端模板,以古希腊神话中战神 Ares 命名。它旨在帮助开发者快速搭建各种 H5 项目,简化开发流程,提高开发效率。
- [单页面(SPA)模板](https://github.com/zhangsanplus/ares-mobile)
- [多页面(MPA)模板](https://github.com/zhangsanplus/ares-mobile/tree/mpa)
如果你需要中后台管理系统模板,推荐你使用 [Ares Admin](https://github.com/zhangsanplus/ares-admin) 模板。
## 🌈 安装和使用
- 安装依赖
```bash
# npm i pnpm -g
pnpm i
```
- 开发运行
```bash
pnpm run dev
```
- 编译构建
```bash
# 测试环境
npm run build:test
# 生产环境
npm run build
```
- 新建页面
```sh
# 添加新页面
npm run new
# 输入页面名称(页面名 + 空格 + 中文标题)
# 例如:home 首页
# 或直接在 `src/pages` 或 `public` 目录下新建 html
```
- 升级依赖
```sh
npm run up
```
## ✨ 特性
- [x] 移动端组件库:`vant`
- [x] 原子化CSS:`unocss`
- [x] 应用程序级的JS语言:`typeScript`
- [x] 移动端适配:`postcss-px-to-viewport`
- [x] 开发环境区分:`development + test + production`
- [x] 移动端调试工具:`eruda`
- [x] 旧版浏览器兼容:`plugin-legacy`
- [x] 代码格式化:`eslint`
- [x] CSS格式化:`stylelint`
- [x] 文件目录格式化:`ls-lint`
- [x] 代码提交规范:`commitlint`
## 如何贡献
1. Fork 代码
2. 创建自己的分支: `git checkout -b feat/xxxx`
3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'`
4. 推送您的分支: `git push origin feat/xxxx`
5. 提交 `pull request`
## Git 贡献提交规范
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
module.exports = {
plugins: ['@vue/babel-plugin-jsx'],
}
import fs from 'node:fs'
import path from 'node:path'
import { readPagesJson, updatePagesJson } from './pages'
import type { Plugin } from 'vite'
interface Options {
enabled: boolean
}
export default (options: Options): Plugin => {
let outDir: string
return {
name: 'vite-plugin-toc-html',
config(config, { command }) {
if (command === 'build') {
outDir = config.build?.outDir || 'dist'
} else {
updatePagesJson()
}
},
configureServer(server) {
if (options.enabled) {
server.middlewares.use((req, res, next) => {
if (req.url === '/__toc__.html') {
res.setHeader('Content-Type', 'text/html')
res.end(getHTML())
} else {
next()
}
})
}
},
closeBundle() {
// 是否输出 __toc__.html 页面到打包目录
if (options.enabled) {
fs.writeFileSync(`${outDir}/__toc__.html`, getHTML())
}
},
}
}
function getHTML() {
const data = readPagesJson()
const template = fs.readFileSync(path.resolve(__dirname, './template.html'), 'utf-8')
return template.replace(
/<%-\s*VITE_INJECT_DATA\s*%>/g,
`<script>\nconst pages = ${JSON.stringify(data, null, 2)}\n</script>`,
)
}
import path from 'node:path'
import process from 'node:process'
import fs from 'fs-extra'
interface PageInfo {
path: string
title: string
}
/**
* 获取页面路由目录
* 读取 pages 和 public 文件夹下面的 html 文件
*/
export function getPages() {
const pagesHtmlFiles = getHtmlFilesList('./src/pages')
const staticHtmlFiles = getHtmlFilesList('./public')
const allHtmlFiles = [...pagesHtmlFiles, ...staticHtmlFiles]
return allHtmlFiles.sort((a, b) => a.path.localeCompare(b.path))
}
/**
* 同步 pages.json
*/
export function updatePagesJson() {
writePagesJson(getPages())
console.log('\x1B[32m\n✨目录同步完成!\n\x1B[0m')
}
/**
* 读取 pages.json
*/
export function readPagesJson() {
const pagesJson = path.resolve(process.cwd(), 'pages.json')
if (!fs.pathExistsSync(pagesJson)) return []
const data = fs.readJsonSync(pagesJson)
return Array.isArray(data) ? data : []
}
/**
* 写入 pages.json
*/
function writePagesJson(data: PageInfo[]) {
const pagesJson = path.resolve(process.cwd(), 'pages.json')
return fs.writeJsonSync(pagesJson, data, { spaces: 2 })
}
/**
* 读取文件夹下面所有 html 文件
* @param {*} folderPath
*/
function getHtmlFilesList(folderPath: string): PageInfo[] {
const fullPath = path.resolve(process.cwd(), folderPath)
return fs.readdirSync(fullPath)
.filter((file: string) => file.endsWith('.html'))
.map((fileName: string) => {
const filePath = `${fullPath}/${fileName}`
const content = fs.readFileSync(filePath, 'utf-8')
const titleMatch = /<title>(.*?)<\/title>/s.exec(content)
const path = fileName.replace(/\.html/, '')
const title = titleMatch?.[1].trim() ?? path
return { path, title }
})
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>目录</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
padding: 2vh;
background: #f3f3f3;
}
table,
tr,
th,
td {
border: 1px solid #ebeef5;
}
table {
width: 100%;
background-color: #fff;
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 10px 8px;
}
tr {
transition: background 0.25s;
}
tr:hover {
background: #f5f6f6;
}
td:nth-child(1) {
text-align: center;
}
td {
cursor: pointer;
user-select: text;
}
a {
display: block;
color: #165dff;
text-decoration: none;
}
strong {
font-weight: normal;
color: #333;
}
.container {
max-width: 600px;
min-height: 80vh;
margin: 0 auto;
color: #444;
padding: 12px;
font-size: 14px;
background-color: #fff;
border-radius: 4px;
}
.msg-box {
margin-top: 20px;
color: #aaa;
font-size: 13px;
line-height: 20px;
padding: 10px;
text-align: center;
}
</style>
<%- VITE_INJECT_DATA %>
</head>
<body>
<div class="container">
<table>
<tr>
<th>序号</th>
<th>页面</th>
<th>标题</th>
</tr>
</table>
<div class="msg-box">
<p>启动时默认打开页面,可在 <strong>vite.config.ts</strong> 中关闭</p>
</div>
</div>
<script>
function gengerateUrl(name) {
const pathname = location.pathname.substring(0, location.pathname.lastIndexOf('/'))
const url = `${location.origin}${pathname}/${name}.html`
return url
}
function handleCopy(url, title) {
if (title) {
copyToClipboard(`${title} ${url}`)
} else {
copyToClipboard(url)
}
}
function copyToClipboard(text) {
const textarea = document.createElement('textarea')
document.body.appendChild(textarea)
textarea.style.position = 'fixed'
textarea.style.top = '10px'
textarea.value = text
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
showToast(`已复制:${text}`)
}
function showToast(message) {
const toast = document.createElement('div')
toast.textContent = message
toast.style.cssText = `
position: fixed;
bottom: 30px;
left: 50%;
z-index: 9999;
padding: 8px 10px;
color: #fff;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
transform: translateX(-50%);
`
document.body.appendChild(toast)
setTimeout(() => {
document.body.removeChild(toast)
}, 1000)
}
window.addEventListener('DOMContentLoaded', () => {
const table = document.querySelector('table')
// eslint-disable-next-line no-undef
pages.forEach((page, index) => {
const pageUrl = gengerateUrl(page.path)
const tr = document.createElement('tr')
const td1 = document.createElement('td')
const td2 = document.createElement('td')
const td3 = document.createElement('td')
const p1 = document.createElement('p')
const p2 = document.createElement('p')
const a = document.createElement('a')
a.target = '_black'
td1.appendChild(p1)
td2.appendChild(p2)
td3.appendChild(a)
tr.appendChild(td1)
tr.appendChild(td2)
tr.appendChild(td3)
p1.textContent = index + 1
p1.title = '点击复制链接'
p1.addEventListener('click', () => {
handleCopy(pageUrl)
})
p2.textContent = page.path
p2.title = '点击复制链接'
p2.addEventListener('click', () => {
handleCopy(pageUrl, page.title)
})
a.href = pageUrl
a.title = `${pageUrl}`
a.textContent = page.title
table.appendChild(tr)
})
})
</script>
</body>
</html>
\ No newline at end of file
export { entryPoints } from './mpa'
export { createVitePlugins } from './plugins'
export { createProxy } from './proxy'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import * as glob from 'glob'
// 找出 src/pages 下面的 html 模板,形成配置文件
export function entryPoints() {
const htmlPaths = glob.sync('src/pages/*.html')
const config: Record<string, string> = {}
for (const htmlPath of htmlPaths) {
if (fs.statSync(htmlPath).isFile()) {
const { name } = path.parse(htmlPath)
config[name] = path.resolve(process.cwd(), htmlPath).replace(/\\/g, '/')
}
}
return config
}
/**
* @name createVitePlugins
* @description 封装plugins数组统一调用
*/
import path from 'node:path'
import process from 'node:process'
import vueLegacy from '@vitejs/plugin-legacy'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import UnoCSS from 'unocss/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import eruda from 'vite-plugin-eruda'
import vueSetupExtend from 'vite-plugin-vue-setup-extend'
import tocHtml from '../vite-plugin-toc-html'
import type { PluginOption } from 'vite'
export function pathResolve(dir: string) {
return path.resolve(process.cwd(), dir)
}
export function createVitePlugins(viteEnv: ImportMetaEnv, _isBuild: boolean) {
const isProd = viteEnv.VITE_APP_ENV === 'production'
const isDev = !isProd
const vitePlugins: PluginOption[] = [
vue(),
vueJsx(),
UnoCSS(),
AutoImport({
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
imports: ['vue', '@vueuse/core'],
dts: pathResolve('types/auto-imports.d.ts'),
}),
Components({
dirs: [pathResolve('src/components')],
resolvers: [
// 自动导入 vant 组件
VantResolver({ importStyle: false }),
// 自动注册图标组件
IconsResolver({
customCollections: ['custom'],
}),
],
dts: pathResolve('types/components.d.ts'),
}),
// https://github.com/unplugin/unplugin-icons
Icons({
compiler: 'vue3',
autoInstall: true,
defaultStyle: 'vertical-align: -0.15em;fill: currentcolor;',
customCollections: {
custom: FileSystemIconLoader('src/icons'),
},
iconCustomizer(collection, icon, props) {
props.width = '1em'
props.height = '1em'
},
}),
vueSetupExtend(),
tocHtml({
enabled: isDev,
}),
]
if (viteEnv.VITE_DEV_TOOLS === 'true') {
vitePlugins.push(eruda())
}
if (isProd) {
// 旧版浏览器支持
vitePlugins.push(vueLegacy())
}
return vitePlugins
}
import type { ProxyOptions } from 'vite'
type ProxyTargetList = Record<string, ProxyOptions>
export function createProxy(env: ImportMetaEnv) {
const { VITE_DEV_PROXY_PATH, VITE_DEV_PROXY_TARGET } = env
const proxy: ProxyTargetList = {}
if (VITE_DEV_PROXY_PATH) {
proxy[VITE_DEV_PROXY_PATH] = {
target: VITE_DEV_PROXY_TARGET,
changeOrigin: true,
rewrite: path => path.replace(new RegExp(`^${VITE_DEV_PROXY_PATH}`), ''),
}
}
return proxy
}
// @ts-check
import antfu from '@antfu/eslint-config'
export default antfu(
{
unocss: true,
ignores: [
'dist-*',
'.history',
],
},
{
rules: {
'curly': 0,
'no-console': 0,
'style/brace-style': 0,
'antfu/if-newline': 0,
'vue/block-order': [2, { order: ['template', 'script', 'style'] }],
'vue/component-name-in-template-casing': [2, 'kebab-case', { registeredComponentsOnly: false }],
'import/order': [2, {
alphabetize: { order: 'asc', caseInsensitive: false },
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
pathGroupsExcludedImportTypes: ['type'],
pathGroups: [
{
pattern: '@/**',
group: 'internal',
},
{
pattern: '~/**',
group: 'internal',
},
],
}],
},
},
)
{
"name": "ares-mobile",
"type": "module",
"version": "1.0.3",
"private": true,
"packageManager": "pnpm@8.15.4",
"description": "A mobile (spa and mpa) template based on Vue3 and Vant4",
"author": "zhangsanplus",
"license": "MIT",
"homepage": "https://github.com/zhangsanplus/ares-mobile/tree/mpa",
"repository": {
"type": "git",
"url": "git+https://github.com/zhangsanplus/ares-mobile.git"
},
"keywords": [
"vue3",
"typescript",
"vant",
"mpa",
"spa",
"template"
],
"engines": {
"node": "18.x"
},
"scripts": {
"dev": "vite --host",
"build": "vue-tsc --noEmit && vite build",
"build:test": "vue-tsc --noEmit && vite build --mode test",
"preinstall": "npx only-allow pnpm",
"prepare": "husky install",
"lint:css": "stylelint **/*.{css,scss,vue} --fix",
"lint:js": "eslint --fix",
"lint:ls": "ls-lint",
"lint": "npm run lint:css && npm run lint:js && npm run lint:ls",
"typecheck": "vue-tsc --noEmit",
"up": "taze major -I",
"release": "bumpp",
"new": "node ./scripts/generate.cjs"
},
"dependencies": {
"@vueuse/core": "^10.9.0",
"axios": "^1.6.0",
"dayjs": "^1.11.10",
"mitt": "^3.0.1",
"vant": "^4.8.5",
"vue": "^3.3.0",
"vue-router": "^4.2.0"
},
"devDependencies": {
"@antfu/eslint-config": "^2.8.2",
"@commitlint/cli": "^19.2.0",
"@commitlint/config-conventional": "^19.1.0",
"@iconify-json/carbon": "^1.1.31",
"@ls-lint/ls-lint": "^2.2.2",
"@types/fs-extra": "^11.0.4",
"@types/node": "20.9.0",
"@unocss/eslint-plugin": "^0.58.6",
"@unocss/preset-rem-to-px": "^0.58.6",
"@vitejs/plugin-legacy": "^5.3.2",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"bumpp": "^9.4.0",
"eslint": "^8.57.0",
"fs-extra": "^11.2.0",
"glob": "^10.3.10",
"husky": "^9.0.11",
"kolorist": "^1.8.0",
"lint-staged": "^15.2.2",
"postcss": "^8.4.35",
"postcss-html": "^1.6.0",
"postcss-px-to-viewport-8-plugin": "^1.2.3",
"postcss-scss": "^4.0.9",
"sass": "^1.72.0",
"stylelint": "^16.2.1",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard": "^36.0.0",
"taze": "^0.13.3",
"terser": "^5.29.2",
"typescript": "^5.4.2",
"unocss": "^0.58.6",
"unplugin-auto-import": "^0.17.5",
"unplugin-icons": "^0.18.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.1.6",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-eruda": "^1.0.1",
"vite-plugin-progress": "^0.0.7",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^2.0.6"
},
"lint-staged": {
"src/**/*.{ts,tsx,vue}": [
"eslint --fix"
],
"src/**/*.{css,vue,scss}": [
"stylelint --fix"
],
"src/**": [
"ls-lint"
]
},
"__npminstall_done": false
}
[
{
"path": "404",
"title": "404-页面未找到"
},
{
"path": "home",
"title": "首页"
},
{
"path": "login",
"title": "登录"
},
{
"path": "my",
"title": "我的"
},
{
"path": "shop",
"title": "商城"
}
]
This diff is collapsed.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404-页面未找到</title>
<style>
.container {
max-width: 500px;
margin: 25vh auto;
text-align: center;
}
.error-code {
font-size: 100px;
font-weight: bold;
color: #555;
}
.error-message {
font-size: 14px;
margin-top: 10px;
color: #aaa;
}
.home-link {
display: inline-block;
margin-top: 40px;
padding: 10px 20px;
font-size: 14px;
font-weight: bold;
text-decoration: none;
color: #fff;
background-color: #165dff;
border-radius: 4px;
transition: background-color 0.3s ease;
}
.home-link:hover {
background-color: #124ee6;
}
</style>
</head>
<body>
<div class="container">
<div class="error-code">404</div>
<div class="error-message">抱歉,页面未找到!</div>
<a href="/" class="home-link">返回首页</a>
</div>
</body>
</html>
\ No newline at end of file
const path = require('node:path')
const process = require('node:process')
const fs = require('fs-extra')
const colors = require('kolorist')
const {
renderVueFile,
renderMainFile,
renderHtmlFile,
} = require('./template.cjs')
console.log(
colors.blue('请输入新页面信息: '),
colors.gray('路由名(kebab-case)+空格+中文标题(可选)'),
)
process.stdin.on('data', async (chunk) => {
// 输入信息
const inputValue = String(chunk).trim().toString()
const [inputPath, inputName] = inputValue.split(' ')
const pagePath = convertKebabCase(inputPath)
const pageName = inputName || pagePath
if (!pagePath) {
console.log(colors.red('路由名必填,请重新输入'))
return
}
// 多页面根目录
const root = path.resolve(process.cwd(), './src/pages')
const htmlFile = path.resolve(root, `${pagePath}.html`)
const pageDir = path.resolve(root, pagePath)
const vueFile = path.resolve(pageDir, 'App.vue')
const entryFile = path.resolve(pageDir, 'main.ts')
// 判断 HTML 文件夹是否存在
const htmlExists = fs.pathExistsSync(htmlFile)
if (htmlExists) {
console.log(colors.red(`页面\`${pagePath}\`已存在,请重新输入`))
return
}
try {
await fs.mkdirs(pageDir)
console.log()
console.log(
`正在创建 html 文件 ${colors.green('➜')} `,
colors.blue(htmlFile),
)
await fs.outputFile(htmlFile, renderHtmlFile(pagePath, pageName))
console.log(
`正在创建 vue 文件 ${colors.green('➜')} `,
colors.blue(vueFile),
)
await fs.outputFile(vueFile, renderVueFile(pagePath, pageName))
console.log(
`正在创建 main 文件 ${colors.green('➜')} `,
colors.blue(entryFile),
)
await fs.outputFile(entryFile, renderMainFile(pagePath))
console.log()
console.log(colors.green('✨创建完成!'))
} catch (e) {
console.log(colors.red(e.message))
}
process.stdin.emit('end')
})
process.stdin.on('end', () => {
process.exit()
})
/**
* CamelCase 命名转成 kebab-case 命名
*/
function convertKebabCase(str) {
return str.replace(/[A-Z]/g, i => `-${i.toLowerCase()}`)
}
function renderVueFile(pagePath, pageName) {
return `<template>
<div class="container">
${pageName || pagePath}
</div>
</template>
<script setup lang="ts">
// import { ref } from 'vue'
</script>
<style lang="scss">
body {
background-color: #f5f6f6;
}
</style>`
}
function renderMainFile() {
return `import 'virtual:uno.css'
import '@/styles/index.scss'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')`
}
function renderHtmlFile(pagePath, pageName) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<meta name="description" content="ares-mobile是一个基于Vant4和Vue3的H5多页面前端模板" />
<meta name="keywords" content="ares-admin,ares-mobile,ares admin,ares mobile,ares,mpa,vue,h5,template">
<meta name="format-detection" content="telephone=no" />
<title>${pageName}</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./${pagePath}/main.ts"></script>
</body>
</html>`
}
module.exports = {
renderVueFile,
renderMainFile,
renderHtmlFile,
}
import request from '@/utils/request'
import type { PagingRequest, PagingResult } from '@/types'
import type { ArticleType } from './types'
export function getArticleList(params?: PagingRequest & ArticleType.ListParams) {
return request.get<PagingResult<ArticleType.ListItem[]>>('https://api.hsmy.fun/mock/list', params)
}
export function getArticleDetail(id: number) {
return request.get<ArticleType.Detail>(`https://api.hsmy.fun/mock/detail/${id}`)
}
export namespace ArticleType {
// 列表项
export interface ListItem {
id: number
title: string
content: string
createTime: string
updateTime: string
}
// 列表查询参数
export interface ListParams {
keyword?: string
startTime?: string
endTime?: string
}
// 详情
export interface Detail extends ListItem {
// 可以添加列表项中没有的其他字段
author: string
views: number
}
}
\ No newline at end of file
<template>
<div v-if="visible" class="loading-mask">
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
const show = () => {
visible.value = true
}
const hide = () => {
visible.value = false
}
defineExpose({
show,
hide
})
</script>
<style scoped>
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
margin: 0 auto;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
margin-top: 10px;
color: #666;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
\ No newline at end of file
<template>
<div class="tab-bar">
<span @click="handleClick('/home.html')">
<i class="iconfont icon-home"></i>
<span>首页</span>
</span>
<span @click="handleClick('/shop.html')">
<i class="iconfont icon-shop"></i>
<span>商城</span>
</span>
<span @click="handleClick('/my.html')">
<i class="iconfont icon-user"></i>
<span>我的</span>
</span>
</div>
</template>
<script setup>
function gengerateUrl(name) {
const pathname = location.pathname.substring(0, location.pathname.lastIndexOf('/'))
const url = `${location.origin}${pathname}${name}`
return url
}
const handleClick = (path) => {
window.location.href = gengerateUrl(path)
}
// // 带参数跳转
// router.push({
// path: '/home',
// query: { id: 1 }
// })
// // 替换当前页面
// router.replace('/home')
// // 带参数对象跳转
// router.push({
// path: '/shop',
// query: {
// category: 'electronics',
// sort: 'price'
// }
// })
</script>
<style scoped>
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: #fff;
display: flex;
box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #666;
text-decoration: none;
}
.tab-item.router-link-active {
color: #3498db;
}
.tab-item i {
font-size: 20px;
}
.tab-item span {
font-size: 12px;
margin-top: 2px;
}
</style>
\ No newline at end of file
# modal 组件使用说明
调用方式一:
```ts
import Modal from '@/components/x-modal'
Modal.open({
title: '我是标题。。。',
content: '我是内容。。。',
onOk() {
console.log('ok')
},
onClose() {
console.log('close')
},
})
```
调用方式二:
```vue
<x-modal
v-model:visible="modalVisible"
title="我是标题。。。"
content="我是内容。。。"
@ok="handleModalOk"
@close="handleModalClose"
></x-modal>
<script lang="ts" setup>
const modalVisible = ref(true)
function handleModalOk() {
console.log('parent ok')
}
function handleModalClose() {
console.log('parent close')
}
</script>
```
/* eslint-disable ts/no-use-before-define */
import { h, render } from 'vue'
import XModal from './x-modal.vue'
import type { App } from 'vue'
interface XModalProps {
visible?: boolean
title?: string
content: string
okText?: string
onClose?: () => void
onOk?: () => void
}
function isFunction(obj: any): obj is (...args: any[]) => any {
return typeof obj === 'function'
}
const Modal = {
install(app: App) {
app.component(XModal.name!, XModal)
},
open(config: XModalProps) {
let container: HTMLElement | null = document.createElement('div')
const handleOk = () => {
if (vm.component) {
vm.component.props.visible = false
}
if (isFunction(config.onOk)) {
config.onOk()
}
}
const handleClose = () => {
if (container) {
render(null, container)
document.body.removeChild(container)
}
container = null
if (isFunction(config.onClose)) {
config.onClose()
}
}
const defaultConfig = {
visible: true,
onOk: handleOk,
onClose: handleClose,
}
const vm = h(XModal, {
...config,
...defaultConfig,
})
render(vm, container)
document.body.appendChild(container)
return {
close: handleClose,
}
},
}
export default Modal
<template>
<div v-if="visible" class="x-modal-wrap" @click.self="handleClose">
<div class="x-modal" :class="{ 'hidden-title': !title }">
<h3 v-if="title" class="x-modal-header">
{{ title }}
</h3>
<div class="x-modal-body">
<span>{{ content }}</span>
</div>
<div class="x-modal-footer">
<button class="x-modal-btn" @click="handleOk">
{{ okText }}
</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup name="x-modal">
import { computed } from 'vue'
const props = withDefaults(defineProps<ModalProps>(), {
okText: '好的',
})
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'ok'): void
(e: 'close'): void
}>()
interface ModalProps {
visible: boolean
content: string
title?: string
okText?: string
}
const visible = computed<boolean>({
get() {
return props.visible
},
set(val) {
emit('update:visible', val)
},
})
function handleOk() {
visible.value = false
emit('ok')
}
function handleClose() {
visible.value = false
emit('close')
}
</script>
<style lang="scss" scoped>
.x-modal {
position: relative;
width: 320px;
margin-top: -10vh;
padding: 26px 0;
text-align: center;
background-color: #fff;
border-radius: 12px;
&-wrap {
position: fixed;
top: 0;
left: 0;
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: rgb(0 0 0 / 75%);
}
&-header {
margin: 0;
padding: 5px 20px;
color: #333;
font-weight: bold;
font-size: 18px;
}
&-body {
display: flex;
align-items: center;
justify-content: center;
padding: 16px 30px 26px;
color: #888;
font-size: 16px;
line-height: 24px;
}
&-footer {
padding: 0 30px;
}
&-btn {
width: 250px;
height: 50px;
color: #fff;
font-weight: bold;
font-size: 18px;
line-height: 50px;
background: var(--color-primary);
border: none;
border-radius: 25px;
}
&.hidden-title {
.x-modal-body {
min-height: 88px;
}
}
}
</style>
import { Loading } from 'vant'
import { render } from 'vue'
import type { Directive, DirectiveBinding } from 'vue'
export type LoadingBinding = boolean
export interface ElementLoading extends HTMLElement {
instance: HTMLElement
}
const cssText = `
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 100;
background: rgba(255, 255, 255, 1);
display: flex;
align-items: center;
justify-content: center;
`
function createInstance(el: ElementLoading, binding: DirectiveBinding<LoadingBinding>) {
if (!el.instance) {
const wrap = document.createElement('div')
wrap.style.cssText = cssText
const vm = h(Loading, {
type: 'spinner',
})
render(vm, wrap)
if (!['relative', 'absolute'].includes(el.style.position)) {
el.style.position = 'relative'
}
el.appendChild(wrap)
el.instance = wrap
}
el.instance.style.display = binding.value ? 'flex' : 'none'
}
const loadingDirective: Directive = {
mounted(el, binding) {
createInstance(el, binding)
},
updated(el, binding) {
createInstance(el, binding)
},
}
export default loadingDirective
export enum ResponseEnum {
/**
* 成功
*/
SUCCESS = 0,
/**
* 登录过期
*/
LOGIN_EXPIRED = 1000,
/**
* 失败
*/
ERROR = 9999,
// 根据实际情况扩展,如:
// FORBIDDEN = 403,
// NOT_FOUND = 404,
// SERVER_ERROR = 500,
}
export const useLoading = () => {
const showLoading = () => {
const loadingInstance = document.querySelector('#global-loading')
if (loadingInstance) {
// @ts-ignore
loadingInstance.show()
}
}
const hideLoading = () => {
const loadingInstance = document.querySelector('#global-loading')
if (loadingInstance) {
// @ts-ignore
loadingInstance.hide()
}
}
return {
showLoading,
hideLoading
}
}
\ No newline at end of file
<svg width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"></path></svg>
\ No newline at end of file
<template>
<div class="basic-layout">
<div class="layout-content">
<slot></slot>
</div>
<TabBar />
<Loading id="global-loading" />
</div>
</template>
<script setup lang="ts">
import TabBar from '@/components/TabBar.vue'
import Loading from '@/components/Loading.vue'
</script>
<style lang="scss" scoped>
.basic-layout {
min-height: 100vh;
background-color: #f5f6f6;
padding-bottom: 50px;
}
.layout-content {
padding: 15px;
}
</style>
\ No newline at end of file
<template>
<div class="blank-layout">
<slot></slot>
<Loading id="global-loading" />
</div>
</template>
<script setup lang="ts">
import Loading from '@/components/Loading.vue'
</script>
<style lang="scss" scoped>
.blank-layout {
min-height: 100vh;
background-color: #f5f6f6;
}
</style>
\ No newline at end of file
declare namespace ArticleType {
interface ListParams {
title: string
}
interface ListItem {
id: number
title: string
category: string
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<meta name="description" content="ares-mobile是一个基于Vant4和Vue3的H5多页面前端模板" />
<meta name="keywords" content="ares-admin,ares-mobile,ares admin,ares mobile,ares,mpa,vue,h5,template">
<meta name="format-detection" content="telephone=no" />
<title>首页</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./home/main.ts"></script>
</body>
</html>
\ No newline at end of file
<template>
<BasicLayout>
<div class="content">
首页内容
</div>
</BasicLayout>
</template>
<script setup lang="ts">
import BasicLayout from '@/layouts/BasicLayout.vue'
</script>
<style lang="scss">
body {
background-color: #f5f6f6;
margin: 0;
padding: 0;
}
.container {
padding-bottom: 50px;
}
.content {
padding: 15px;
}
</style>
\ No newline at end of file
import 'virtual:uno.css'
import '@/styles/index.scss'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<meta name="description" content="ares-mobile是一个基于Vant4和Vue3的H5多页面前端模板" />
<meta name="keywords" content="ares-admin,ares-mobile,ares admin,ares mobile,ares,mpa,vue,h5,template">
<meta name="format-detection" content="telephone=no" />
<title>登录</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./login/main.ts"></script>
</body>
</html>
\ No newline at end of file
<template>
<BlankLayout>
<div class="login-container">
登录内容
</div>
</BlankLayout>
</template>
<script setup lang="ts">
import BlankLayout from '@/layouts/BlankLayout.vue'
</script>
<style lang="scss">
body {
background-color: #f5f6f6;
}
</style>
\ No newline at end of file
import 'virtual:uno.css'
import '@/styles/index.scss'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<meta name="description" content="ares-mobile是一个基于Vant4和Vue3的H5多页面前端模板" />
<meta name="keywords" content="ares-admin,ares-mobile,ares admin,ares mobile,ares,mpa,vue,h5,template">
<meta name="format-detection" content="telephone=no" />
<title>我的</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./my/main.ts"></script>
</body>
</html>
\ No newline at end of file
<template>
<BasicLayout>
<div class="content">
我的内容
</div>
</BasicLayout>
</template>
<script setup lang="ts">
import BasicLayout from '@/layouts/BasicLayout.vue'
</script>
<style lang="scss">
body {
background-color: #f5f6f6;
margin: 0;
padding: 0;
}
.container {
padding-bottom: 50px;
}
.content {
padding: 15px;
}
</style>
\ No newline at end of file
import 'virtual:uno.css'
import '@/styles/index.scss'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<meta name="description" content="ares-mobile是一个基于Vant4和Vue3的H5多页面前端模板" />
<meta name="keywords" content="ares-admin,ares-mobile,ares admin,ares mobile,ares,mpa,vue,h5,template">
<meta name="format-detection" content="telephone=no" />
<title>商城</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./shop/main.ts"></script>
</body>
</html>
\ No newline at end of file
<template>
<BasicLayout>
<div class="content">
商城内容
</div>
</BasicLayout>
</template>
<script setup lang="ts">
import BasicLayout from '@/layouts/BasicLayout.vue'
</script>
<style lang="scss">
body {
background-color: #f5f6f6;
margin: 0;
padding: 0;
}
.container {
padding-bottom: 50px;
}
.content {
padding: 15px;
}
</style>
\ No newline at end of file
import 'virtual:uno.css'
import '@/styles/index.scss'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
\ No newline at end of file
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
redirect: '/home.html'
},
{
path: '/home.html',
component: () => import('../pages/home/App.vue')
},
{
path: '/shop.html',
component: () => import('../pages/shop/App.vue')
},
{
path: '/my.html',
component: () => import('../pages/my/App.vue')
},
{
path: '/login',
component: () => import('../pages/login/App.vue')
}
]
})
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path !== '/login' && !token) {
next('/login')
} else {
const loadingInstance = document.querySelector('#global-loading')
if (loadingInstance) {
// @ts-ignore
loadingInstance.show()
}
next()
}
})
router.afterEach(() => {
setTimeout(() => {
const loadingInstance = document.querySelector('#global-loading')
if (loadingInstance) {
// @ts-ignore
loadingInstance.hide()
}
}, 500) // 添加一个小延迟,确保页面内容加载完成
})
export default router
\ No newline at end of file
:root {
--color-primary: #165dff;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
position: relative;
width: 100%;
height: 100%;
color: #333;
font-size: 14px;
line-height: 1.5;
}
#app {
width: 100%;
height: 100%;
}
\ No newline at end of file
@import './global.scss';
@import './vant.scss';
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
}
.slide-enter-from {
transform: translateX(100%);
}
.slide-leave-to {
transform: translateX(-100%);
}
\ No newline at end of file
@import 'vant/lib/index.css';
:root {
--van-primary-color: var(--color-primary);
--van-button-default-height: 36px;
}
\ No newline at end of file
// 分页请求参数
export interface PagingRequest {
page: number
pageSize: number
}
// 分页响应结果
export interface PagingResult<T> {
code: number
msg: string
data: {
list: T
total: number
page: number
pageSize: number
}
}
// 通用响应结果
export interface ApiResult<T = any> {
code: number
msg: string
data: T
}
\ No newline at end of file
const TOKEN_KEY = 'app-token'
export function isLogin() {
return !!localStorage.getItem(TOKEN_KEY)
}
export function getToken() {
return localStorage.getItem(TOKEN_KEY)
}
export function setToken(token: string) {
localStorage.setItem(TOKEN_KEY, token)
}
export function clearToken() {
localStorage.removeItem(TOKEN_KEY)
}
import mitt from 'mitt'
// https://www.npmjs.com/package/mitt
interface Events extends Record<string | symbol, unknown> {
foo: string | number
bar: number
}
export default mitt<Events>()
// emitter.on(foo, (e) => {});
// emitter.emit(foo, 42);
/**
* 获取当前页面的URL参数
*/
export function getCurrentURLParams(): Map<string, string> {
const params = new Map<string, string>()
new URLSearchParams(window.location.search).forEach((value, key) => {
params.set(key, value)
})
return params
}
/**
* 获取指定范围内的随机整数
*/
export function randomNumber(min: number, max: number): number {
return Math.floor(Math.random() * (max - min) + min)
}
/**
* sleep
*/
export function sleep(ms = 800) {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* isAndroid
*/
export function isAndroid() {
return /Android/i.test(navigator.userAgent)
}
const { toString } = Object.prototype
export function is(val: unknown, type: string): boolean {
return toString.call(val) === `[object ${type}]`
}
export function isString(val: any): val is string {
return is(val, 'String')
}
export function isNumber(val: any): val is number {
return is(val, 'Number')
}
export function isBoolean(val: any): val is boolean {
return is(val, 'Boolean')
}
export function isObject(val: any): val is Record<string, any> {
return val !== null && is(val, 'Object')
}
export function isEmptyObject(val: any): val is boolean {
return isObject(val) && Object.keys(val).length === 0
}
export function isArray(val: any): val is any[] {
return val && Array.isArray(val)
}
export function isNull(val: any): val is null {
return is(val, 'Null')
}
export function isUndefined(val: any): val is undefined {
return is(val, 'Undefined')
}
export function isFunction(val: any): val is (...args: any[]) => any {
return typeof val === 'function'
}
export function isFile(val: any): val is File {
return is(val, 'File')
}
export function isBlob(val: any): val is Blob {
return is(val, 'Blob')
}
export function isRegExp(val: any) {
return is(val, 'RegExp')
}
export function isExternal(val: any) {
return /^(https?:|mailto:|tel:)/.test(val)
}
import axios from 'axios'
import { showToast } from 'vant'
import { ResponseEnum } from '@/enums/http'
import { getToken } from './auth'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import { useLoading } from '@/hooks/useLoading'
const { showLoading, hideLoading } = useLoading()
class Request {
private instance: AxiosInstance
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config)
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
showLoading()
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
hideLoading()
return Promise.reject(error)
}
)
// 响应拦截器
this.instance.interceptors.response.use(
(response) => {
hideLoading()
const res = response.data
// 响应数据为二进制流
if (res instanceof ArrayBuffer) return res
// 响应错误
if (res.code !== ResponseEnum.SUCCESS) {
return Promise.reject(res.msg || 'Request Error')
}
return res
},
(error) => {
hideLoading()
if (axios.isCancel(error)) {
console.error(error.message)
} else if (error?.message) {
showToast(error.message)
}
return Promise.reject(error)
}
)
}
// 修改 get 方法的参数
get<T>(url: string, params?: any): Promise<T> {
return this.instance.get(url, { params })
}
post<T>(url: string, data?: any): Promise<T> {
return this.instance.post(url, data)
}
put<T>(url: string, data?: any): Promise<T> {
return this.instance.put(url, data)
}
delete<T>(url: string, params?: any): Promise<T> {
return this.instance.delete(url, { params })
}
}
export default new Request({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
{
// https://cn.vitejs.dev/guide/features.html#typescript
"compilerOptions": {
"target": "esnext",
"jsx": "preserve",
"lib": ["esnext", "dom"],
"experimentalDecorators": true,
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"types": [
"node",
"vite/client"
],
"strict": true,
"importHelpers": true,
"sourceMap": true,
"esModuleInterop": true,
"isolatedModules": true,
"skipLibCheck": true
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/**/*.d.ts",
"vite.config.ts"
],
"exclude": ["node_modules", "dist"]
}
This diff is collapsed.
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
Loading: typeof import('./../src/components/Loading.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TabBar: typeof import('./../src/components/TabBar.vue')['default']
XModal: typeof import('./../src/components/x-modal/x-modal.vue')['default']
}
}
interface ImportMetaEnv {
/** 环境变量 */
VITE_APP_ENV: 'production' | 'development' | 'test'
/** 开发服务端口号 */
VITE_DEV_PORT: number
/** 开发代理路径 */
VITE_DEV_PROXY_PATH?: string
/** 开发代理地址 */
VITE_DEV_PROXY_TARGET?: string
/** 调试工具 eruda */
VITE_DEV_TOOLS: 'true' | 'false'
/** 打包生成的文件目录 */
VITE_BUILD_OUTPUT_DIR: string
/** 接口地址 */
VITE_API_BASE_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
/**
* 普通函数
*/
interface Fn<T = any, R = T> {
(...arg: T[]): R
}
/**
* 异步函数
*/
interface PromiseFn<T = any, R = T> {
(...arg: T[]): Promise<R>
}
/**
* 分页请求
*/
interface PagingRequest {
pageNum: number
pageSize: number
}
/**
* 返回分页信息
*/
interface PagingResult<T> {
count: number
list: T
}
/**
* 返回封装
*/
interface HttpResponse<T = unknown> {
msg: string
code: number
data: T
}
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, any>
export default component
}
import presetRemToPx from '@unocss/preset-rem-to-px'
import {
defineConfig,
presetAttributify,
presetUno,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
/**
* 官方预设:https://alfred-skyblue.github.io/unocss-docs-cn/presets/
* 调试地址:http://localhost:2023/__unocss.html
* 开发调试:https://unocss.dev/tools/inspector
*/
export default defineConfig({
presets: [
// 默认预设
presetUno(),
// 启用属性化模式的其他规则
presetAttributify(),
// 将 rem 转换为 px
presetRemToPx({
// baseFontSize: 4, // 1rem = 4px
}),
],
transformers: [
// 用于 CSS 指令(如 @apply)的转换器
transformerDirectives(),
// 用于 Windi CSS 的变体组转换器
transformerVariantGroup(),
],
theme: {
colors: {
primary: '#165dff',
success: '#00b42a',
warning: '#faad14',
danger: '#f53f3f',
},
},
shortcuts: {
btn: 'px-4 py-2 flex items-center rounded-1 border-none text-white bg-primary hover:(bg-primary/70) active:(bg-primary/90)',
},
})
import path from 'node:path'
import process from 'node:process'
import { defineConfig, loadEnv } from 'vite'
import { createProxy, createVitePlugins, entryPoints } from './build/vite'
import type { ConfigEnv, UserConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
const root = process.cwd()
const isBuild = command === 'build'
const isProd = mode === 'production'
const env = loadEnv(mode, root) as ImportMetaEnv
return {
base: './',
root: path.resolve(root, 'src/pages'),
publicDir: path.resolve(root, 'public'),
resolve: {
alias: [
{
find: /@\//,
replacement: `${path.resolve(__dirname, './src')}/`,
},
],
},
server: {
host: '0.0.0.0',
port: env.VITE_DEV_PORT,
proxy: createProxy(env),
open: '/__toc__.html',
},
plugins: createVitePlugins(env, isBuild),
esbuild: {
drop: isProd ? ['console', 'debugger'] : [],
},
build: {
emptyOutDir: true,
outDir: path.resolve(root, env.VITE_BUILD_OUTPUT_DIR),
chunkSizeWarningLimit: 2000,
rollupOptions: {
input: entryPoints(),
output: {
manualChunks: {
vue: ['vue', '@vueuse/core'],
},
},
},
},
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment