feat: init

This commit is contained in:
xjs 2025-05-12 16:25:16 +08:00
commit a8ee21b558
254 changed files with 45230 additions and 0 deletions

106
.commitlintrc.cjs Normal file
View File

@ -0,0 +1,106 @@
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
const scopes = fs
.readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name.replace(/s$/, ''))
// precomputed scope
const scopeComplete = execSync('git status --porcelain || true')
.toString()
.trim()
.split('\n')
.find((r) => ~r.indexOf('M src'))
?.replace(/(\/)/g, '%%')
?.match(/src%%((\w|-)*)/)?.[1]
?.replace(/s$/, '')
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'subject-case': [0],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'wip',
'workflow',
'types',
'release',
],
],
},
prompt: {
/** @use `pnpm commit :f` */
alias: {
f: 'docs: fix typos',
r: 'docs: update README',
s: 'style: update code format',
b: 'build: bump dependencies',
c: 'chore: update config',
},
customScopesAlign: !scopeComplete ? 'top' : 'bottom',
defaultScope: scopeComplete,
scopes: [...scopes, 'mock'],
allowEmptyIssuePrefixs: false,
allowCustomIssuePrefixs: false,
// English
typesAppend: [
{ value: 'wip', name: 'wip: work in process' },
{ value: 'workflow', name: 'workflow: workflow improvements' },
{ value: 'types', name: 'types: type definition file changes' },
],
// 中英文对照版
// messages: {
// type: '选择你要提交的类型 :',
// scope: '选择一个提交范围 (可选):',
// customScope: '请输入自定义的提交范围 :',
// subject: '填写简短精炼的变更描述 :\n',
// body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
// breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
// footerPrefixsSelect: '选择关联issue前缀 (可选):',
// customFooterPrefixs: '输入自定义issue前缀 :',
// footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
// confirmCommit: '是否提交或修改commit ?',
// },
// types: [
// { value: 'feat', name: 'feat: 新增功能' },
// { value: 'fix', name: 'fix: 修复缺陷' },
// { value: 'docs', name: 'docs: 文档变更' },
// { value: 'style', name: 'style: 代码格式' },
// { value: 'refactor', name: 'refactor: 代码重构' },
// { value: 'perf', name: 'perf: 性能优化' },
// { value: 'test', name: 'test: 添加疏漏测试或已有测试改动' },
// { value: 'build', name: 'build: 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
// { value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
// { value: 'revert', name: 'revert: 回滚 commit' },
// { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
// { value: 'wip', name: 'wip: 正在开发中' },
// { value: 'workflow', name: 'workflow: 工作流程改进' },
// { value: 'types', name: 'types: 类型定义文件修改' },
// ],
// emptyScopesAlias: 'empty: 不填写',
// customScopesAlias: 'custom: 自定义',
},
}

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off # 关闭最大行长度限制
trim_trailing_whitespace = false # 关闭末尾空格修剪

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
src/uni_modules/

130
.eslintrc-auto-import.json Normal file
View File

@ -0,0 +1,130 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onAddToFavorites": true,
"onBackPress": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onError": true,
"onErrorCaptured": true,
"onHide": true,
"onLaunch": true,
"onLoad": true,
"onMounted": true,
"onNavigationBarButtonTap": true,
"onNavigationBarSearchInputChanged": true,
"onNavigationBarSearchInputClicked": true,
"onNavigationBarSearchInputConfirmed": true,
"onNavigationBarSearchInputFocusChanged": true,
"onPageNotFound": true,
"onPageScroll": true,
"onPullDownRefresh": true,
"onReachBottom": true,
"onReady": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onResize": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onShareAppMessage": true,
"onShareTimeline": true,
"onShow": true,
"onTabItemTap": true,
"onThemeChange": true,
"onUnhandledRejection": true,
"onUnload": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useRequest": true,
"useSlots": true,
"useUpload": true,
"useUpload2": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"DirectiveBinding": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"onWatcherCleanup": true,
"useId": true,
"useModel": true,
"useTemplateRef": true,
"tabbarList": true,
"iphoneBottom": true,
"useNavbarWeixin": true,
"cities": true,
"getCities": true,
"useCityInfo": true,
"useRules": true,
"rules": true,
"optionalSubjectList": true,
"requireSubjectList": true,
"optionalSubject": true,
"requireSubject": true,
"unSortTypeList": true,
"useUnSortType": true,
"useUniversityType": true,
"useRegionInfo": true,
"useUniversityLevel": true,
"useNatureList": true,
"useUniversityRank": true,
"useCityNewTop": true,
"useCityNewDetail": true,
"newDetail": true,
"newsDetail": true,
"useNewsList": true,
"newsList": true,
"useLogin": true,
"useWxInfo": true,
"useUniversityInfo": true,
"universityBaseInfo": true
}
}

97
.eslintrc.cjs Normal file
View File

@ -0,0 +1,97 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-essential',
// eslint-plugin-import 插件, @see https://www.npmjs.com/package/eslint-plugin-import
'plugin:import/recommended',
// eslint-config-airbnb-base 插件 已经改用 eslint-config-standard 插件
'standard',
// 1. 接入 prettier 的规则
'prettier',
'plugin:prettier/recommended',
'./.eslintrc-auto-import.json',
],
overrides: [
{
env: {
node: true,
},
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script',
},
},
],
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: [
'@typescript-eslint',
'vue',
// 2. 加入 prettier 的 eslint 插件
'prettier',
// eslint-import-resolver-typescript 插件,@see https://www.npmjs.com/package/eslint-import-resolver-typescript
'import',
],
rules: {
// 3. 注意要加上这一句,开启 prettier 自动修复的功能
'prettier/prettier': 'error',
// turn on errors for missing imports
'import/no-unresolved': 'off',
// 对后缀的检测,否则 import 一个ts文件也会报错,需要手动添加'.ts', 增加了下面的配置后就不用了
'import/extensions': [
'error',
'ignorePackages',
{ js: 'never', jsx: 'never', ts: 'never', tsx: 'never' },
],
// 只允许1个默认导出,关闭,否则不能随意export xxx
'import/prefer-default-export': ['off'],
'no-console': ['off'],
// 'no-unused-vars': ['off'],
// '@typescript-eslint/no-unused-vars': ['off'],
// 解决vite.config.ts报错问题
'import/no-extraneous-dependencies': 'off',
'no-plusplus': 'off',
'no-shadow': 'off',
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-underscore-dangle': 'off',
'no-use-before-define': 'off',
'no-undef': 'off',
'no-unused-vars': 'off',
'no-param-reassign': 'off',
'@typescript-eslint/no-unused-vars': 'off',
// 避免 `eslint` 对于 `typescript` 函数重载的误报
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
},
// eslint-import-resolver-typescript 插件,@see https://www.npmjs.com/package/eslint-import-resolver-typescript
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {},
},
},
globals: {
$t: true,
uni: true,
UniApp: true,
wx: true,
WechatMiniprogram: true,
getCurrentPages: true,
UniHelper: true,
Page: true,
App: true,
NodeJS: true,
},
}

41
.github/workflows/auto-merge.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Auto Merge Base to Other Branches
on:
push:
branches:
- base
workflow_dispatch: # 手动触发
jobs:
auto-merge:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
- name: Merge base into main
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout main
git merge base --no-ff -m "Auto merge base into main"
git push origin main
- name: Merge base into i18n
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout i18n
git merge base --no-ff -m "Auto merge base into i18n"
git push origin i18n
- name: Merge base into tabbar
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout tabbar
git merge base --no-ff -m "Auto merge base into tabbar"
git push origin tabbar

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# 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?
.hbuilderx
.stylelintcache
.eslintcache
docs/.vitepress/dist
docs/.vitepress/cache
# lock 文件还是不要了,我主要的版本写死就好了
# pnpm-lock.yaml
# package-lock.json
# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
# `git rm -r --cached .` 然后提交 commit 即可。
# git rm -r --cached file1 file2 ## 针对某些文件
# git rm -r --cached dir1 dir2 ## 针对某些文件夹
# git rm -r --cached . ## 针对所有文件
# 更新 uni-app 官方版本
# npx @dcloudio/uvm@latest

