init
Some checks are pending
Check / lint (push) Waiting to run
Check / typecheck (push) Waiting to run
Check / build (build, 18.x, ubuntu-latest) (push) Waiting to run
Check / build (build, 18.x, windows-latest) (push) Waiting to run
Check / build (build:app, 18.x, ubuntu-latest) (push) Waiting to run
Check / build (build:app, 18.x, windows-latest) (push) Waiting to run
Check / build (build:mp-weixin, 18.x, ubuntu-latest) (push) Waiting to run
Check / build (build:mp-weixin, 18.x, windows-latest) (push) Waiting to run

This commit is contained in:
scout 2024-11-11 14:46:14 +08:00
parent 974b5212f0
commit b54bfe63ad
1319 changed files with 215562 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

BIN
.github/images/preview.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

71
.github/workflows/check.yml vendored Normal file
View File

@ -0,0 +1,71 @@
name: Check
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18.x
cache: pnpm
- name: Install
run: pnpm i
- name: Lint
run: pnpm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18.x
cache: pnpm
- name: Install
run: pnpm i
- name: TypeCheck
run: pnpm run type-check
build:
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
matrix:
node_version: [18.x]
os: [ubuntu-latest, windows-latest]
build_cmd: [build, 'build:mp-weixin', 'build:app']
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- name: Set node version to ${{ matrix.node_version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node_version }}
cache: pnpm
- name: Install
run: pnpm i
- name: Build
run: pnpm run ${{ matrix.build_cmd }}

21
.gitignore vendored Normal file
View File

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

3
.npmrc Normal file
View File

@ -0,0 +1,3 @@
strict-peer-dependencies=false
auto-install-peers=true
shamefully-hoist=true

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

@ -0,0 +1,11 @@
{
"recommendations": [
"antfu.vite",
"antfu.iconify",
"antfu.unocss",
"vue.volar",
"dbaeumer.vscode-eslint",
"editorConfig.editorConfig",
"uni-helper.uni-helper-vscode"
]
}

16
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug h5",
"type": "chrome",
"runtimeArgs": [
"--remote-debugging-port=9222"
],
"request": "launch",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}",
"preLaunchTask": "uni:h5"
}
]
}

86
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,86 @@
{
// Enable the ESlint flat config support
"eslint.experimental.useFlatConfig": true,
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{
"rule": "style/*",
"severity": "off"
},
{
"rule": "*-indent",
"severity": "off"
},
{
"rule": "*-spacing",
"severity": "off"
},
{
"rule": "*-spaces",
"severity": "off"
},
{
"rule": "*-order",
"severity": "off"
},
{
"rule": "*-dangle",
"severity": "off"
},
{
"rule": "*-newline",
"severity": "off"
},
{
"rule": "*quotes",
"severity": "off"
},
{
"rule": "*semi",
"severity": "off"
}
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
],
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"vite.config.*": "pages.config.*, manifest.config.*, uno.config.*, volar.config.*, *.env, .env.*"
},
"search.exclude": {
"**/node_modules": true,
"**/uni_modules": true,
"**/build": true,
"**/dist": true,
"**/.git": true,
"**/.vscode": true
},
"i18n-ally.localesPaths": [
"src/uni_modules/tmui/locale",
"src/uni_modules/wot-design-uni/locale",
"src/uni_modules/wot-design-uni/locale/lang",
"src/uni_modules/tmui/tool/dayjs/locale",
"src/uni_modules/uni-popup/components/uni-popup/i18n",
"src/uni_modules/z-paging/components/z-paging/i18n",
"src/uni_modules/tmui/tool/dayjs/esm/locale"
],
"vue-i18n.i18nPaths": "src\\uni_modules\\tmui\\locale,src\\uni_modules\\wot-design-uni\\locale,src\\uni_modules\\wot-design-uni\\locale\\lang,src\\uni_modules\\tmui\\tool\\dayjs\\locale,src\\uni_modules\\uni-popup\\components\\uni-popup\\i18n,src\\uni_modules\\z-paging\\components\\z-paging\\i18n,src\\uni_modules\\tmui\\tool\\dayjs\\esm\\locale"
}

16
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "uni:h5",
"type": "npm",
"script": "dev --devtools",
"isBackground": true,
"problemMatcher": "$vite",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

8
env/.env.dev vendored Normal file
View File

@ -0,0 +1,8 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'dev'
# 是否显示console
VITE_SHOW_CONSOLE = true
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = true
# baseUrl
VITE_BASEURL = 'http://warehouse.szjixun.cn/oa_backend'

8
env/.env.prod vendored Normal file
View File

@ -0,0 +1,8 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'prod'
# 是否显示console
VITE_SHOW_CONSOLE = true
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = true
# baseUrl
VITE_BASEURL = 'https://oa-a.szjixun.cn/api'

8
env/.env.test vendored Normal file
View File

@ -0,0 +1,8 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
# 是否显示console
VITE_SHOW_CONSOLE = true
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = true
# baseUrl
VITE_BASEURL = 'https://warehouse.szjixun.cn/oa_backend'

22
index.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="static/logo.svg">
<style>
</style>
<script>
const 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>
<title></title>
</head>
<body>
<div id="app">
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

65
package.json Normal file
View File

@ -0,0 +1,65 @@
{
"name": "unihelper",
"version": "0.0.0",
"private": true,
"packageManager": "pnpm@8.14.1",
"license": "MIT",
"scripts": {
"test:h5": "uni --mode test",
"prod:h5": "uni --mode prod",
"build:h5:test": "uni build --mode test",
"build:h5:prod": "uni build --mode prod"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-app-plus": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-components": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-h5": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-mp-alipay": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-mp-baidu": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-mp-jd": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-mp-lark": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-mp-qq": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-mp-toutiao": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-mp-xhs": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-quickapp-webview": "3.0.0-alpha-4000020240111001",
"@uni-helper/axios-adapter": "^1.5.2",
"@uni-helper/localforage-adapter": "^1.0.2",
"@uni-helper/uni-use": "^0.19.12",
"@vueuse/core": "^9.13.0",
"axios": "^1.7.2",
"dayjs": "^1.11.12",
"nzh": "^1.0.13",
"vconsole": "^3.15.1",
"vue": "^3.3.8",
"vue-i18n": "^9.6.5"
},
"devDependencies": {
"@dcloudio/types": "^3.4.7",
"@dcloudio/uni-automator": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-cli-shared": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-stacktracey": "3.0.0-alpha-4000020240111001",
"@dcloudio/uni-vue-devtools": "3.0.0-alpha-4000020240111001",
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-4000020240111001",
"@iconify-json/carbon": "^1.1.27",
"@types/node": "^20.11.4",
"@uni-helper/uni-app-types": "^0.5.12",
"@uni-helper/uni-env": "^0.1.1",
"@uni-helper/unocss-preset-uni": "^0.2.9",
"@uni-helper/volar-service-uni-pages": "^0.2.14",
"@uni-ku/root": "^0.0.1",
"@vue/runtime-core": "^3.3.8",
"@vue/tsconfig": "^0.5.1",
"lint-staged": "^15.2.0",
"pinia": "2.0.36",
"sass": "^1.77.8",
"simple-git-hooks": "^2.9.0",
"typescript": "^5.3.3",
"unocss": "^0.58.9",
"unocss-applet": "^0.8.2",
"vite": "^5.0.11",
"vue-tsc": "^1.8.27"
}
}

9983
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

30
src/App.vue Normal file
View File

