初始提交

This commit is contained in:
焦龙言
2026-07-24 09:43:10 +08:00
commit b5b4721456
793 changed files with 116305 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
+27
View File
@@ -0,0 +1,27 @@
.DS_Store
node_modules/
unpackage/
dist/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.project
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
# 要提交 .env 来确保 jit v2 的开发 watch mode
!.env
src/ignore
pnpm-lock.yaml
+7
View File
@@ -0,0 +1,7 @@
# 设置国内镜像地址
registry=http://registry.npmmirror.com/
# registry=http://registry.npmjs.com/
# 这个是给 pnpm 用的
shamefully-hoist=true
ignore-engines=true
+78
View File
@@ -0,0 +1,78 @@
# ShiErFeng 移动端
基于 `uni-app` 的 `vite` + `vue3` + `tailwindcss` 开发。
## 特性
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/) - 快 & 稳定
- 🎨 [TailwindCSS](https://tailwindcss.com/) - 世界上最流行,生态最好的原子化CSS框架
- 😃 [集成 Iconify](https://github.com/egoist/tailwindcss-icons) - [icones.js.org](https://icones.js.org/) 中的所有图标都为你所用
- 📥 [API 自动加载](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 无需引入
- 🧬 [uni-app 条件编译样式](https://tw.icebreaker.top/docs/quick-start/uni-app-css-macro) - 帮助你在多端更灵活的使用 `TailwindCSS`
- 🦾 [TypeScript](https://www.typescriptlang.org/) & [ESLint](https://eslint.org/) & [Stylelint](https://stylelint.io/) - 样式,类型,统一的校验与格式化规则,保证你的代码风格和质量
## 快速开始
> [!IMPORTANT]
> 推荐使用 `"node": "^20.19.0 || >=22.12.0"` 的 Node.js 版本进行开发!
>
> 另外谨慎升级 `package.json` 中锁定的 `pinia`/`vue`/`@vue/*` 相关包的版本,新版本可能 `uni-app` 没有兼容,造成一些奇怪的 bug
### vscode
使用 `vscode` 的开发者,请先安装 [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 智能提示与感应插件
其他 IDE 请参考: <https://tw.icebreaker.top/docs/quick-start/intelliSense>
### 更换 Appid
把 `src/manifest.json` 中的 `appid`, 更换为你自己的 `appid`, 比如 `uni-app` / `mp-weixin` 平台。
## 升级依赖
- `pnpm up:pkg` 升级除了 `uni-app` 相关的其他依赖
- `pnpm up:uniapp` 升级 `uni-app` 相关的依赖
推荐先使用 `pnpm up:pkg` 升级, 再使用 `pnpm up:uniapp` 进行升级,因为 `pnpm up:uniapp` 很有可能会进行版本的降级已达到和 `uni-app` 版本匹配的效果
## 切换镜像源
默认情况下,走的是淘宝镜像源 : `registry.npmmirror.com`
假如你需要修改镜像源,请修改目录下的 `.npmrc` 文件,然后重新进行 `pnpm i` 安装包即可
## 包管理器
本项目默认使用 `pnpm@10` 进行管理,当然你也可以切换到其他包管理器,比如 `yarn`, `npm`
你只需要把 `pnpm-lock.yaml` 删掉,然后把 `package.json` 中的 `packageManager` 字段去除或者换成你具体的包管理器版本,然后重新安装即可
### weapp-ide-cli
本项目已经集成 `weapp-ide-cli` 可以通过 `cli` 对 `ide` 进行额外操作
- `pnpm open:dev` 打开微信开发者工具,引入 `dist/dev/mp-weixin`
- `pnpm open:build` 打开微信开发者工具,引入 `dist/build/mp-weixin`
[详细信息](https://www.npmjs.com/package/weapp-ide-cli)
## tailwindcss 生态
详见:<https://github.com/aniftyco/awesome-tailwindcss>
你可以在这里找到许多现成的UI,组件模板。
## 单位转换
- `rem` -> `rpx` (默认开启, 见 `vite.config.ts` 中 `uvtw` 插件的 `rem2rpx` 选项)
- `px` -> `rpx` (默认不开启,可在 `postcss.config.ts` 中引入 `postcss-pxtransform` 开启配置)
## Tips
- 升级 `uni-app` 依赖的方式为 `npx @dcloudio/uvm` 后,选择对应的 `Package Manager` 即可。而升级其他包的方式,可以使用 `pnpm up -Li`,这个是 `pnpm` 自带的方式。
- 使用 `vscode` 记得安装官方插件 `stylelint`,`tailwindcss`, 已在 `.vscode/extensions.json` 中设置推荐
+29
View File
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport =
'CSS' in window &&
typeof CSS.supports === 'function' &&
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') +
'" />',
)
</script>
<style>
html {
font-size: 16px !important;
}
</style>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
# netlify 部署文件
# 用于部署此项目的 h5 产物,可删除
[build]
publish = "dist/build/h5"
command = "pnpm run build:h5"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
+98
View File
@@ -0,0 +1,98 @@
{
"name": "shierfeng-fastapi-app",
"version": "1.9.0",
"packageManager": "pnpm@10.28.1",
"scripts": {
"dev": "uni -p mp-weixin",
"build": "npm run build:mp-weixin",
"dev:app": "uni -p app",
"dev:custom": "uni -p",
"dev:h5": "uni --host",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"open:dev": "weapp open -p dist/dev/mp-weixin",
"open:build": "weapp open -p dist/build/mp-weixin",
"weapp:login": "weapp login",
"upload:dev": "weapp upload -p dist/dev/mp-weixin -v 1.0.0 -d \"dev version\"",
"upload:build": "weapp upload -p dist/build/mp-weixin -v 1.0.0 -d \"release version\"",
"postinstall": "weapp-tw patch",
"up:pkg": "pnpm up -rLi \"!@dcloudio/*\"",
"up:uniapp": "pnpx @dcloudio/uvm@latest"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4080720251210001",
"@dcloudio/uni-app-harmony": "3.0.0-4080720251210001",
"@dcloudio/uni-app-plus": "3.0.0-4080720251210001",
"@dcloudio/uni-components": "3.0.0-4080720251210001",
"@dcloudio/uni-h5": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-alipay": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-baidu": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-harmony": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-jd": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-lark": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-qq": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-weixin": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-xhs": "3.0.0-4080720251210001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4080720251210001",
"@vue/shared": "3.4.21",
"@weapp-tailwindcss/merge-v3": "^0.1.5",
"node-forge": "^1.4.0",
"pinia": "2.2.4",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9",
"vuex": "^4.0.2"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-4080720251210001",
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
"@dcloudio/uni-stacktracey": "3.0.0-4080720251210001",
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
"@egoist/tailwindcss-icons": "^1.9.2",
"@icebreakers/stylelint-config": "^1.2.5",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/svg-spinners": "^1.2.4",
"@types/node": "^24.10.1",
"@vue/runtime-core": "^3.5.27",
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"sass": "^1.97.3",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3",
"unplugin-auto-import": "^20.3.0",
"vite": "5.4.21",
"weapp-ide-cli": "^5.0.1",
"weapp-tailwindcss": "^4.9.8"
},
"pnpm": {
"onlyBuiltDependencies": [
"weapp-tailwindcss",
"@weapp-tailwindcss/merge-v3"
]
}
}
+8
View File
@@ -0,0 +1,8 @@
import process from "node:process";
const isH5 = process.env.UNI_PLATFORM === "h5";
const isApp = process.env.UNI_PLATFORM === "app";
const WeappTailwindcssDisabled = isH5 || isApp;
const isMp = !isH5 && !isApp;
export { isApp, isH5, isMp, WeappTailwindcssDisabled };
+13
View File
@@ -0,0 +1,13 @@
import type { AcceptedPlugin } from "postcss";
import autoprefixer from "autoprefixer";
import tailwindcss from "tailwindcss";
import cssMacro from "weapp-tailwindcss/css-macro/postcss";
const plugins: AcceptedPlugin[] = [tailwindcss(), autoprefixer()];
// 可以使用 postcss-pxtransform 来进行 px 转 rpx 的功能
// 详见: https://tw.icebreaker.top/docs/quick-start/css-unit-transform#px-%E8%BD%AC-rpx
plugins.push(cssMacro);
export default plugins;
+79
View File
@@ -0,0 +1,79 @@
<script setup>
import config from "./config";
import { getToken } from "@/utils/auth";
import { useConfigStore } from "@/store";
import { getCurrentInstance } from "vue";
import { onLaunch } from "@dcloudio/uni-app";
const { proxy } = getCurrentInstance();
onLaunch(() => {
initApp();
});
// 初始化应用
function initApp() {
// 初始化应用配置
initConfig();
// 检查用户登录状态
//#ifdef H5
checkLogin();
//#endif
}
function initConfig() {
useConfigStore().setConfig(config);
}
function checkLogin() {
if (!getToken()) {
proxy.$tab.reLaunch("/pages/login");
}
}
</script>
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Global Reset for UniApp/Mobile */
page,
body {
height: 100%;
min-height: 100%;
overflow-x: hidden;
/* Prevent default bounce effect on iOS if needed, or just handle overflow */
}
/* Global box-sizing for consistency with Tailwind preflight */
page,
view,
scroll-view,
image,
text,
button,
input,
textarea,
label,
navigator {
box-sizing: border-box;
}
/* 修复 H5 端 uni.showToast 图标在引入 Tailwind 后可能偏左的问题 */
/* #ifdef H5 */
uni-toast img,
uni-toast svg {
display: inline-block !important;
}
/* #endif */
/* Hide scrollbar for Chrome/Safari/Webkit */
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
</style>
+62
View File
@@ -0,0 +1,62 @@
import request from "@/utils/request";
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid,
};
return request({
url: "/login",
header: {
"Content-Type": "application/x-www-form-urlencoded",
},
headers: {
isToken: false,
},
method: "post",
data: data,
});
}
// 注册方法
export function register(data) {
return request({
url: "/register",
headers: {
isToken: false,
},
method: "post",
data: data,
});
}
// 获取用户详细信息
export function getInfo() {
return request({
url: "/getInfo",
method: "get",
});
}
// 退出方法
export function logout() {
return request({
url: "/logout",
method: "post",
});
}
// 获取验证码
export function getCodeImg() {
return request({
url: "/captchaImage",
headers: {
isToken: false,
},
method: "get",
timeout: 20000,
});
}
@@ -0,0 +1,52 @@
import request from "@/utils/request";
// 查询字典数据列表
export function listData(query) {
return request({
url: "/system/dict/data/list",
method: "get",
params: query,
});
}
// 查询字典数据详细
export function getData(dictCode) {
return request({
url: "/system/dict/data/" + dictCode,
method: "get",
});
}
// 根据字典类型查询字典数据信息
export function getDicts(dictType) {
return request({
url: "/system/dict/data/type/" + dictType,
method: "get",
});
}
// 新增字典数据
export function addData(data) {
return request({
url: "/system/dict/data",
method: "post",
data: data,
});
}
// 修改字典数据
export function updateData(data) {
return request({
url: "/system/dict/data",
method: "put",
data: data,
});
}
// 删除字典数据
export function delData(dictCode) {
return request({
url: "/system/dict/data/" + dictCode,
method: "delete",
});
}
@@ -0,0 +1,60 @@
import request from "@/utils/request";
// 查询字典类型列表
export function listType(query) {
return request({
url: "/system/dict/type/list",
method: "get",
params: query,
});
}
// 查询字典类型详细
export function getType(dictId) {
return request({
url: "/system/dict/type/" + dictId,
method: "get",
});
}
// 新增字典类型
export function addType(data) {
return request({
url: "/system/dict/type",
method: "post",
data: data,
});
}
// 修改字典类型
export function updateType(data) {
return request({
url: "/system/dict/type",
method: "put",
data: data,
});
}
// 删除字典类型
export function delType(dictId) {
return request({
url: "/system/dict/type/" + dictId,
method: "delete",
});
}
// 刷新字典缓存
export function refreshCache() {
return request({
url: "/system/dict/type/refreshCache",
method: "delete",
});
}
// 获取字典选择框列表
export function optionselect() {
return request({
url: "/system/dict/type/optionselect",
method: "get",
});
}
@@ -0,0 +1,41 @@
import upload from "@/utils/upload";
import request from "@/utils/request";
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword) {
const data = {
oldPassword,
newPassword,
};
return request({
url: "/system/user/profile/updatePwd",
method: "put",
data: data,
});
}
// 查询用户个人信息
export function getUserProfile() {
return request({
url: "/system/user/profile",
method: "get",
});
}
// 修改用户个人信息
export function updateUserProfile(data) {
return request({
url: "/system/user/profile",
method: "put",
data: data,
});
}
// 用户头像上传
export function uploadAvatar(data) {
return upload({
url: "/system/user/profile/avatar",
name: data.name,
filePath: data.filePath,
});
}
+109
View File
@@ -0,0 +1,109 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const createPinia: typeof import('pinia').createPinia
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineStore: typeof import('pinia').defineStore
const effectScope: typeof import('vue').effectScope
const getActivePinia: typeof import('pinia').getActivePinia
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const mapActions: typeof import('pinia').mapActions
const mapGetters: typeof import('pinia').mapGetters
const mapState: typeof import('pinia').mapState
const mapStores: typeof import('pinia').mapStores
const mapWritableState: typeof import('pinia').mapWritableState
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onAddToFavorites: typeof import('@dcloudio/uni-app').onAddToFavorites
const onBackPress: typeof import('@dcloudio/uni-app').onBackPress
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onError: typeof import('@dcloudio/uni-app').onError
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onHide: typeof import('@dcloudio/uni-app').onHide
const onLaunch: typeof import('@dcloudio/uni-app').onLaunch
const onLoad: typeof import('@dcloudio/uni-app').onLoad
const onMounted: typeof import('vue').onMounted
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app').onNavigationBarButtonTap
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputChanged
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputClicked
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputConfirmed
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputFocusChanged
const onPageNotFound: typeof import('@dcloudio/uni-app').onPageNotFound
const onPageScroll: typeof import('@dcloudio/uni-app').onPageScroll
const onPullDownRefresh: typeof import('@dcloudio/uni-app').onPullDownRefresh
const onReachBottom: typeof import('@dcloudio/uni-app').onReachBottom
const onReady: typeof import('@dcloudio/uni-app').onReady
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onResize: typeof import('@dcloudio/uni-app').onResize
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onShareAppMessage: typeof import('@dcloudio/uni-app').onShareAppMessage
const onShareTimeline: typeof import('@dcloudio/uni-app').onShareTimeline
const onShow: typeof import('@dcloudio/uni-app').onShow
const onTabItemTap: typeof import('@dcloudio/uni-app').onTabItemTap
const onThemeChange: typeof import('@dcloudio/uni-app').onThemeChange
const onUnhandledRejection: typeof import('@dcloudio/uni-app').onUnhandledRejection
const onUnload: typeof import('@dcloudio/uni-app').onUnload
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const storeToRefs: typeof import('pinia').storeToRefs
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useModel: typeof import('vue').useModel
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
+26
View File
@@ -0,0 +1,26 @@
// 应用全局配置
export default {
baseUrl: "http://localhost:9099",
// 应用信息
appInfo: {
// 应用名称
name: "ShiErFeng-FastAPI-APP",
// 应用版本
version: "1.9.0",
// 应用logo
logo: "/static/logo.png",
// 官方网站
site_url: "https://vfadmin.insistence.tech",
// 政策协议
agreements: [
{
title: "隐私政策",
url: "/pages/common/privacy/index",
},
{
title: "用户服务协议",
url: "/pages/common/agreement/index",
},
],
},
};
+8
View File
@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<object, object, any>;
export default component;
}
+17
View File
@@ -0,0 +1,17 @@
// @ts-nocheck
import { createSSRApp } from "vue";
import App from "./App.vue";
import store from "./store"; // store
import { install } from "./plugins"; // plugins
import "./permission"; // permission
import { useDict } from "@/utils/dict";
export function createApp() {
const app = createSSRApp(App);
app.use(store);
app.config.globalProperties.useDict = useDict;
install(app);
return {
app,
};
}
+70
View File
@@ -0,0 +1,70 @@
{
"name": "ShiErFeng-FastAPI移动端",
"appid": "__UNI__25A9D80",
"description": "",
"versionName": "1.9.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueCompiler": "uni-app",
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": {
"dSYMs": false
},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "wxccd7e2a0911b3397",
"setting": {
"urlCheck": false,
"es6": false,
"minified": true,
"postcss": true
},
"optimization": {
"subPackages": true
},
"usingComponents": true
},
"vueVersion": "3",
"h5": {
"devServer": {
"port": 9090,
"https": false
},
"title": "ShiErFeng-FastAPI-APP",
"router": {
"mode": "hash",
"base": "./"
}
}
}
+132
View File
@@ -0,0 +1,132 @@
{
"pages": [
{
"path": "pages/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/index",
"style": {
"navigationBarTitleText": "ShiErFeng-FastAPI移动端",
"navigationStyle": "custom"
}
},
{
"path": "pages/work/index",
"style": {
"navigationBarTitleText": "工作台"
}
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/mine/avatar/index",
"style": {
"navigationBarTitleText": "修改头像"
}
},
{
"path": "pages/mine/info/index",
"style": {
"navigationBarTitleText": "个人信息"
}
},
{
"path": "pages/mine/info/edit",
"style": {
"navigationBarTitleText": "编辑资料"
}
},
{
"path": "pages/mine/pwd/index",
"style": {
"navigationBarTitleText": "修改密码"
}
},
{
"path": "pages/mine/setting/index",
"style": {
"navigationBarTitleText": "应用设置"
}
},
{
"path": "pages/mine/help/index",
"style": {
"navigationBarTitleText": "常见问题"
}
},
{
"path": "pages/mine/about/index",
"style": {
"navigationBarTitleText": "关于我们"
}
},
{
"path": "pages/common/agreement/index",
"style": {
"navigationBarTitleText": "用户协议"
}
},
{
"path": "pages/common/privacy/index",
"style": {
"navigationBarTitleText": "隐私协议"
}
},
{
"path": "pages/common/webview/index",
"style": {
"navigationBarTitleText": "浏览网页"
}
},
{
"path": "pages/common/textview/index",
"style": {
"navigationBarTitleText": "浏览文本"
}
}
],
"tabBar": {
"color": "#000000",
"selectedColor": "#000000",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index",
"iconPath": "static/images/tabbar/home.png",
"selectedIconPath": "static/images/tabbar/home_.png",
"text": "首页"
},
{
"pagePath": "pages/work/index",
"iconPath": "static/images/tabbar/work.png",
"selectedIconPath": "static/images/tabbar/work_.png",
"text": "工作台"
},
{
"pagePath": "pages/mine/index",
"iconPath": "static/images/tabbar/mine.png",
"selectedIconPath": "static/images/tabbar/mine_.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "ShiErFeng",
"navigationBarBackgroundColor": "#FFFFFF"
}
}
@@ -0,0 +1,63 @@
<template>
<view class="min-h-full bg-white px-5 py-8">
<view class="mb-6 text-center">
<text class="text-xl font-bold text-gray-900">用户协议</text>
</view>
<view class="space-y-6 text-justify text-sm leading-relaxed text-gray-600">
<view>
<text class="mb-2 block font-bold text-gray-800"
>1. 服务条款的确认和接纳</text
>
<text
>欢迎使用ShiErFeng-FastAPI移动端框架(以下简称“本产品”)。本产品的服务所有权和运作权归ShiErFeng-FastAPI所有。用户在使用本产品前,请仔细阅读本协议。当您点击“同意”或实际使用本产品服务时,即视为您已阅读并完全同意本协议的所有条款。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>2. 用户账号与安全</text
>
<text
>您在注册账号时,应提供真实、准确、最新和完整的个人资料。您有责任维护账号和密码的安全,并对以您账号进行的所有活动承担法律责任。如发现账号被非法使用,请立即通知我们。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">3. 用户行为规范</text>
<text
>用户在使用本产品过程中,必须遵守国家法律法规,不得利用本产品从事违法违规活动,包括但不限于:发布虚假信息、侵犯他人知识产权、传播淫秽色情信息、进行网络攻击等。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>4. 服务内容的变更、中断或终止</text
>
<text
>鉴于网络服务的特殊性,我们有权随时变更、中断或终止部分或全部网络服务。对于因服务器维护、网络故障等原因导致的服务中断,我们将尽量提前公告,但不对因此产生的损失承担责任。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">5. 知识产权声明</text>
<text
>本产品包含的所有内容(包括但不限于文字、图片、音频、视频、软件代码等)的知识产权归ShiErFeng-FastAPI或相关权利人所有。未经授权,任何人不得擅自使用、复制、修改或传播。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">6. 免责声明</text>
<text
>本产品按“现状”提供,不包含任何明示或暗示的保证。我们不对因不可抗力、黑客攻击、系统不稳定等原因导致的服务中断或数据丢失承担责任。</text
>
</view>
</view>
<view class="mt-10 text-center text-xs text-gray-400">
<text>更新日期:2024年01月01日</text>
</view>
</view>
</template>
<script setup></script>
@@ -0,0 +1,87 @@
<template>
<view class="min-h-full bg-white px-5 py-8">
<view class="mb-6 text-center">
<text class="text-xl font-bold text-gray-900">隐私协议</text>
</view>
<view class="space-y-6 text-justify text-sm leading-relaxed text-gray-600">
<view>
<text
>ShiErFeng-FastAPI非常重视您的隐私保护。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的个人信息。请您在使用本产品前仔细阅读本政策。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>1. 我们如何收集和使用您的个人信息</text
>
<text>为了向您提供更好的服务,我们可能会收集您的以下信息:</text>
<text class="mt-1 block pl-2"
>•
账号信息:当您注册或登录时,我们需要收集您的用户名、密码、手机号码等信息。</text
>
<text class="mt-1 block pl-2"
>•
设备信息:为了保障账户安全和优化服务,我们会收集您的设备型号、操作系统版本、唯一设备标识符等信息。</text
>
<text class="mt-1 block pl-2"
>•
日志信息:当您使用本产品时,我们会自动收集您的操作日志、IP地址、访问时间等信息。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>2. 我们如何共享、转让、公开披露您的个人信息</text
>
<text
>我们不会向任何第三方共享、转让或公开披露您的个人信息,但以下情况除外:</text
>
<text class="mt-1 block pl-2">• 获得您的明确授权或同意;</text>
<text class="mt-1 block pl-2">• 根据法律法规或司法机关的要求;</text>
<text class="mt-1 block pl-2"
>• 为了维护国家安全、公共安全或重大公共利益。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>3. 我们如何保护您的个人信息</text
>
<text
>我们采取了多种安全措施来保护您的个人信息,包括数据加密、访问控制、安全审计等。我们会尽力防止您的信息被未经授权的访问、篡改或泄露。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">4. 您的权利</text>
<text
>您有权访问、更正或删除您的个人信息。您也可以随时注销账号。如您需要行使这些权利,请通过“联系我们”章节中的方式与我们联系。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>5. 本政策如何更新</text
>
<text
>随着法律法规的变化或业务的发展,我们可能会不时更新本隐私政策。更新后的政策将在本产品中发布,并在生效前通过适当方式通知您。</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">6. 联系我们</text>
<text
>如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们:</text
>
<text class="mt-1 block">邮箱:xxx@xxx.com</text>
</view>
</view>
<view class="mt-10 text-center text-xs text-gray-400">
<text>更新日期:2024年01月01日</text>
</view>
</view>
</template>
<script setup></script>
@@ -0,0 +1,37 @@
<template>
<view class="h-full overflow-y-auto p-4">
<view class="bg-white rounded-xl shadow-sm p-4">
<view
class="text-lg font-bold text-gray-900 mb-3 border-b border-gray-100 pb-2"
>
{{ title }}
</view>
<text class="block text-sm text-gray-600 leading-relaxed">
{{ content }}
</text>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
const title = ref("");
const content = ref("");
onLoad((options) => {
title.value = options.title;
content.value = options.content;
uni.setNavigationBarTitle({
title: options.title,
});
});
</script>
<style>
page {
height: 100%;
background-color: #f9fafb;
}
</style>
@@ -0,0 +1,33 @@
<template>
<view v-if="params.url">
<web-view :webview-styles="webviewStyles" :src="`${params.url}`"></web-view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
const params = ref({});
const webviewStyles = ref({
progress: {
color: "#FF3333",
},
});
const props = defineProps({
src: {
type: String,
default: null,
},
});
onLoad((event) => {
params.value = event;
if (event.title) {
uni.setNavigationBarTitle({
title: event.title,
});
}
});
</script>
+208
View File
@@ -0,0 +1,208 @@
<template>
<view class="flex h-full flex-col bg-gray-50">
<!-- Header Area -->
<view class="bg-white pb-6 rounded-b-3xl shadow-sm z-50">
<!-- Title -->
<view class="pt-12 pb-4 px-4 flex justify-center items-center">
<text class="text-lg font-bold text-gray-800">ShiErFeng-FastAPI移动端</text>
</view>
<!-- Search & Notification -->
<view class="px-4 pb-2">
<view class="flex items-center space-x-3">
<!-- Search Input -->
<view class="flex-1 relative">
<view
class="absolute left-3 top-0 bottom-0 flex items-center text-gray-400"
>
<view class="i-mdi-magnify text-lg"></view>
</view>
<input
type="text"
placeholder="搜索..."
placeholder-class="text-gray-400"
class="w-full h-10 rounded-full bg-gray-100 pl-10 pr-4 text-sm text-gray-800"
/>
</view>
<!-- Notification -->
<view
class="relative flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 text-gray-600 active:scale-95 transition-transform border border-gray-100"
>
<view class="i-mdi-bell-outline text-xl"></view>
<view
class="absolute top-2.5 right-2.5 h-2 w-2 rounded-full bg-red-500 border border-white"
></view>
</view>
</view>
</view>
</view>
<scroll-view
scroll-y
class="flex-1 overflow-hidden"
:show-scrollbar="false"
>
<view class="p-4 space-y-5 pb-24">
<!-- Hero Card -->
<view
class="relative overflow-hidden rounded-3xl bg-gradient-to-r from-indigo-500 to-purple-600 p-6 text-white shadow-xl shadow-indigo-200"
>
<view
class="absolute -right-10 -top-10 h-32 w-32 rounded-full bg-white/20 blur-2xl"
></view>
<view
class="absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-white/20 blur-2xl"
></view>
<view class="relative z-10">
<view class="mb-4 flex items-center justify-between">
<view
class="rounded-lg bg-white/20 px-3 py-1 text-xs font-medium backdrop-blur-sm"
>系统运行正常</view
>
<view class="text-xs opacity-80">刚刚更新</view>
</view>
<view class="flex items-end justify-between">
<view>
<view class="text-3xl font-bold">85%</view>
<view class="text-sm opacity-90">工作效率</view>
</view>
<view
class="h-10 w-24 overflow-hidden rounded-lg bg-white/10 p-1"
>
<view class="h-full w-full flex items-end justify-between px-1">
<view class="w-1 bg-white/50 h-2 rounded-full"></view>
<view class="w-1 bg-white/70 h-4 rounded-full"></view>
<view class="w-1 bg-white/40 h-3 rounded-full"></view>
<view class="w-1 bg-white/90 h-6 rounded-full"></view>
<view class="w-1 bg-white/60 h-4 rounded-full"></view>
</view>
</view>
</view>
</view>
</view>
<!-- Stats Grid -->
<view class="grid grid-cols-2 gap-4">
<view
class="rounded-2xl bg-white p-4 shadow-sm border border-gray-100 flex items-center justify-between"
>
<view>
<view class="text-xs text-gray-500 mb-1">新通知</view>
<view class="text-2xl font-bold text-gray-800">12</view>
</view>
<view
class="flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 text-blue-500"
>
<view class="i-mdi-bell text-xl"></view>
</view>
</view>
<view
class="rounded-2xl bg-white p-4 shadow-sm border border-gray-100 flex items-center justify-between"
>
<view>
<view class="text-xs text-gray-500 mb-1">待办任务</view>
<view class="text-2xl font-bold text-gray-800">5</view>
</view>
<view
class="flex h-12 w-12 items-center justify-center rounded-full bg-purple-50 text-purple-500"
>
<view class="i-mdi-calendar text-xl"></view>
</view>
</view>
</view>
<!-- Quick Functions -->
<view>
<view class="mb-4 flex items-center justify-between">
<view class="text-base font-bold text-gray-800">快捷入口</view>
</view>
<view
class="grid grid-cols-4 gap-2 rounded-2xl bg-white p-4 shadow-sm"
>
<view class="flex flex-col items-center space-y-2">
<view
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-orange-50 text-orange-500"
>
<view class="i-mdi-file-document-edit text-xl"></view>
</view>
<view class="text-xs font-medium text-gray-600">申请</view>
</view>
<view class="flex flex-col items-center space-y-2">
<view
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-green-50 text-green-500"
>
<view class="i-mdi-newspaper text-xl"></view>
</view>
<view class="text-xs font-medium text-gray-600">新闻</view>
</view>
<view class="flex flex-col items-center space-y-2">
<view
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-indigo-50 text-indigo-500"
>
<view class="i-mdi-chart-bar text-xl"></view>
</view>
<view class="text-xs font-medium text-gray-600">统计</view>
</view>
<view class="flex flex-col items-center space-y-2">
<view
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-gray-50 text-gray-400"
>
<view class="i-mdi-calendar text-xl"></view>
</view>
<view class="text-xs font-medium text-gray-600">计划</view>
</view>
</view>
</view>
<!-- Recent Apps -->
<view>
<view class="mb-4 flex items-center justify-between">
<view class="text-base font-bold text-gray-800">最近更新</view>
<view class="text-xs text-blue-500">查看全部</view>
</view>
<view class="space-y-3">
<view
class="flex items-center rounded-2xl bg-white p-4 shadow-sm border border-gray-50"
>
<view
class="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-50 text-blue-500"
>
<view class="i-mdi-clipboard-text-outline text-xl"></view>
</view>
<view class="ml-4 flex-1">
<view class="font-bold text-gray-800">请假申请</view>
<view class="text-xs text-gray-500">等待经理审批</view>
</view>
<view
class="rounded-full bg-yellow-50 px-3 py-1 text-xs font-medium text-yellow-600"
>待处理</view
>
</view>
<view
class="flex items-center rounded-2xl bg-white p-4 shadow-sm border border-gray-50"
>
<view
class="flex h-12 w-12 items-center justify-center rounded-xl bg-green-50 text-green-500"
>
<view class="i-mdi-newspaper text-xl"></view>
</view>
<view class="ml-4 flex-1">
<view class="font-bold text-gray-800">周报</view>
<view class="text-xs text-gray-500">提交成功</view>
</view>
<view
class="rounded-full bg-green-50 px-3 py-1 text-xs font-medium text-green-600"
>已完成</view
>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
const globalConfig = getApp().globalData.config;
</script>
+224
View File
@@ -0,0 +1,224 @@
<template>
<view
class="flex h-full flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6 pb-20 overflow-hidden"
>
<!-- Logo Section -->
<view class="mb-6 flex flex-col items-center">
<view
class="mb-4 flex size-16 items-center justify-center rounded-2xl bg-white shadow-lg"
>
<image
class="size-10"
:src="globalConfig.appInfo.logo"
mode="widthFix"
/>
</view>
<text class="text-xl font-bold tracking-wide text-gray-800"
>ShiErFeng-FastAPI移动端登录</text
>
</view>
<!-- Form Section -->
<view class="w-full rounded-3xl bg-white/80 p-6 shadow-xl backdrop-blur-md">
<!-- Username -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-account text-xl"></view>
</view>
<input
v-model="loginForm.username"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
type="text"
placeholder="请输入账号"
maxlength="30"
/>
</view>
<!-- Password -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-lock text-xl"></view>
</view>
<input
v-model="loginForm.password"
type="password"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="请输入密码"
maxlength="20"
/>
</view>
<!-- Captcha -->
<view
class="mb-8 flex items-center justify-between"
v-if="captchaEnabled"
>
<view class="group relative mr-3 flex-1">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-security text-xl"></view>
</view>
<input
v-model="loginForm.code"
type="number"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="验证码"
maxlength="4"
/>
</view>
<view
class="h-12 w-28 overflow-hidden rounded-xl bg-gray-100 shadow-sm transition-opacity active:opacity-80"
@click="getCode"
>
<image :src="codeUrl" class="size-full object-cover"></image>
</view>
</view>
<!-- Login Button -->
<button
@click="handleLogin"
class="flex h-12 w-full items-center justify-center rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-transform active:scale-95"
>
登 录
</button>
<!-- Footer Links -->
<view class="mt-6 flex flex-col items-center space-y-3">
<view class="flex items-center text-sm text-gray-500" v-if="register">
<text>没有账号?</text>
<text
@click="handleUserRegister"
class="ml-1 font-medium text-blue-600 active:opacity-70"
>立即注册</text
>
</view>
<view
class="flex flex-wrap items-center justify-center text-xs text-gray-400"
>
<text>登录即代表同意</text>
<text
@click="handleUserAgrement"
class="mx-1 text-blue-500 active:opacity-70"
>《用户协议》</text
>
<text>和</text>
<text
@click="handlePrivacy"
class="mx-1 text-blue-500 active:opacity-70"
>《隐私协议》</text
>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getToken } from "@/utils/auth";
import { getCodeImg } from "@/api/login";
import { useConfigStore, useUserStore } from "@/store";
const { proxy } = getCurrentInstance();
const globalConfig = useConfigStore().config;
const codeUrl = ref("");
// 验证码开关
const captchaEnabled = ref(true);
// 用户注册开关
const register = ref(false);
const loginForm = ref({
username: "admin",
password: "admin123",
code: "",
uuid: "",
});
// 用户注册
function handleUserRegister() {
proxy.$tab.redirectTo(`/pages/register`);
}
// 隐私协议
function handlePrivacy() {
proxy.$tab.navigateTo(`/pages/common/privacy/index`);
}
// 用户协议
function handleUserAgrement() {
proxy.$tab.navigateTo(`/pages/common/agreement/index`);
}
// 获取图形验证码
function getCode() {
getCodeImg().then((res) => {
captchaEnabled.value =
res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img;
loginForm.value.uuid = res.uuid;
}
});
}
// 登录方法
async function handleLogin() {
if (loginForm.value.username === "") {
proxy.$modal.msgError("请输入账号");
} else if (loginForm.value.password === "") {
proxy.$modal.msgError("请输入密码");
} else if (loginForm.value.code === "" && captchaEnabled.value) {
proxy.$modal.msgError("请输入验证码");
} else {
proxy.$modal.loading("登录中,请耐心等待...");
pwdLogin();
}
}
// 密码登录
async function pwdLogin() {
useUserStore()
.login(loginForm.value)
.then(() => {
proxy.$modal.closeLoading();
loginSuccess();
})
.catch(() => {
if (captchaEnabled.value) {
getCode();
}
});
}
// 登录成功后,处理函数
function loginSuccess(result) {
// 设置用户信息
useUserStore()
.getInfo()
.then((res) => {
proxy.$tab.reLaunch("/pages/index");
});
}
onLoad(() => {
//#ifdef H5
if (getToken()) {
proxy.$tab.reLaunch("/pages/index");
}
//#endif
});
getCode();
</script>
<style lang="scss" scoped>
page {
background-color: #ffffff;
}
</style>
@@ -0,0 +1,68 @@
<template>
<view class="flex h-full flex-col bg-gray-50 pb-10">
<!-- Header -->
<view class="flex flex-col items-center py-10">
<image
class="size-20 rounded-xl shadow-md"
src="/static/logo200.png"
mode="widthFix"
/>
<text class="mt-4 text-xl font-bold text-gray-800"
>ShiErFeng-FastAPI移动端</text
>
</view>
<!-- Info List -->
<view class="bg-white">
<!-- Version -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4"
>
<text class="text-base text-gray-800">版本信息</text>
<text class="text-sm text-gray-500">v{{ version }}</text>
</view>
<!-- Email -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4"
>
<text class="text-base text-gray-800">官方邮箱</text>
<text class="text-sm text-gray-500">xxx@xxx.com</text>
</view>
<!-- Hotline -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4"
>
<text class="text-base text-gray-800">服务热线</text>
<text class="text-sm text-gray-500">400-999-9999</text>
</view>
<!-- Website -->
<view class="flex items-center justify-between px-5 py-4">
<text class="text-base text-gray-800">公司网站</text>
<text class="text-sm text-blue-500 underline active:opacity-70">{{
url
}}</text>
</view>
</view>
<!-- Copyright -->
<view class="mt-10 text-center text-xs text-gray-400">
Copyright &copy; 2026 insistence.tech All Rights Reserved.
</view>
</view>
</template>
<script setup>
import { useConfigStore } from "@/store";
const url = useConfigStore().config.appInfo.site_url;
const version = useConfigStore().config.appInfo.version;
</script>
<style>
page {
background-color: #f9fafb;
}
</style>
@@ -0,0 +1,678 @@
<template>
<view class="h-full bg-black">
<view class="relative h-full w-full">
<view class="min-h-[750rpx] w-full">
<view
v-if="isShowImg"
class="relative box-border overflow-hidden"
:style="
'width:' +
cropperInitW +
'px;height:' +
cropperInitH +
'px;background:#000'
"
>
<view
class="relative"
:style="
'width:' +
cropperW +
'px;height:' +
cropperH +
'px;left:' +
cropperL +
'px;top:' +
cropperT +
'px'
"
>
<image
:src="imageSrc"
:style="
'width:' +
cropperW +
'px;height:' +
cropperH +
'px;display:block;margin:0 auto;'
"
></image>
<view
class="absolute z-10 bg-white/30"
@touchstart.stop="contentStartMove"
@touchmove.stop="contentMoveing"
@touchend.stop="contentTouchEnd"
:style="
'left:' +
cutL +
'px;top:' +
cutT +
'px;right:' +
cutR +
'px;bottom:' +
cutB +
'px'
"
>
<view
class="relative block h-full w-full overflow-visible outline outline-1 outline-[rgba(102,153,255,0.75)]"
>
<view class="uni-cropper-dashed-h"></view>
<view class="uni-cropper-dashed-v"></view>
<view
class="uni-cropper-line-t"
data-drag="top"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-line-r"
data-drag="right"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-line-b"
data-drag="bottom"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-line-l"
data-drag="left"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-t"
data-drag="top"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-tr"
data-drag="topTight"
></view>
<view
class="uni-cropper-point point-r"
data-drag="right"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-rb"
data-drag="rightBottom"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-b"
data-drag="bottom"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-bl"
data-drag="bottomLeft"
></view>
<view
class="uni-cropper-point point-l"
data-drag="left"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-lt"
data-drag="leftTop"
></view>
</view>
</view>
</view>
</view>
</view>
<view class="fixed bottom-8 left-0 w-full px-10">
<button
class="mt-4 w-full rounded-full bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg transition-all active:scale-95"
@click="getImage"
>
选择头像
</button>
<button
class="mt-4 w-full rounded-full bg-gradient-to-r from-red-500 to-red-600 text-white shadow-lg transition-all active:scale-95"
@click="getImageInfo"
>
提交
</button>
</view>
<canvas
canvas-id="myCanvas"
:style="
'position:absolute;border: 1px solid red; width:' +
imageW +
'px;height:' +
imageH +
'px;top:-9999px;left:-9999px;'
"
></canvas>
</view>
</view>
</template>
<script>
import config from "@/config";
import { useUserStore } from "@/store";
import { uploadAvatar } from "@/api/system/user";
const baseUrl = config.baseUrl;
let sysInfo = uni.getSystemInfoSync();
let SCREEN_WIDTH = sysInfo.screenWidth;
let PAGE_X, // 手按下的x位置
PAGE_Y, // 手按下y的位置
PR = sysInfo.pixelRatio, // dpi
T_PAGE_X, // 手移动的时候x的位置
T_PAGE_Y, // 手移动的时候Y的位置
CUT_L, // 初始化拖拽元素的left值
CUT_T, // 初始化拖拽元素的top值
CUT_R, // 初始化拖拽元素的
CUT_B, // 初始化拖拽元素的
CUT_W, // 初始化拖拽元素的宽度
CUT_H, // 初始化拖拽元素的高度
IMG_RATIO, // 图片比例
IMG_REAL_W, // 图片实际的宽度
IMG_REAL_H, // 图片实际的高度
DRAFG_MOVE_RATIO = 1, //移动时候的比例,
INIT_DRAG_POSITION = 100, // 初始化屏幕宽度和裁剪区域的宽度之差,用于设置初始化裁剪的宽度
DRAW_IMAGE_W = sysInfo.screenWidth; // 设置生成的图片宽度
export default {
/**
* 页面的初始数据
*/
data() {
return {
imageSrc: useUserStore().avatar,
isShowImg: false,
// 初始化的宽高
cropperInitW: SCREEN_WIDTH,
cropperInitH: SCREEN_WIDTH,
// 动态的宽高
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH,
// 动态的left top值
cropperL: 0,
cropperT: 0,
transL: 0,
transT: 0,
// 图片缩放值
scaleP: 0,
imageW: 0,
imageH: 0,
// 裁剪框 宽高
cutL: 0,
cutT: 0,
cutB: SCREEN_WIDTH,
cutR: "100%",
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: DRAFG_MOVE_RATIO,
};
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
this.loadImage();
},
methods: {
setData: function (obj) {
let that = this;
Object.keys(obj).forEach(function (key) {
that.$set(that.$data, key, obj[key]);
});
},
getImage: function () {
var _this = this;
uni.chooseImage({
success: function (res) {
_this.setData({
imageSrc: res.tempFilePaths[0],
});
_this.loadImage();
},
});
},
loadImage: function () {
var _this = this;
uni.getImageInfo({
src: _this.imageSrc,
success: function success(res) {
IMG_RATIO = 1 / 1;
if (IMG_RATIO >= 1) {
IMG_REAL_W = SCREEN_WIDTH;
IMG_REAL_H = SCREEN_WIDTH / IMG_RATIO;
} else {
IMG_REAL_W = SCREEN_WIDTH * IMG_RATIO;
IMG_REAL_H = SCREEN_WIDTH;
}
let minRange = IMG_REAL_W > IMG_REAL_H ? IMG_REAL_W : IMG_REAL_H;
INIT_DRAG_POSITION =
minRange > INIT_DRAG_POSITION ? INIT_DRAG_POSITION : minRange;
// 根据图片的宽高显示不同的效果 保证图片可以正常显示
if (IMG_RATIO >= 1) {
let cutT = Math.ceil(
(SCREEN_WIDTH / IMG_RATIO -
(SCREEN_WIDTH / IMG_RATIO - INIT_DRAG_POSITION)) /
2,
);
let cutB = cutT;
let cutL = Math.ceil(
(SCREEN_WIDTH - SCREEN_WIDTH + INIT_DRAG_POSITION) / 2,
);
let cutR = cutL;
_this.setData({
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH / IMG_RATIO,
// 初始化left right
cropperL: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cropperT: Math.ceil(
(SCREEN_WIDTH - SCREEN_WIDTH / IMG_RATIO) / 2,
),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
// 图片缩放值
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO,
});
} else {
let cutL = Math.ceil(
(SCREEN_WIDTH * IMG_RATIO - SCREEN_WIDTH * IMG_RATIO) / 2,
);
let cutR = cutL;
let cutT = Math.ceil((SCREEN_WIDTH - INIT_DRAG_POSITION) / 2);
let cutB = cutT;
_this.setData({
cropperW: SCREEN_WIDTH * IMG_RATIO,
cropperH: SCREEN_WIDTH,
// 初始化left right
cropperL: Math.ceil(
(SCREEN_WIDTH - SCREEN_WIDTH * IMG_RATIO) / 2,
),
cropperT: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
// 图片缩放值
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO,
});
}
_this.setData({
isShowImg: true,
});
uni.hideLoading();
},
});
},
// 拖动时候触发的touchStart事件
contentStartMove(e) {
PAGE_X = e.touches[0].pageX;
PAGE_Y = e.touches[0].pageY;
},
// 拖动时候触发的touchMove事件
contentMoveing(e) {
var _this = this;
var dragLengthX = (PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
var dragLengthY = (PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
// 左移
if (dragLengthX > 0) {
if (this.cutL - dragLengthX < 0) dragLengthX = this.cutL;
} else {
if (this.cutR + dragLengthX < 0) dragLengthX = -this.cutR;
}
if (dragLengthY > 0) {
if (this.cutT - dragLengthY < 0) dragLengthY = this.cutT;
} else {
if (this.cutB + dragLengthY < 0) dragLengthY = -this.cutB;
}
this.setData({
cutL: this.cutL - dragLengthX,
cutT: this.cutT - dragLengthY,
cutR: this.cutR + dragLengthX,
cutB: this.cutB + dragLengthY,
});
PAGE_X = e.touches[0].pageX;
PAGE_Y = e.touches[0].pageY;
},
contentTouchEnd() {},
// 获取图片
getImageInfo() {
var _this = this;
uni.showLoading({
title: "图片生成中...",
});
// 将图片写入画布
const ctx = uni.createCanvasContext("myCanvas");
ctx.drawImage(_this.imageSrc, 0, 0, IMG_REAL_W, IMG_REAL_H);
ctx.draw(true, () => {
// 获取画布要裁剪的位置和宽度 均为百分比 * 画布中图片的宽度 保证了在微信小程序中裁剪的图片模糊 位置不对的问题 canvasT = (_this.cutT / _this.cropperH) * (_this.imageH / pixelRatio)
var canvasW =
((_this.cropperW - _this.cutL - _this.cutR) / _this.cropperW) *
IMG_REAL_W;
var canvasH =
((_this.cropperH - _this.cutT - _this.cutB) / _this.cropperH) *
IMG_REAL_H;
var canvasL = (_this.cutL / _this.cropperW) * IMG_REAL_W;
var canvasT = (_this.cutT / _this.cropperH) * IMG_REAL_H;
uni.canvasToTempFilePath({
x: canvasL,
y: canvasT,
width: canvasW,
height: canvasH,
destWidth: canvasW,
destHeight: canvasH,
quality: 0.5,
canvasId: "myCanvas",
success: function (res) {
uni.hideLoading();
let data = { name: "avatarfile", filePath: res.tempFilePath };
uploadAvatar(data).then((response) => {
useUserStore().SET_AVATAR(baseUrl + response.imgUrl);
uni.showToast({ title: "修改成功", icon: "success" });
uni.navigateBack();
});
},
});
});
},
// 设置大小的时候触发的touchStart事件
dragStart(e) {
T_PAGE_X = e.touches[0].pageX;
T_PAGE_Y = e.touches[0].pageY;
CUT_L = this.cutL;
CUT_R = this.cutR;
CUT_B = this.cutB;
CUT_T = this.cutT;
},
// 设置大小的时候触发的touchMove事件
dragMove(e) {
var _this = this;
var dragType = e.target.dataset.drag;
switch (dragType) {
case "right":
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
if (CUT_R + dragLength < 0) dragLength = -CUT_R;
this.setData({
cutR: CUT_R + dragLength,
});
break;
case "left":
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
if (CUT_L - dragLength < 0) dragLength = CUT_L;
if (CUT_L - dragLength > this.cropperW - this.cutR)
dragLength = CUT_L - (this.cropperW - this.cutR);
this.setData({
cutL: CUT_L - dragLength,
});
break;
case "top":
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
if (CUT_T - dragLength < 0) dragLength = CUT_T;
if (CUT_T - dragLength > this.cropperH - this.cutB)
dragLength = CUT_T - (this.cropperH - this.cutB);
this.setData({
cutT: CUT_T - dragLength,
});
break;
case "bottom":
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
if (CUT_B + dragLength < 0) dragLength = -CUT_B;
this.setData({
cutB: CUT_B + dragLength,
});
break;
case "rightBottom":
var dragLengthX = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
var dragLengthY = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
if (CUT_B + dragLengthY < 0) dragLengthY = -CUT_B;
if (CUT_R + dragLengthX < 0) dragLengthX = -CUT_R;
let cutB = CUT_B + dragLengthY;
let cutR = CUT_R + dragLengthX;
this.setData({
cutB: cutB,
cutR: cutR,
});
break;
default:
break;
}
},
},
};
</script>
<style scoped>
/* 横向虚线 */
.uni-cropper-dashed-h {
position: absolute;
top: 33.33333333%;
left: 0;
width: 100%;
height: 33.33333333%;
border-top: 1rpx dashed rgba(255, 255, 255, 0.5);
border-bottom: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 纵向虚线 */
.uni-cropper-dashed-v {
position: absolute;
left: 33.33333333%;
top: 0;
width: 33.33333333%;
height: 100%;
border-left: 1rpx dashed rgba(255, 255, 255, 0.5);
border-right: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 四个方向的线 为了之后的拖动事件*/
.uni-cropper-line-t {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
top: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: n-resize;
}
.uni-cropper-line-t::before {
content: "";
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-r {
position: absolute;
display: block;
background-color: #69f;
top: 0;
right: 0rpx;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: e-resize;
}
.uni-cropper-line-r::before {
content: "";
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-line-b {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
bottom: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: s-resize;
}
.uni-cropper-line-b::before {
content: "";
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-l {
position: absolute;
display: block;
background-color: #69f;
top: 0;
left: 0;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: w-resize;
}
.uni-cropper-line-l::before {
content: "";
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-point {
width: 5rpx;
height: 5rpx;
background-color: #69f;
opacity: 0.75;
position: absolute;
z-index: 3;
}
.point-t {
top: -3rpx;
left: 50%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-tr {
top: -3rpx;
left: 100%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-r {
top: 50%;
left: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-rb {
left: 100%;
top: 100%;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
cursor: n-resize;
width: 36rpx;
height: 36rpx;
background-color: #69f;
position: absolute;
z-index: 1112;
opacity: 1;
}
.point-b {
left: 50%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-bl {
left: 0%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-l {
left: 0%;
top: 50%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-lt {
left: 0%;
top: 0%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
</style>
@@ -0,0 +1,91 @@
<template>
<view class="flex h-full flex-col overflow-y-auto bg-gray-50 p-4 pb-24">
<view v-for="(item, findex) in list" :key="findex" class="mb-6">
<view
class="mb-3 ml-2 flex items-center text-base font-bold text-gray-800"
>
<view :class="['mr-2 text-lg text-blue-500', item.icon]"></view
>{{ item.title }}
</view>
<view
class="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-100"
>
<view
v-for="(child, zindex) in item.childList"
:key="zindex"
class="relative"
hover-class="bg-gray-50"
@click="handleText(child)"
>
<view class="p-4 text-sm text-gray-600">{{ child.title }}</view>
<view
class="h-px w-full bg-gray-100"
v-if="zindex !== item.childList.length - 1"
></view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const list = ref([
{
icon: "i-mdi-github",
title: "ShiErFeng-FastAPI问题",
childList: [
{
title: "ShiErFeng-FastAPI开源吗?",
content: "开源",
},
{
title: "ShiErFeng-FastAPI可以商用吗?",
content: "可以",
},
{
title: "ShiErFeng-FastAPI官网地址多少?",
content: "https://vfadmin.insistence.tech",
},
{
title: "ShiErFeng-FastAPI文档地址多少?",
content: "https://vfadmin.insistence.tech",
},
],
},
{
icon: "i-mdi-help-circle-outline",
title: "其他问题",
childList: [
{
title: "如何退出登录?",
content: "请点击[我的] - [应用设置] - [退出登录]即可退出登录",
},
{
title: "如何修改用户头像?",
content: "请点击[我的] - [选择头像] - [点击提交]即可更换用户头像",
},
{
title: "如何修改登录密码?",
content: "请点击[我的] - [应用设置] - [修改密码]即可修改登录密码",
},
],
},
]);
function handleText(item) {
proxy.$tab.navigateTo(
`/pages/common/textview/index?title=${item.title}&content=${item.content}`,
);
}
</script>
<style>
page {
height: 100%;
background-color: #f9fafb;
}
</style>
@@ -0,0 +1,209 @@
<template>
<view class="flex h-full flex-col bg-gray-50 overflow-hidden">
<!-- Header Section -->
<view
class="relative overflow-hidden bg-gradient-to-r from-blue-500 to-indigo-600 pb-20 pt-16 text-white shadow-lg"
>
<view
class="absolute -right-10 -top-10 size-64 rounded-full bg-white/10 blur-3xl"
></view>
<view
class="absolute -bottom-10 -left-10 size-40 rounded-full bg-white/10 blur-2xl"
></view>
<view class="relative z-10 flex items-center justify-between px-6">
<view class="flex items-center space-x-4">
<!-- Avatar -->
<view
class="relative overflow-hidden rounded-full border-4 border-white/30 bg-white/20 shadow-xl transition-transform active:scale-95"
>
<image
v-if="avatar"
@click="handleToAvatar"
:src="avatar"
class="size-20 object-cover"
/>
<view
v-else
class="flex size-20 items-center justify-center bg-white text-gray-400"
>
<view class="i-mdi-account text-5xl"></view>
</view>
</view>
<!-- User Info -->
<view class="flex flex-col">
<template v-if="name">
<view
class="text-xl font-bold tracking-wide"
@click="handleToInfo"
>
{{ name }}
</view>
<view
class="mt-1 flex items-center text-sm text-blue-100"
@click="handleToInfo"
>
<text>查看个人信息</text>
<view class="i-mdi-chevron-right ml-1 text-xs"></view>
</view>
</template>
<view v-else class="text-xl font-bold" @click="handleToLogin">
点击登录
</view>
</view>
</view>
</view>
</view>
<!-- Content Section -->
<view class="relative z-20 -mt-12 flex-1 px-4 overflow-y-auto">
<!-- Quick Actions -->
<view
class="mb-4 flex items-center justify-between rounded-2xl bg-white p-4 shadow-lg shadow-gray-200/50"
>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleJiaoLiuQun"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-pink-50 text-pink-500"
>
<view class="i-mdi-account-group text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">交流群</text>
</view>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleBuilding"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-blue-50 text-blue-500"
>
<view class="i-mdi-face-agent text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">在线客服</text>
</view>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleBuilding"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-purple-50 text-purple-500"
>
<view class="i-mdi-forum text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">反馈社区</text>
</view>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleBuilding"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-green-50 text-green-500"
>
<view class="i-mdi-thumb-up text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">点赞我们</text>
</view>
</view>
<!-- Menu List -->
<view
class="overflow-hidden rounded-2xl bg-white shadow-lg shadow-gray-200/50"
>
<view
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
@click="handleToEditInfo"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-account-edit text-xl text-blue-500"></view>
<text class="text-base text-gray-700">编辑资料</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
<view
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
@click="handleHelp"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-help-circle text-xl text-orange-500"></view>
<text class="text-base text-gray-700">常见问题</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
<view
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
@click="handleAbout"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-heart-outline text-xl text-red-500"></view>
<text class="text-base text-gray-700">关于我们</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
<view
class="group flex items-center justify-between p-4 transition-colors active:bg-gray-50"
@click="handleToSetting"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-cog-outline text-xl text-gray-500"></view>
<text class="text-base text-gray-700">应用设置</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { useUserStore } from "@/store";
import { computed, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const name = computed(() => userStore.name);
const avatar = computed(() => userStore.avatar);
const windowHeight = computed(() => uni.getSystemInfoSync().windowHeight - 50);
function handleToInfo() {
proxy.$tab.navigateTo("/pages/mine/info/index");
}
function handleToEditInfo() {
proxy.$tab.navigateTo("/pages/mine/info/edit");
}
function handleToSetting() {
proxy.$tab.navigateTo("/pages/mine/setting/index");
}
function handleToLogin() {
proxy.$tab.reLaunch("/pages/login");
}
function handleToAvatar() {
proxy.$tab.navigateTo("/pages/mine/avatar/index");
}
function handleHelp() {
proxy.$tab.navigateTo("/pages/mine/help/index");
}
function handleAbout() {
proxy.$tab.navigateTo("/pages/mine/about/index");
}
function handleJiaoLiuQun() {
proxy.$modal.showToast("模块建设中~");
}
function handleBuilding() {
proxy.$modal.showToast("模块建设中~");
}
</script>
@@ -0,0 +1,145 @@
<template>
<view class="flex h-full flex-col overflow-y-auto bg-white p-4">
<view class="space-y-5">
<!-- Nickname -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700"
>用户昵称</text
>
<input
v-model="user.nickName"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="text"
placeholder="请输入昵称"
/>
</view>
<!-- Phone -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700"
>手机号码</text
>
<input
v-model="user.phonenumber"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="number"
placeholder="请输入手机号码"
maxlength="11"
/>
</view>
<!-- Email -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700">邮箱</text>
<input
v-model="user.email"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="text"
placeholder="请输入邮箱"
/>
</view>
<!-- Sex -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700">性别</text>
<view class="flex space-x-4">
<view
v-for="item in sexs"
:key="item.value"
@click="user.sex = item.value"
class="flex flex-1 items-center justify-center rounded-xl border py-2.5 transition-all active:scale-95"
:class="
user.sex === item.value
? 'bg-blue-50 border-blue-500 text-blue-600'
: 'bg-gray-50 border-transparent text-gray-500'
"
>
<text class="text-sm font-medium">{{ item.text }}</text>
</view>
</view>
</view>
<!-- Submit Button -->
<view class="pt-6">
<button
@click="submit"
class="flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-all active:scale-95 active:bg-blue-600"
>
提 交
</button>
</view>
</view>
</view>
</template>
<script setup>
import { getUserProfile, updateUserProfile } from "@/api/system/user";
import { ref, getCurrentInstance } from "vue";
import { onLoad } from "@dcloudio/uni-app";
const { proxy } = getCurrentInstance();
const user = ref({
nickName: "",
phonenumber: "",
email: "",
sex: "",
});
const sexs = [
{
text: "男",
value: "0",
},
{
text: "女",
value: "1",
},
];
function getUser() {
getUserProfile().then((response) => {
const { nickName, phonenumber, email, sex } = response.data;
user.value = { nickName, phonenumber, email, sex };
});
}
function submit() {
if (!user.value.nickName) {
proxy.$modal.msgError("用户昵称不能为空");
return;
}
if (!user.value.phonenumber) {
proxy.$modal.msgError("手机号码不能为空");
return;
}
if (!/^1[3|4|5|6|7|8|9][0-9]\d{8}$/.test(user.value.phonenumber)) {
proxy.$modal.msgError("请输入正确的手机号码");
return;
}
if (!user.value.email) {
proxy.$modal.msgError("邮箱地址不能为空");
return;
}
if (!/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(user.value.email)) {
proxy.$modal.msgError("请输入正确的邮箱地址");
return;
}
updateUserProfile(user.value).then((response) => {
proxy.$modal.msgSuccess("修改成功");
setTimeout(() => {
proxy.$tab.navigateBack();
}, 1500);
});
}
onLoad(() => {
getUser();
});
</script>
<style>
page {
height: 100%;
background-color: #ffffff;
}
</style>
@@ -0,0 +1,121 @@
<template>
<view class="flex h-full flex-col overflow-y-auto bg-gray-50 pt-4 pb-10">
<view class="overflow-hidden bg-white shadow-sm">
<!-- Nickname -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-blue-50 text-blue-500"
>
<view class="i-mdi-account text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">昵称</text>
</view>
<text class="text-sm text-gray-500">{{ user.nickName }}</text>
</view>
<!-- Phone -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-green-50 text-green-500"
>
<view class="i-mdi-phone text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">手机号码</text>
</view>
<text class="text-sm text-gray-500">{{ user.phonenumber }}</text>
</view>
<!-- Email -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-indigo-50 text-indigo-500"
>
<view class="i-mdi-email text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">邮箱</text>
</view>
<text class="text-sm text-gray-500">{{ user.email }}</text>
</view>
<!-- Post -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-orange-50 text-orange-500"
>
<view class="i-mdi-shield-account text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">岗位</text>
</view>
<text class="text-sm text-gray-500">{{ postGroup }}</text>
</view>
<!-- Role -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-purple-50 text-purple-500"
>
<view class="i-mdi-badge-account text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">角色</text>
</view>
<text class="text-sm text-gray-500">{{ roleGroup }}</text>
</view>
<!-- Create Time -->
<view
class="flex items-center justify-between px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-pink-50 text-pink-500"
>
<view class="i-mdi-calendar text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">创建日期</text>
</view>
<text class="text-sm text-gray-500">{{ user.createTime }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { getUserProfile } from "@/api/system/user";
import { ref } from "vue";
const user = ref({});
const roleGroup = ref("");
const postGroup = ref("");
function getUser() {
getUserProfile().then((response) => {
user.value = response.data;
roleGroup.value = response.roleGroup;
postGroup.value = response.postGroup;
});
}
getUser();
</script>
<style>
page {
height: 100%;
background-color: #f9fafb;
}
</style>
@@ -0,0 +1,90 @@
<template>
<view class="min-h-screen bg-white p-4">
<view class="space-y-5">
<!-- Old Password -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700">旧密码</text>
<input
v-model="user.oldPassword"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="password"
placeholder="请输入旧密码"
/>
</view>
<!-- New Password -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700">新密码</text>
<input
v-model="user.newPassword"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="password"
placeholder="请输入新密码"
/>
</view>
<!-- Confirm Password -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700"
>确认密码</text
>
<input
v-model="user.confirmPassword"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="password"
placeholder="请确认新密码"
/>
</view>
<!-- Submit Button -->
<view class="pt-6">
<button
@click="submit"
class="flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-all active:scale-95 active:bg-blue-600"
>
提 交
</button>
</view>
</view>
</view>
</template>
<script setup>
import { updateUserPwd } from "@/api/system/user";
import { ref, reactive, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const user = reactive({
oldPassword: "",
newPassword: "",
confirmPassword: "",
});
function submit() {
if (!user.oldPassword) {
proxy.$modal.msgError("旧密码不能为空");
return;
}
if (!user.newPassword) {
proxy.$modal.msgError("新密码不能为空");
return;
}
if (user.newPassword.length < 6 || user.newPassword.length > 20) {
proxy.$modal.msgError("长度在 6 到 20 个字符");
return;
}
if (!user.confirmPassword) {
proxy.$modal.msgError("确认密码不能为空");
return;
}
if (user.newPassword !== user.confirmPassword) {
proxy.$modal.msgError("两次输入的密码不一致");
return;
}
updateUserPwd(user.oldPassword, user.newPassword).then((response) => {
proxy.$modal.msgSuccess("修改成功");
setTimeout(() => {
proxy.$tab.navigateBack();
}, 1500);
});
}
</script>
@@ -0,0 +1,87 @@
<template>
<view class="flex h-full flex-col overflow-y-auto bg-gray-50 pt-4 pb-10">
<view class="bg-white">
<!-- Change Password -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
@click="handleToPwd"
>
<view class="flex items-center">
<view class="i-mdi-lock text-xl text-gray-600 mr-3"></view>
<text class="text-base text-gray-800">修改密码</text>
</view>
<view class="i-mdi-chevron-right text-base text-gray-400"></view>
</view>
<!-- Check Update -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
@click="handleToUpgrade"
>
<view class="flex items-center">
<view class="i-mdi-refresh text-xl text-gray-600 mr-3"></view>
<text class="text-base text-gray-800">检查更新</text>
</view>
<view class="i-mdi-chevron-right text-base text-gray-400"></view>
</view>
<!-- Clean Cache -->
<view
class="flex items-center justify-between px-5 py-4 active:bg-gray-50"
@click="handleCleanTmp"
>
<view class="flex items-center">
<view class="i-mdi-delete text-xl text-gray-600 mr-3"></view>
<text class="text-base text-gray-800">清理缓存</text>
</view>
<view class="i-mdi-chevron-right text-base text-gray-400"></view>
</view>
</view>
<!-- Logout -->
<view class="mt-8 px-4">
<view
class="flex h-12 w-full items-center justify-center rounded-xl bg-red-50 text-base font-semibold text-red-600 transition-colors active:bg-red-100"
@click="handleLogout"
>退出登录</view
>
</view>
</view>
</template>
<script setup>
import { useUserStore } from "@/store";
import { getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
function handleToPwd() {
proxy.$tab.navigateTo("/pages/mine/pwd/index");
}
function handleToUpgrade() {
proxy.$modal.showToast("模块建设中~");
}
function handleCleanTmp() {
proxy.$modal.showToast("模块建设中~");
}
function handleLogout() {
proxy.$modal.confirm("确定注销并退出系统吗?").then(() => {
useUserStore()
.logOut()
.then(() => {})
.finally(() => {
proxy.$tab.reLaunch("/pages/index");
});
});
}
</script>
<style>
page {
height: 100%;
background-color: #f9fafb;
}
</style>
@@ -0,0 +1,206 @@
<template>
<view
class="flex h-full flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6 pb-20 overflow-hidden"
>
<!-- Logo Section -->
<view class="mb-6 flex flex-col items-center">
<view
class="mb-4 flex size-16 items-center justify-center rounded-2xl bg-white shadow-lg"
>
<image
class="size-10"
:src="globalConfig.appInfo.logo"
mode="widthFix"
/>
</view>
<text class="text-xl font-bold tracking-wide text-gray-800"
>ShiErFeng-FastAPI移动端注册</text
>
</view>
<!-- Form Section -->
<view class="w-full rounded-3xl bg-white/80 p-6 shadow-xl backdrop-blur-md">
<!-- Username -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-account text-xl"></view>
</view>
<input
v-model="registerForm.username"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
type="text"
placeholder="请输入账号"
maxlength="30"
/>
</view>
<!-- Password -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-lock text-xl"></view>
</view>
<input
v-model="registerForm.password"
type="password"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="请输入密码"
maxlength="20"
/>
</view>
<!-- Confirm Password -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-lock text-xl"></view>
</view>
<input
v-model="registerForm.confirmPassword"
type="password"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="请输入重复密码"
maxlength="20"
/>
</view>
<!-- Captcha -->
<view
class="mb-8 flex items-center justify-between"
v-if="captchaEnabled"
>
<view class="group relative mr-3 flex-1">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-security text-xl"></view>
</view>
<input
v-model="registerForm.code"
type="number"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="验证码"
maxlength="4"
/>
</view>
<view
class="h-12 w-28 overflow-hidden rounded-xl bg-gray-100 shadow-sm transition-opacity active:opacity-80"
@click="getCode"
>
<image :src="codeUrl" class="size-full object-cover"></image>
</view>
</view>
<!-- Register Button -->
<button
@click="handleRegister"
class="flex h-12 w-full items-center justify-center rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-transform active:scale-95"
>
注 册
</button>
<!-- Footer Links -->
<view class="mt-6 flex flex-col items-center space-y-3">
<view class="flex items-center text-sm text-gray-500">
<text
@click="handleUserLogin"
class="ml-1 font-medium text-blue-600 active:opacity-70"
>使用已有账号登录</text
>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { getCodeImg, register } from "@/api/login";
import { ref, getCurrentInstance } from "vue";
import { useConfigStore } from "@/store";
const { proxy } = getCurrentInstance();
const globalConfig = useConfigStore().config;
const codeUrl = ref("");
// 验证码开关
const captchaEnabled = ref(true);
const registerForm = ref({
username: "",
password: "",
confirmPassword: "",
code: "",
uuid: "",
});
// 用户登录
function handleUserLogin() {
proxy.$tab.navigateTo(`/pages/login`);
}
// 获取图形验证码
function getCode() {
getCodeImg().then((res) => {
captchaEnabled.value =
res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img;
registerForm.value.uuid = res.uuid;
}
});
}
// 注册方法
async function handleRegister() {
if (registerForm.value.username === "") {
proxy.$modal.msgError("请输入您的账号");
} else if (registerForm.value.password === "") {
proxy.$modal.msgError("请输入您的密码");
} else if (registerForm.value.confirmPassword === "") {
proxy.$modal.msgError("请再次输入您的密码");
} else if (
registerForm.value.password !== registerForm.value.confirmPassword
) {
proxy.$modal.msgError("两次输入的密码不一致");
} else if (registerForm.value.code === "" && captchaEnabled.value) {
proxy.$modal.msgError("请输入验证码");
} else {
proxy.$modal.loading("注册中,请耐心等待...");
userRegister();
}
}
// 用户注册
async function userRegister() {
register(registerForm.value)
.then((res) => {
proxy.$modal.closeLoading();
uni.showModal({
title: "系统提示",
content:
"恭喜你,您的账号 " + registerForm.value.username + " 注册成功!",
success: function (res) {
if (res.confirm) {
uni.redirectTo({ url: `/pages/login` });
}
},
});
})
.catch(() => {
if (captchaEnabled.value) {
getCode();
}
});
}
getCode();
</script>
<style lang="scss" scoped>
page {
background-color: #ffffff;
height: 100%;
}
</style>
@@ -0,0 +1,235 @@
<template>
<view class="flex h-full flex-col bg-gray-50 overflow-hidden">
<scroll-view scroll-y class="flex-1" :show-scrollbar="false">
<view class="p-4 pb-24 space-y-6">
<!-- Banner -->
<view
class="relative h-40 w-full overflow-hidden rounded-2xl shadow-lg shadow-indigo-100"
>
<swiper
class="h-40 w-full"
:current="swiperDotIndex"
@change="changeSwiper"
autoplay
interval="3000"
circular
>
<swiper-item v-for="(item, index) in data" :key="index">
<view class="h-full w-full" @click="clickBannerItem(item)">
<image
:src="item.image"
mode="scaleToFill"
class="block h-full w-full"
/>
</view>
</swiper-item>
</swiper>
<!-- Custom Dots -->
<view
class="absolute bottom-3 right-0 left-0 flex justify-center space-x-2 pointer-events-none"
>
<view
v-for="(item, index) in data"
:key="index"
class="h-1.5 rounded-full transition-all duration-300"
:class="current === index ? 'bg-white w-4' : 'bg-white/50 w-1.5'"
>
</view>
</view>
</view>
<!-- AI Tools Section -->
<view>
<view class="mb-4 flex items-center space-x-2">
<view class="h-4 w-1 rounded-full bg-indigo-500"></view>
<text class="text-base font-bold text-gray-800">AI 生产力</text>
</view>
<view class="grid grid-cols-2 gap-4">
<view
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
@click="handleToAiChat"
>
<view
class="absolute -right-4 -top-4 size-20 rounded-full bg-indigo-50 opacity-50"
></view>
<view class="relative z-10 flex flex-col">
<view
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-indigo-500 text-white shadow-md shadow-indigo-200"
>
<view class="i-mdi-chat text-xl"></view>
</view>
<text class="font-bold text-gray-800">智能对话</text>
<text class="mt-1 text-xs text-gray-500">智能助手</text>
</view>
</view>
<view
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
@click="handleBuilding"
>
<view
class="absolute -right-4 -top-4 size-20 rounded-full bg-purple-50 opacity-50"
></view>
<view class="relative z-10 flex flex-col">
<view
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-purple-500 text-white shadow-md shadow-purple-200"
>
<view class="i-mdi-image-multiple text-xl"></view>
</view>
<text class="font-bold text-gray-800">图像生成</text>
<text class="mt-1 text-xs text-gray-500">创意工坊</text>
</view>
</view>
</view>
</view>
<!-- System Management Section -->
<view>
<view class="mb-4 flex items-center space-x-2">
<view class="h-4 w-1 rounded-full bg-blue-500"></view>
<text class="text-base font-bold text-gray-800">系统管理</text>
</view>
<view class="rounded-2xl bg-white p-5 shadow-sm">
<view class="flex flex-wrap justify-between gap-y-4">
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleToUserList"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-blue-50 text-blue-500 transition-colors group-active:bg-blue-100"
>
<view class="i-mdi-account text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>用户管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-orange-50 text-orange-500 transition-colors group-active:bg-orange-100"
>
<view class="i-mdi-badge-account text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>角色管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-green-50 text-green-500 transition-colors group-active:bg-green-100"
>
<view class="i-mdi-office-building text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>部门管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-red-50 text-red-500 transition-colors group-active:bg-red-100"
>
<view class="i-mdi-cog text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>配置管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-cyan-50 text-cyan-500 transition-colors group-active:bg-cyan-100"
>
<view class="i-mdi-book-open-page-variant text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>字典管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-yellow-50 text-yellow-500 transition-colors group-active:bg-yellow-100"
>
<view class="i-mdi-bullhorn text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>通知公告</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-pink-50 text-pink-500 transition-colors group-active:bg-pink-100"
>
<view class="i-mdi-file-document-outline text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>日志管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-gray-50 text-gray-500 transition-colors group-active:bg-gray-100"
>
<view class="i-mdi-dots-horizontal text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600">更多</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const current = ref(0);
const swiperDotIndex = ref(0);
const data = ref([
{ image: "/static/images/banner/banner01.jpg" },
{ image: "/static/images/banner/banner02.jpg" },
{ image: "/static/images/banner/banner03.jpg" },
]);
function clickBannerItem(item) {
console.info(item);
}
function changeSwiper(e) {
current.value = e.detail.current;
}
function handleToAiChat() {
handleBuilding();
}
function handleToUserList() {
handleBuilding();
}
function handleBuilding() {
proxy.$modal.msg("模块建设中~");
}
</script>
+43
View File
@@ -0,0 +1,43 @@
import { getToken } from "@/utils/auth";
// 登录页面
const loginPage = "/pages/login";
// 页面白名单
const whiteList = [
"/pages/login",
"/pages/register",
"/pages/common/webview/index",
"/pages/common/agreement/index",
"/pages/common/privacy/index",
];
// 检查地址白名单
function checkWhite(url) {
const path = url.split("?")[0];
return whiteList.indexOf(path) !== -1;
}
// 页面跳转验证拦截器
let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
list.forEach((item) => {
uni.addInterceptor(item, {
invoke(to) {
if (getToken()) {
if (to.url === loginPage) {
uni.reLaunch({ url: "/" });
}
return true;
} else {
if (checkWhite(to.url)) {
return true;
}
uni.reLaunch({ url: loginPage });
return false;
}
},
fail(err) {
console.log(err);
},
});
});
+60
View File
@@ -0,0 +1,60 @@
import { useUserStore } from "@/store";
function authPermission(permission) {
const all_permission = "*:*:*";
const permissions = useUserStore().permissions;
if (permission && permission.length > 0) {
return permissions.some((v) => {
return all_permission === v || v === permission;
});
} else {
return false;
}
}
function authRole(role) {
const super_admin = "admin";
const roles = useUserStore().roles;
if (role && role.length > 0) {
return roles.some((v) => {
return super_admin === v || v === role;
});
} else {
return false;
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission);
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
return permissions.some((item) => {
return authPermission(item);
});
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermiAnd(permissions) {
return permissions.every((item) => {
return authPermission(item);
});
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role);
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {
return roles.some((item) => {
return authRole(item);
});
},
// 验证用户是否含有指定角色,必须全部拥有
hasRoleAnd(roles) {
return roles.every((item) => {
return authRole(item);
});
},
};
@@ -0,0 +1,12 @@
import tab from "./tab";
import auth from "./auth";
import modal from "./modal";
export function install(app) {
// 页签操作
app.config.globalProperties.$tab = tab;
// 认证对象
app.config.globalProperties.$auth = auth;
// 模态框对象
app.config.globalProperties.$modal = modal;
}
@@ -0,0 +1,78 @@
export default {
// 消息提示
msg(content) {
uni.showToast({
title: content,
icon: "none",
});
},
// 错误消息
msgError(content) {
uni.showToast({
title: content,
icon: "error",
});
},
// 成功消息
msgSuccess(content) {
uni.showToast({
title: content,
icon: "success",
});
},
// 隐藏消息
hideMsg(content) {
uni.hideToast();
},
// 弹出提示
alert(content, title) {
uni.showModal({
title: title || "系统提示",
content: content,
showCancel: false,
});
},
// 确认窗体
confirm(content, title) {
return new Promise((resolve, reject) => {
uni.showModal({
title: title || "系统提示",
content: content,
cancelText: "取消",
confirmText: "确定",
success: function (res) {
if (res.confirm) {
resolve(res.confirm);
}
},
});
});
},
// 提示信息
showToast(option) {
if (typeof option === "object") {
uni.showToast(option);
} else {
uni.showToast({
title: option,
icon: "none",
duration: 2500,
});
}
},
// 打开遮罩层
loading(content) {
uni.showLoading({
title: content,
icon: "none",
});
},
// 关闭遮罩层
closeLoading() {
try {
uni.hideLoading();
} catch (e) {
console.log(e);
}
},
};
+30
View File
@@ -0,0 +1,30 @@
export default {
// 关闭所有页面,打开到应用内的某个页面
reLaunch(url) {
return uni.reLaunch({
url: url,
});
},
// 跳转到tabBar页面,并关闭其他所有非tabBar页面
switchTab(url) {
return uni.switchTab({
url: url,
});
},
// 关闭当前页面,跳转到应用内的某个页面
redirectTo(url) {
return uni.redirectTo({
url: url,
});
},
// 保留当前页面,跳转到应用内的某个页面
navigateTo(url) {
return uni.navigateTo({
url: url,
});
},
// 关闭当前页面,返回上一页面或多级页面
navigateBack() {
return uni.navigateBack();
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+9
View File
@@ -0,0 +1,9 @@
import { createPinia } from "pinia";
import { useUserStore } from "./modules/user";
import { useConfigStore } from "./modules/config";
const pinia = createPinia();
export default pinia;
export { useUserStore, useConfigStore };
@@ -0,0 +1,13 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useConfigStore = defineStore("config", () => {
const config = ref();
const setConfig = (val) => {
config.value = val;
};
return {
config,
setConfig,
};
});
@@ -0,0 +1,56 @@
import { defineStore } from "pinia";
const useDictStore = defineStore("dict", {
state: () => ({
dict: new Array(),
}),
actions: {
// 获取字典
getDict(_key) {
if (_key == null && _key == "") {
return null;
}
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
return this.dict[i].value;
}
}
} catch (e) {
return null;
}
},
// 设置字典
setDict(_key, value) {
if (_key !== null && _key !== "") {
this.dict.push({
key: _key,
value: value,
});
}
},
// 删除字典
removeDict(_key) {
var bln = false;
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
this.dict.splice(i, 1);
return true;
}
}
} catch (e) {
bln = false;
}
return bln;
},
// 清空字典
cleanDict() {
this.dict = new Array();
},
// 初始字典
initDict() {},
},
});
export default useDictStore;
@@ -0,0 +1,125 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import config from "@/config";
import storage from "@/utils/storage";
import constant from "@/utils/constant";
import { isHttp, isEmpty } from "@/utils/validate";
import { getInfo, login, logout } from "@/api/login";
import { getToken, removeToken, setToken } from "@/utils/auth";
import defAva from "@/static/images/profile.jpg";
const baseUrl = config.baseUrl;
export const useUserStore = defineStore("user", () => {
const token = ref(getToken());
const id = ref(storage.get(constant.id));
const name = ref(storage.get(constant.name));
const avatar = ref(storage.get(constant.avatar));
const roles = ref(storage.get(constant.roles));
const permissions = ref(storage.get(constant.permissions));
const SET_TOKEN = (val) => {
token.value = val;
};
const SET_ID = (val) => {
id.value = val;
storage.set(constant.id, val);
};
const SET_NAME = (val) => {
name.value = val;
storage.set(constant.name, val);
};
const SET_AVATAR = (val) => {
avatar.value = val;
storage.set(constant.avatar, val);
};
const SET_ROLES = (val) => {
roles.value = val;
storage.set(constant.roles, val);
};
const SET_PERMISSIONS = (val) => {
permissions.value = val;
storage.set(constant.permissions, val);
};
// 登录
const loginAction = (userInfo) => {
const username = userInfo.username.trim();
const password = userInfo.password;
const code = userInfo.code;
const uuid = userInfo.uuid;
return new Promise((resolve, reject) => {
login(username, password, code, uuid)
.then((res) => {
setToken(res.token);
SET_TOKEN(res.token);
resolve();
})
.catch((error) => {
reject(error);
});
});
};
// 获取用户信息
const getInfoAction = () => {
return new Promise((resolve, reject) => {
getInfo()
.then((res) => {
const user = res.user;
let avatar = user.avatar || "";
if (!isHttp(avatar)) {
avatar = isEmpty(avatar) ? defAva : baseUrl + avatar;
}
const userid =
isEmpty(user) || isEmpty(user.userId) ? "" : user.userId;
const username =
isEmpty(user) || isEmpty(user.userName) ? "" : user.userName;
if (res.roles && res.roles.length > 0) {
SET_ROLES(res.roles);
SET_PERMISSIONS(res.permissions);
} else {
SET_ROLES(["ROLE_DEFAULT"]);
}
SET_ID(userid);
SET_NAME(username);
SET_AVATAR(avatar);
resolve(res);
})
.catch((error) => {
reject(error);
});
});
};
// 退出系统
const logOutAction = () => {
return new Promise((resolve, reject) => {
logout(token.value)
.then(() => {
SET_TOKEN("");
SET_ROLES([]);
SET_PERMISSIONS([]);
removeToken();
storage.clean();
resolve();
})
.catch((error) => {
reject(error);
});
});
};
return {
token,
id,
name,
avatar,
roles,
permissions,
SET_AVATAR,
login: loginAction,
getInfo: getInfoAction,
logOut: logOutAction,
};
});
+10
View File
@@ -0,0 +1,10 @@
{
"light": {
"navBgColor": "#f6f6f6",
"navTxtStyle": "black"
},
"dark": {
"navBgColor": "#191919",
"navTxtStyle": "white"
}
}
View File
+13
View File
@@ -0,0 +1,13 @@
const TokenKey = "App-Token";
export function getToken() {
return uni.getStorageSync(TokenKey);
}
export function setToken(token) {
return uni.setStorageSync(TokenKey, token);
}
export function removeToken() {
return uni.removeStorageSync(TokenKey);
}
+58
View File
@@ -0,0 +1,58 @@
/**
* 显示消息提示框
* @param content 提示的标题
*/
export function toast(content) {
uni.showToast({
icon: "none",
title: content,
});
}
/**
* 显示模态弹窗
* @param content 提示的标题
*/
export function showConfirm(content) {
return new Promise((resolve, reject) => {
uni.showModal({
title: "提示",
content: content,
cancelText: "取消",
confirmText: "确定",
success: function (res) {
resolve(res);
},
});
});
}
/**
* 参数处理
* @param params 参数
*/
export function tansParams(params) {
let result = "";
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && value !== "" && typeof value !== "undefined") {
if (typeof value === "object") {
for (const key of Object.keys(value)) {
if (
value[key] !== null &&
value[key] !== "" &&
typeof value[key] !== "undefined"
) {
let params = propName + "[" + key + "]";
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
}
}
} else {
result += part + encodeURIComponent(value) + "&";
}
}
}
return result;
}
@@ -0,0 +1,9 @@
const constant = {
avatar: "user_avatar",
id: "user_id",
name: "user_name",
roles: "user_roles",
permissions: "user_permissions",
};
export default constant;
+30
View File
@@ -0,0 +1,30 @@
import useDictStore from "@/store/modules/dict";
import { getDicts } from "@/api/system/dict/data";
import { ref, toRefs } from "vue";
/**
* 获取字典数据
*/
export function useDict(...args) {
const res = ref({});
return (() => {
args.forEach((dictType, index) => {
res.value[dictType] = [];
const dicts = useDictStore().getDict(dictType);
if (dicts) {
res.value[dictType] = dicts;
} else {
getDicts(dictType).then((resp) => {
res.value[dictType] = resp.data.map((p) => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass,
}));
useDictStore().setDict(dictType, res.value[dictType]);
});
}
});
return toRefs(res.value);
})();
}
@@ -0,0 +1,6 @@
export default {
401: "认证失败,无法访问系统资源",
403: "当前操作没有权限",
404: "访问资源不存在",
default: "系统未知错误,请反馈给管理员",
};
@@ -0,0 +1,55 @@
import store from "@/store";
/**
* 字符权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkPermi(value) {
if (value && value instanceof Array && value.length > 0) {
const permissions = store.getters && store.getters.permissions;
const permissionDatas = value;
const all_permission = "*:*:*";
const hasPermission = permissions.some((permission) => {
return (
all_permission === permission || permissionDatas.includes(permission)
);
});
if (!hasPermission) {
return false;
}
return true;
} else {
console.error(
`need roles! Like checkPermi="['system:user:add','system:user:edit']"`,
);
return false;
}
}
/**
* 角色权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkRole(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = store.getters && store.getters.roles;
const permissionRoles = value;
const super_admin = "admin";
const hasRole = roles.some((role) => {
return super_admin === role || permissionRoles.includes(role);
});
if (!hasRole) {
return false;
}
return true;
} else {
console.error(`need roles! Like checkRole="['admin','editor']"`);
return false;
}
}
+131
View File
@@ -0,0 +1,131 @@
import config from "@/config";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { useUserStore } from "@/store/modules/user";
import { toast, showConfirm, tansParams } from "@/utils/common";
import {
decryptTransportErrorResponse,
decryptTransportResponse,
encryptTransportRequest,
invalidateTransportKeyMeta,
resetTransportRequestConfig,
shouldRetryTransportWithFreshKey,
} from "@/utils/transportCrypto";
let timeout = 10000;
const baseUrl = config.baseUrl;
const request = async (config) => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
config.header = config.header || {};
config.headers = config.headers || {};
if (getToken() && !isToken) {
config.header["Authorization"] = "Bearer " + getToken();
}
try {
config = await encryptTransportRequest(config);
// get请求映射params参数
if (config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.url = url;
}
return await new Promise((resolve, reject) => {
uni.request({
method: config.method || "get",
timeout: config.timeout || timeout,
url: config.baseUrl || baseUrl + config.url,
data: config.data,
header: config.header,
dataType: "json",
success: async (response) => {
try {
const res = await decryptTransportResponse(response, config);
if (shouldRetryTransportWithFreshKey(res) && !config.__transportRetried) {
invalidateTransportKeyMeta();
config.__transportRetried = true;
config.headers.repeatSubmit = false;
resetTransportRequestConfig(config);
resolve(await request(config));
return;
}
const code = res.data.code || 200;
const msg = errorCode[code] || res.data.msg || errorCode["default"];
if (code === 401) {
showConfirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
).then((res) => {
if (res.confirm) {
useUserStore()
.logOut()
.then(() => {
uni.reLaunch({ url: "/pages/login" });
});
}
});
const error = new Error("无效的会话,或者会话已过期,请重新登录。");
error.response = res;
reject(error);
} else if (code === 500) {
const error = new Error(msg);
error.response = res;
reject(error);
} else if (code !== 200) {
const error = new Error(msg);
error.response = res;
reject(error);
} else {
resolve(res.data);
}
} catch (error) {
reject(error);
}
},
fail: reject,
});
});
} catch (error) {
error = await decryptTransportErrorResponse(error, config);
if (shouldRetryTransportWithFreshKey(error) && !config.__transportRetried) {
invalidateTransportKeyMeta();
config.__transportRetried = true;
config.headers.repeatSubmit = false;
resetTransportRequestConfig(config);
return request(config);
}
const response = error.response;
const responseStatus = response?.status ?? response?.statusCode;
const responseCode = response?.data?.code;
const responseMsg = response?.data?.msg;
if (responseMsg) {
uni.showToast({
title: responseMsg,
icon: "none",
duration: responseStatus === 429 || responseCode === 429 ? 5000 : 3000,
});
throw error;
}
let { message } = error;
if (message === "Network Error") {
message = "后端接口连接异常";
} else if (message && message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message && message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
if (message) {
toast(message);
}
throw error;
}
};
export default request;
@@ -0,0 +1,38 @@
import constant from "./constant";
// 存储变量名
let storageKey = "storage_data";
// 存储节点变量名
let storageNodeKeys = [
constant.avatar,
constant.id,
constant.name,
constant.roles,
constant.permissions,
];
const storage = {
set: function (key, value) {
if (storageNodeKeys.indexOf(key) != -1) {
let tmp = uni.getStorageSync(storageKey);
tmp = tmp ? tmp : {};
tmp[key] = value;
uni.setStorageSync(storageKey, tmp);
}
},
get: function (key) {
let storageData = uni.getStorageSync(storageKey) || {};
return storageData[key] || "";
},
remove: function (key) {
let storageData = uni.getStorageSync(storageKey) || {};
delete storageData[key];
uni.setStorageSync(storageKey, storageData);
},
clean: function () {
uni.removeStorageSync(storageKey);
},
};
export default storage;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,436 @@
import config from "@/config";
const TRANSPORT_BASE_URL = config.baseUrl;
const EXCLUDED_URL_PATTERNS = [
"/transport/crypto/frontend-config",
"/transport/crypto/public-key",
"/common/download",
"/common/download/resource",
];
const TRANSPORT_FRONTEND_CONFIG_CACHE_KEY = "transportCryptoFrontendConfig";
const TRANSPORT_FRONTEND_CONFIG_URL = "/transport/crypto/frontend-config";
const TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS = 60;
const DEFAULT_TRANSPORT_ENVELOPE_VERSION = "1";
const DEFAULT_REQUEST_ENVELOPE_ALGORITHM = "RSA_OAEP_AES_256_GCM";
const DEFAULT_RESPONSE_ENVELOPE_ALGORITHM = "AES_256_GCM";
const DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH = 4096;
let cachedTransportPolicy = null;
let inflightTransportPolicyPromise = null;
/**
* 获取当前 Unix 秒级时间戳。
*
* @returns {number} 当前时间戳
*/
function getNowTimestamp() {
return Math.floor(Date.now() / 1000);
}
/**
* 判断请求地址是否命中固定排除名单。
*
* @param {string} url 请求地址
* @returns {boolean} 是否命中排除规则
*/
function matchExcludedUrl(url = "") {
return EXCLUDED_URL_PATTERNS.some((pattern) => url.includes(pattern));
}
/**
* 判断请求路径是否命中路径前缀列表。
*
* @param {string} path 待匹配路径
* @param {string[]} pathPatterns 路径前缀集合
* @returns {boolean} 是否匹配成功
*/
function matchPathPrefix(path = "", pathPatterns = []) {
return pathPatterns.some(
(pattern) => path === pattern || path.startsWith(`${pattern}/`),
);
}
/**
* 从绝对地址中提取 pathname,或直接返回相对地址的路径部分。
*
* @param {string} url 请求地址
* @returns {string} 标准化后的路径
*/
function parseAbsoluteUrlPath(url = "") {
const normalizedUrl = String(url || "");
if (!normalizedUrl) {
return "/";
}
if (
!normalizedUrl.startsWith("http://") &&
!normalizedUrl.startsWith("https://")
) {
return normalizedUrl.split("?")[0] || "/";
}
const pathMatch = normalizedUrl.match(/^https?:\/\/[^/]+(\/[^?#]*)?/i);
return pathMatch?.[1] || "/";
}
/**
* 解析基础 API 地址对应的路径前缀。
*
* @param {string} baseUrl 基础 API 地址
* @returns {string} 基础路径前缀
*/
function getBaseApiPath(baseUrl = TRANSPORT_BASE_URL) {
if (!baseUrl) {
return "";
}
const baseApiPath = parseAbsoluteUrlPath(baseUrl);
return baseApiPath === "/" ? "" : baseApiPath;
}
/**
* 计算后端用于 AAD 与策略匹配的标准请求路径。
*
* @param {string} url 请求地址
* @param {string} baseUrl 基础 API 地址
* @returns {string} 标准化请求路径
*/
function getRequestPath(url = "", baseUrl = TRANSPORT_BASE_URL) {
const baseApiPath = getBaseApiPath(baseUrl);
const pathname = parseAbsoluteUrlPath(url);
if (baseApiPath && pathname.startsWith(baseApiPath)) {
const normalizedPath = pathname.slice(baseApiPath.length);
return normalizedPath || "/";
}
return pathname || "/";
}
/**
* 标准化后端下发的路径列表配置。
*
* @param {Array} paths 原始路径集合
* @returns {string[]} 标准化后的路径数组
*/
function normalizePaths(paths) {
if (!Array.isArray(paths)) {
return [];
}
return paths.map((path) => String(path || "").trim()).filter(Boolean);
}
/**
* 从请求头对象中读取指定字段。
*
* @param {Object} headers 请求头对象
* @param {string} name 请求头名称
* @returns {*} 请求头值
*/
function getHeaderValue(headers, name) {
if (!headers) {
return undefined;
}
return headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()];
}
/**
* 将后端配置响应转换为前端统一使用的策略对象。
*
* @param {Object} payload 后端返回的配置数据
* @returns {Object} 标准化后的传输加密策略
*/
function normalizeTransportPolicy(payload) {
return {
transportCryptoEnabled: Boolean(payload?.transportCryptoEnabled),
transportCryptoMode: String(payload?.transportCryptoMode || "off"),
transportCryptoActive: Boolean(payload?.transportCryptoActive),
envelopeVersion: String(
payload?.envelopeVersion || DEFAULT_TRANSPORT_ENVELOPE_VERSION,
),
publicKeyUrl: String(payload?.publicKeyUrl || "/transport/crypto/public-key"),
requestEnvelopeAlgorithm: String(
payload?.requestEnvelopeAlgorithm || DEFAULT_REQUEST_ENVELOPE_ALGORITHM,
),
responseEnvelopeAlgorithm: String(
payload?.responseEnvelopeAlgorithm || DEFAULT_RESPONSE_ENVELOPE_ALGORITHM,
),
enabledPaths: normalizePaths(payload?.enabledPaths),
requiredPaths: normalizePaths(payload?.requiredPaths),
excludePaths: normalizePaths(payload?.excludePaths),
maxEncryptedGetUrlLength: Number(
payload?.maxEncryptedGetUrlLength || DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH,
),
configExpireAt: Number(payload?.configExpireAt || 0),
retryAt: Number(payload?.retryAt || payload?.configExpireAt || 0),
};
}
/**
* 构建不可用场景下的本地兜底策略。
*
* @returns {Object} 明文回退策略
*/
function buildFallbackTransportPolicy() {
const nowTimestamp = getNowTimestamp();
return {
transportCryptoEnabled: false,
transportCryptoMode: "off",
transportCryptoActive: false,
envelopeVersion: DEFAULT_TRANSPORT_ENVELOPE_VERSION,
publicKeyUrl: "/transport/crypto/public-key",
requestEnvelopeAlgorithm: DEFAULT_REQUEST_ENVELOPE_ALGORITHM,
responseEnvelopeAlgorithm: DEFAULT_RESPONSE_ENVELOPE_ALGORITHM,
enabledPaths: [],
requiredPaths: [],
excludePaths: [...EXCLUDED_URL_PATTERNS],
maxEncryptedGetUrlLength: DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH,
configExpireAt:
nowTimestamp + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
retryAt: nowTimestamp + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
};
}
/**
* 基于旧策略生成短期可重试的缓存策略。
*
* @param {Object} policy 旧的策略对象
* @returns {Object} 可重试策略
*/
function buildRetryableTransportPolicy(policy) {
const normalizedPolicy = normalizeTransportPolicy(policy);
return {
...normalizedPolicy,
retryAt: getNowTimestamp() + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
};
}
/**
* 判断当前策略是否仍在可用期内。
*
* @param {Object} policy 待校验策略
* @returns {boolean} 是否可继续使用
*/
function isUsableTransportPolicy(policy) {
if (!policy || !policy.publicKeyUrl || !policy.retryAt) {
return false;
}
return policy.retryAt > getNowTimestamp();
}
/**
* 从本地缓存加载最近一次持久化的策略。
*
* @returns {Object|null} 缓存策略
*/
function loadPersistedTransportPolicy() {
const persistedTransportPolicy = uni.getStorageSync(
TRANSPORT_FRONTEND_CONFIG_CACHE_KEY,
);
if (!persistedTransportPolicy) {
return null;
}
return normalizeTransportPolicy(persistedTransportPolicy);
}
/**
* 请求后端传输加密前端配置。
* 这里直接使用原始 uni.request,避免策略初始化阶段反向依赖统一 request
* 包装器,导致“是否加密尚未判定时又要先走加密请求”的循环依赖。
*
* @returns {Promise<Object>} Uni 请求响应结果
*/
function requestFrontendConfig() {
return new Promise((resolve, reject) => {
uni.request({
url: `${TRANSPORT_BASE_URL}${TRANSPORT_FRONTEND_CONFIG_URL}`,
method: "GET",
timeout: 10000,
success: resolve,
fail: reject,
});
});
}
/**
* 获取请求加密使用的标准路径。
*
* @param {string} url 请求地址
* @param {string} baseUrl 基础 API 地址
* @returns {string} 标准请求路径
*/
export function getTransportRequestPath(
url = "",
baseUrl = TRANSPORT_BASE_URL,
) {
return getRequestPath(url, baseUrl);
}
/**
* 获取当前生效的传输加密策略。
*
* @returns {Object} 当前策略对象
*/
export function getTransportCryptoPolicy() {
return cachedTransportPolicy || buildFallbackTransportPolicy();
}
/**
* 清空当前策略缓存与持久化数据。
*
* @returns {void}
*/
export function invalidateTransportCryptoPolicy() {
cachedTransportPolicy = null;
inflightTransportPolicyPromise = null;
uni.removeStorageSync(TRANSPORT_FRONTEND_CONFIG_CACHE_KEY);
}
/**
* 确保本地已加载一份可用的传输加密策略。
*
* @param {boolean} forceRefresh 是否强制从后端刷新
* @returns {Promise<Object>} 当前可用策略
*/
export async function ensureTransportCryptoPolicyLoaded(forceRefresh = false) {
if (!forceRefresh && !cachedTransportPolicy) {
const persistedTransportPolicy = loadPersistedTransportPolicy();
if (isUsableTransportPolicy(persistedTransportPolicy)) {
cachedTransportPolicy = persistedTransportPolicy;
}
}
if (!forceRefresh && isUsableTransportPolicy(cachedTransportPolicy)) {
return cachedTransportPolicy;
}
if (inflightTransportPolicyPromise) {
return inflightTransportPolicyPromise;
}
inflightTransportPolicyPromise = requestFrontendConfig()
.then((response) => {
const payload = normalizeTransportPolicy(response?.data?.data || {});
cachedTransportPolicy = payload;
uni.setStorageSync(TRANSPORT_FRONTEND_CONFIG_CACHE_KEY, payload);
inflightTransportPolicyPromise = null;
return cachedTransportPolicy;
})
.catch((error) => {
const staleTransportPolicy =
cachedTransportPolicy || loadPersistedTransportPolicy();
inflightTransportPolicyPromise = null;
cachedTransportPolicy = staleTransportPolicy
? buildRetryableTransportPolicy(staleTransportPolicy)
: buildFallbackTransportPolicy();
uni.setStorageSync(
TRANSPORT_FRONTEND_CONFIG_CACHE_KEY,
cachedTransportPolicy,
);
if (staleTransportPolicy) {
console.warn(
"加载传输加密前端配置失败,当前继续沿用最近一次后端策略",
error,
);
} else {
console.warn("加载传输加密前端配置失败,当前回退为明文请求策略", error);
}
return cachedTransportPolicy;
});
return inflightTransportPolicyPromise;
}
/**
* 判断当前请求是否需要执行请求体加密。
*
* @param {Object} requestConfig 请求配置
* @param {Object} transportPolicy 传输加密策略
* @returns {boolean} 是否启用请求加密
*/
export function shouldEncryptRequest(
requestConfig,
transportPolicy = getTransportCryptoPolicy(),
) {
if (!transportPolicy.transportCryptoActive) {
return false;
}
const requestPath = getRequestPath(
requestConfig.url,
requestConfig.baseUrl || TRANSPORT_BASE_URL,
);
if (matchPathPrefix(requestPath, transportPolicy.excludePaths || [])) {
return false;
}
if (
(transportPolicy.enabledPaths || []).length &&
!matchPathPrefix(requestPath, transportPolicy.enabledPaths || [])
) {
return false;
}
if ((requestConfig.headers || {}).encrypt === false) {
return false;
}
if (matchExcludedUrl(requestConfig.url)) {
return false;
}
const contentType =
getHeaderValue(requestConfig.header, "Content-Type") ||
getHeaderValue(requestConfig.headers, "Content-Type") ||
"";
if (String(contentType).includes("multipart/form-data")) {
return false;
}
return true;
}
/**
* 判断当前响应是否需要执行自动解密。
*
* @param {Object} requestConfig 请求配置
* @param {Object} transportPolicy 传输加密策略
* @returns {boolean} 是否启用响应解密
*/
export function shouldEncryptResponse(
requestConfig,
transportPolicy = getTransportCryptoPolicy(),
) {
const requestPath = getRequestPath(
requestConfig.url,
requestConfig.baseUrl || TRANSPORT_BASE_URL,
);
if (matchPathPrefix(requestPath, transportPolicy.excludePaths || [])) {
return false;
}
if (
(transportPolicy.enabledPaths || []).length &&
!matchPathPrefix(requestPath, transportPolicy.enabledPaths || [])
) {
return false;
}
if ((requestConfig.headers || {}).encryptResponse === false) {
return false;
}
if (matchExcludedUrl(requestConfig.url)) {
return false;
}
if (requestConfig.__transportCryptoEnabledForRequest === true) {
return true;
}
if (requestConfig.__transportCryptoEnabledForRequest === false) {
return false;
}
return transportPolicy.transportCryptoActive;
}
/**
* 判断查询参数是否需要封装为加密信封。
*
* @param {Object} requestConfig 请求配置
* @param {Object} transportPolicy 传输加密策略
* @returns {boolean} 是否启用查询参数加密
*/
export function shouldEncryptQuery(
requestConfig,
transportPolicy = getTransportCryptoPolicy(),
) {
if ((requestConfig.headers || {}).encryptQuery === false) {
return false;
}
return shouldEncryptRequest(requestConfig, transportPolicy);
}
@@ -0,0 +1,214 @@
import forge from "node-forge/lib/forge";
import "node-forge/lib/util";
import "node-forge/lib/asn1";
import "node-forge/lib/oids";
import "node-forge/lib/cipher";
import "node-forge/lib/cipherModes";
import "node-forge/lib/aes";
import "node-forge/lib/jsbn";
import "node-forge/lib/pkcs1";
import "node-forge/lib/prime";
import "node-forge/lib/random";
import "node-forge/lib/md";
import "node-forge/lib/sha256";
import "node-forge/lib/mgf1";
import "node-forge/lib/pem";
import "node-forge/lib/pki";
let randomBytePool = "";
const FORGE_RANDOM_REFILL_BYTES = 4096;
/**
* 将 Uint8Array 转为 Forge 兼容的二进制字符串。
*
* @param {Uint8Array} uint8Array 字节数组
* @returns {string} 二进制字符串
*/
function uint8ArrayToBytes(uint8Array) {
return Array.from(uint8Array, (item) => String.fromCharCode(item)).join("");
}
/**
* 获取当前运行时的全局对象引用。
*
* @returns {Object|undefined} 全局对象
*/
function getRuntimeGlobal() {
if (typeof globalThis !== "undefined") {
return globalThis;
}
if (typeof self !== "undefined") {
return self;
}
if (typeof window !== "undefined") {
return window;
}
if (typeof global !== "undefined") {
return global;
}
return undefined;
}
/**
* 通过 Web Crypto 同步获取随机字节。
*
* @param {number} length 需要的字节长度
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
*/
function getWebCryptoRandomBytes(length) {
const runtimeCrypto = getRuntimeGlobal()?.crypto;
if (!runtimeCrypto?.getRandomValues) {
return null;
}
const bytes = new Uint8Array(length);
runtimeCrypto.getRandomValues(bytes);
return uint8ArrayToBytes(bytes);
}
/**
* 在 app-plus iOS 端通过原生 NSUUID 获取随机字节兜底。
*
* @param {number} length 需要的字节长度
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
*/
function getAppPlusIosRandomBytes(length) {
// #ifdef APP-PLUS
if (typeof plus === "undefined") {
return null;
}
const osName = String(plus.os?.name || "").toLowerCase();
if (osName !== "ios" || typeof plus.ios?.importClass !== "function") {
return null;
}
try {
const uuidClass = plus.ios.importClass("NSUUID");
let randomBytes = "";
while (randomBytes.length < length) {
const uuidObject = plus.ios.invoke(uuidClass, "UUID");
const uuidText = String(plus.ios.invoke(uuidObject, "UUIDString") || "")
.replace(/-/g, "")
.toLowerCase();
plus.ios.deleteObject(uuidObject);
if (!uuidText) {
return null;
}
for (
let index = 0;
index < uuidText.length && randomBytes.length < length;
index += 2
) {
const byteValue = parseInt(uuidText.slice(index, index + 2), 16);
if (isNaN(byteValue)) {
return null;
}
randomBytes += String.fromCharCode(byteValue);
}
}
return randomBytes;
} catch (error) {
return null;
}
// #endif
return null;
}
/**
* 在 app-plus Android 端通过原生 SecureRandom 获取随机字节兜底。
*
* @param {number} length 需要的字节长度
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
*/
function getAppPlusAndroidRandomBytes(length) {
// #ifdef APP-PLUS
if (typeof plus === "undefined") {
return null;
}
const osName = String(plus.os?.name || "").toLowerCase();
if (osName !== "android" || typeof plus.android?.importClass !== "function") {
return null;
}
try {
plus.android.importClass("java.security.SecureRandom");
const secureRandom =
typeof plus.android.newObject === "function"
? plus.android.newObject("java.security.SecureRandom")
: null;
if (!secureRandom) {
return null;
}
plus.android.importClass(secureRandom);
let randomBytes = "";
for (let index = 0; index < length; index += 1) {
randomBytes += String.fromCharCode(secureRandom.nextInt(256));
}
if (typeof plus.android.autoCollection === "function") {
plus.android.autoCollection(secureRandom);
}
return randomBytes;
} catch (error) {
return null;
}
// #endif
return null;
}
/**
* 在随机池不足时尝试同步补充一段新的随机字节。
*
* @param {number} length 当前至少需要的字节长度
* @returns {void}
*/
function refillRandomBytePool(length) {
const refillLength = Math.max(length, FORGE_RANDOM_REFILL_BYTES);
const randomBytes =
getWebCryptoRandomBytes(refillLength) ||
getAppPlusAndroidRandomBytes(refillLength) ||
getAppPlusIosRandomBytes(refillLength);
if (randomBytes) {
randomBytePool += randomBytes;
}
}
/**
* 从随机数池中按需提取字节串。
*
* @param {number} length 需要提取的字节长度
* @returns {string} Forge 使用的二进制字符串
*/
function consumeRandomBytes(length) {
if (randomBytePool.length < length) {
refillRandomBytePool(length);
}
if (randomBytePool.length < length) {
throw new Error("传输加密随机数池不足,且当前运行环境无法同步补充安全随机数");
}
const bytes = randomBytePool.slice(0, length);
randomBytePool = randomBytePool.slice(length);
return bytes;
}
forge.random.getBytesSync = function getBytesSync(length) {
return consumeRandomBytes(length);
};
forge.random.getBytes = function getBytes(length, callback) {
const bytes = consumeRandomBytes(length);
if (typeof callback === "function") {
callback(null, bytes);
}
return bytes;
};
/**
* 向 Forge 随机数池预填充平台侧生成的随机字节。
*
* @param {string} bytes 待注入的二进制字符串
* @returns {void}
*/
export function primeForgeRandomBytes(bytes) {
randomBytePool += String(bytes || "");
}
export default forge;
+72
View File
@@ -0,0 +1,72 @@
import { useUserStore } from "@/store";
import config from "@/config";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { toast, showConfirm, tansParams } from "@/utils/common";
let timeout = 10000;
const baseUrl = config.baseUrl;
export default function upload(config) {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
config.header = config.header || {};
if (getToken() && !isToken) {
config.header["Authorization"] = "Bearer " + getToken();
}
// get请求映射params参数
if (config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.url = url;
}
return new Promise((resolve, reject) => {
uni.uploadFile({
timeout: config.timeout || timeout,
url: baseUrl + config.url,
filePath: config.filePath,
name: config.name || "file",
header: config.header,
formData: config.formData,
success: (res) => {
let result = JSON.parse(res.data);
const code = result.code || 200;
const msg = errorCode[code] || result.msg || errorCode["default"];
if (code === 200) {
resolve(result);
} else if (code == 401) {
showConfirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
).then((res) => {
if (res.confirm) {
useUserStore()
.logOut()
.then((res) => {
uni.reLaunch({ url: "/pages/login/login" });
});
}
});
reject("无效的会话,或者会话已过期,请重新登录。");
} else if (code === 500) {
toast(msg);
reject("500");
} else if (code !== 200) {
toast(msg);
reject(code);
}
},
fail: (error) => {
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
toast(message);
reject(error);
},
});
});
}
+124
View File
@@ -0,0 +1,124 @@
/**
* 路径匹配器
* @param {string} pattern
* @param {string} path
* @returns {Boolean}
*/
export function isPathMatch(pattern, path) {
const regexPattern = pattern
.replace(/\//g, "\\/")
.replace(/\*\*/g, ".*")
.replace(/\*/g, "[^\\/]*");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
/**
* 判断value字符串是否为空
* @param {string} value
* @returns {Boolean}
*/
export function isEmpty(value) {
if (
value == null ||
value == "" ||
value == undefined ||
value == "undefined"
) {
return true;
}
return false;
}
/**
* 判断url是否是http或https
* @param {string} url
* @returns {Boolean}
*/
export function isHttp(url) {
return url.indexOf("http://") !== -1 || url.indexOf("https://") !== -1;
}
/**
* 判断path是否为外链
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ["admin", "editor"];
return valid_map.indexOf(str.trim()) >= 0;
}
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg =
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
return reg.test(url);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validLowerCase(str) {
const reg = /^[a-z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUpperCase(str) {
const reg = /^[A-Z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validAlphabets(str) {
const reg = /^[A-Za-z]+$/;
return reg.test(str);
}
/**
* @param {string} email
* @returns {Boolean}
*/
export function validEmail(email) {
const reg =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return reg.test(email);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function isString(str) {
return typeof str === "string" || str instanceof String;
}
/**
* @param {Array} arg
* @returns {Boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === "undefined") {
return Object.prototype.toString.call(arg) === "[object Array]";
}
return Array.isArray(arg);
}
@@ -0,0 +1,3 @@
import { icebreaker } from "@icebreakers/stylelint-config";
export default icebreaker();
+53
View File
@@ -0,0 +1,53 @@
import type { Config } from "tailwindcss";
import { getIconCollections, iconsPlugin } from "@egoist/tailwindcss-icons";
import cssMacro from "weapp-tailwindcss/css-macro";
import { isMp } from "./platform";
export default <Config>{
content: ["./index.html", "./src/**/*.{html,js,ts,jsx,tsx,vue}"],
theme: {
extend: {
// colors: {
// // 你可以在这里进行颜色的扩展
// primary: {
// 'DEFAULT': 'var(--color-primary, #0089FF)',
// 'light-3': 'var(--color-primary-light-3, rgb(85, 199, 255))',
// 'light-5': 'var(--color-primary-light-5, rgb(130, 217, 255))',
// 'light-7': 'var(--color-primary-light-7, rgb(175, 235, 255))',
// 'light-9': 'var(--color-primary-light-9, rgb(219, 252, 255))',
// 'dark-2': 'var(--color-primary-dark-2, rgb(0, 135, 204))',
// },
// },
},
},
// https://tw.icebreaker.top/docs/quick-start/uni-app-css-macro
plugins: [
cssMacro({
variantsMap: {
wx: "MP-WEIXIN",
"-wx": {
value: "MP-WEIXIN",
negative: true,
},
// 定义多个条件判断
// mv: {
// value: 'H5 || MP-WEIXIN'
// },
// '-mv': {
// value: 'H5 || MP-WEIXIN',
// negative: true
// }
},
}),
iconsPlugin({
// 在这里可以选择你要使用的 icon, 更多详见:
// https://icon-sets.iconify.design/
collections: getIconCollections(["svg-spinners", "mdi"]),
}),
],
corePlugins: {
// 小程序去使用 h5 的 preflight 和响应式 container 没有意义
preflight: !isMp,
container: !isMp,
},
};
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "esnext",
"outDir": "true",
"jsx": "preserve",
"lib": ["esnext", "dom"],
"useDefineForClassFields": true,
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"types": ["@dcloudio/types"],
"allowJs": true,
"strict": true,
"sourceMap": true,
"esModuleInterop": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+70
View File
@@ -0,0 +1,70 @@
import uni from "@dcloudio/vite-plugin-uni";
import { defineConfig } from "vite";
import { UnifiedViteWeappTailwindcssPlugin } from "weapp-tailwindcss/vite";
import { WeappTailwindcssDisabled } from "./platform";
import postcssPlugins from "./postcss.config";
function patchNodeForgeMiniProgramRuntime() {
return {
name: "patch-node-forge-mini-program-runtime",
enforce: "pre",
transform(code: string, id: string) {
if (id.includes("node-forge/lib/util.js")) {
return {
code: code.replace(
/return typeof self === 'undefined' \? window : self;/,
"return typeof globalThis !== 'undefined' ? globalThis : (typeof self === 'undefined' ? (typeof window === 'undefined' ? {} : window) : self);",
),
map: null,
};
}
if (id.includes("node-forge/lib/random.js")) {
return {
code: code.replace(
/var _crypto = globalScope\.crypto \|\| globalScope\.msCrypto;/,
"var _crypto = (globalScope && (globalScope.crypto || globalScope.msCrypto)) || null;",
),
map: null,
};
}
return null;
},
};
}
// https://vitejs.dev/config/
export default defineConfig(async () => {
// 新版本的 unplugin-auto-import 改成了只有 esm 格式的产物,而 uni-app 目前必须 cjs 格式
// 所以需要改成动态 import 的写法来进行引入
// 详见 https://github.com/sonofmagic/uni-app-vite-vue3-tailwind-vscode-template/issues/29
const { default: AutoImport } = await import("unplugin-auto-import/vite");
return {
// uvtw 一定要放在 uni 后面
plugins: [
uni(),
patchNodeForgeMiniProgramRuntime(),
UnifiedViteWeappTailwindcssPlugin({
rem2rpx: true,
disabled: WeappTailwindcssDisabled,
}),
AutoImport({
imports: ["vue", "uni-app", "pinia"],
dts: "./src/auto-imports.d.ts",
}),
],
// 内联 postcss 注册 tailwindcss
css: {
postcss: {
plugins: postcssPlugins,
},
// https://vitejs.dev/config/shared-options.html#css-preprocessoroptions
preprocessorOptions: {
scss: {
silenceDeprecations: ["legacy-js-api"],
},
},
},
};
});