5
.husky/commit-msg Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run the commit-msg hook
npx --no-install commitlint --edit

5
.husky/pre-commit Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run the pre-commit hook
npx --no-install -- lint-staged

6
.npmrc Normal file
View File

@ -0,0 +1,6 @@
# registry = https://registry.npmjs.org
registry = https://registry.npmmirror.com
strict-peer-dependencies=false
auto-install-peers=true
shamefully-hoist=true

12
.prettierignore Normal file
View File

@ -0,0 +1,12 @@
# unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
auto-import.d.ts
# vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
uni-pages.d.ts
# 插件生成的文件
src/pages.json
src/manifest.json
# 忽略自动生成文件
src/service/app/**

19
.prettierrc.cjs Normal file
View File

@ -0,0 +1,19 @@
// @see https://prettier.io/docs/en/options
module.exports = {
singleQuote: true,
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: false,
trailingComma: 'all',
endOfLine: 'auto',
htmlWhitespaceSensitivity: 'ignore',
overrides: [
{
files: '*.json',
options: {
trailingComma: 'none',
},
},
],
}

1
.stylelintignore Normal file
View File

@ -0,0 +1 @@
src/uni_modules/

58
.stylelintrc.cjs Normal file
View File

@ -0,0 +1,58 @@
// .stylelintrc.cjs
module.exports = {
root: true,
extends: [
// stylelint-config-standard 替换成了更宽松的 stylelint-config-recommended
'stylelint-config-recommended',
// stylelint-config-standard-scss 替换成了更宽松的 stylelint-config-recommended-scss
'stylelint-config-recommended-scss',
'stylelint-config-recommended-vue/scss',
'stylelint-config-html/vue',
'stylelint-config-recess-order',
],
plugins: ['stylelint-prettier'],
overrides: [
// 扫描 .vue/html 文件中的<style>标签内的样式
{
files: ['**/*.{vue,html}'],
customSyntax: 'postcss-html',
},
{
files: ['**/*.{css,scss}'],
customSyntax: 'postcss-scss',
},
],
// 自定义规则
rules: {
'prettier/prettier': true,
// 允许 global 、export 、v-deep等伪类
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global', 'export', 'v-deep', 'deep'],
},
],
'unit-no-unknown': [
true,
{
ignoreUnits: ['rpx'],
},
],
// 处理小程序page标签不认识的问题
'selector-type-no-unknown': [
true,
{
ignoreTypes: ['page'],
},
],
'comment-empty-line-before': 'never', // never|always|always-multi-line|never-multi-line
'custom-property-empty-line-before': 'never',
'no-empty-source': null,
'comment-no-empty': null,
'no-duplicate-selectors': null,
'scss/comment-no-empty': null,
'selector-class-pattern': null,
'font-family-no-missing-generic-family-keyword': null,
},
}

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

@ -0,0 +1,18 @@
{
"recommendations": [
"vue.volar",
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"antfu.unocss",
"antfu.iconify",
"evils.uniapp-vscode",
"uni-helper.uni-helper-vscode",
"uni-helper.uni-app-schemas-vscode",
"uni-helper.uni-highlight-vscode",
"uni-helper.uni-ui-snippets-vscode",
"uni-helper.uni-app-snippets-vscode",
"mrmlnc.vscode-json5",
"streetsidesoftware.code-spell-checker"
]
}

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

@ -0,0 +1,66 @@
{
// 默认格式化工具选择prettier
"editor.defaultFormatter": "esbenp.prettier-vscode",
// 保存的时候自动格式化
"editor.formatOnSave": true,
//开启自动修复
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
// 配置stylelint检查的文件类型范围
"stylelint.validate": ["css", "scss", "vue", "html"], // 与package.json的scripts对应
"stylelint.enable": true,
"css.validate": false,
"less.validate": false,
"scss.validate": false,
"[shellscript]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[dotenv]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 配置语言的文件关联
"files.associations": {
"pages.json": "jsonc", // pages.json 可以写注释
"manifest.json": "jsonc" // manifest.json 可以写注释
},
"cSpell.words": [
"Aplipay",
"climblee",
"commitlint",
"dcloudio",
"iconfont",
"qrcode",
"refresherrefresh",
"scrolltolower",
"tabbar",
"Toutiao",
"unibest",
"uvui",
"Wechat",
"WechatMiniprogram",
"Weixin"
],
"typescript.tsdk": "node_modules\\typescript\\lib",
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
".eslintrc.cjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,.stylelintrc.*,.eslintrc-auto-import.json,.editorconfig,.commitlint.cjs"
},
"vetur.validation.template": false,
"vetur.validation.script": false,
"vetur.validation.style": false,
"vetur.experimental.templateInterpolationService": true
}

56
.vscode/vue3.code-snippets vendored Normal file
View File

@ -0,0 +1,56 @@
{
// Place your unibest 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Print unibest Vue3 SFC": {
"scope": "vue",
"prefix": "v3",
"body": [
"<route lang=\"json5\" type=\"page\">",
"{",
" layout: 'default',",
" style: {",
" navigationBarTitleText: '$1',",
" },",
"}",
"</route>\n",
"<template>",
" <view class=\"\">$2</view>",
"</template>\n",
"<script lang=\"ts\" setup>",
"//$3",
"</script>\n",
"<style lang=\"scss\" scoped>",
"//$4",
"</style>\n",
],
},
"Print unibest style": {
"scope": "vue",
"prefix": "st",
"body": ["<style lang=\"scss\" scoped>", "//", "</style>\n"],
},
"Print unibest script": {
"scope": "vue",
"prefix": "sc",
"body": ["<script lang=\"ts\" setup>", "//$3", "</script>\n"],
},
"Print unibest template": {
"scope": "vue",
"prefix": "te",
"body": ["<template>", " <view class=\"\">$1</view>", "</template>\n"],
},
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 菲鸽
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

30
README.md Normal file
View File