@ -0,0 +1,30 @@
<script setup>
import {useStatus} from "@/store/status";
const {statusBarHeight}= useStatus()
const root = document.documentElement
root.style.setProperty('--statusBarHeight',`${statusBarHeight.value}px`)
</script>
<style lang="scss">
@import "@/static/css/color.scss";
/* #ifdef APP-NVUE */
@import '@/uni_modules/tmui/scss/nvue.css';
/* #endif */
/* #ifndef APP-NVUE */
@import '@/uni_modules/tmui/scss/noNvue.css';
/* #endif */
*{
box-sizing: border-box;
}
/*解决阅览图片关闭按钮会显示在状态栏区域的问题*/
#u-a-p>div>div{
margin-top:var(--statusBarHeight)
}
/*不显示滚动条的类*/
.no-scroll {
-ms-overflow-style: none; /* IE 和 Edge */
scrollbar-width: none; /* Firefox */
}
.no-scroll::-webkit-scrollbar {
display: none; /* Webkit 浏览器 */
}
</style>

15
src/api/login/index.js Normal file
View File

@ -0,0 +1,15 @@
import request from '@/service/index.js'
export const login = (data) => {
return request({
url: '/oa/login',
method: 'POST',
data,
})
}
export const send = (data) => {
return request({
url: '/oa/send',
method: 'POST',
data,
})
}

View File

@ -0,0 +1,29 @@
<script setup>
import TmImage from "@/uni_modules/tmui/components/tm-image/tm-image.vue";
import {useAuth} from "@/store/auth";
import { useClockIn } from "@/store/clockIn/index.js";
const {userInfo}=useAuth()
const {workingTimeInfoData,actionTypeData} = useClockIn()
</script>
<template>
<div class="flex-shrink-0 pl-[16rpx] pr-[40rpx] flex items-center rounded-[8rpx] w-[686rpx] h-[154rpx] bg-white">
<div class="rounded-full overflow-hidden w-[96rpx] h-[96rpx]">
<tm-image preview :width="96" :height="96" :src="userInfo.Avatar"></tm-image>
</div>
<div class="ml-[20rpx]">
<div class="flex items-center">
<div class="text-[32rpx] text-black">{{ userInfo.NickName }}</div>
<div class="mx-[14rpx] h-[30rpx] w-[1rpx] bg-[#F7F7F7]"></div>
<div class="w-[40rpx] h-[40rpx]">
<img v-if="actionTypeData.isWorkDay ===1" class="w-[40rpx] h-[40rpx]" src="@/static/image/clockIn/zu3275@3x.png" alt="">
<img v-else class="w-[40rpx] h-[40rpx]" src="@/static/image/clockIn/rest3275@2x.png" alt="">
</div>
</div>
<div class="mt-[5rpx] flex">
<div class="text-[24rpx] text-[#999999]">{{ workingTimeInfoData.WorkTimeTemplateName }}</div>
<div class="text-[#46299D] text-[24rpx]">考勤规则</div>
</div>
</div>
<slot name="right"></slot>
</div>
</template>

View File

@ -0,0 +1,28 @@
/* #ifdef H5 */
uni-page {
opacity: 0;
}
uni-page.animation-before {
/* 在页面上使用 transform 会导致页面内的 fixed 定位渲染为 absolute需要在动画完成后移除 */
transform: translateY(20px);
}
uni-page.animation-leave {
transition: all .3s ease;
}
uni-page.animation-enter {
transition: all .3s ease;
}
uni-page.animation-show {
opacity: 1;
}
uni-page.animation-after {
/* 在页面上使用 transform 会导致页面内的 fixed 定位渲染为 absolute需要在动画完成后移除 */
transform: translateY(0);
}
/* #endif */

View File

@ -0,0 +1,37 @@
<script>
import './index.css'
export default {
// #ifdef H5
onLaunch: function() {
this.show()
this.$router.beforeEach((to, from, next) => {
this.hide(next)
})
this.$router.afterEach(() => {
setTimeout(this.show, 50)
})
},
methods: {
hide(callback) {
const classList = document.querySelector('uni-page').classList
classList.add('animation-before', 'animation-leave')
classList.remove('animation-show')
setTimeout(() => {
classList.remove('animation-before', 'animation-leave')
callback && callback()
}, 200)
},
show() {
const classList = document.querySelector('uni-page').classList
classList.add('animation-before')
setTimeout(() => {
classList.add('animation-enter', 'animation-after', 'animation-show')
setTimeout(() => {
classList.remove('animation-before', 'animation-after', 'animation-enter')
}, 200)
}, 20)
}
},
// #endif
}
</script>

View File