@ -0,0 +1,30 @@
# 六维生涯
六维生涯小程序
## ⚙️ 环境
- node>=18
- pnpm>=7.30
- Vue Official>=2.1.10
- TypeScript>=5.0
## &#x1F4C2; 快速开始
执行 `pnpm create unibest` 创建项目
执行 `pnpm i` 安装依赖
执行 `pnpm dev` 运行 `H5`
## 📦 运行(支持热更新)
- web平台: `pnpm dev:h5`, 然后打开 [http://localhost:9000/](http://localhost:9000/)。
- weixin平台:`pnpm dev:mp-weixin` 然后打开微信开发者工具,导入本地文件夹,选择本项目的`dist/dev/mp-weixin` 文件。
- APP平台:`pnpm dev:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/dev/app` 文件夹,选择运行到模拟器(开发时优先使用),或者运行的安卓/ios基座。
## 🔗 发布
- web平台: `pnpm build:h5`,打包后的文件在 `dist/build/h5`,可以放到web服务器,如nginx运行。如果最终不是放在根目录,可以在 `manifest.config.ts` 文件的 `h5.router.base` 属性进行修改。
- weixin平台:`pnpm build:mp-weixin`, 打包后的文件在 `dist/build/mp-weixin`,然后通过微信开发者工具导入,并点击右上角的“上传”按钮进行上传。
- APP平台:`pnpm build:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/build/app` 文件夹,选择发行 - APP云打包。

26
env/.env vendored Normal file
View File

@ -0,0 +1,26 @@
VITE_APP_TITLE = '六纬生涯'
VITE_APP_PORT = 9000
VITE_UNI_APPID = 'H57F2ACE4'
VITE_WX_APPID = 'wxc2399d3aa57174db'
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
VITE_APP_PUBLIC_BASE=
VITE_SERVER_BASEURL = 'https://tuiwuv1.ycymedu.com'
VITE_STATIC_SERVER_BASEURL = "https://api.static.ycymedu.com"
VITE_UPLOAD_BASEURL = 'https://api.v3.ycymedu.com/upload'
# 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
# 下面的变量如果没有设置,会默认使用 VITE_SERVER_BASEURL or VITE_UPLOAD_BASEURL
VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://api.v3.ycymedu.com'
VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://api.v3.ycymedu.com'
VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://api.v3.ycymedu.com'
VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = 'https://api.v3.ycymedu.com'
VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = 'https://api.v3.ycymedu.com'
VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = 'https://api.v3.ycymedu.com'
# h5是否需要配置代理
VITE_APP_PROXY=false
VITE_APP_PROXY_PREFIX = '/api'

6
env/.env.development vendored Normal file
View File

@ -0,0 +1,6 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = true

6
env/.env.production vendored Normal file
View File

@ -0,0 +1,6 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = true
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = false

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

@ -0,0 +1,4 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false

BIN
favicon.ico Normal file

Binary file not shown.

26
index.html Normal file
View File

@ -0,0 +1,26 @@
<!doctype html>
<html build-time="%BUILD_TIME%">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<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>
<title>unibest</title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

102
manifest.config.ts Normal file
View File

@ -0,0 +1,102 @@
// manifest.config.ts
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
import path from 'node:path'
import { loadEnv } from 'vite'
// 获取环境变量的范例
const env = loadEnv(process.env.NODE_ENV!, path.resolve(process.cwd(), 'env'))
const {
VITE_APP_TITLE,
VITE_UNI_APPID,
VITE_WX_APPID,
VITE_APP_PUBLIC_BASE,
VITE_FALLBACK_LOCALE,
} = env
export default defineManifestConfig({
name: VITE_APP_TITLE,
appid: VITE_UNI_APPID,
description: '',
versionName: '1.0.0',
versionCode: '100',
transformPx: false,
locale: VITE_FALLBACK_LOCALE, // 'zh-Hans'
h5: {
router: {
base: VITE_APP_PUBLIC_BASE,
mode: 'history',
},
},
/* 5+App特有相关 */
'app-plus': {
usingComponents: true,
nvueStyleCompiler: 'uni-app',
compilerVersion: 3,
compatible: {
ignoreVersion: true,
},
splashscreen: {
alwaysShowBeforeRender: true,
waiting: true,
autoclose: true,
delay: 0,
},
/* 模块配置 */
modules: {},
/* 应用发布信息 */
distribute: {
/* android打包配置 */
android: {
minSdkVersion: 30,
targetSdkVersion: 30,
abiFilters: ['armeabi-v7a', 'arm64-v8a'],
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打包配置 */
ios: {},
/* SDK配置 */
sdkConfigs: {},
/* 图标配置 */
icons: {
android: {},
ios: {},
},
},
},
/* 小程序特有相关 */
'mp-weixin': {
appid: VITE_WX_APPID,
setting: {
urlCheck: false,
},
usingComponents: true,
optimization: {
subPackages: true,
},
// requiredPrivateInfos: ['getLocation'],
// requiredBackgroundModes: ['audio'],
lazyCodeLoading: 'requiredComponents',
// __usePrivacyCheck__: true,
},
uniStatistics: {
enable: false,
},
vueVersion: '3',
})

View File

@ -0,0 +1,13 @@
import type { GenerateServiceProps } from 'openapi-ts-request'
export default [
{
schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
serversPath: './src/service/app',
requestLibPath: `import request from '@/utils/request';\n import { CustomRequestOptions } from '@/interceptors/request';`,
requestOptionsType: 'CustomRequestOptions',
isGenReactQuery: true,
reactQueryMode: 'vue',
isGenJavaScript: false,
},
] as GenerateServiceProps[]

167
package.json Normal file
View File

@ -0,0 +1,167 @@
{
"name": "volunteer-4",
"type": "commonjs",
"version": "2.5.5",
"description": "unibest - 最好的 uniapp 开发模板",
"author": {
"name": "feige996",
"zhName": "菲鸽",
"email": "1020103647@qq.com",
"github": "https://github.com/feige996",
"gitee": "https://gitee.com/feige996"
},
"license": "MIT",
"repository": "https://github.com/feige996/unibest",
"repository-gitee": "https://gitee.com/feige996/unibest",
"repository-deprecated": "https://github.com/codercup/unibest",
"bugs": {
"url": "https://github.com/feige996/unibest/issues"
},
"homepage": "https://feige996.github.io/unibest/",
"engines": {
"node": ">=18",
"pnpm": ">=7.30"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"uvm": "npx @dcloudio/uvm@latest",
"uvm-rm": "node ./scripts/postupgrade.js",
"postuvm": "echo upgrade uni-app success!",
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev": "uni",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp": "uni -p mp-weixin",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"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:mp-xhs": "uni -p mp-xhs",
"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:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp": "uni build -p mp-weixin",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-jd": "uni build -p mp-jd",
"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:mp-xhs": "uni build -p mp-xhs",
"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",
"prepare": "git init && husky install ",
"type-check": "vue-tsc --noEmit",
"cz": "czg",
"openapi-ts-request": "openapi-ts"
},
"lint-staged": {
"**/*.{html,vue,ts,cjs,json,md}": [
"prettier --write"
],
"**/*.{vue,js,ts,jsx,tsx}": [
"eslint --cache --fix"
],
"**/*.{vue,css,scss,html}": [
"stylelint --fix"
]
},
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4020920240930001",
"@dcloudio/uni-app-plus": "3.0.0-4020920240930001",
"@dcloudio/uni-components": "3.0.0-4020920240930001",
"@dcloudio/uni-h5": "3.0.0-4020920240930001",
"@dcloudio/uni-mp-weixin": "3.0.0-4020920240930001",
"@tanstack/vue-query": "^5.62.16",
"abortcontroller-polyfill": "^1.7.8",
"dayjs": "1.11.10",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
"pinyin-pro": "^3.26.0",
"qs": "6.5.3",
"vue": "3.4.21",
"wot-design-uni": "^1.4.0"
},
"devDependencies": {
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.3",
"@dcloudio/types": "^3.4.14",
"@dcloudio/uni-automator": "3.0.0-4020920240930001",
"@dcloudio/uni-cli-shared": "3.0.0-4020920240930001",
"@dcloudio/uni-stacktracey": "3.0.0-4020920240930001",
"@dcloudio/vite-plugin-uni": "3.0.0-4020920240930001",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@iconify-json/carbon": "^1.2.4",
"@rollup/rollup-darwin-x64": "^4.28.0",
"@types/node": "^20.17.9",
"@types/wechat-miniprogram": "^3.4.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@uni-helper/uni-types": "1.0.0-alpha.3",
"@uni-helper/vite-plugin-uni-layouts": "^0.1.10",
"@uni-helper/vite-plugin-uni-manifest": "^0.2.7",
"@uni-helper/vite-plugin-uni-pages": "0.2.20",
"@uni-helper/vite-plugin-uni-platform": "^0.0.4",
"@unocss/preset-legacy-compat": "^0.59.4",
"@vue/runtime-core": "^3.5.13",
"@vue/tsconfig": "^0.1.3",
"autoprefixer": "^10.4.20",
"commitlint": "^18.6.1",
"czg": "^1.9.4",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.32.0",
"husky": "^8.0.3",
"lint-staged": "^15.2.10",
"openapi-ts-request": "^1.1.2",
"postcss": "^8.4.49",
"postcss-html": "^1.7.0",
"postcss-scss": "^4.0.9",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "1.77.8",
"stylelint": "^16.11.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended": "^14.0.1",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-prettier": "^5.0.2",
"svg-sprite-loader": "^6.0.11",
"terser": "^5.36.0",
"typescript": "^5.7.2",
"unocss": "^0.58.9",
"unocss-applet": "^0.7.8",
"unplugin-auto-import": "^0.17.8",
"vite": "5.2.8",
"vite-plugin-restart": "^0.4.2",
"vue-tsc": "^1.8.27"
},
"minimize": {
"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --minimize"
}
}

59
pages.config.ts Normal file
View File

@ -0,0 +1,59 @@
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
export default defineUniPages({
globalStyle: {
navigationBarTextStyle: 'black',
navigationBarTitleText: '六纬生涯',
navigationBarBackgroundColor: '#fff',
backgroundColor: '#F8F8F8',
},
easycom: {
autoscan: true,
custom: {
'^wd-(.*)': 'wot-design-uni/components/wd-$1/wd-$1.vue',
'^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
'z-paging/components/z-paging$1/z-paging$1.vue',
},
},
tabBar: {
color: '#7A7E83',
selectedColor: '#3370ff',
borderStyle: 'white',
backgroundColor: '#ffffff',
height: '60px',
fontSize: '10px',
iconWidth: '16px',
spacing: '3px',
list: [
{
iconPath: '/static/tabBar/news.png',
selectedIconPath: '/static/tabBar/news-active.png',
pagePath: 'pages/evaluation/index/index',
text: '测评',
},
{
iconPath: '/static/tabBar/center.png',
selectedIconPath: '/static/tabBar/center-active.png',
pagePath: 'pages/ucenter/index/index',
text: '我的',
},
],
},
pages: [],
preloadRule: {
'pages/evaluation/index/index': {
network: 'all',
packages: ['pages-evaluation-sub'],
},
},
condition: {
current: 0,
list: [
{
name: '',
path: '',
query: '',
},
],
},
})

14559
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

41
project.config.json Normal file
View File

@ -0,0 +1,41 @@
{
"appid": "wxd9369129a1e69342",
"compileType": "miniprogram",
"libVersion": "3.7.8",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"compileWorklet": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
},
"simulatorPluginLibVersion": {}
}