@ -0,0 +1,155 @@
<script setup>
import {nextTick, ref,computed,watch} from "vue";
import dayjs from "dayjs";
import {useCalendar} from "@/store/calendar";
const {generateCalendarData}= useCalendar()
const current = ref(1);
const swiperItems = [0, 1, 2];
const showDays=(item)=>{
if (current.value===item){
return currentDays.value
}else{
if (current.value-item===1 ||
current.value - item === -2){
return preDays.value
}else{
return nextDays.value
}
}
}
const props=defineProps({
value:{
type:String,
default:''
}
})
const swiperHeight=()=>{
return `${showDays(current.value)?.length/7 * 72}rpx`
}
const emit = defineEmits(['update:value','change-dates']);
const currentDays = ref([])
const preDays = ref([])
const nextDays = ref([])
const nextMonth = computed(()=>{
return dayjs(props.value, 'YYYY-MM').add(1, 'month').format('YYYY-MM')
})
const preMonth = computed(()=>{
return dayjs(props.value, 'YYYY-MM').subtract(1, 'month').format('YYYY-MM')
})
const initDates=()=>{
currentDays.value=generateCalendarData(props.value)
preDays.value=generateCalendarData(preMonth.value)
nextDays.value=generateCalendarData(nextMonth.value)
emit('change-dates',showDays(current.value))
}
watch(()=>props.value,()=>{
initDates()
})
const initLoad=()=>{
initDates()
}
initLoad()
function updateMonthsData(direction) {
if (direction === "prev") {
emit('update:value',preMonth.value)
} else if (direction === "next") {
emit('update:value',nextMonth.value)
}
nextTick(()=>{
initDates()
})
}
function handleSwiperChange(e) {
const pre = current.value;
const current2 = e.detail.current;
/* //
*current - pre === 1, -2时是下一个月/
*current -pre === -1, 2时是上一个月或者上一周
*/
current.value = current2;
if (current2 - pre === 1 || current2 - pre === -2) {
updateMonthsData('next');
} else {
updateMonthsData('prev');
}
}
const contract=ref(false)
</script>
<template>
<div class="x-calendar">
<div class="content1">
<div class="wrap1" v-for="day in ['日', '一', '二', '三', '四', '五', '六']" :key="day">
{{ day }}
</div>
</div>
<div class="content4"></div>
<div class="content2"></div>
<swiper :duration="500" :current="current" :style="{height: contract?'72rpx':swiperHeight(),transition: `height 0.2s ease`}" :indicator-dots="false" :circular="true" @change="handleSwiperChange">
<swiper-item v-for="(item, index) in swiperItems" :key="item">
<div class="content3">
<div class="wrap1" v-for="(day,index1) in showDays(item)" :key="day.date">
<slot name="cell" :data="day" :index="index1" ></slot>
</div>
</div>
</swiper-item>
</swiper>
<div class="content5" @click="contract=!contract">
<div :class="`triangle ${contract?'rotate':''}`"></div>
</div>
</div>
</template>
<style scoped lang="scss">
.x-calendar {
.content5{
padding-top: 22rpx;
padding-bottom: 22rpx;
display: flex;
justify-content: center;
align-items: center;
.triangle {
width: 0;
height: 0;
border-left: 14rpx solid transparent;
border-right: 14rpx solid transparent;
border-bottom: 12rpx solid #C2C2C2;
&.rotate{
transform: rotate(180deg);
}
}
}
.content4{
margin-top: 32rpx;
width: 100%;
height: 1rpx;
background-color: #EFEFF5;
margin-bottom: 24rpx;
}
.content3 {
display: flex;
flex-wrap: wrap;
.wrap1 {
flex: 1 0 calc(100% / 7);
}
}
.content1 {
margin-top: 46rpx;
display: flex;
.wrap1 {
font-size: 28rpx;
color: #191919;
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
}
.content2 {
height: 4rpx;
}
}
</style>

View File

@ -0,0 +1,49 @@
.ayi-captcha {
position: relative;
width: 100%;
overflow: hidden;
&__input {
position: absolute;
left: -100%;
height: 100%;
width: 200%;
opacity: 0;
}
&__code {
display: flex;
width: 100%;
}
&__item {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
border-radius: 10rpx;
}
&__value {
font-size: 40rpx;
font-weight: 500;
}
&__cursor {
height: 40rpx;
width: 4rpx;
background-color: #000;
animation: flash 1s infinite ease;
}
/* &.is-border {
.ayi-captcha__item {
border: 2rpx solid $uni-color-primary;
}
}*/
@keyframes flash {
0% {
opacity: 0.2;
}
50% {
opacity: 0.5;
}
100% {
opacity: 0.2;
}
}
}

View File

@ -0,0 +1,63 @@
<template>
<div class="ayi-captcha" :class="{ 'is-border': border }">
<input class="ayi-captcha__input" v-model="value" type="number" :focus="focus" :maxlength="len" @focus="onFocus" @blur="onBlur" @input="onInput" />
<div class="ayi-captcha__code">
<div class="ayi-captcha__item" :style="{ height: setRpx(height), margin: `0 ${setRpx(gutter)}`, backgroundColor }" v-for="(_, index) in list" :key="index">
<span class="ayi-captcha__value">{{ value[index] }}</span>
<div class="ayi-captcha__cursor" v-if="value.length == index && focus"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue"
import { setRpx } from "./tools.js"
const props = defineProps({
modelValue: String,
focus: Boolean,
height: {
type: [String, Number],
default: 140
},
len: {
type: Number,
default: 4
},
gutter: {
type: Number,
default: 20
},
border: Boolean,
backgroundColor: {
type: String,
default: "#ebecee"
}
})
const emit = defineEmits(["update:modelValue", "done"])
const value = ref<string>(props.modelValue || "")
const focus = ref<boolean>(false)
const list = computed(() => new Array(props.len).fill(1))
watch(
() => props.modelValue,
(val: any) => {
value.value = val
}
)
function onFocus() {
focus.value = true
}
function onBlur() {
focus.value = false
}
function onInput(e: any) {
const val = e.detail.value
emit("update:modelValue", val)
if (val.length === props.len) {
emit("done", val)
}
}
</script>
<style lang="scss" scoped>
@import "./index.scss";
</style>

View File

@ -0,0 +1,25 @@
function getTag(value) {
if (value == null) {
return value === undefined ? "[object Undefined]" : "[object Null]"
}
return toString.call(value)
}
function isObjectLike(value) {
return typeof value === "object" && value !== null
}
export function isNumber(value) {
return typeof value === "number" || (isObjectLike(value) && getTag(value) == "[object Number]")
}
export function isBoolean(value) {
return typeof value === "boolean"
}
export function isArray(value) {
return Array.isArray(value)
}
export function setRpx(val ) {
return isArray(val) ? val.map(setRpx).join(" ") : isNumber(val) ? `${val}rpx` : val
}

View File

@ -0,0 +1,50 @@
<script setup>
import {ref,nextTick} from 'vue'
import WdPopup from "@/uni_modules/wot-design-uni/components/wd-popup/wd-popup.vue";
import TmButton from "@/uni_modules/tmui/components/tm-button/tm-button.vue";
const confirmState=ref(false)
const cancel=ref(true)
let onConfirm=null
let onCancel=null
const confirm=ref(true)
const contentText=ref('')
const sendCancel=()=>{
confirmState.value=false
if (typeof onCancel==='function'){
onCancel()
}
}
const sendConfirm=()=>{
confirmState.value=false
if (typeof onConfirm==='function'){
onConfirm()
}
}
const showConfirm=({content,onConfirm:confirm,onCancel:cancel})=>{
confirmState.value=true
contentText.value=content
onConfirm=confirm
onCancel=cancel
}
defineExpose({
showConfirm
})
</script>
<template>
<wd-popup custom-style="border-radius: 16rpx;" modal-style="background-color: rgba(0,0,0,0.3);" v-model="confirmState">
<div class="flex flex-col w-[640rpx] h-[402rpx]">
<div class="flex justify-center items-center h-[288rpx] text-[32rpx] font-bold text-[#1A1A1A]">
{{contentText}}
</div>
<div class="flex flex-grow border-t-solid border-[#E7E7E7] border-1rpx text-[32rpx]">
<div class="flex justify-center items-center text-[#1A1A1A]">
<tm-button @click="sendCancel" :width="319" @touchstart="cancel=false" @touchend="cancel=true" :fontSize="32" :height="112" :margin="[0]" :font-color="'#1A1A1A'" :transprent="cancel" text label="取消"></tm-button>
</div>
<div class="h-[112rpx] w-[1rpx] bg-[#E7E7E7]"></div>
<div class="flex justify-center items-center text-[#CF3050]">
<tm-button @click="sendConfirm" @touchstart="confirm=false" @touchend="confirm=true" :width="319" :fontSize="32" :transprent="confirm" :height="112" :margin="[0]" :font-color="'#46299D'" text label="确定"></tm-button>
</div>
</div>
</div>
</wd-popup>
</template>

View File

@ -0,0 +1,19 @@
import { createApp } from 'vue';
import confirmPopup from '@/components/x-confirm/index.vue'
export default function useConfirm() {
function showConfirm(obj) {
const instance = createApp(confirmPopup,{
//监听消息关闭事件
onAfterLeave:()=>{
instance.unmount();
document.body.removeChild(mountNode);
}
});
const mountNode = document.createElement('div');
document.body.appendChild(mountNode);
const vm = instance.mount(mountNode);
vm.showConfirm(obj)
}
return { showConfirm };
}

View File

@ -0,0 +1,53 @@
<script setup>
import { computed } from 'vue';
import dayjs from 'dayjs';
import {theme} from "@/config/theme";
const props = defineProps({
format: {
type: String,
default: 'YYYY-MM-DD'
},
value: {
type: String,
default: ''
},
showSuffix:{
type: Object,
default: () => {
return{ year: '年', month: '月', day: '日', hour: '时', minute: '分', second: '秒' }
}
},
show: {
type: Boolean,
default: false
},
onConfirm: Function,
})
const showDetail = computed(() => ({
year: props.format.includes('YYYY'),
month: props.format.includes('MM'),
day: props.format.includes('DD'),
hour: props.format.includes('HH'),
minute: props.format.includes('mm'),
second: props.format.includes('ss'),
ampm: props.format.includes('A'),
}))
const defaultValue = computed(() => dayjs(`${props.value}-01-01`).valueOf());
</script>
<template>
<tm-time-picker
:showDetail="showDetail"
:show="show"
:model-str="value"
:default-value="defaultValue"
:color="theme.colors.primary"
v-bind="{...$props,...$attrs}"
/>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,55 @@
import { createApp, h, ref } from 'vue';
import xDateSelect from './index.vue';
import dayjs from "dayjs";
//根据传入的日期格式和日期 生成对应的时间戳
function generateTimestamp(format, date) {
// 直接使用传入的 format 字符串作为解析格式
const dateStr = dayjs(date, format);
if (!dateStr.isValid()) {
throw new Error('Invalid date');
}
return dateStr.valueOf(); // 返回时间戳
}
let datePickerApp = null;
let container=null
export function openDatePicker(options = {}) {
// 如果已经存在一个日期选择器实例,先销毁它
if (datePickerApp) {
datePickerApp.unmount();
datePickerApp = null; // 防止重复销毁
}
if (container){
//清除dom
document.body.removeChild(container);
}
// 创建日期选择器实例
datePickerApp = createApp({
setup() {
const showdate = ref(true)
// 处理确认事件
const handleConfirm = (value) => {
options?.onConfirm?.(dayjs(value).format(options.format)); // 使用可选链调用
showdate.value = false; // 关闭日期选择器
};
return () => h(xDateSelect, {
...options,
defaultValue: generateTimestamp(options.format, options.value),
'onUpdate:show': (newValue) => {
showdate.value = newValue;
if (!newValue) {
datePickerApp?.unmount(); // 确保不会重复销毁
datePickerApp = null;
}
},
onConfirm: handleConfirm,
show: showdate.value,
});
}
})
container = document.createElement('div');
document.body.appendChild(container);
datePickerApp.mount(container);
}

View File

@ -0,0 +1,16 @@
<script setup>
/*setTimeout(()=>{
location.reload()
},2000)*/
</script>
<template>
<div>加载错误加载错误加载错误加载错误加载错误加载错误加载错误加载错误</div>
<div>加载错误加载错误加载错误加载错误加载错误加载错误加载错误加载错误</div>
<div>加载错误加载错误加载错误加载错误加载错误加载错误加载错误加载错误</div>
<div>加载错误加载错误加载错误加载错误加载错误加载错误加载错误加载错误</div>
<div>加载错误加载错误加载错误加载错误加载错误加载错误加载错误加载错误</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,39 @@
import { createVNode, render } from 'vue'
import Loading from './index.vue'
const vnode = createVNode(Loading)
export const vLoading = {
mounted(el, binding) {
render(vnode, el)
formatterClass(el, binding)
},
updated(el, binding) {
if (binding.value) {
vnode?.component?.exposed.show()
} else {
vnode?.component?.exposed.hide()
}
formatterClass(el, binding)
},
unmounted() {
vnode?.component?.exposed.hide()
},
}
function formatterClass(el, binding) {
const classStr = el.className
const hasTargetClass = classStr.includes('loading-parent')
if (binding.value) {
if (!hasTargetClass) {
el.classList.add('loading-parent')
el.style.position = 'relative'
}
} else {
if (hasTargetClass) {
el.classList.remove('loading-parent')
el.style.position = ''
}
}
}

View File

@ -0,0 +1,57 @@
<!-- -->
<template>
<div v-if="isShow" class="absolute inset-0 w-full h-full overflow-hidden z-[3] bg-[rgba(255,255,255,0.8)]">
<div class="flex flex-col justify-center items-center absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2">
<wd-loading :color="theme.colors.primary" />
<div class="mt-[2rpx] text-[25rpx]">
{{tip}}
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import WdLoading from "@/uni_modules/wot-design-uni/components/wd-loading/wd-loading.vue";
import {theme} from "@/config/theme";
const props = defineProps({
tip: {
type: String,
default() {
return "加载中...";
},
},
maskBackground: {
type: String,
default() {
return "rgba(255, 255, 255, 0.8)";
},
},
loadingColor: {
type: String,
default() {
return "rgba(255, 255, 255, 1)";
},
},
textColor: {
type: String,
default() {
return "rgba(255, 255, 255, 1)";
},
},
});
const isShow = ref(false);
const show = () => {
isShow.value = true;
};
const hide = () => {
isShow.value = false;
};
defineExpose({
show,
hide,
isShow,
});
</script>

View File

@ -0,0 +1,52 @@
<script setup>
import message from './message/index.vue'
import { ref } from 'vue';
const visible = ref(false);
const messageText = ref('');
const messageType = ref('');
const emit = defineEmits(['after-leave']);
const onAfterLeave=()=>{
emit('after-leave');
}
const showMessage=({ type = 'warning', message, duration = 2000 })=> {
messageText.value = message;
messageType.value = type;
visible.value = true;
setTimeout(() => {
hideMessage()
}, duration);
}
const hideMessage=()=> {
visible.value = false;
}
defineExpose({
showMessage,
hideMessage
});
</script>
<template>
<transition name="fade" @after-leave="onAfterLeave">
<message v-if="visible" :text="messageText" :type="messageType" class="message-popup"></message>
</transition>
</template>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.1s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.message-popup {
position: fixed;
top: 184rpx;
left: 50%;
transform: translateX(-50%);
z-index: 99999;
transition: all 0.2s ease-in-out;
cursor: pointer;
user-select: none;
}
</style>

View File

@ -0,0 +1,68 @@
<script setup>
import {ref} from 'vue'
const props=defineProps({
type:{
type:String,
default:'success'
},
text:{
type:String,
default:''
}
})
const typeList=ref({
success:{
imgSrc:new URL(`@/static/image/login/xzsu@3x.png`, import.meta.url).href,
borderColor:'#C5E7D5',
bgColor:'#EDF7F2'
},
error:{
imgSrc:new URL(`@/static/image/login/gth@3x.png`, import.meta.url).href,
borderColor:'#F3CBD3',
bgColor:'#FBEEF1'
},
warning:{
imgSrc:new URL(`@/static/image/login/warn@3x.png`, import.meta.url).href,
borderColor:'#FAE0B5',
bgColor:'#FEF7ED'
}
})
</script>
<template>
<div class="content4" :style="{border:`2rpx solid ${typeList[type].borderColor}`,backgroundColor:typeList[type].bgColor}">
<div class="wrap1">
<img :src="typeList[type].imgSrc" alt="">
</div>
<div class="wrap2">{{text}}</div>
</div>
</template>
<style scoped lang="scss">
.content4{
filter: drop-shadow(0px 0px 5px rgba(0,0,0,0.1));
box-sizing: border-box;
padding-left: 30rpx;
padding-right: 30rpx;
width: 686rpx;
height: 92rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
.wrap2{
color: #000;
font-size: 28rpx;
}
.wrap1{
margin-right: 18rpx;
width: 40rpx;
height: 40rpx;
img{
width: 40rpx;
height: 40rpx;
}
}
}
</style>

View File

@ -0,0 +1,19 @@
import { createApp } from 'vue';
import MessagePopup from '@/components/x-message/index.vue'
export default function useMessagePopup() {
function showMessage(obj) {
const messageInstance = createApp(MessagePopup,{
//监听消息关闭事件
onAfterLeave:()=>{
messageInstance.unmount();
document.body.removeChild(mountNode);
}
});
const mountNode = document.createElement('div');
document.body.appendChild(mountNode);
const vm = messageInstance.mount(mountNode);
vm.showMessage(obj)
}
return { showMessage };
}

View File

@ -0,0 +1,11 @@
<script setup>
import tmNavbar from '@/uni_modules/tmui/components/tm-navbar/tm-navbar.vue';
import {useStatus} from "@/store/status"
const {currentNavbar} = useStatus()
</script>
<template>
<tm-navbar :hideBack="false" hideHome :title="currentNavbar.title"/>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,20 @@
<script setup>
import {ref} from 'vue'
import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
const pagingRef=ref(null)
defineExpose({
pagingRef
})
</script>
<template>
<z-paging ref="pagingRef" :show-scrollbar="false" :refresher-end-bounce-enabled="false" :refresher-complete-duration="500" :refresher-complete-delay="500" :refresher-fps="60" show-refresher-update-time use-virtual-list :fixed="false" v-bind="{ ...$attrs, ...$props}">
<template v-for="(slot, name) in $slots" :key="name" #[name]>
<slot :name="name"></slot>
</template>
</z-paging>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,19 @@
<script setup>
</script>
<template>
<div class="tab">
<slot></slot>
</div>
</template>
<style scoped lang="scss">
.tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,79 @@
<script setup>
import { theme } from '@/config/theme'
import tabbarItem from './components/tabbar-item/index.vue'
const emit = defineEmits(['update:active'])
const props=defineProps({
active:{
type:Number,
default:0
},
list:{
type:Array,
default:()=>[]
}
})
const clickItem = (item) => {
emit('update:active',item.value)
}
</script>
<template>
<div class="tabbar-container">
<div class="tabbar">
<tabbar-item v-for="item in list" :key="item.url" @click="clickItem(item)">
<div class="tab-content">
<img :src="item.value === active ? item.selectedIconPath : item.iconPath" :style="{width:item.iconWidth?item.iconWidth:'34rpx',height:item.iconHeight?item.iconHeight:'40rpx'}" class="tab-icon">
<div class="tab-text" :style="{ color: active === item.value ? theme.colors.primary : '#666666' }">
{{ item.text }}
</div>
</div>
</tabbar-item>
</div>
<!--底部安全区-->
<div class="content-placeholder"></div>
</div>
</template>
<style scoped lang="scss">
.tabbar-container {
flex-shrink: 0;
width: 100%;
background-color: #ffffff;
overflow: hidden;
.tabbar {
display: flex;
padding-top: 20rpx;
height: 104rpx;
.tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.tab-content {
display: flex;
flex-direction: column;
align-items: center;
.tab-icon {
width: 40rpx;
height: 40rpx;
}
.tab-text {
margin-top: 10rpx;
font-size: 20rpx;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.content-placeholder {
height: 58rpx;
}
}
</style>

1
src/config/index.js Normal file
View File

@ -0,0 +1 @@
export const baiduMapAK='hMPtCMLVem8gOeuFud9CEXC8k7woOWiq'

View File

@ -0,0 +1,58 @@
// 图片资源导入
import clockInIcon from '@/static/image/tabbar/wz@3x1.png' // 打卡图标
import clockInSelectedIcon from '@/static/image/tabbar/zu3499@3x.png' // 打卡选中图标
import attendanceIcon from '@/static/image/tabbar/attendance2.png' // 考勤图标
import attendanceSelectedIcon from '@/static/image/tabbar/kaoqin.png' // 考勤选中图标
import myIcon from '@/static/image/tabbar/my1.png' // 我的图标
import mySelectedIcon from '@/static/image/tabbar/my2.png' // 我的选中图标
import applyIcon from '@/static/image/apply/zu3809@3x.png' // 申请图标
import applySelectedIcon from '@/static/image/apply/zu3808@3x.png' // 申请选中图标
import spIcon from '@/static/image/apply/sp.png' // 审批图标
import spSelectedIcon from '@/static/image/apply/zu3812@3x.png' // 审批选中图标
// Tabbar 配置
export const tabbar = [
{
text: "打卡",
iconPath: clockInIcon,
selectedIconPath: clockInSelectedIcon,
value: 0,
iconWidth: '33.32rpx',
iconHeight: '40rpx',
url: '/pages/clockIn/index'
},
{
text: "考勤",
iconPath: attendanceIcon,
selectedIconPath: attendanceSelectedIcon,
value: 1,
url: '/pages/attendance/index'
},
{
text: "我的",
iconPath: myIcon,
selectedIconPath: mySelectedIcon,
value: 3,
url: '/pages/mine/index'
}
]
// 审批页面 Tabbar 配置
export const approveTabbar = [
{
text: "申请",
iconPath: applyIcon,
selectedIconPath: applySelectedIcon,
value: 0,
iconWidth: '33.32rpx',
iconHeight: '40rpx'
},
{
text: "审批中心",
iconPath: spIcon,
selectedIconPath: spSelectedIcon,
value: 1,
iconWidth: '37rpx',
iconHeight: '40rpx'
}
]

12
src/config/theme/index.js Normal file
View File

@ -0,0 +1,12 @@
export const theme = {
colors: {
primary: '#46299D',
secondary: '#35495e',
accent: '#ff4081',
error: '#f44336',
warning: '#ff9800',
info: '#2196f3',
success: '#4caf50'
}
};

8
src/config/tmui/index.js Normal file
View File

@ -0,0 +1,8 @@
//config.ts
import {theme} from "@/config/theme/index.js";
export const config = {
theme:{
primary:theme.colors.primary
}
}

51
src/main.js Normal file
View File

@ -0,0 +1,51 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import dayjs from "dayjs";
import 'virtual:uno.css'
import VConsole from "vconsole";
import '@/utils/uni.webview.js'
import tmui from "@/uni_modules/tmui"
import {config} from "@/config/tmui/index.js";
import 'dayjs/locale/zh-cn';
import xLoaderror from '@/components/x-loaderror/index.vue'
import { vLoading } from "@/components/x-loading/index.js"
import messagePopup from '@/components/x-message/useMessagePopup'
import pageAnimation from '@/components/page-animation/index.vue'
const {showMessage}=messagePopup()
dayjs.locale('zh-cn')
if (import.meta.env.VITE_SHOW_CONSOLE){
new VConsole()
}
export function createApp() {
const app = createSSRApp(App)
app.use(tmui,{...config})
app.directive("loading", vLoading)
app.mixin(pageAnimation)
app.component('x-loaderror',xLoaderror)
app.directive('no-space', {
mounted(el) {
el.addEventListener('input', (e) => {
const originalValue = e.target.value;
const newValue = originalValue.replace(/\s/g, '');
if (originalValue !== newValue) {
e.target.value = newValue;
e.target.dispatchEvent(new Event('input'));
}
});
}
})
window.message = ['success', 'error', 'warning'].reduce((acc, type) => {
acc[type] = (message) => {
if (typeof message === 'string') {
showMessage({ type, message });
} else if (typeof message === 'object') {
showMessage({ type, ...message });
}
};
return acc;
}, {});
return {
app,
}
}
createApp()

101
src/manifest.json Normal file
View File

@ -0,0 +1,101 @@
{
"name": "oa-host",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"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": {},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true,
"darkmode": true,
"themeLocation": "theme.json"
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"darkmode": true,
"template": "index.html",
"async": {
"loading": "AsyncLoading",
"error": "AsyncError",
"delay": 0,
"timeout": 60000
},
"sdkConfigs": {
// 使
"maps": {
"qqmap": {
// https://lbs.qq.com/dev/console/key/manage
"key": ""
},
"google": {
// HBuilderX 3.2.10+https://developers.google.com/maps/documentation/javascript/get-api-key
"key": ""
},
"amap": {
// HBuilderX 3.6.0+https://console.amap.com/dev/key/app
"key": "",
// HBuilderX 3.6.0+https://console.amap.com/dev/key/app
"securityJsCode": "",
// HBuilderX 3.6.0+https://lbs.amap.com/api/jsapi-v2/guide/abc/prepare
"serviceHost": ""
},
"bmap": {
// HBuilderX 3.99+http://lbsyun.baidu.com/apiconsole/key#/home
"key": "T4vteUoFUBsv6a5cxtw6kOInWb5nloxc"
}
}
}
}
}

31
src/pages.json Normal file
View File

@ -0,0 +1,31 @@
{
"easycom":{
"autoscan": true,
"custom":{
"^tm-(.*)": "@/tmui/components/tm-$1/tm-$1.vue"
}
},
"pages": [
{
"path": "pages/index/index",
"type": "page"
},
{
"path": "pages/login/index",
"type": "page",
"style": {}
}
],
"globalStyle": {
"backgroundColor": "#FFFFFF",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextstyle": "black",
"navigationBarTitleText":""
},
"subPackages": []
}

17
src/pages/index/index.vue Normal file
View File

@ -0,0 +1,17 @@
<script setup>
import XTabbar from "@/components/x-tabbar/index.vue"
import { tabbar } from '@/config/tabbar/index.js'
import {useStatus} from "@/store/status"
const {tabBarIndex}= useStatus()
</script>
<template>
<div class="flex flex-col h-[100vh]" >
123
</div>
</template>
<style scoped lang="scss">
</style>

367
src/pages/login/index.vue Normal file
View File

@ -0,0 +1,367 @@
<script setup>
import {theme} from "@/config/theme";
import xCaptcha from '@/components/x-captcha/index.vue'
import {ref} from 'vue'
import {login, send} from "@/api/login";
import {useAuth} from "@/store/auth";
const { token, refreshToken ,userInfo}=useAuth()
const formData=ref({
telNum: '',
password: '',
code: '',
})
const currentLoginType=ref('password')
const showPassword=ref(true)
const changeType=()=>{
currentLoginType.value=currentLoginType.value==='password'?'code':'password'
}
const changeType1=()=>{
currentLoginType.value=currentLoginType.value==='password'?'code':'password'
verificationCode.value=false
}
const agreement=ref(false)
const sendLogin= async ()=>{
const res=await login(formData.value)
if (res.status===0){
userInfo.value=res.data.AccountInfo
token.value=res.data.Token
refreshToken.value = res.data.RefreshToken;
message.success('登录成功')
uni.navigateTo({
url:'/pages/index/index'
})
}
}
const checkPassWordAndTelNum = () => {
const { telNum, password } = formData.value;
if (!telNum || !password) {
message.warning(!telNum ? '请输入手机号' : '请输入密码')
return false;
}
return true;
};
const checkTelNum = () => {
const { telNum } = formData.value;
if (!telNum) {
message.warning('请输入手机号' )
return false;
}
return true;
};
const sendCode=async ()=>{
const res=await send({telNum:formData.value.telNum})
if (res.status===0){
message.success('发送成功')
}
}
const countdown = ref(0);
const isCounting = ref(false);
const loginClick=async ()=>{
if (currentLoginType.value==='password'){
if (!checkPassWordAndTelNum()){
return
}
loading.value=true
await sendLogin()
loading.value=false
}else if (currentLoginType.value==='code'){
if (!checkTelNum()){
return
}
loading.value=true
await sendCode()
startCountdown()
loading.value=false
verificationCode.value=true
}
}
const verificationCode=ref(false)
const loading=ref(false)
const startCountdown = () => {
countdown.value = 60;
isCounting.value = true;
const interval = setInterval(() => {
countdown.value -= 1;
if (countdown.value <= 0) {
clearInterval(interval);
isCounting.value = false;
}
}, 1000)
}
const sendCodeClick=()=>{
startCountdown()
}
const updateCode=async (e)=>{
if (e.length===6){
await sendLogin()
}
}
</script>
<template>
<div class="outer-layer">
<div class="content1">
<div class="wrap1">
登录 <span class="wrap1_1">OA考勤</span>
</div>
<div class="wrap2">使用你的手机号登录</div>
</div>
<template v-if="!verificationCode">
<div class="content2">
<div class="wrap1">
<div class="wrap1_1">手机号</div>
<div class="wrap1_2">
<input type="number" style="width: 310rpx" placeholder-style="color: #B8B8B8;" v-model="formData.telNum" placeholder="请输入手机号">
</div>
<div v-if="formData.telNum" class="wrap1_3" @click="formData.telNum=''">
<image src="@/static/image/login/check-circle-filled@3x.png"></image>
</div>
</div>
<div class="wrap2" v-show="currentLoginType==='password'">
<div class="wrap2_1">密码</div>
<div class="wrap2_2">
<input :password="showPassword" style="width: 310rpx" placeholder-style="color: #B8B8B8;" v-model="formData.password" placeholder="请输入密码">
</div>
<div class="wrap2_3">
<div class="wrap2_3_1" @click="formData.password=''">
<img v-show="formData.password" src="@/static/image/login/check-circle-filled@3x.png"/>
</div>
<div class="wrap2_3_2" @click="showPassword=!showPassword" >
<img v-show="showPassword" class="wrap2_3_2_1" src="@/static/image/login/browse@3x.png"/>
<img v-show="!showPassword" class="wrap2_3_2_2" src="@/static/image/login/browse-off@3x.png"/>
</div>
</div>
</div>
<div class="wrap3">
<tm-button :fontSize="28" :fontColor="theme.colors.primary" transprent text @click="changeType" >{{currentLoginType==='password'?'验证码登录':'密码登录'}}</tm-button>
</div>
</div>
<div class="content3">
<div class="wrap1">
<div class="wrap1_1">
<tm-checkbox :color="theme.colors.primary" v-model="agreement" :margin="[0,0]" class="wrap1_1_1" :size="30" :round="10"></tm-checkbox>
</div>
<div class="wrap1_2">
已阅读并同意
<div class="wrap1_2_1">用户服务协议</div>
<div class="wrap1_2_2">隐私政策</div>
</div>
</div>
<div class="wrap2">
<tm-button v-if="agreement" @click="loginClick" linear="right" :color="theme.colors.primary" :loading="loading" icon="tmicon-tongzhifill" :height="96" block>{{currentLoginType==='password'?'登录':'获取验证码'}}</tm-button>
<tm-button v-if="!agreement" fontColor="#fff" color="#D9D9DB" :height="96" block>{{currentLoginType==='password'?'登录':'获取验证码'}}</tm-button>
</div>
</div>
</template>
<template v-if="verificationCode">
<div class="content4">
<div class="wrap1">
<div class="wrap1_1">已发送验证码至</div>
<div class="wrap1_2">{{formData.telNum}}</div>
</div>
<div class="wrap2">
<x-captcha @done="updateCode" v-model:model-value="formData.code" :len="6" :focus="true" :gutter="10" :height="100"></x-captcha>
</div>
<div class="wrap3">
<div class="wrap3_1">
<tm-button :fontSize="28" :fontColor="isCounting?'#BDBDBD':theme.colors.primary" transprent text @click="sendCodeClick" >重新发送 <span class="wrap3_1_1">{{countdown===0?'':`(${countdown})`}}</span></tm-button>
</div>
<div class="wrap3_2">
<tm-button :fontSize="28" :fontColor="theme.colors.primary" transprent text @click="changeType1" >密码登录</tm-button>
</div>
</div>
</div>
</template>
</div>
</template>
<style scoped lang="scss">
.outer-layer{
box-sizing: border-box;
background-image: url("@/static/image/login/gdz@3x.png");
background-repeat: no-repeat;
background-size: cover;
width: 100vw;
height: 100vh;
padding-right: 56rpx;
padding-left: 56rpx;
.content4{
margin-top: 84rpx;
.wrap3{
display: flex;
justify-content: space-between;
.wrap3_1{
.wrap3_1_1{
margin-left: 20rpx;
}
}
}
.wrap2{
margin-top: 12rpx;
}
.wrap1{
display: flex;
.wrap1_2{
font-size: 28rpx;
color: #191818;
}
.wrap1_1{
margin-right: 20rpx;
font-size: 28rpx;
color: #BDBDBD;
}
}
}
.content3{
margin-top: 46rpx;
.wrap2{
margin-top: 24rpx;
}
.wrap1{
display: flex;
justify-content: center;
align-items: center;
.wrap1_1{
margin-right: 6rpx;
}
.wrap1_2{
display: flex;
font-size: 28rpx;
.wrap1_2_1{
color: $theme-primary;
}
.wrap1_2_2{
color: $theme-primary;
}
}
}
}
.content2{
margin-top: 136rpx;
.wrap3{
display: flex;
justify-content: flex-end;
}
.wrap2{
border-radius: 8rpx;
overflow: hidden;
margin-top: 20rpx;
padding-top: 16rpx;
padding-bottom: 16rpx;
background-color: #F9F9F9;
display: flex;
align-items: center;
height: 96rpx;
padding-right: 22rpx;
.wrap2_4{
margin-left: 20rpx;
img{
width: 36rpx;
height: 26rpx;
}
}
.wrap2_3{
display: flex;
align-items: center;
.wrap2_3_1{
width: 36rpx;
height: 36rpx;
img{
width: 36rpx;
height: 36rpx;
}
}
.wrap2_3_2{
display: flex;
justify-content: center;
align-items: center;
margin-left: 20rpx;
margin-right: auto;
.wrap2_3_2_2{
width: 36rpx;
height: 26rpx;
}
.wrap2_3_2_1{
width: 36rpx;
height: 26rpx;
}
}
}
.wrap2_2{
padding-left: 38rpx;
display: flex;
justify-content: center;
align-items: center;
input{
font-size: 32rpx;
}
}
.wrap2_1{
border-right: 1rpx solid #D1D1D1;
color: #000;
display: flex;
justify-content: center;
align-items: center;
width: 174rpx;
}
}
.wrap1{
border-radius: 8rpx;
overflow: hidden;
padding-top: 16rpx;
padding-bottom: 16rpx;
background-color: #F9F9F9;
display: flex;
align-items: center;
height: 96rpx;
.wrap1_3{
width: 36rpx;
height: 36rpx;
image{
width: 36rpx;
height: 36rpx;
}
}
.wrap1_2{
padding-left: 38rpx;
display: flex;
justify-content: center;
align-items: center;
input{
font-size: 32rpx;
}
}
.wrap1_1{
border-right: 1rpx solid #D1D1D1;
color: #000;
display: flex;
justify-content: center;
align-items: center;
width: 174rpx;
}
}
}
.content1{
margin-top: 300rpx;
.wrap1{
line-height: 90rpx;
font-weight: bold;
font-size: 64rpx;
color: #191818;
.wrap1_1{
color: $theme-primary;
}
}
.wrap2{
font-size: 28rpx;
color: #191818;
}
}
}
</style>

91
src/service/index.js Normal file
View File

@ -0,0 +1,91 @@
import Request from '@/service/request/index.js'
import {useAuth} from "@/store/auth";
const { token ,refreshToken,userInfo}=useAuth()
let isRefreshing = false;
let refreshSubscribers = [];
const request = new Request({
baseURL: import.meta.env.VITE_BASEURL,
timeout: 1000 * 60 * 5,
interceptors: {
//实例的请求拦截器
requestInterceptors: (config) => {
config.headers['Content-Type'] = config.method === 'get' ?
'application/x-www-form-urlencoded' :
'application/json';
config.headers['Authorization'] = token.value
if (config.isFormData) {
config.headers['Content-Type'] = 'multipart/form-data';
}
return config;
},
//实例的响应拦截器
responseInterceptors: async (res) => {
if(res.data.status===1){
message.warning(res.data.msg)
}
if (res.data.status === 401) {
return getRefreshToken(res);
// uni.navigateTo({
// url:'/pages/login/index'
// })
}
if ([200, 201, 204].includes(res.status)) {
return res.config.responseType === 'blob' ? res : res;
} else {
/* message.error(res.data.msg || 'An error occurred.');*/
return Promise.reject(new Error(res.data.msg || 'An error occurred.'));
}
}
}
})
async function getRefreshToken(response) {
if (!isRefreshing) {
isRefreshing = true;
const refreshTokenT = refreshToken.value;
if (refreshTokenT) {
try {
const data = { refreshToken:refreshTokenT };
const res = await request.instance.post('/user/refresh/token', data);
if (res.code === 200) {
token.value = res.data.Token;
refreshToken.value = res.data.RefreshToken;
userInfo.value = res.data.AccountInfo
response.config.headers['Authorization'] = res.data.Token;
uni.navigateTo({
url:'/pages/index/index'
})
return request.request(response.config);
} else {
message.error(res.message || res.msg);
throw new Error(res.message || res.msg);
}
} catch (error) {
uni.navigateTo({
url:'/pages/login/index'
})
throw error
} finally {
isRefreshing = false;
refreshSubscribers.forEach(callback => callback());
refreshSubscribers = [];
}
} else {
uni.navigateTo({
url:'/pages/login/index'
})
throw new Error('No refresh token available.');
}
} else {
return new Promise(resolve => {
refreshSubscribers.push(() => resolve(request.request(response.config)));
});
}
}
const fontRequest = (config) => {
if (['get', 'GET'].includes(config.method)) {
config.params = config.data;
}
return request.request(config);
};
export default fontRequest;

View File

@ -0,0 +1,80 @@
import axios from 'axios';
import { createUniAppAxiosAdapter } from '@uni-helper/axios-adapter'
axios.defaults.adapter = createUniAppAxiosAdapter()
class Request {
// axios 实例
instance;
// 拦截器对象
interceptorsObj;
// * 存放取消请求控制器Map
abortControllerMap;
constructor(config) {
this.instance = axios.create(config);
// * 初始化存放取消请求控制器Map
this.abortControllerMap = new Map();
this.interceptorsObj = config.interceptors;
// 拦截器执行顺序 接口请求 -> 实例请求 -> 全局请求 -> 实例响应 -> 全局响应 -> 接口响应
this.instance.interceptors.request.use((res) => {
const controller = new AbortController();
const url = res.url || '';
res.signal = controller.signal;
this.abortControllerMap.set(url, controller);
return res;
}, (err) => err);
// 使用实例拦截器
this.instance.interceptors.request.use(this.interceptorsObj?.requestInterceptors, this.interceptorsObj?.requestInterceptorsCatch);
this.instance.interceptors.response.use(this.interceptorsObj?.responseInterceptors, this.interceptorsObj?.responseInterceptorsCatch);
// 全局响应拦截器保证最后执行
this.instance.interceptors.response.use(
// 因为我们接口的数据都在res.data下所以我们直接返回res.data
(res) => {
const url = res.config.url || '';
this.abortControllerMap.delete(url);
return res.data;
}, (err) => err);
}
request(config) {
return new Promise((resolve, reject) => {
// 如果我们为单个请求设置拦截器,这里使用单个请求的拦截器
if (config.interceptors?.requestInterceptors) {
config = config.interceptors.requestInterceptors(config);
}
this.instance
.request(config)
.then((res) => {
// 如果我们为单个响应设置拦截器,这里使用单个响应的拦截器
if (config.interceptors?.responseInterceptors) {
res = config.interceptors.responseInterceptors(res);
}
resolve(res);
})
.catch((err) => {
reject(err);
});
// .finally(() => {})
});
}
/**
* 取消全部请求
*/
cancelAllRequest() {
for (const [, controller] of this.abortControllerMap) {
controller.abort();
}
this.abortControllerMap.clear();
}
/**
* 取消指定的请求
* @param url 待取消的请求URL
*/
cancelRequest(url) {
const urlList = Array.isArray(url) ? url : [url];
for (const _url of urlList) {
this.abortControllerMap.get(_url)?.abort();
this.abortControllerMap.delete(_url);
}
}
}
export default Request;

View File

@ -0,0 +1 @@
$theme-primary: #46299D;

96
src/static/css/index.scss Normal file
View File

@ -0,0 +1,96 @@
#app{
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.loader {
position: relative;
width: 154px;
height: 154px;
border-radius: 10px;
}
.loader div {
width: 8%;
height: 24%;
background: $theme-primary;
position: absolute;
left: 50%;
top: 30%;
opacity: 0;
border-radius: 50px;
animation: fade458 1s linear infinite;
}
@keyframes fade458 {
from {
opacity: 1;
}
to {
opacity: 0.25;
}
}
.loader .bar1 {
transform: rotate(0deg) translate(0, -130%);
animation-delay: 0s;
}
.loader .bar2 {
transform: rotate(30deg) translate(0, -130%);
animation-delay: -1.1s;
}
.loader .bar3 {
transform: rotate(60deg) translate(0, -130%);
animation-delay: -1s;
}
.loader .bar4 {
transform: rotate(90deg) translate(0, -130%);
animation-delay: -0.9s;
}
.loader .bar5 {
transform: rotate(120deg) translate(0, -130%);
animation-delay: -0.8s;
}
.loader .bar6 {
transform: rotate(150deg) translate(0, -130%);
animation-delay: -0.7s;
}
.loader .bar7 {
transform: rotate(180deg) translate(0, -130%);
animation-delay: -0.6s;
}
.loader .bar8 {
transform: rotate(210deg) translate(0, -130%);
animation-delay: -0.5s;
}
.loader .bar9 {
transform: rotate(240deg) translate(0, -130%);
animation-delay: -0.4s;
}
.loader .bar10 {
transform: rotate(270deg) translate(0, -130%);
animation-delay: -0.3s;
}
.loader .bar11 {
transform: rotate(300deg) translate(0, -130%);
animation-delay: -0.2s;
}
.loader .bar12 {
transform: rotate(330deg) translate(0, -130%);
animation-delay: -0.1s;
}

29
src/static/error.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
<div>导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用导入使用</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

1
src/static/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="512" height="512" viewBox="0 0 512 512"><defs><clipPath id="master_svg0_25_97"><rect x="0" y="0" width="512" height="512" rx="0"/></clipPath><clipPath id="master_svg1_25_11"><rect x="11" y="39" width="490" height="435" rx="0"/></clipPath></defs><g style="mix-blend-mode:passthrough" clip-path="url(#master_svg0_25_97)"><g clip-path="url(#master_svg1_25_11)"><g><path d="M51.4931,294.222767578125L205.214,437.551767578125C211.594,443.498767578125,220.016,446.812767578125,228.778,446.812767578125C237.54,446.812767578125,245.962,443.498767578125,252.342,437.551767578125L254.554,435.512767578125C238.306,411.638767578125,228.778,382.751767578125,228.778,351.655767578125C228.778,269.073767578125,295.812,202.124767578125,378.5,202.124767578125C402.575,202.124767578125,425.288,207.817767578125,445.45,217.842767578125C446.215,212.320767578125,446.556,206.797767578125,446.556,201.19076757812502L446.556,196.262767578125C446.556,136.874967578125,403.595,86.238267578125,344.983,76.467747578125C306.191,70.010717578125,266.719,82.669897578125,238.986,110.36716757812499L228.778,120.562467578125L218.569,110.36716757812499C190.837,82.669897578125,151.365,70.010717578125,112.573,76.467747578125C53.9601,86.238267578125,11,136.874967578125,11,196.262767578125L11,201.19076757812502C11,236.448767578125,25.6319,270.17876757812496,51.4931,294.222767578125ZM378.5,473.999767578125C446.13,473.999767578125,501,419.199767578125,501,351.655767578125C501,284.112767578125,446.13,229.312767578125,378.5,229.312767578125C310.87,229.312767578125,256,284.112767578125,256,351.655767578125C256,419.199767578125,310.87,473.999767578125,378.5,473.999767578125Z" fill="#2B9939" fill-opacity="1"/></g><g style="mix-blend-mode:passthrough"><path d="M322,415L441,415L441,293.5L419,293.5L419,393L344.5,393L344.5,293.5L322,293.5L322,415Z" fill="#FFFFFF" fill-opacity="1"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

20
src/store/auth/index.js Normal file
View File

@ -0,0 +1,20 @@
import {createGlobalState,useStorage} from '@vueuse/core'
import {uniStorage} from "@/utils/uniStorage.js"
import {ref} from 'vue'
export const useAuth = createGlobalState(() => {
const token = useStorage('token', '', uniStorage)
const refreshToken = useStorage('refreshToken', '', uniStorage)
const userInfo = useStorage('userInfo', {}, uniStorage)
const leaderList = useStorage('leaderList', [], uniStorage)
const isLeader=ref(false)
// const leaderList=ref([])
return {
leaderList,
userInfo,
token,
refreshToken,
}
})

15
src/store/status/index.js Normal file
View File

@ -0,0 +1,15 @@
import {ref} from 'vue'
import {createGlobalState, useStorage} from "@vueuse/core";
import {uniStorage} from "@/utils/uniStorage";
export const useStatus =createGlobalState(()=>{
const currentNavbar=ref({title:'',url:''})
const applyTabbarIndex=ref(0)
const statusBarHeight = ref(window?.plus?.navigator?.getStatusbarHeight() ?? 0)
const tabBarIndex = useStorage('tabBarIndex', 0, uniStorage)
return {
statusBarHeight,
applyTabbarIndex,
currentNavbar,
tabBarIndex
}
})

View File

@ -0,0 +1,12 @@
full.scss
full.min.css
full.css
mian.css
mian.scss
scss/fonts
scss/colors.scss
scss/fuzhu.scss
scss/fuzhu-full.scss
scss/theme.scss
package.json
readme.md

Some files were not shown because too many files have changed in this diff Show More