View File

@ -0,0 +1,24 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "six-dimensional",
"setting": {
"compileHotReLoad": true,
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"useApiHostProcess": true,
"showShadowRootInWxmlPanel": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false
},
"libVersion": "3.7.8",
"condition": {}
}

36
scripts/postupgrade.js Normal file
View File

@ -0,0 +1,36 @@
// # 执行 `pnpm upgrade` 后会升级 `uniapp` 相关依赖
// # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积
// # 只需要执行下面的命令即可
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { exec } = require('child_process')
// 定义要执行的命令
const dependencies = [
'@dcloudio/uni-app-harmony',
// TODO: 如果需要某个平台的小程序,请手动删除或注释掉
'@dcloudio/uni-mp-alipay',
'@dcloudio/uni-mp-baidu',
'@dcloudio/uni-mp-jd',
'@dcloudio/uni-mp-kuaishou',
'@dcloudio/uni-mp-lark',
'@dcloudio/uni-mp-qq',
'@dcloudio/uni-mp-toutiao',
'@dcloudio/uni-mp-xhs',
'@dcloudio/uni-quickapp-webview',
// i18n模板要注释掉下面的
'vue-i18n',
]
// 使用exec执行命令
exec(`pnpm un ${dependencies.join(' ')}`, (error, stdout, stderr) => {
if (error) {
// 如果有错误,打印错误信息
console.error(`执行出错: ${error}`)
return
}
// 打印正常输出
console.log(`stdout: ${stdout}`)
// 如果有错误输出,也打印出来
console.error(`stderr: ${stderr}`)
})

62
src/App.vue Normal file
View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
onLaunch(() => {
console.log('App Launch')
})
onShow(() => {
console.log('App Show')
})
onHide(() => {
console.log('App Hide')
})
</script>
<style lang="scss">
::-webkit-scrollbar {
display: none;
}
/* stylelint-disable selector-type-no-unknown */
button::after {
border: none;
}
swiper,
scroll-view {
flex: 1;
height: 100%;
overflow: hidden;
}
image {
width: 100%;
height: 100%;
vertical-align: middle;
}
// 单行省略,优先使用 unocss: text-ellipsis
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 两行省略
.ellipsis-2 {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
// 三行省略
.ellipsis-3 {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
</style>

View File

@ -0,0 +1,51 @@
<route lang="json5" type="page">
{
style: {
navigationBarTitleText: '六纬AI小助手',
},
needLogin: true,
}
</route>
<template>
<web-view :src="url" @message="handleChildMessage" :update-title="false" />
</template>
<script setup lang="ts">
import { useUserStore } from '@/store'
const userStore = useUserStore()
//chat.ycymedu.com
//chatv2.ycymedu.com
const url = ref(
`https://chat.ycymedu.com?userId=${userStore.userInfo.estimatedAchievement.wxId}&subjectGroup=${userStore.userInfo.estimatedAchievement.subjectGroup}&expectedScore=${userStore.userInfo.estimatedAchievement.expectedScore}&provinceName=${userStore.userInfo.estimatedAchievement.provinceName}&locationCode=${userStore.userInfo.estimatedAchievement.provinceCode}&token=${userStore.userInfo.token}&timestamp=${new Date().getTime()}`,
)
const handleChildMessage = (event) => {
console.log('子应用传递的消息', event)
}
onLoad((options) => {
if (options.id) {
url.value += `&reportId=${options.id}`
}
if (options.type) {
url.value += `&reportType=${options.type}`
}
if (options.fileId) {
url.value += `&fileId=${options.fileId}`
}
// if (options.locationCode) {
// url.value += `&locationCode=${options.locationCode}`
// }
// const recorderManager = uni.getRecorderManager()
// recorderManager.onError((res) => {
// console.log('录音错误', res)
// })
// recorderManager.onStop((res) => {
// console.log('录音停止', res)
// })
})
</script>
<style lang="scss" scoped></style>

0
src/components/.gitkeep Normal file
View File

View File

@ -0,0 +1,165 @@
<template>
<!-- TabBar占位块 - 与TabBar高度一致 -->
<view
v-if="showPlaceholder"
class="tabbar-placeholder"
:style="{ height: `${tabbarTotalHeight}px` }"
></view>
<!-- TabBar组件 -->
<view class="custom-tabbar pb-safe">
<view class="tabbar-content">
<view
class="tabbar-item"
v-for="item in tabbarList"
:key="item.id"
:class="[item.centerItem ? 'center-item' : '']"
@click="changeItem(item)"
>
<view class="item-top">
<image
class="w-full h-full object-contain item-icon"
:src="currentPage == item.id ? item.selectIcon : item.icon"
></image>
</view>
<view class="item-bottom" :class="[currentPage == item.id ? 'item-active' : '']">
<text>{{ item.text }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { TabesItem } from '@/service/app/types'
import { tabbarList } from '@/hooks/useTabbarList'
import { ref, computed, onMounted } from 'vue'
defineProps({
currentPage: {
type: Number,
default: 0,
},
// 是否显示占位块
showPlaceholder: {
type: Boolean,
default: true,
},
})
// 安全区域底部高度
const safeAreaBottom = ref(0)
// 计算TabBar总高度 (TabBar高度 + 安全区域高度)
const tabbarTotalHeight = computed(() => {
// 100rpx转为px,不同设备可能有差异
const tabbarHeight = uni.upx2px(100)
return tabbarHeight + safeAreaBottom.value
})
const changeItem = (item: TabesItem) => {
if (item.navigatorItem) {
uni.navigateTo({
url: item.path,
})
} else {
uni.switchTab({
url: item.path,
})
}
}
onMounted(() => {
uni.hideTabBar()
// 获取系统信息以计算安全区域
uni.getSystemInfo({
success: (res) => {
if (res.safeAreaInsets) {
safeAreaBottom.value = res.safeAreaInsets.bottom || 0
}
},
})
})
// 暴露高度信息给父组件
defineExpose({
tabbarTotalHeight,
})
</script>
<style scoped>
.custom-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-top: 1px solid #f5f5f5;
z-index: 999;
}
.tabbar-content {
display: flex;
height: 100rpx;
padding: 0 20rpx;
position: relative;
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 10rpx 0;
}
.item-top {
display: flex;
align-items: center;
justify-content: center;
}
.item-bottom {
font-size: 20rpx;
margin-top: 6rpx;
color: #999;
}
.item-active {
color: #3370ff;
}
.center-item {
position: relative;
}
.item-icon {
width: 48rpx;
height: 48rpx;
}
.center-item .item-icon {
width: 98rpx;
height: 98rpx;
}
.center-item .item-bottom {
position: absolute;
bottom: 5rpx;
}
/* 占位块样式 */
.tabbar-placeholder {
width: 100%;
box-sizing: border-box;
}
/* 安全区域适配 */
.pb-safe {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
</style>

View File

@ -0,0 +1,14 @@
<template>
<custom-tab-bar :current-page="currentPage" :safe-area-inset-bottom="true" />
</template>
<script setup lang="ts">
import CustomTabBar from './CustomTabBar.vue'
defineProps({
currentPage: {
type: Number,
default: 0,
},
})
</script>

View File

@ -0,0 +1,120 @@
<template>
<view
class="fab-button"
@touchstart.stop="startDrag"
@touchmove.stop="onDrag"
@touchend.stop="endDrag"
:style="{ right: position.x + 'px', bottom: position.y + 'px' }"
>
<image
class="w-full h-full rounded-full"
src="https://api.static.ycymedu.com/src/images/home/customerService.svg"
></image>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const props = defineProps({
initialX: {
type: Number,
default: 0,
},
initialY: {
type: Number,
default: 0,
},
})
const systemInfo = uni.getWindowInfo()
const position = ref({ x: props.initialX, y: props.initialY })
const startPosition = ref({ x: 0, y: 0 })
const startTime = ref(0)
const longPressTimer = ref<number | null>(null)
const canDrag = ref(false)
const moveDistance = ref(0)
const startDrag = (event: TouchEvent) => {
startPosition.value = { x: event.touches[0].clientX, y: event.touches[0].clientY }
startTime.value = Date.now()
canDrag.value = false
moveDistance.value = 0
// 设置长按定时器,300ms后允许拖动
longPressTimer.value = setTimeout(() => {
if (!canDrag.value) {
canDrag.value = true
}
}, 300) as unknown as number
}
const onDrag = (event: TouchEvent) => {
const deltaX = event.touches[0].clientX - startPosition.value.x
const deltaY = event.touches[0].clientY - startPosition.value.y
// 计算移动距离
moveDistance.value = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
// 如果移动距离超过阈值,立即允许拖动
if (moveDistance.value > 10 && !canDrag.value) {
canDrag.value = true
if (longPressTimer.value) {
clearTimeout(longPressTimer.value)
longPressTimer.value = null
}
}
if (!canDrag.value) return
position.value = { x: position.value.x - deltaX, y: position.value.y - deltaY }
startPosition.value = { x: event.touches[0].clientX, y: event.touches[0].clientY }
}
const endDrag = () => {
// 清除长按定时器
if (longPressTimer.value) {
clearTimeout(longPressTimer.value)
longPressTimer.value = null
}
// 如果移动距离小于阈值且触摸时间小于300ms,则触发点击事件
if (!canDrag.value && moveDistance.value < 10 && Date.now() - startTime.value < 300) {
handleClick()
return
}
// 如果是拖动结束,处理边界
if (canDrag.value) {
const windowWidth = systemInfo.windowWidth
const windowHeight = systemInfo.windowHeight
const buttonWidth = 128 // 按钮宽度
const buttonHeight = 128 // 按钮高度
if (position.value.x < 0) position.value.x = 0
if (position.value.y < 0) position.value.y = 0
if (position.value.x + buttonWidth > windowWidth) position.value.x = windowWidth - buttonWidth
if (position.value.y + buttonHeight > windowHeight)
position.value.y = windowHeight - buttonHeight
}
}
const handleClick = () => {
uni.navigateTo({
url: '/pages-sub/customerService/index/index',
})
}
</script>
<style scoped>
.fab-button {
position: fixed;
width: 128rpx;
height: 128rpx;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<view
:class="`mx-5 rounded-lg bg-white px-[32rpx] ${userStore.userInfo.openid ? 'py-[56rpx]' : 'py-[26rpx]'}`"
>
<view
class="flex items-center justify-between mx-[34rpx] py-[26rpx]"
style="border-bottom: 2rpx solid #ededed"
@click="handleChange"
v-if="userStore.userInfo.openid"
>
<text class="text-[44rpx] text-[#333]">
{{
userStore.userInfo.estimatedAchievement.expectedScore
? userStore.userInfo.estimatedAchievement.expectedScore
: '输入模考/高考成绩'
}}
</text>
<image
class="w-[42rpx] h-[39rpx]"
src="https://api.static.ycymedu.com/src/images/home/pen.svg"
></image>
</view>
<view class="flex items-center justify-center" v-else>
<image
class="w-[74%] h-[50rpx]"
mode="widthFix"
src="https://api.static.ycymedu.com/pagefirstloginbg.png"
/>
</view>
<view class="mt-[56rpx] flex items-center justify-between" v-if="userStore.userInfo.openid">
<button
class="w-[240rpx]! h-[88rpx]! border-[#1580FF]! text-[#1580FF]! text-[30rpx]! font-normal! mr-[32rpx] flex! items-center! justify-center! rounded-[8rpx]!"
plain
@click="navigatorTo"
>
一键填报
</button>
<button
class="w-[350rpx]! h-[88rpx]! text-[#fff]! text-[30rpx]! bg-[#1580FF]! font-normal flex! items-center! justify-center! rounded-[8rpx]!"
@click="navigatorToAi"
>
智能填报
</button>
</view>
<view class="flex items-center justify-between mt-[26rpx]" v-else>
<button
class="h-[78rpx]! w-full! text-[#fff]! text-[30rpx]! bg-[#1580FF]! font-normal flex! items-center! justify-center! rounded-[8rpx]!"
@click="navigatorToLogin"
>
登录/注册
</button>
</view>
</view>
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
const handleChange = () => {
uni.navigateTo({
url: '/pages-sub/home/inputScore/index',
})
}
const navigatorTo = () => {
if (userStore.userInfo.estimatedAchievement.expectedScore === '') {
handleChange()
} else {
uni.navigateTo({
url: '/pages-sub/home/autoFill/index',
})
}
}
const navigatorToAi = () => {
if (userStore.userInfo.estimatedAchievement.expectedScore === '') {
handleChange()
} else {
uni.navigateTo({
url: '/pages-evaluation-sub/aiAutoFill/index',
})
}
}
const navigatorToLogin = () => {
uni.navigateTo({
url: '/login-sub/index',
})
}
</script>

View File

@ -0,0 +1,52 @@
<template>
<view class="mx-[36rpx] mt-[48rpx]">
<view
class="flex items-center justify-between"
hover-class="none"
:hover-stop-propagation="false"
>
<text class="text-[32rpx] text-[#333333] font-semibold">高考资讯</text>
<image
class="w-[40rpx] h-[40rpx]"
src="https://api.static.ycymedu.com/src/images/home/right.svg"
@click="toNewsPage"
></image>
</view>
<view
class="truncate flex flex-col py-[32rpx]"
style="border-bottom: 2rpx solid #eee"
hover-class="none"
:hover-stop-propagation="false"
v-for="item in newsList"
:key="item.id"
@click="handleClick(item)"
>
<text class="truncate text-[28rpx] text-[#333333] font-normal mb-[16rpx] max-w-full">
{{ item.title }}
</text>
<text class="text-[24rpx] color-[#999999] font-normal">{{ item.createTime }}</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { useCityNewTop, newsList } from '@/hooks/useCityInfoHook'
import { News } from '@/types/app-type'
onShow(() => {
useCityNewTop()
})
const handleClick = (item: News) => {
uni.navigateTo({
url: `/pages-sub/home/news/index?newsId=${item.id}`,
})
}
const toNewsPage = () => {
uni.navigateTo({
url: '/pages-sub/home/news/newsList',
})
}
</script>

View File

@ -0,0 +1,223 @@
<template>
<view class="mt-[44rpx]" hover-class="none" :hover-stop-propagation="false">
<view
class="flex items-center justify-between mb-[30rpx] mx-[36rpx]"
hover-class="none"
:hover-stop-propagation="false"
>
<text class="text-[32rpx] text-[#333333] font-semibold">热门排行榜</text>
<image
class="w-[40rpx] h-[40rpx]"
src="https://api.static.ycymedu.com/src/images/home/right.svg"
@click="toSchool('0')"
></image>
</view>
<view
class="flex items-center overflow-x-auto hot-rank-outer gap-[16rpx] h-[462rpx] px-[32rpx]"
hover-class="none"
:hover-stop-propagation="false"
>
<!-- 骨架屏 -->
<view
v-if="isLoading"
v-for="(_skeleton, index) in skeletonItems"
:key="'skeleton-' + index"
:class="`hot-rank-item flex-none skeleton-item`"
hover-class="none"
:hover-stop-propagation="false"
>
<view class="skeleton-text mx-[32rpx] mt-[32rpx] h-[40rpx] w-[120rpx] rounded"></view>
<view class="flex items-center justify-left mt-[30rpx] mx-[32rpx]" v-for="i in 3" :key="i">
<view class="skeleton-text w-[20rpx] h-[28rpx] mr-[10rpx] rounded"></view>
<view class="skeleton-image w-[80rpx] h-[80rpx] rounded-full flex-none mr-[16rpx]"></view>
<view class="flex flex-col w-full">
<view class="skeleton-text h-[28rpx] w-[120rpx] rounded"></view>
<view class="skeleton-text h-[22rpx] w-[100rpx] mt-[10rpx] rounded"></view>
</view>
</view>
</view>
<!-- 真实数据 -->
<view
v-else
:class="`hot-rank-item flex-none`"
hover-class="none"
:hover-stop-propagation="false"
v-for="typeWrap in universityTypeRankList"
:key="typeWrap.type"
@click="toSchool(typeWrap.type)"
v-show="typeWrap.rows.length > 0"
>
<text class="font-semibold text-[#303030] text-[32rpx] inline-block mx-[32rpx] mt-[32rpx]">
{{ typeWrap.name }}
</text>
<view
class="flex items-center justify-left mt-[30rpx] mx-[32rpx]"
v-for="(item, index) in typeWrap.rows"
:key="index"
>
<text class="font-[28rpx] text-[#999999] font-normal mr-[10rpx]">
{{ item.rank }}
</text>
<image
class="w-[80rpx] h-[80rpx] rounded-full flex-none mr-[16rpx]"
:src="item.logo"
></image>
<view class="truncate flex flex-col" hover-class="none">
<text class="font-normal text-[#333333] text-[28rpx] truncate">
{{ item.universityName }}
</text>
<text class="text-[22rpx] text-[#999999] font-normal mt-[10rpx]">
{{ item.cityName }}.{{ item.uType }}
</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { useUnSortType } from '@/hooks/useUnSortType'
import { getUniversityRank } from '@/service/index/api'
const toSchool = (id: string) => {
uni.navigateTo({
url: `/pages-sub/home/schoolRank/index?type=${id}`,
})
}
const { unSortTypeList } = useUnSortType()
let universityTypeRankList = ref([])
const isLoading = ref(true)
// 创建默认的骨架屏数据
const skeletonItems = ref([
{ type: 'skeleton-1', name: '综合排名', rows: [] },
{ type: 'skeleton-2', name: '理工排名', rows: [] },
{ type: 'skeleton-3', name: '文科排名', rows: [] },
{ type: 'skeleton-4', name: '医科排名', rows: [] },
])
watch(
() => unSortTypeList.value,
(newVal) => {
if (newVal && newVal.length > 0) {
Promise.all(
newVal.map((item) =>
getUniversityRank({
Year: 2023,
Type: item.type,
PageIndex: 1,
PageSize: 3,
}),
),
)
.then((res) => {
universityTypeRankList.value = []
res.forEach((item, index) => {
universityTypeRankList.value.push({
...newVal[index],
rows: (item.result as { rows: any[] }).rows,
loaded: true,
})
})
isLoading.value = false
})
.catch(() => {
isLoading.value = false
})
}
},
{ immediate: true },
)
</script>
<style lang="scss" scoped>
.hot-rank-item {
width: 356rpx;
height: 452rpx;
position: relative;
background: #fff;
border-radius: 8px;
}
.hot-rank-item::before {
content: '';
width: 360rpx;
height: 456rpx;
position: absolute;
border-radius: 8px;
top: -2rpx;
left: -2rpx;
z-index: -1;
}
.hot-rank-outer .hot-rank-item:nth-child(1) {
background: linear-gradient(180deg, #caddff 0%, #eaf1ff 23%, #fff 100%);
}
.hot-rank-outer .hot-rank-item:nth-child(1)::before {
background: linear-gradient(
180deg,
rgba(201.8750050663948, 221.00000202655792, 255, 1),
rgba(233.7500050663948, 241.39999777078629, 255, 1)
);
}
.hot-rank-outer .hot-rank-item:nth-child(2) {
background: linear-gradient(180deg, #cef5e1 0%, #ddf7ea 23%, #fff 100%);
}
.hot-rank-outer .hot-rank-item:nth-child(2)::before {
background: linear-gradient(
180deg,
rgba(205.5883178114891, 245.07227271795273, 225.3302800655365, 1),
rgba(221.00000202655792, 247.00000047683716, 234.00000125169754, 1)
);
}
.hot-rank-outer .hot-rank-item:nth-child(3)::before {
background: linear-gradient(180deg, rgba(245, 237, 255, 1), rgba(245, 237, 255, 1));
}
.hot-rank-outer .hot-rank-item:nth-child(3) {
background: linear-gradient(180deg, #f7e7ff 0%, rgba(245, 237, 255, 0) 23%, #fff 100%);
}
.hot-rank-outer .hot-rank-item:nth-child(4)::before {
background: linear-gradient(180deg, rgba(255, 228, 196, 1), rgba(255, 228, 196, 1));
}
.hot-rank-outer .hot-rank-item:nth-child(4) {
background: linear-gradient(180deg, #ffe4c4 0%, rgba(255, 228, 196, 0) 23%, #fff 100%);
}
.hot-rank-outer .hot-rank-item:nth-child(5)::before {
background: linear-gradient(180deg, rgba(213, 255, 196, 0), rgba(213, 255, 196, 0));
}
.hot-rank-outer .hot-rank-item:nth-child(5) {
background: linear-gradient(180deg, #e5ffc4 0%, rgba(213, 255, 196, 0) 23%, #fff 100%);
}
/* 骨架屏样式 */
.skeleton-text,
.skeleton-image {
background-color: #e0e0e0;
animation: pulse 1.5s infinite;
}
.skeleton-item {
background: linear-gradient(180deg, #eaeaea 0%, #f5f5f5 23%, #fff 100%);
}
@keyframes pulse {
0% {
opacity: 0.5;
}
50% {
opacity: 1;
}
100% {
opacity: 0.5;
}
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<view class="grid grid-cols-4 grid-rows-2 items-center gap-2 mt-[48rpx] px-[36rpx]">
<view
v-for="item in subMenus"
:key="item.id"
class="flex items-center justify-center flex-col"
@click="goPath(item.path, item.isTab)"
>
<view class="relative w-[88rpx] h-[88rpx]">
<image
class="skeleton w-[88rpx] h-[88rpx] rounded-full absolute"
:class="{ hidden: item.loaded }"
></image>
<image
:src="item.icon"
class="w-[88rpx] h-[88rpx] absolute"
:class="{ 'opacity-0': !item.loaded }"
mode="widthFix"
@load="handleLoad(item)"
></image>
</view>
<view class="text-[24rpx] text-[#303030] mt-[8rpx]">{{ item.name }}</view>
</view>
</view>
</template>
<script lang="ts" setup>
const subMenus = ref([
{
id: 1,
name: '找大学',
path: '/pages-sub/home/college/index',
icon: 'https://api.static.ycymedu.com/src/images/home/college.svg',
isTab: false,
loaded: false,
},
{
id: 2,
name: '查专业',
path: '/pages-sub/home/major/index',
icon: 'https://api.static.ycymedu.com/src/images/home/major.svg',
isTab: false,
loaded: false,
},
// 看职业
{
id: 3,
name: '看职业',
path: '/pages-sub/home/career/index',
icon: 'https://api.static.ycymedu.com/src/images/home/career.svg',
isTab: false,
loaded: false,
},
// 批次线
{
id: 4,
name: '批次线',
path: '/pages-sub/home/line/index',
icon: 'https://api.static.ycymedu.com/src/images/home/line.svg',
isTab: false,
loaded: false,
},
// 查位次
{
id: 5,
name: '查位次',
path: '/pages-evaluation-sub/rank/index',
icon: 'https://api.static.ycymedu.com/src/images/home/rank.svg',
isTab: false,
loaded: false,
},
// 查扩缩招
{
id: 6,
name: '查扩缩招',
path: '/pages-sub/home/expand/index',
icon: 'https://api.static.ycymedu.com/src/images/home/expand.svg',
isTab: false,
loaded: false,
},
// 专业测评
{
id: 7,
name: '专业测评',
path: '/pages/evaluation/index/index',
icon: 'https://api.static.ycymedu.com/src/images/home/evaluation.svg',
isTab: true,
loaded: false,
},
// 大学甄别
{
id: 8,
name: '大学甄别',
path: '/pages-sub/home/distinguish/index',
icon: 'https://api.static.ycymedu.com/src/images/home/distinguish.svg',
isTab: false,
loaded: false,
},
])
const goPath = (path: string, isTab: boolean) => {
if (isTab) {
uni.switchTab({
url: path,
})
} else {
uni.navigateTo({
url: path,
})
}
}
const handleLoad = (item: any) => {
item.loaded = true
}
</script>
<style>
.skeleton {
background-color: #e0e0e0; /* 骨架屏的背景色 */
animation: pulse 1.5s infinite; /* 添加动画效果 */
}
@keyframes pulse {
0% {
opacity: 0.5;
}
50% {
opacity: 1;
}
100% {
opacity: 0.5;
}
}
</style>

View File

@ -0,0 +1,209 @@
<template>
<view class="navbar">
<!-- 状态栏占位 -->
<view
v-if="safeAreaInsetTop"
class="status-bar"
:style="{ height: statusBarHeight + 'px', backgroundColor: bgColor }"
></view>
<!-- 导航栏主体 -->
<view
class="navbar-content"
:class="[contentClass, fixed ? 'navbar-fixed' : '', bordered ? 'navbar-border' : '']"
:style="{
backgroundColor: bgColor,
height: navHeight + 'px',
top: fixed ? (safeAreaInsetTop ? statusBarHeight : 0) + 'px' : '0',
}"
>
<!-- 左侧区域 -->
<view class="navbar-left" @click="handleClickLeft">
<view v-if="leftArrow" class="back-icon">
<view class="i-carbon-chevron-left text-[40rpx] text-[#333] font-semibold" />
</view>
<slot name="left"></slot>
</view>
<!-- 中间标题区域 -->
<view class="navbar-title">
<slot name="title">
<text class="title-text">{{ title }}</text>
</slot>
</view>
<!-- 右侧区域 -->
<view class="navbar-right">
<slot name="right"></slot>
</view>
</view>
<!-- 占位元素 -->
<view
v-if="placeholder && fixed"
:style="{
height: `${navHeight}px`,
backgroundColor: bgColor,
}"
></view>
<slot name="background"></slot>
</view>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
defineProps({
title: {
type: String,
default: '',
},
leftArrow: {
type: Boolean,
default: false,
},
fixed: {
type: Boolean,
default: false,
},
placeholder: {
type: Boolean,
default: false,
},
bordered: {
type: Boolean,
default: true,
},
safeAreaInsetTop: {
type: Boolean,
default: true,
},
bgColor: {
type: String,
default: '#ffffff',
},
contentClass: {
type: String,
default: 'justify-between',
},
})
const emit = defineEmits(['clickLeft'])
// 获取系统信息
const systemInfo = uni.getWindowInfo()
const deviceInfo = uni.getDeviceInfo()
const statusBarHeight = systemInfo.statusBarHeight || 0
// 动态计算导航栏高度
const navHeight = computed(() => {
// 获取屏幕信息
const { screenWidth } = systemInfo
const { platform } = deviceInfo
// 将px转换为rpx的比例
const ratio = 750 / screenWidth
// 根据平台设置不同的导航栏高度
if (platform === 'ios') {
return 88 / ratio // iOS 一般是 44pt,转换为px
} else if (platform === 'android') {
return 96 / ratio // Android 一般是 48dp,转换为px
} else {
return 88 / ratio // 默认值
}
})
const handleClickLeft = () => {
emit('clickLeft')
}
</script>
<style scoped>
.navbar {
width: 100%;
}
.status-bar {
width: 100%;
background-color: inherit;
}
.navbar-content {
width: 100%;
display: flex;
align-items: center;
/* justify-content: space-between; */
padding: 0 16rpx;
box-sizing: border-box;
background-color: #fff;
}
.navbar-fixed {
position: fixed;
left: 0;
width: 100%;
z-index: 999;
}
.navbar-border {
border-bottom: 1rpx solid #eee;
}
.navbar-left {
display: flex;
align-items: center;
height: 100%;
min-width: 100rpx;
}
.back-icon {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.navbar-title {
/* flex: 1; */
text-align: center;
overflow: hidden;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.title-text {
font-size: 34rpx;
color: #333;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.navbar-right {
display: flex;
align-items: center;
min-width: 52rpx;
justify-content: flex-end;
height: 100%;
}
@font-face {
font-family: 'iconfont';
src: url('data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAKYAAsAAAAABlAAAAJMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACCcApcdgE2AiQDCAsGAAQgBYRnBzYbmQXIHpIkBQQKkYCABBEPz2/t/XN3twEbowBkQTxQEQ1RKaKSxEOi0agkJKF5Qvj/f037IFKwlZ2dWU2tJu0EhPwHkBwgOVAclKcvAQpI/v/fz/08XECy+YBymmPQiwIcSmhAY4uSFcgJ+IaxC1zCYwLtRjWSnZ2rGgQWBowLxCPrVBBYllQqNTQ0VISaBXEHtTRNUwW4jb4f/xYEC0kqMzDx6CGrQuKXxKc6Zf7POYQgQHs5kIwjYwEoxK3G/DpRwbi0dlNwKKjAL4lf6vw/R2zVWvTPIwuiCnp2wCRUZ3yJX5pJFVDfByyAFR2AblMAX/OR3t7+zOJi8GyyfzC1uQXLZvtnk/0zyfTy+PvH0/Xp5OzR98/H797/+/fDu3d/3739+/fd+/+nmxvLc5vrS+sry2vz84tLs9Mzc4vzs9NTM/Ozc1OzM3MzU/Mz0wvTU4vTk0tTE8uTEyuT4yv/G0E3XUxv7wwNbu/s9G8fbO9v7+3sb+3ubW4dbO4dbO3vbu4dbO3JzqPFtRE4gEGAX0NBkL+hpCZALkEp5FKUQqE0NHlXJIGrDNAOcEQBCHU+kXT5QNblC7kEv1EK9Y9SB/8o7YYu2m0YXrJLouNIjQJhH+QbVkVZrUQ+YuqzUJdzxPMHhdIj0+hg4o0D8ogj5r5bSoQUxjADz+A8hBDQFEYwh3mommXTul7Vm5ZtqAqJHIdoKCDYDyQ3mCqUG1YKn5+C0s0yiJ/qKVAQedKAhg6Y3mEHJBQaWKnvLVMiiEIxGAY8Aw6HIAhAJmEIzIIOUjLTTAB1taL1QvNq+fYN7QDjcc2okeioaOmy5LFXt3QAAAAA')
format('woff2');
}
.back-text {
font-family: 'iconfont' !important;
font-size: 48rpx;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333;
}
</style>

216
src/components/tab/Tabs.vue Normal file
View File

@ -0,0 +1,216 @@
<template>
<view class="tab-container">
<!-- tab标题栏 -->
<scroll-view
scroll-x
class="tab-scroll-view"
:scroll-left="scrollLeft"
scroll-with-animation
show-scrollbar="false"
:id="tabScrollId"
>
<view class="tab-items-container">
<view
v-for="(item, index) in tabs"
:key="index"
class="tab-item"
:class="{ active: currentIndex === index }"
@click="handleTabClick(index)"
:id="`tab-item-${index}`"
>
<text class="tab-text">{{ item.title }}</text>
</view>
<!-- 独立的滑块元素 -->
<view
class="tab-line"
:style="{
transform: `translateX(${lineLeft}px)`,
width: `${lineWidth}rpx`,
backgroundColor: props.themeColor,
}"
></view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, watch, nextTick, onMounted } from 'vue'
// 唯一ID,防止多个Tab组件冲突
const tabScrollId = `tab-scroll-${Date.now()}`
// 定义组件属性
const props = defineProps({
// tabs数据,格式:[{title: '标签1'}, {title: '标签2'}]
tabs: {
type: Array,
default: () => [],
},
// 默认选中的索引
modelValue: {
type: Number,
default: 0,
},
// 主题色
themeColor: {
type: String,
default: '#3C9CFD',
},
// 滑块宽度
lineWidth: {
type: [Number, String],
default: 48,
},
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'change'])
// 当前激活的索引
const currentIndex = ref(props.modelValue)
// 滚动位置
const scrollLeft = ref(0)
// 滑块位置
const lineLeft = ref(0)
// 监听props变化
watch(
() => props.modelValue,
(newVal) => {
if (currentIndex.value !== newVal) {
currentIndex.value = newVal
updateTabPosition()
}
},
)
// 监听currentIndex变化
watch(
() => currentIndex.value,
(newVal, oldVal) => {
if (newVal !== oldVal) {
updateTabPosition()
// 向父组件同步更新
emit('update:modelValue', newVal)
emit('change', {
index: newVal,
item: props.tabs[newVal] || {},
})
}
},
)
// 监听tabs变化,重新计算滑块位置
watch(
() => props.tabs,
() => {
nextTick(() => {
updateTabPosition()
})
},
{ deep: true },
)
// 组件挂载后初始化
onMounted(() => {
nextTick(() => {
updateTabPosition()
})
})
const instance = getCurrentInstance()
// 更新标签位置和滑块位置 - 优化版
const updateTabPosition = () => {
nextTick(() => {
// 创建查询对象
const query = uni.createSelectorQuery().in(instance)
// 获取滚动视图和当前选中标签的信息
query.select(`#${tabScrollId}`).boundingClientRect()
query.select(`#tab-item-${currentIndex.value}`).boundingClientRect()
query.exec((res) => {
if (res && res[0] && res[1]) {
const scrollView = res[0]
const currentTab = res[1]
// 1. 计算滑块位置 - 直接使用当前标签的中心位置
const tabCenter = currentTab.left + currentTab.width / 2 - scrollView.left
// 2. 计算滑块左侧应该在的位置(居中)
const lineWidthPx = uni.upx2px(Number(props.lineWidth))
lineLeft.value = tabCenter - lineWidthPx / 2
// 3. 计算滚动位置,使选中的标签居中显示在滚动视图中
const offsetLeft = currentTab.left - scrollView.left
scrollLeft.value = offsetLeft - scrollView.width / 2 + currentTab.width / 2
}
})
})
}
// 点击标签切换
const handleTabClick = (index) => {
if (currentIndex.value !== index) {
currentIndex.value = index
}
}
</script>
<style scoped>
.tab-container {
width: 100%;
}
.tab-scroll-view {
white-space: nowrap;
width: 100%;
height: 88rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #f5f5f5;
position: relative;
}
.tab-items-container {
display: inline-flex;
height: 100%;
position: relative;
width: 100%;
justify-content: space-around;
}
.tab-item {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 32rpx;
position: relative;
height: 100%;
}
.tab-text {
font-size: 28rpx;
color: #333333;
transition: all 0.3s;
}
.tab-item.active .tab-text {
color: v-bind('props.themeColor');
}
.tab-line {
position: absolute;
height: 6rpx;
border-radius: 6rpx;
bottom: 0;
left: 0;
/* 平滑过渡效果 */
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
</style>

31
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
/** 网站标题,应用名称 */
readonly VITE_APP_TITLE: string
/** 服务端口号 */
readonly VITE_SERVER_PORT: string
/** 后台接口地址 */
readonly VITE_SERVER_BASEURL: string
/** H5是否需要代理 */
readonly VITE_APP_PROXY: 'true' | 'false'
/** H5是否需要代理,需要的话有个前缀 */
readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
/** 上传图片地址 */
readonly VITE_UPLOAD_BASEURL: string
/** 是否清除console */
readonly VITE_DELETE_CONSOLE: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

0
src/hooks/.gitkeep Normal file
View File

View File

@ -0,0 +1,88 @@
import { getNewsTop, getProvinceInitialization, getNewsDetailInfo } from '@/service/index/api'
import { useCityStore } from '@/store/city'
import { useUserStore } from '@/store/user'
import { City, News, NewsDetail } from '@/types/app-type'
import { pinyin } from 'pinyin-pro'
interface Province {
provincename: string
}
const cityStore = useCityStore()
const userStore = useUserStore()
export const cities = []
export const useCityInfo = () => {
getProvinceInitialization().then((res) => {
if (res.result) {
const list = res.result as Province[]
const li = groupByFirstLetter(list)
cityStore.setCities(li)
const defaultCity = list.filter((item) => item.provincename === '山东省')[0] as City
if (userStore.userInfo.city.code === '0') {
userStore.setUserCity(defaultCity)
}
}
})
}
// 按照首字母分组
const groupByFirstLetter = (lis: Province[]): { letter: string; provinces: Province[] }[] => {
const grouped: { [key: string]: Province[] } = {}
for (let i = 0; i < lis.length; i++) {
const firstLetter = pinyin(lis[i].provincename, {
pattern: 'first',
toneType: 'none',
})[0].toUpperCase()
if (!grouped[firstLetter]) {
grouped[firstLetter] = []
}
grouped[firstLetter].push(lis[i])
}
return Object.keys(grouped)
.sort()
.map((key) => ({ letter: key, provinces: grouped[key] }))
}
export const newsList = ref([])
export const useCityNewTop = () => {
const fetchNewTopFun = (provinceCode) => {
getNewsTop({ Top: 4, CategoryId: 1, provinceCode }).then((res) => {
if (res.code === 200) {
newsList.value = res.result as { title: string }[]
}
})
}
if (userStore.userInfo.city.code === '0') {
userStore.$subscribe((mutation, state) => {
if ((mutation.events as { key: string }).key === 'city') {
fetchNewTopFun(state.userInfo.city.code)
}
})
} else {
fetchNewTopFun(userStore.userInfo.city.code)
}
}
export const newsDetail = ref<NewsDetail>({
coverImg: '',
createTime: '',
id: 0,
summary: '',
title: '',
author: '',
click: 0,
detail: '',
})
export const useCityNewDetail = (id: number) => {
getNewsDetailInfo({ id }).then((res) => {
if (res.code === 200) {
newsDetail.value = res.result as NewsDetail
}
})
}

View File

@ -0,0 +1,15 @@
let cachedIphoneStyle = ''
;(function () {
wx.getSystemInfo({