feat: init

master
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.jsonscripts
"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.

After

Width:  |  Height:  |  Size: 14 KiB

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(() => {
// 100rpxpx
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
// pxrpx
const ratio = 750 / screenWidth
//
if (platform === 'ios') {
return 88 / ratio // iOS 44ptpx
} else if (platform === 'android') {
return 96 / ratio // Android 48dppx
} 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'
// IDTab
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({
success(res) {
const system = res.system.indexOf('iOS')
const isIphoneXOrAbove = res.statusBarHeight > 20
if (isIphoneXOrAbove && system !== -1) {
cachedIphoneStyle = 'bottom: 40rpx;'
}
},
})
})()
export const iphoneBottom = () => cachedIphoneStyle

View File

@ -0,0 +1,41 @@
import { getUniversityType, getRegionInfo, getNature } from '@/service/index/api'
interface Region {
code: string
name: string
parentcode: string
simplename: string
pinyin: string
}
export const useRegionInfo = () => {
const regionList = ref([])
getRegionInfo().then((res) => {
if (res.code === 200) {
regionList.value = res.result as Region[]
}
})
return { regionList }
}
export const useUniversityType = () => {
const typeList = ref([])
getUniversityType().then((res) => {
if (res.code === 200) {
typeList.value = res.result as { id: number; name: string }[]
}
})
return { typeList }
}
export const useNatureList = () => {
const natureList = ref([])
getNature().then((res) => {
if (res.code === 200) {
natureList.value = res.result as { id: number; name: string }[]
}
})
return { natureList }
}

View File

@ -0,0 +1,62 @@
import { onReady } from '@dcloudio/uni-app'
import { getIsTabbar, getLastItem } from '@/utils/index'
export default () => {
// 获取页面栈
const pages = getCurrentPages()
const isTabbar = getIsTabbar()
// 页面滚动到底部时的操作,通常用于加载更多数据
const onScrollToLower = () => {}
// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getWindowInfo()
// #ifdef MP-WEIXIN
// 基于小程序的 Page 类型扩展 uni-app 的 Page
type PageInstance = Page.PageInstance & WechatMiniprogram.Page.InstanceMethods<any>
// 获取当前页面实例,数组最后一项
const pageInstance = getLastItem(getCurrentPages()) as PageInstance
// 页面渲染完毕,绑定动画效果
onReady(() => {
// 动画效果,导航栏背景色
pageInstance.animate(
'.fly-navbar',
[{ backgroundColor: 'transparent' }, { backgroundColor: '#f8f8f8' }],
1000,
{
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
},
)
// 动画效果,导航栏标题
pageInstance.animate(
'.fly-navbar .title',
[{ color: 'transparent' }, { color: '#000' }],
1000,
{
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
},
)
// 动画效果,导航栏返回按钮
pageInstance.animate('.fly-navbar .left-icon', [{ color: '#fff' }, { color: '#000' }], 1000, {
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
})
})
// #endif
return {
pages,
isTabbar,
onScrollToLower,
safeAreaInsets,
}
}

44
src/hooks/useRequest.ts Normal file
View File

@ -0,0 +1,44 @@
import { UnwrapRef } from 'vue'
type IUseRequestOptions<T> = {
/** 是否立即执行 */
immediate?: boolean
/** 初始化数据 */
initialData?: T
}
/**
* useRequest
* @param func Promise
* @param options {immediate, initialData}
* @param options.immediate false
* @param options.initialData undefined
* @returns {loading, error, data, run}
*/
export default function useRequest<T>(
func: () => Promise<IResData<T>>,
options: IUseRequestOptions<T> = { immediate: false },
) {
const loading = ref(false)
const error = ref(false)
const data = ref<T>(options.initialData)
const run = async () => {
loading.value = true
return func()
.then((res) => {
data.value = res.result as UnwrapRef<T>
error.value = false
return data.value
})
.catch((err) => {
error.value = err
throw err
})
.finally(() => {
loading.value = false
})
}
options.immediate && run()
return { loading, error, data, run }
}

View File

@ -0,0 +1,22 @@
import { TabesItem } from '@/service/app'
const tabbarList = ref<TabesItem[]>([
{
id: 3,
path: '/pages/evaluation/index/index',
icon: '/static/tabBar/news.png',
selectIcon: '/static/tabBar/news-active.png',
text: '测评',
centerItem: false,
},
{
id: 4,
path: '/pages/ucenter/index/index',
icon: '/static/tabBar/center.png',
selectIcon: '/static/tabBar/center-active.png',
text: '我的',
centerItem: false,
},
])
export { tabbarList }

View File

@ -0,0 +1,35 @@
// 高校排名类型
import { getUnSortType, getUniversityRank } from '@/service/index/api'
type UnSortType = { type: number; name: string }[]
export const useUnSortType = () => {
let unSortTypeList = ref([])
getUnSortType().then((res) => {
unSortTypeList.value = res.result as UnSortType
})
return { unSortTypeList }
}
export const useUniversityRank = ({
Year,
Type,
PageSize,
PageIndex,
}: {
Year: number
Type: number
PageSize: number
PageIndex: number
}) => {
let universityRankList = ref([])
getUniversityRank({ Year, Type, PageSize, PageIndex }).then((res) => {
if (res.code === 200) {
const _res = res.result as { rows: any[] }
universityRankList.value = _res.rows
}
})
return { universityRankList }
}

69
src/hooks/useUpload.ts Normal file
View File

@ -0,0 +1,69 @@
// TODO: 别忘加更改环境变量的 VITE_UPLOAD_BASEURL 地址。
import { getEnvBaseUploadUrl } from '@/utils'
const VITE_UPLOAD_BASEURL = `${getEnvBaseUploadUrl()}`
/**
* useUpload
* @param formData {name: '菲鸽'}
* @returns {loading, error, data, run}
*/
export default function useUpload<T = string>(formData: Record<string, any> = {}) {
const loading = ref(false)
const error = ref(false)
const data = ref<T>()
const run = () => {
// #ifdef MP-WEIXIN
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
// 微信小程序在2023年10月17日之后使用本API需要配置隐私协议
uni.chooseMedia({
count: 1,
mediaType: ['image'],
success: (res) => {
loading.value = true
const tempFilePath = res.tempFiles[0].tempFilePath
uploadFile<T>({ tempFilePath, formData, data, error, loading })
},
fail: (err) => {
console.error('uni.chooseMedia err->', err)
error.value = true
},
})
// #endif
// #ifndef MP-WEIXIN
uni.chooseImage({
count: 1,
success: (res) => {
loading.value = true
const tempFilePath = res.tempFilePaths[0]
uploadFile<T>({ tempFilePath, formData, data, error, loading })
},
fail: (err) => {
console.error('uni.chooseImage err->', err)
error.value = true
},
})
// #endif
}
return { loading, error, data, run }
}
function uploadFile<T>({ tempFilePath, formData, data, error, loading }) {
uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
filePath: tempFilePath,
name: 'file',
formData,
success: (uploadFileRes) => {
data.value = uploadFileRes.data as T
},
fail: (err) => {
console.error('uni.uploadFile err->', err)
error.value = true
},
complete: () => {
loading.value = false
},
})
}

View File

@ -0,0 +1,3 @@
export { routeInterceptor } from './route'
export { requestInterceptor } from './request'
export { prototypeInterceptor } from './prototype'

View File

@ -0,0 +1,13 @@
export const prototypeInterceptor = {
install() {
// 解决低版本手机不识别 array.at() 导致运行报错的问题
if (typeof Array.prototype.at !== 'function') {
// eslint-disable-next-line no-extend-native
Array.prototype.at = function (index: number) {
if (index < 0) return this[this.length + index]
if (index >= this.length) return undefined
return this[index]
}
}
},
}

View File

@ -0,0 +1,51 @@
/* eslint-disable no-param-reassign */
import qs from 'qs'
import { useUserStore } from '@/store'
import { platform } from '@/utils/platform'
export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
// 拦截器配置
const httpInterceptor = {
// 拦截前触发
invoke(options: CustomRequestOptions) {
// 接口请求支持通过 query 参数配置 queryString
if (options.query) {
const queryStr = qs.stringify(options.query)
if (options.url.includes('?')) {
options.url += `&${queryStr}`
} else {
options.url += `?${queryStr}`
}
}
// 1. 请求超时
options.timeout = 10000 // 10s
// 2. (可选)添加小程序端请求头标识
options.header = {
platform, // 可选,与 uniapp 定义的平台一致,告诉后台来源
...options.header,
}
// 3. 添加 token 请求头标识
const userStore = useUserStore()
const { token } = userStore.userInfo as unknown as IUserInfo
if (options.url.includes('coze.cn')) {
options.header.Authorization = `Bearer pat_NhhZGW7sxkuyP4mJrPrVyZx20b3m6lymg0y2Ln9EyM0CV9q2f9t3rlGbtzppLQua`
} else if (token) {
options.header.Authorization = `Bearer ${token}`
}
},
}
export const requestInterceptor = {
install() {
// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)
},
}

54
src/interceptors/route.ts Normal file
View File

@ -0,0 +1,54 @@
/**
* by on 2024-03-06
*
*
* 便使
*/
import { useUserStore } from '@/store'
import { needLoginPages as _needLoginPages, getNeedLoginPages } from '@/utils'
// TODO Check
const loginRoute = '/login-sub/index'
const isLogined = () => {
const userStore = useUserStore()
return userStore.isLoginFlag
}
const isDev = import.meta.env.DEV
// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
const navigateToInterceptor = {
// 注意这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
invoke({ url }: { url: string }) {
// console.log(url) // /pages/route-interceptor/index?name=feige&age=30
const path = url.split('?')[0]
let needLoginPages: string[] = []
// 为了防止开发时出现BUG这里每次都获取一下。生产环境可以移到函数外性能更好
if (isDev) {
needLoginPages = getNeedLoginPages()
} else {
needLoginPages = _needLoginPages
}
const isNeedLogin = needLoginPages.includes(path)
if (!isNeedLogin) {
return true
}
const hasLogin = isLogined()
if (hasLogin) {
return true
}
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
uni.navigateTo({ url: redirectRoute })
return false
},
}
export const routeInterceptor = {
install() {
uni.addInterceptor('navigateTo', navigateToInterceptor)
uni.addInterceptor('reLaunch', navigateToInterceptor)
uni.addInterceptor('redirectTo', navigateToInterceptor)
uni.addInterceptor('switchTab', navigateToInterceptor)
},
}

View File

@ -0,0 +1,216 @@
<template>
<Overlay :show="show" @update:show="handleClose">
<view
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center bg-white p-[40rpx] rounded-[32rpx]"
@click.stop
>
<image
class="w-[200rpx] h-[200rpx]"
src="https://api-static-zhiy.oss-cn-shanghai.aliyuncs.com/tw/liuweishengyalogo.png"
mode="aspectFit"
></image>
<view class="flex flex-col items-center">
<text class="text-[26rpx] mt-[20rpx] mb-[40rpx]" :selectable="false">
{{ phone ? '申请使用您的手机号' : '申请获取您的个人信息' }}
</text>
<button
class="w-[493rpx]! mb-[40rpx] h-[88rpx]! rounded-[44rpx] text-[32rpx] text-white flex items-center justify-center"
:class="checked.length > 0 ? 'bg-[#1580FF]' : 'bg-[#BFBFBF]'"
@click.stop="handleClick"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
:disabled="checked.length === 0"
>
手机号快捷登录
</button>
<view class="flex items-center flex-nowrap">
<CheckboxGroup v-model="checked" class="check-class mr-[10rpx]">
<Checkbox name="1" cell shape="button" class="custom-checkbox"></Checkbox>
</CheckboxGroup>
<view class="flex items-center">
<text class="text-[24rpx] whitespace-nowrap">
已阅读并同意
<text class="text-[#1580FF]" @click.stop="handleClickUserAgreement">
<text>用户协议</text>
</text>
<text class="text-[#1580FF]" @click.stop="handleClickPrivacyPolicy">
<text>隐私条款</text>
</text>
</text>
</view>
</view>
</view>
</view>
</Overlay>
</template>
<script lang="ts" setup>
import { useLogin } from '@/login-sub/hooks/useUserInfo'
import Overlay from './Overlay.vue'
import Checkbox from './check-group/Checkbox.vue'
import CheckboxGroup from './check-group/CheckboxGroup.vue'
import {
getSessionKey,
getVolunteerInitialization,
getWxUserInfo,
setWxInfo,
} from '@/service/index/api'
import { useUserStore } from '@/store/user'
import { City } from '@/types/app-type'
defineProps({
show: {
type: Boolean,
default: false,
},
})
defineOptions({
options: {
styleIsolation: 'shared',
},
})
const emits = defineEmits(['update:show', 'authReady'])
const userStore = useUserStore()
const handleClose = () => {
emits('update:show', false)
}
const phone = ref(true) //
const checked = ref([]) //
const getPhoneInfo = ref(null)
const handleClickUserAgreement = () => {
uni.navigateTo({
url: '/login-sub/userAgreement',
})
}
const handleClickPrivacyPolicy = () => {
uni.navigateTo({
url: '/login-sub/privacyPolicy',
})
}
const getPhoneNumber = async (e: any) => {
if (e.detail.errMsg == 'getPhoneNumber:ok') {
const detail = e.detail
let _getPhoneInfo = {
iv: detail.iv,
encryptedData: detail.encryptedData,
code: detail.code,
}
getPhoneInfo.value = _getPhoneInfo
await getUserInfo(detail.code)
} else if (e.detail.errMsg == 'getPhoneNumber:fail not login') {
uni.showToast({
title: '请先登录',
icon: 'none',
})
} else {
uni.showToast({
title: '获取手机号失败',
icon: 'none',
})
}
}
const handleClick = () => {
if (!checked.value) {
uni.showToast({
title: '您需先同意《服务条款》和《隐私条款》',
icon: 'none',
})
return
}
}
const getUserInfo = async (_code: string) => {
let userInfo = (await useLogin()) as { code: string; errMsg: string }
if (userInfo.errMsg == 'login:ok') {
const resp = await getSessionKey({ JsCode: userInfo.code })
if (resp.code == 200) {
const result = resp.result as { accessToken: string; openId: string }
userStore.setUserToken(result.accessToken)
userStore.setUserOpenId(result.openId)
setWxInfo({ code: _code, openId: result.openId })
//
getWxUserInfo().then((resp) => {
const infoData = resp.result as unknown as {
userExtend: { provinceCode: string }
zyBatches: any[]
batchDataUrl: string
batchName: string
avatar: string
nickName: string
}
userStore.setEstimatedAchievement(infoData.userExtend)
userStore.setZyBatches(infoData.zyBatches)
userStore.setBatchDataUrl(infoData.batchDataUrl)
userStore.setBatchName(infoData.batchName)
userStore.setUserAvatar(infoData.avatar)
userStore.setUserNickName(infoData.nickName)
if (resp.code === 200) {
//
getVolunteerInitialization().then((res) => {
let list = res.result as any[]
let code = infoData.userExtend ? infoData.userExtend.provinceCode : ''
let addressItem: City
if (code !== '') {
for (let i = 0; i < list.length; i++) {
if (list[i].code == code) {
addressItem = list[i]
}
}
}
userStore.setUserCity(addressItem)
handleClose()
emits('authReady')
})
}
})
}
} else {
uni.showToast({
title: '您需先授权',
icon: 'none',
})
}
}
</script>
<style lang="scss" scoped>
:deep(.custom-checkbox) {
display: flex;
align-items: center;
justify-content: center;
.checkbox__icon {
border-radius: 50%;
height: 32rpx;
width: 32rpx;
margin: 0;
}
.custom-box {
width: 32rpx;
height: 32rpx;
border: 1px solid #ddd;
border-radius: 50%;
}
}
:deep(.checkbox-active) {
border-color: #fff !important;
background-color: #fff !important;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<view
v-if="show"
class="overlay"
:class="{ 'overlay-show': show }"
@click="handleClick"
:style="{ zIndex }"
>
<slot></slot>
</view>
</template>
<script lang="ts" setup>
const props = defineProps({
show: {
type: Boolean,
default: false,
},
zIndex: {
type: Number,
default: 10,
},
closeOnClickOverlay: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['click', 'update:show'])
const handleClick = (event: Event) => {
emit('click', event)
if (props.closeOnClickOverlay) {
emit('update:show', false)
}
}
</script>
<style scoped>
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.7);
transition: all 0.3s ease;
}
.overlay-show {
opacity: 1;
visibility: visible;
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<view
class="checkbox"
:class="{
'checkbox--disabled': isDisabled,
'checkbox-active': isChecked,
'checkbox-disabled': isDisabled,
}"
@click="handleClick"
>
<view class="checkbox__icon" :class="{ 'checkbox__icon--checked': isChecked }">
<text v-show="isChecked" class="i-carbon-checkmark checkbox__icon-check"></text>
</view>
<view class="checkbox__label">
<slot>{{ label }}</slot>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue'
const props = defineProps({
name: {
type: [String, Number],
required: true,
},
label: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['change'])
interface CheckboxGroupContext {
modelValue: ComputedRef<any[]>
disabled: ComputedRef<boolean>
max: ComputedRef<number>
selectedCount: ComputedRef<number>
toggleOption: (option: { value: string | number }) => void
}
// checkbox group
const checkboxGroup = inject<CheckboxGroupContext>('checkboxGroup', {
modelValue: computed(() => []),
disabled: computed(() => false),
max: computed(() => 0),
selectedCount: computed(() => 0),
toggleOption: () => {},
})
//
const isChecked = computed(() => {
const modelValue = checkboxGroup.modelValue.value
return modelValue.includes(props.name)
})
//
const isDisabled = computed(() => {
const max = checkboxGroup.max.value
const selectedCount = checkboxGroup.selectedCount.value
// group
return (
props.disabled ||
checkboxGroup.disabled.value ||
(max > 1 && !isChecked.value && selectedCount >= max)
)
})
//
const handleClick = () => {
if (isDisabled.value) return
checkboxGroup.toggleOption({
value: props.name,
})
}
</script>
<style scoped>
.checkbox {
display: inline-flex;
align-items: center;
cursor: pointer;
font-size: 28rpx;
}
.checkbox--disabled {
cursor: not-allowed;
opacity: 0.5;
}
.checkbox__icon {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #dcdfe6;
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8rpx;
transition: all 0.2s;
}
.checkbox__icon--checked {
background-color: #0083ff;
border-color: #0083ff;
}
.checkbox__icon-check {
color: #fff;
font-size: 32rpx;
}
.checkbox__label {
line-height: 1;
}
.checkbox-active {
background-color: #0083ff;
border-color: #0083ff;
}
.checkbox-disabled {
opacity: 0.5;
cursor: not-allowed !important;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<view class="checkbox-group">
<slot></slot>
</view>
</template>
<script lang="ts" setup>
import { provide, computed } from 'vue'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
max: {
type: Number,
default: 0,
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change'])
//
const innerValue = computed(() => props.modelValue)
//
const toggleOption = (option: { label: string; value: string | number }) => {
const currentValue = innerValue.value
const index = currentValue.indexOf(option.value)
let newValue = [...currentValue]
if (index === -1) {
if (props.max === 1) {
newValue = [option.value]
} else if (props.max && currentValue.length >= props.max) {
uni.showToast({
title: `最多只能选择${props.max}`,
icon: 'none',
})
return
} else {
newValue.push(option.value)
}
} else {
newValue.splice(index, 1)
}
emit('update:modelValue', newValue)
emit('change', newValue)
}
// checkbox
provide('checkboxGroup', {
modelValue: computed(() => props.modelValue),
disabled: computed(() => props.disabled),
max: computed(() => props.max),
selectedCount: computed(() => props.modelValue.length),
toggleOption,
})
</script>
<style scoped lang="scss">
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
</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
// pxrpx
const ratio = 750 / screenWidth
//
if (platform === 'ios') {
return 88 / ratio // iOS 44ptpx
} else if (platform === 'android') {
return 96 / ratio // Android 48dppx
} 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>

View File

@ -0,0 +1,89 @@
<template>
<label
class="radio-wrapper"
:class="{ 'radio-wrapper--disabled': isDisabled }"
@click.stop="handleClick"
>
<radio
class="radio"
:value="String(name)"
:checked="isChecked"
:disabled="isDisabled"
:color="isChecked ? '#0083ff' : ''"
:name="String(name)"
/>
<view class="radio-label" :class="{ 'radio-label--active': isChecked }">
<slot>{{ label }}</slot>
</view>
</label>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue'
const props = defineProps({
name: {
type: [String, Number],
required: true,
},
label: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
})
interface RadioGroupContext {
modelValue: ComputedRef<string | number>
disabled: ComputedRef<boolean>
toggleOption: (value: string | number) => void
}
// radio group
const radioGroup = inject<RadioGroupContext>('radioGroup', {
modelValue: computed(() => ''),
disabled: computed(() => false),
toggleOption: () => {},
})
//
const isChecked = computed(() => {
return radioGroup.modelValue.value === props.name
})
//
const isDisabled = computed(() => {
return props.disabled || radioGroup.disabled.value
})
//
const handleClick = () => {
if (isDisabled.value) return
radioGroup.toggleOption(props.name)
}
</script>
<style scoped lang="scss">
.radio-wrapper {
display: inline-flex;
align-items: center;
font-size: 28rpx;
padding: 8rpx 0;
&--disabled {
opacity: 0.5;
}
}
.radio-label {
margin-left: 10rpx;
line-height: 1;
&--active {
color: #0083ff;
}
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<radio-group class="radio-group" :value="modelValue" @change="handleChange">
<slot></slot>
</radio-group>
</template>
<script lang="ts" setup>
import { provide, computed } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number],
default: '',
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change'])
//
const handleChange = (e: any) => {
const value = e.detail.value
emit('update:modelValue', value)
emit('change', value)
}
//
const toggleOption = (value: string | number) => {
emit('update:modelValue', value)
emit('change', value)
}
// radio
provide('radioGroup', {
modelValue: computed(() => props.modelValue),
disabled: computed(() => props.disabled),
toggleOption,
})
</script>
<style scoped lang="scss">
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
</style>

View File

@ -0,0 +1,27 @@
import { setWxInfo } from '@/service/index/api'
//uniapp 登陆获取用户信息
export const useLogin = () => {
return new Promise(function (resolve, reject) {
uni.login({
success: function (res) {
if (res.code) {
resolve(res)
} else {
reject(res)
}
},
fail: function (err) {
reject(err)
},
})
})
}
export const useWxInfo = ({ code, openId }) => {
setWxInfo({ code, openId }).then((res) => {
if (res.code === 200) {
console.log(res.result)
}
})
}

59
src/login-sub/index.vue Normal file
View File

@ -0,0 +1,59 @@
<route lang="json5" type="page">
{
style: {
navigationStyle: 'custom',
},
}
</route>
<template>
<view class="h-screen flex flex-col">
<Navbar
safeAreaInsetTop
:bordered="false"
:fixed="true"
:placeholder="true"
left-arrow
bgColor="transparent"
@click-left="handleClickLeft"
>
<template #title>
<text class="text-[#1F2329] text-[36rpx] font-medium">六维生涯</text>
</template>
</Navbar>
<view class="flex flex-col justify-center items-center flex-1 pb-safe mt-[-100px]">
<image
class="w-[424rpx] h-[424rpx]"
src="https://api-static-zhiy.oss-cn-shanghai.aliyuncs.com/tw/liuweishengyalogo.png"
mode="aspectFit"
></image>
<view
class="px-[32rpx] py-[16rpx] bg-[#3370FF] rounded-[40rpx] text-white text-[32rpx] font-medium flex items-center justify-center"
@click="handleLogin"
>
立即登录
</view>
</view>
<LoginMask v-model:show="show" @auth-ready="handleAuthReady" />
</view>
</template>
<script setup lang="ts">
import LoginMask from './components/LoginMask.vue'
import Navbar from './components/navbar/Navbar.vue'
const show = ref(false)
const handleLogin = () => {
show.value = true
}
//
const handleAuthReady = () => {
uni.navigateBack()
}
const handleClickLeft = () => {
uni.navigateBack()
}
</script>

View File

@ -0,0 +1,7 @@
<template>
<web-view src="https://api.static.ycymedu.com/lwxy.html" />
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,7 @@
<template>
<web-view src="https://api.static.ycymedu.com/lwuser.html" />
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>

21
src/main.ts Normal file
View File

@ -0,0 +1,21 @@
import '@/style/index.scss'
import { VueQueryPlugin } from '@tanstack/vue-query'
import 'virtual:uno.css'
import { createSSRApp } from 'vue'
import App from './App.vue'
import { prototypeInterceptor, requestInterceptor, routeInterceptor } from './interceptors'
import store from './store'
export function createApp() {
const app = createSSRApp(App)
app.use(store)
app.use(routeInterceptor)
app.use(requestInterceptor)
app.use(prototypeInterceptor)
app.use(VueQueryPlugin)
return {
app,
}
}

87
src/manifest.json Normal file
View File

@ -0,0 +1,87 @@
{
"name": "六纬生涯",
"appid": "H57F2ACE4",
"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\"/>"
],
"minSdkVersion": 30,
"targetSdkVersion": 30,
"abiFilters": [
"armeabi-v7a",
"arm64-v8a"
]
},
"ios": {},
"sdkConfigs": {},
"icons": {
"android": {},
"ios": {}
}
},
"compatible": {
"ignoreVersion": true
}
},
"quickapp": {},
"mp-weixin": {
"appid": "wxc2399d3aa57174db",
"setting": {
"urlCheck": false
},
"usingComponents": true,
"optimization": {
"subPackages": true
},
"lazyCodeLoading": "requiredComponents"
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"router": {
"base": "",
"mode": "history"
}
}
}

View File

@ -0,0 +1,121 @@
<template>
<CheckboxGroup v-model="defValue" checked-color="#1580FF" @change="handleChange" v-bind="$attrs">
<Checkbox
v-for="item in list"
:key="item[valueKey]"
:name="item[valueKey]"
cell
shape="button"
class="custom-checkbox"
:style="checkboxStyle"
>
{{ item[labelKey] }}
</Checkbox>
</CheckboxGroup>
</template>
<script lang="ts" setup>
import Checkbox from './Checkbox.vue'
import CheckboxGroup from './CheckboxGroup.vue'
const props = defineProps({
list: {
type: Array,
default: () => [],
},
labelKey: {
type: String,
default: 'name',
},
valueKey: {
type: String,
default: 'code',
},
defaultValue: {
type: Array<string>,
default: () => [],
},
width: {
type: [String, Number],
default: '216rpx',
},
height: {
type: [String, Number],
default: '60rpx',
},
})
defineOptions({
options: {
styleIsolation: 'shared',
},
})
const emits = defineEmits(['change'])
const defValue = ref<string[]>([])
//
onMounted(() => {
if (props.defaultValue?.length) {
defValue.value = [...props.defaultValue]
}
})
const handleChange = (val: unknown) => {
defValue.value = val as string[]
emits('change', val)
}
watch(
() => props.defaultValue,
(newVal) => {
defValue.value = [...newVal]
},
)
//
const checkboxStyle = computed(() => {
const width = typeof props.width === 'number' ? `${props.width}rpx` : props.width
const height = typeof props.height === 'number' ? `${props.height}rpx` : props.height
return {
'--checkbox-width': width,
'--checkbox-height': height,
}
})
</script>
<style lang="scss" scoped>
:deep(.custom-checkbox) {
//
--checkbox-width: 216rpx;
--checkbox-height: 60rpx;
--checkbox-bg: #f7f8fa;
--checkbox-radius: 8rpx;
.checkbox {
width: var(--checkbox-width);
height: var(--checkbox-height);
min-width: var(--checkbox-width);
background-color: var(--checkbox-bg);
border-radius: var(--checkbox-radius);
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid var(--checkbox-bg);
}
.checkbox__icon {
display: none;
}
}
:deep(.checkbox-group) {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
padding: 32rpx 16rpx 16rpx;
}
:deep(.checkbox-active) {
border-color: #1580ff !important;
.checkbox__label {
color: #1580ff !important;
}
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<view
class="checkbox"
:class="{
'checkbox--disabled': isDisabled,
'checkbox-active': isChecked,
'checkbox-disabled': isDisabled,
}"
@click="handleClick"
>
<view class="checkbox__icon" :class="{ 'checkbox__icon--checked': isChecked }">
<text v-if="isChecked" class="i-carbon-checkmark checkbox__icon-check"></text>
</view>
<view class="checkbox__label">
<slot>{{ label }}</slot>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue'
const props = defineProps({
name: {
type: [String, Number],
required: true,
},
label: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['change'])
interface CheckboxGroupContext {
modelValue: ComputedRef<any[]>
disabled: ComputedRef<boolean>
max: ComputedRef<number>
selectedCount: ComputedRef<number>
toggleOption: (option: { value: string | number }) => void
}
// checkbox group
const checkboxGroup = inject<CheckboxGroupContext>('checkboxGroup', {
modelValue: computed(() => []),
disabled: computed(() => false),
max: computed(() => 0),
selectedCount: computed(() => 0),
toggleOption: () => {},
})
//
const isChecked = computed(() => {
const modelValue = checkboxGroup.modelValue.value
return modelValue.includes(props.name)
})
//
const isDisabled = computed(() => {
const max = checkboxGroup.max.value
const selectedCount = checkboxGroup.selectedCount.value
// group
return (
props.disabled ||
checkboxGroup.disabled.value ||
(max > 1 && !isChecked.value && selectedCount >= max)
)
})
//
const handleClick = () => {
if (isDisabled.value) return
checkboxGroup.toggleOption({
value: props.name,
})
}
</script>
<style scoped>
.checkbox {
display: inline-flex;
align-items: center;
cursor: pointer;
font-size: 28rpx;
}
.checkbox--disabled {
cursor: not-allowed;
opacity: 0.5;
}
.checkbox__icon {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #dcdfe6;
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8rpx;
transition: all 0.2s;
}
.checkbox__icon--checked {
background-color: #0083ff;
border-color: #0083ff;
}
.checkbox__icon-check {
color: #fff;
font-size: 32rpx;
}
.checkbox__label {
line-height: 1;
}
.checkbox-active {
background-color: #0083ff;
border-color: #0083ff;
}
.checkbox-disabled {
opacity: 0.5;
cursor: not-allowed !important;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<view class="checkbox-group">
<slot></slot>
</view>
</template>
<script lang="ts" setup>
import { provide, computed } from 'vue'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
max: {
type: Number,
default: 0,
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change'])
//
const innerValue = computed(() => props.modelValue)
//
const toggleOption = (option: { label: string; value: string | number }) => {
const currentValue = innerValue.value
const index = currentValue.indexOf(option.value)
let newValue = [...currentValue]
if (index === -1) {
if (props.max === 1) {
newValue = [option.value]
} else if (props.max && currentValue.length >= props.max) {
uni.showToast({
title: `最多只能选择${props.max}`,
icon: 'none',
})
return
} else {
newValue.push(option.value)
}
} else {
newValue.splice(index, 1)
}
emit('update:modelValue', newValue)
emit('change', newValue)
}
// checkbox
provide('checkboxGroup', {
modelValue: computed(() => props.modelValue),
disabled: computed(() => props.disabled),
max: computed(() => props.max),
selectedCount: computed(() => props.modelValue.length),
toggleOption,
})
</script>
<style scoped lang="scss">
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<view class="drop-menu" ref="dropMenuRef">
<view class="drop-menu__bar">
<view
v-for="(item, index) in titles"
:key="index"
class="drop-menu__item"
:class="{
'drop-menu__item--active': index === activeIndex || item.activation,
'drop-menu__disable': item.disabled,
}"
@click="handleTitleClick(index)"
>
<text class="drop-menu__title">{{ item.title }}</text>
<text
class="drop-menu__arrow i-carbon-chevron-down"
:class="{ 'drop-menu__arrow--active': index === activeIndex }"
></text>
</view>
</view>
<view class="drop-menu__content-wrapper">
<slot></slot>
</view>
<view
v-if="activeIndex !== -1"
class="drop-menu__mask"
:style="{ top: maskTop }"
@click="closeDropMenu"
></view>
</view>
</template>
<script lang="ts" setup>
import { ref, provide } from 'vue'
const props = defineProps({
zIndex: {
type: Number,
default: 10,
},
duration: {
type: Number,
default: 200,
},
direction: {
type: String,
default: 'down',
},
})
//
const titles = ref<any[]>([])
//
const activeIndex = ref(-1)
const maskTop = ref('88px')
//
const addTitle = (options) => {
titles.value.push({ ...options })
}
const instance = getCurrentInstance()
const dropMenuRef = ref()
//
const handleTitleClick = (index: number) => {
if (titles.value[index].disabled) {
return
}
//
if (activeIndex.value === index) {
activeIndex.value = -1
} else {
//
activeIndex.value = index
}
const query = uni.createSelectorQuery().in(instance.proxy)
query
.select('.drop-menu')
.boundingClientRect((data: { top: number }) => {
maskTop.value = `${data.top}px`
})
.exec()
}
//
const closeDropMenu = () => {
activeIndex.value = -1
}
//
provide('dropMenu', {
activeIndex,
addTitle,
closeDropMenu,
zIndex: props.zIndex,
duration: props.duration,
direction: props.direction,
titles, // titles
})
defineExpose({
closeDropMenu,
})
</script>
<style scoped lang="scss">
.drop-menu {
position: relative;
background: #fff;
z-index: 10;
}
.drop-menu__bar {
position: relative;
display: flex;
height: 88rpx;
background: #fff;
z-index: 12;
}
.drop-menu__item {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.drop-menu__item--active {
color: #0083ff;
}
.drop-menu__title {
font-size: 28rpx;
max-width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.drop-menu__arrow {
font-size: 20rpx;
margin-left: 8rpx;
transition: transform 0.2s;
}
.drop-menu__arrow--active {
transform: rotate(180deg);
}
.drop-menu__content-wrapper {
position: relative;
width: 100%;
}
.drop-menu__mask {
position: fixed;
width: 100%;
left: 0;
right: 0;
top: 88rpx;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 9;
}
.drop-menu__disable {
color: #999;
}
</style>

View File

@ -0,0 +1,217 @@
<template>
<view
class="drop-menu-item"
:class="[customClass, { 'drop-menu-item--show': isShow }]"
:style="{
'z-index': zIndex,
'transition-duration': `${duration}ms`,
}"
>
<view class="drop-menu-item__wrapper" :class="{ 'drop-menu-item__wrapper--show': isShow }">
<!-- 默认选项列表 -->
<scroll-view v-if="!$slots.default" scroll-y class="drop-menu-item__content">
<view class="drop-menu-item__option-list">
<view
v-for="(option, index) in options"
:key="index"
class="drop-menu-item__option"
:class="{ 'drop-menu-item__option--active': isOptionActive(option) }"
@click="handleOptionClick(option)"
>
<text class="drop-menu-item__text">{{ getOptionText(option) }}</text>
<text v-if="isOptionActive(option)" class="drop-menu-item__icon"></text>
</view>
</view>
</scroll-view>
<!-- 自定义内容插槽 -->
<view v-else class="drop-menu-item__custom-content">
<slot></slot>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, inject, onMounted, computed, watch } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number, Object],
default: '',
},
title: {
type: String,
required: true,
},
options: {
type: Array,
default: () => [],
},
labelKey: {
type: String,
default: 'text',
},
valueKey: {
type: String,
default: 'value',
},
customClass: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
activation: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change', 'open'])
//
const { activeIndex, addTitle, closeDropMenu, zIndex, duration, direction, titles } = inject(
'dropMenu',
) as any
//
const itemIndex = ref(-1)
//
const isShow = computed(() => activeIndex.value === itemIndex.value)
//
watch(isShow, (newVal) => {
if (newVal) {
emit('open')
}
})
//
const getOptionText = (option: any) => {
if (typeof option === 'object' && props.labelKey) {
return option[props.labelKey]
}
return option
}
//
const getOptionValue = (option: any) => {
if (typeof option === 'object' && props.valueKey) {
return option[props.valueKey]
}
return option
}
//
const isOptionActive = (option: any) => {
const optionValue = getOptionValue(option)
return props.modelValue === optionValue
}
//
const handleOptionClick = (option: any) => {
const value = getOptionValue(option)
emit('update:modelValue', value)
emit('change', value)
closeDropMenu()
}
//
onMounted(() => {
//
itemIndex.value = titles.value.length
//
addTitle({ title: props.title, disabled: props.disabled, activation: props.activation })
})
//
watch(
() => props.title,
(newTitle) => {
//
if (titles.value[itemIndex.value].title !== newTitle) {
titles.value[itemIndex.value].title = newTitle
}
},
{ immediate: false },
)
watch(
() => props.activation,
(newVal) => {
titles.value[itemIndex.value].activation = newVal
},
)
</script>
<style scoped>
.drop-menu-item {
position: relative;
width: 100%;
}
.drop-menu-item__wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
background: #fff;
transform: translateY(-5px);
transition: all 0.25s ease;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
opacity: 0;
visibility: hidden;
z-index: 11;
}
.drop-menu-item__wrapper--show {
transform: translateY(0);
opacity: 1;
visibility: visible;
}
.drop-menu-item__content {
max-height: 400rpx;
}
.drop-menu-item__custom-content {
width: 100%;
background: #fff;
transform-origin: top;
}
.drop-menu-item__option-list {
padding: 12rpx 0;
}
.drop-menu-item__option {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
line-height: 1.2;
cursor: pointer;
}
.drop-menu-item__option:active {
background-color: #f2f2f2;
}
.drop-menu-item__option--active {
color: #0083ff;
}
.drop-menu-item__text {
font-size: 28rpx;
}
.drop-menu-item__icon {
font-size: 32rpx;
color: #0083ff;
}
</style>

View File

@ -0,0 +1,200 @@
<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 icon-class" />
</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'
const props = 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
// pxrpx
const ratio = 750 / screenWidth
//
if (platform === 'ios') {
return 88 / ratio // iOS 44ptpx
} else if (platform === 'android') {
return 96 / ratio // Android 48dppx
} 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: 99;
}
.navbar-border {
border-bottom: 1rpx solid #eee;
}
.navbar-left {
display: flex;
align-items: center;
height: 100%;
}
.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%;
}
.back-text {
font-size: 48rpx;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333;
}
</style>

View File

@ -0,0 +1,194 @@
<template>
<view class="custom-picker">
<view class="picker-mask" @touchmove.stop.prevent></view>
<picker-view
:value="currentIndex"
:indicator-style="indicatorStyle"
:style="{
border: 'none',
'border-top': 'none',
'border-bottom': 'none',
}"
@change="handleChange"
class="picker-view"
:mask-class="'picker-mask'"
:indicator-class="'picker-indicator'"
>
<picker-view-column class="border-none">
<view
v-for="(item, index) in formattedList"
:key="index"
class="picker-item"
:class="{ 'picker-item-selected': currentIndex[0] === index }"
>
{{ item }}
</view>
</picker-view-column>
</picker-view>
<!-- <view class="picker-indicator-line"></view> -->
</view>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number, Object],
default: '',
},
list: {
type: Array,
required: true,
default: () => [],
},
valueKey: {
type: String,
default: '',
},
labelKey: {
type: String,
default: '',
},
indicatorStyle: {
type: String,
default: 'height: 50px;',
},
})
const emit = defineEmits(['update:modelValue', 'change'])
//
const formattedList = computed(() => {
if (!props.list.length) return []
// ['123', '456']
if (typeof props.list[0] !== 'object') {
return props.list
}
// 使labelKey
if (props.labelKey) {
return props.list.map((item) => item[props.labelKey])
}
// 使valueKey
if (props.valueKey) {
return props.list.map((item) => item[props.valueKey])
}
//
return props.list
})
//
const currentIndex = computed(() => {
if (!props.modelValue || !props.list.length) return [0]
let targetValue = props.modelValue
if (typeof props.list[0] === 'object' && props.valueKey) {
targetValue = props.modelValue[props.valueKey]
}
const index = props.list.findIndex((item) => {
if (typeof item === 'object' && props.valueKey) {
return item[props.valueKey] === targetValue
}
return item === targetValue
})
return [index > -1 ? index : 0]
})
//
const handleChange = (e: any) => {
const index = e.detail.value[0]
const selectedItem = props.list[index]
let value = selectedItem
if (typeof selectedItem === 'object' && props.valueKey) {
value = selectedItem[props.valueKey]
}
emit('update:modelValue', value)
emit('change', {
value,
index,
item: selectedItem,
})
}
</script>
<style scoped>
.custom-picker {
width: 100%;
height: 400rpx;
position: relative;
background-color: #fff;
}
.picker-view {
width: 100%;
height: 100%;
}
/* 覆盖picker-view的默认边框 */
.picker-view :deep(.uni-picker-view-indicator),
.picker-view :deep(.uni-picker-view-column),
.picker-view :deep(.uni-picker-view-group) {
border: none !important;
border-top: none !important;
border-bottom: none !important;
}
.picker-mask {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 2;
}
.picker-indicator {
height: 112rpx;
}
.picker-indicator-line {
position: absolute;
left: 0;
right: 0;
top: 50%;
margin-top: -40rpx;
height: 112rpx;
pointer-events: none;
z-index: 1;
}
.picker-item {
text-align: center;
font-size: 34rpx;
color: #7c7c7c;
transition: all 0.2s;
display: flex;
justify-content: center;
align-items: center;
}
.picker-item-selected {
color: #000;
font-weight: 500;
transform: scale(1.05);
}
/* 添加触摸反馈 */
.picker-view :deep(.uni-picker-view-wrapper) {
-webkit-overflow-scrolling: touch;
}
.picker-view :deep(.uni-picker-view-content) {
touch-action: pan-y;
}
</style>

View File

@ -0,0 +1,126 @@
<route lang="json5" type="page">
{
style: {
navigationStyle: 'custom',
},
}
</route>
<template>
<view class="flex flex-col h-screen relative custom-bg">
<Navbar
safeAreaInsetTop
:bordered="false"
leftArrow
@clickLeft="handleBack"
bg-color="transparent"
>
<template #title>
<text class="text-[#1F2329] text-[36rpx] font-medium text-[#fff]">能力测评报告</text>
</template>
</Navbar>
<view class="flex-1 overflow-auto relative mt-[40rpx] flex flex-col">
<view class="flex flex-col flex-1 overflow-auto pb-[20rpx]">
<!-- 顶部卡片 -->
<view
class="flex flex-col pt-[32rpx] mx-[24rpx] bg-[#fff] px-[30rpx] pt-[30rpx] border-class"
>
<text class="text-[#333] text-[28rpx] mb-[14rpx] z-2 font-700">你的弱势能力为</text>
<text class="text-[#117CFC] text-[36rpx] z-2">
{{ studyRecord.title.replace('你的弱势能力:', '') }}
</text>
</view>
<!-- 雷达图占位 -->
<LineReport :echart-data="studyRecord.linChart" :description="studyRecord.description" />
<AbilityDimension :report-items="studyRecord.reportItems" />
</view>
<!-- 底部AI智能顾问 -->
<AiFooter :pageId="pageId" :pageType="pageType" />
</view>
</view>
</template>
<script setup lang="ts">
import Navbar from '@/pages-evaluation-sub/components/navbar/Navbar.vue'
import LineReport from '../components/interestChart/LineReport.vue'
import AbilityDimension from '../components/AbilityDimension.vue'
import AiFooter from '../components/AiFooter.vue'
import { handleBack } from '../hooks/useEvaluateBack'
import { getAbilityDimension } from '@/service/index/api'
const pageType = ref(0)
const pageId = ref(0)
const studyRecord = ref({
description: '',
title: '',
linChart: [],
reportItems: [],
hTag: '',
})
onLoad((options) => {
pageType.value = +options.type
pageId.value = options.id
getAbilityDimension({ ScaleId: pageId.value }).then((resp) => {
if (resp.code === 200) {
studyRecord.value = resp.result as {
description: string
title: string
linChart: any[]
reportItems: any[]
hTag: string
}
}
})
})
</script>
<style scoped lang="scss">
.custom-bg {
background: linear-gradient(184deg, #0d79fc 0%, #2186fc 100%);
}
:deep(.icon-class) {
color: #fff !important;
}
.header-bg {
width: calc(100% - 80rpx);
height: 244rpx;
position: absolute;
top: 0;
left: 40rpx;
z-index: 1;
}
.type-tag {
font-size: 24rpx;
min-width: 40rpx;
text-align: center;
}
.position-tag {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
font-size: 26rpx;
text-align: center;
}
.table-row {
align-items: center;
font-size: 26rpx;
color: #333;
}
.avatar-item image {
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.border-class {
border-radius: 20rpx 20rpx 0 0;
padding-bottom: 42rpx;
margin-bottom: -14rpx;
}
</style>

View File

@ -0,0 +1,125 @@
<route lang="json5" type="page">
{
style: {
navigationStyle: 'custom',
},
}
</route>
<template>
<view class="flex flex-col h-screen relative custom-bg">
<Navbar
safeAreaInsetTop
:bordered="false"
leftArrow
@clickLeft="handleBack"
bg-color="transparent"
>
<template #title>
<text class="text-[#1F2329] text-[36rpx] font-medium text-[#fff]">性格测评报告</text>
</template>
</Navbar>
<view class="flex-1 flex flex-col overflow-auto">
<view class="overflow-auto relative mt-[40rpx] flex-1 pb-[20rpx]">
<!-- 顶部卡片 -->
<view class="flex flex-col pt-[32rpx] px-[84rpx] h-[244rpx] mb-[-116rpx] font-700">
<image
src="https://api.static.ycymedu.com/src/images/evaluate/bg.png"
class="header-bg"
/>
<text class="text-[#333] text-[28rpx] mb-[14rpx] z-2">您的性格类型为</text>
<text class="text-[#117CFC] text-[36rpx] z-2">{{ studyRecord.title }}</text>
</view>
<!-- 雷达图占位 -->
<CharacterChart :linChart="studyRecord.linChart" :description="studyRecord.description" />
<DependenciesChart
:mainDomain="studyRecord.reportItem.mainDomain"
:major="studyRecord.reportItem.major"
:occupation="studyRecord.reportItem.occupation"
/>
</view>
<!-- 底部AI智能顾问 -->
<AiFooter :pageId="pageId" :pageType="pageType" />
</view>
</view>
</template>
<script setup lang="ts">
import Navbar from '@/pages-evaluation-sub/components/navbar/Navbar.vue'
import CharacterChart from '../components/interestChart/CharacterChart.vue'
import DependenciesChart from '../components/interestChart/DependenciesChart.vue'
import AiFooter from '../components/AiFooter.vue'
import { getMBTIDimension } from '@/service/index/api'
import { handleBack } from '../hooks/useEvaluateBack'
const pageType = ref(0)
const pageId = ref(0)
const studyRecord = ref({
description: '',
title: '',
linChart: {},
reportItem: { mainDomain: '', major: '', occupation: '' },
hTag: '',
})
onLoad((options) => {
pageType.value = +options.type
pageId.value = options.id
getMBTIDimension({ ScaleId: pageId.value }).then((resp) => {
if (resp.code === 200) {
studyRecord.value = resp.result as {
description: string
title: string
linChart: any
reportItem: { mainDomain: string; major: string; occupation: string }
hTag: string
}
}
})
})
</script>
<style scoped lang="scss">
.custom-bg {
background: linear-gradient(184deg, #0d79fc 0%, #2186fc 100%);
}
:deep(.icon-class) {
color: #fff !important;
}
.header-bg {
width: calc(100% - 80rpx);
height: 244rpx;
position: absolute;
top: 0;
left: 40rpx;
z-index: 1;
}
.type-tag {
font-size: 24rpx;
min-width: 40rpx;
text-align: center;
}
.position-tag {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
font-size: 26rpx;
text-align: center;
}
.table-row {
align-items: center;
font-size: 26rpx;
color: #333;
}
.avatar-item image {
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
</style>

View File

@ -0,0 +1,123 @@
<route lang="json5" type="page">
{
style: {
navigationStyle: 'custom',
},
}
</route>
<template>
<view class="flex flex-col h-screen relative custom-bg">
<Navbar
safeAreaInsetTop
:bordered="false"
leftArrow
@clickLeft="handleBack"
bg-color="transparent"
>
<template #title>
<text class="text-[#1F2329] text-[36rpx] font-medium text-[#fff]">兴趣测评报告</text>
</template>
</Navbar>
<view class="flex-1 overflow-auto relative mt-[40rpx]">
<view class="overflow-auto relative mt-[40rpx] flex-1 pb-[20rpx]">
<!-- 顶部卡片 -->
<view class="flex flex-col pt-[32rpx] px-[84rpx] h-[244rpx] mb-[-116rpx] font-700">
<image
src="https://api.static.ycymedu.com/src/images/evaluate/bg.png"
class="header-bg"
/>
<text class="text-[#333] text-[28rpx] mb-[14rpx] z-2">您的兴趣类型为</text>
<text class="text-[#117CFC] text-[36rpx] z-2">{{ studyRecord.title }}</text>
</view>
<!-- 雷达图占位 -->
<InterestRadar :picData="studyRecord.picCharts" />
<!-- 类型说明 -->
<TypeDetail :reportItems="studyRecord.reportItems" />
<!-- 适合职业 -->
<!-- <IntroMajor :tag="studyRecord.hTag" /> -->
<!-- 兴趣分析与代表人物 -->
<InterestingThings :tag="studyRecord.hTag" :description="studyRecord.description" />
</view>
<!-- 底部AI智能顾问 -->
<AiFooter :pageId="pageId" :pageType="pageType" />
</view>
</view>
</template>
<script setup lang="ts">
import Navbar from '@/pages-evaluation-sub/components/navbar/Navbar.vue'
import TypeDetail from '../components/TypeDetail.vue'
import InterestRadar from '../components/interestChart/InterestRadar.vue'
// import IntroMajor from '../components/IntroMajor.vue'
import { getHollandDimensionInfo } from '@/service/index/api'
import InterestingThings from '../components/InterestingThings.vue'
import AiFooter from '../components/AiFooter.vue'
import { handleBack } from '../hooks/useEvaluateBack'
const pageType = ref(0)
const pageId = ref(0)
const studyRecord = ref({ description: '', title: '', picCharts: {}, reportItems: [], hTag: '' })
onLoad((options) => {
pageType.value = +options.type
pageId.value = options.id
getHollandDimensionInfo({ ScaleId: pageId.value.toString() }).then((resp) => {
if (resp.code === 200) {
studyRecord.value = resp.result as {
description: string
title: string
picCharts: any
reportItems: any[]
hTag: string
}
}
})
})
</script>
<style scoped lang="scss">
.custom-bg {
background: linear-gradient(184deg, #0d79fc 0%, #2186fc 100%);
}
:deep(.icon-class) {
color: #fff !important;
}
.header-bg {
width: calc(100% - 80rpx);
height: 244rpx;
position: absolute;
top: 0;
left: 40rpx;
z-index: 1;
}
.type-tag {
font-size: 24rpx;
min-width: 40rpx;
text-align: center;
}
.position-tag {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
font-size: 26rpx;
text-align: center;
}
.table-row {
align-items: center;
font-size: 26rpx;
color: #333;
}
.avatar-item image {
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
</style>

View File

@ -0,0 +1,125 @@
<route lang="json5" type="page">
{
style: {
navigationStyle: 'custom',
},
}
</route>
<template>
<view :scroll-y="true" class="flex flex-col h-screen relative custom-bg">
<Navbar
safeAreaInsetTop
:bordered="false"
leftArrow
@clickLeft="handleBack"
bg-color="transparent"
>
<template #title>
<text class="text-[#1F2329] text-[36rpx] font-medium text-[#fff]">职业锚测评报告</text>
</template>
</Navbar>
<view class="flex-1 overflow-auto relative mt-[40rpx]">
<view class="overflow-auto relative mt-[40rpx] flex-1 pb-[20rpx]">
<!-- 顶部卡片 -->
<view class="flex flex-col pt-[32rpx] px-[84rpx] h-[244rpx] mb-[-116rpx] font-700">
<image
src="https://api.static.ycymedu.com/src/images/evaluate/bg.png"
class="header-bg"
/>
<text class="text-[#333] text-[28rpx] mb-[14rpx] z-2">您的职业价值观</text>
<text class="text-[#117CFC] text-[40rpx] z-2">{{ studyRecord.tag }}</text>
</view>
<OpinionChart :pic-charts="studyRecord.picCharts" />
<AbilityDimension :report-items="studyRecord.reportItems" />
</view>
<!-- 底部AI智能顾问 -->
<AiFooter :pageId="pageId" :pageType="pageType" />
</view>
</view>
</template>
<script setup lang="ts">
import Navbar from '@/pages-evaluation-sub/components/navbar/Navbar.vue'
import OpinionChart from '../components/interestChart/OpinionChart.vue'
import AbilityDimension from '../components/AbilityDimension.vue'
import AiFooter from '../components/AiFooter.vue'
import { handleBack } from '../hooks/useEvaluateBack'
import { getOpinionAbout } from '@/service/index/api'
const pageType = ref(0)
const pageId = ref(0)
const studyRecord = ref({
description: '',
title: '',
picCharts: { indicator: [], radars: [] },
reportItems: [],
tag: '',
})
onLoad((options) => {
pageType.value = +options.type
pageId.value = options.id
getOpinionAbout({ ScaleId: pageId.value }).then((resp) => {
if (resp.code === 200) {
studyRecord.value = resp.result as {
description: string
title: string
picCharts: { radars: any[]; indicator: any[] }
reportItems: any[]
tag: string
}
}
})
})
</script>
<style scoped lang="scss">
.custom-bg {
background: linear-gradient(184deg, #0d79fc 0%, #2186fc 100%);
}
:deep(.icon-class) {
color: #fff !important;
}
.header-bg {
width: calc(100% - 80rpx);
height: 244rpx;
position: absolute;
top: 0;
left: 40rpx;
z-index: 1;
}
.type-tag {
font-size: 24rpx;
min-width: 40rpx;
text-align: center;
}
.position-tag {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
font-size: 26rpx;
text-align: center;
}
.table-row {
align-items: center;
font-size: 26rpx;
color: #333;
}
.avatar-item image {
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.border-class {
border-radius: 20rpx 20rpx 0 0;
padding-bottom: 42rpx;
margin-bottom: -14rpx;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<view class="mx-[30rpx] mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar title="能力纬度详细介绍" />
<view class="flex flex-col gap-[40rpx]">
<view v-for="(item, index) in reportItems" :key="index">
<view class="text-[32rpx] text-[#000] font-700">{{ item.title }}</view>
<view class="text-[26rpx] text-[#666] mt-[6rpx]">
{{ item.resolving || item.description }}
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import TitleBar from './TitleBar.vue'
defineProps({
reportItems: {
type: Array<any>,
default: () => [],
},
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,40 @@
<template>
<!-- 底部AI智能顾问 -->
<view
class="pt-[16rpx] px-[32rpx] flex items-center justify-center bg-[#fff] pb-safe sticky bottom-0 z-999"
@click="toAiAssistant"
v-if="aiShow"
>
<view
class="rounded-[8rpx] border-[#1580FF] border-[2rpx] border-solid w-full py-[14rpx] flex items-center justify-center"
>
<image
src="https://api.static.ycymedu.com/images/btn-bottom.png"
class="w-[52rpx] h-[52rpx] mr-[10rpx]"
></image>
<text class="text-[#1580FF] text-[32rpx] font-700">智能AI顾问</text>
</view>
</view>
<view v-else class="pb-safe"></view>
</template>
<script setup lang="ts">
const aiShow = ref(false)
const props = defineProps({
pageId: {
type: Number,
default: 0,
},
pageType: {
type: Number,
default: 0,
},
})
const toAiAssistant = () => {
uni.navigateTo({
url: `/aiService-sub/index/index?id=${props.pageId}&type=${props.pageType}`,
})
}
</script>

View File

@ -0,0 +1,37 @@
<template>
<view
class="text-[26rpx] text-[#000] mx-[24rpx] bg-[#fff] flex flex-col px-[20rpx] gap-[10rpx] pb-[20rpx] custom-class"
>
<view class="flex gap-[10rpx] w-full">
<view class="w-[94rpx] py-[12rpx] bg-color text-center">类型</view>
<view class="py-[12rpx] text-center bg-color flex-1">详解</view>
</view>
<view class="flex gap-[10rpx] w-full" v-for="(item, index) in reportItems" :key="index">
<view class="w-[94rpx] py-[12rpx] bg-color text-center">
<view>{{ item.tag }}</view>
<view>{{ item.title }}</view>
</view>
<view class="py-[22rpx] px-[14rpx] text-start bg-color flex-1">{{ item.resolving }}</view>
</view>
</view>
</template>
<script setup lang="ts">
const props = defineProps({
reportItems: {
type: Array<any>,
default: () => [],
},
})
</script>
<style lang="scss" scoped>
.bg-color {
background-color: #f5faff;
border-radius: 8rpx;
}
.custom-class {
border-radius: 0 0 20rpx 20rpx;
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<view class="interest-analysis mx-[30rpx] mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar title="兴趣分析与代表人物" />
<text class="text-[26rpx] text-[#666] leading-[40rpx] mb-[30rpx] block">
{{ description }}
</text>
<view class="flex items-center gap-[10rpx] mb-[16rpx]">
<view class="w-[5rpx] h-[30rpx] bg-[#1580FF]"></view>
<text class="text-[#1580FF] text-[32rpx]">代表人物</text>
<text class="text-[#999] text-[24rpx]">以下代表人物仅作为参考</text>
</view>
<view class="avatars-list grid grid-cols-4 gap-x-[52rpx] gap-y-[18rpx]">
<view
class="avatar-item flex flex-col items-center"
v-for="(person, index) in personList"
:key="index"
>
<image
:src="person.avatarUrl"
class="w-[120rpx] h-[120rpx] rounded-full"
mode="aspectFill"
></image>
<text class="mt-[10rpx] text-[26rpx] text-[28rpx] font-normal text-center">
{{ person.nickName }}
</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { getTagMapPerson } from '@/service/index/api'
import TitleBar from './TitleBar.vue'
const props = defineProps({
tag: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
})
const personList = ref([])
watch(
() => props.tag,
(newV) => {
getTagMapPerson({ tag: newV }).then((resp) => {
if (resp.code === 200) {
personList.value = resp.result as any[]
}
})
},
)
</script>

View File

@ -0,0 +1,53 @@
<template>
<view class="mx-[30rpx] mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar title="推荐专业" />
<text class="text-[24rpx] text-[#999]">以下专业仅作为参考</text>
<view class="w-full">
<view class="flex py-[15rpx] px-[36rpx] bg-[#F5FAFF]">
<text class="w-[129rpx] text-center font-bold">专业大类</text>
<text class="flex-1 text-center font-bold">专业类</text>
<text class="flex-1 text-center font-bold">专业名称</text>
</view>
<view
:class="`flex py-[15rpx] text-[#333] text-[24rpx] px-[36rpx] ${index % 2 === 0 ? 'bg-[#fff]' : 'bg-[#F5FAFF]'}`"
v-for="(major, index) in majorList"
:key="index"
>
<text class="w-[129rpx] text-center">{{ major.tradeName }}</text>
<text class="flex-1 text-center">{{ major.categoryName }}</text>
<text class="flex-1 text-center">{{ major.name }}</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { getTagMapPro } from '@/service/index/api'
import TitleBar from './TitleBar.vue'
const props = defineProps({
tag: {
type: String,
default: '',
},
})
const majorList = ref([])
watch(
() => props.tag,
(newV) => {
getTagMapPro({ tag: newV }).then((resp) => {
if (resp.code === 200) {
majorList.value = resp.result as any[]
}
})
},
)
</script>
<style lang="scss" scoped>
@import '@/pages-evaluation-sub/styles/parallelogram.scss';
</style>

View File

@ -0,0 +1,38 @@
<template>
<view class="mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar :title="title" />
<view v-for="(item, index) in items" :key="index" class="suggestion-item">
<view class="text-[32rpx] font-700">{{ item.title }}</view>
<view class="text-[26rpx] font-400 mt-[10rpx] text-[#666]">
{{ item.description instanceof Array ? item.description.join(',') : item.description }}
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import TitleBar from './TitleBar.vue'
defineProps({
items: {
type: Array<{ title: string; description: string | [] }>,
default: () => [],
},
title: {
type: String,
default: '',
},
})
</script>
<style lang="scss" scoped>
.suggestion-item {
&:not(:last-child) {
margin-bottom: 50rpx;
}
&:first-child {
margin-top: 30rpx;
}
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<view class="mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx]">
<TitleBar :title="title" />
<view class="text-[26rpx] text-[#666] mt-[10rpx] text-center">
<text
class="text-[22rpx] px-[12rpx] py-[4rpx] rounded-[20rpx] bg-[rgba(250,142,35,0.15)] text-[#FA8E23]"
>
{{ item.notes }}
</text>
<view class="mt-[20rpx] text-center">
<view class="text-[#000] text-[26rpx] font-700">学习风格表现</view>
<view class="mt-[10rpx]" v-for="(item, index) in item.learning_performance" :key="index">
{{ item }}
</view>
</view>
<view class="mt-[20rpx] text-center">
<view class="text-[#000] text-[26rpx] font-700">学习风格特点</view>
<view class="mt-[10rpx]" v-for="(item, index) in item.features" :key="index">
{{ item }}
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import TitleBar from './TitleBar.vue'
defineProps({
item: {
type: Object,
default: () => {},
},
title: {
type: String,
default: '',
},
})
</script>

View File

@ -0,0 +1,63 @@
<template>
<view class="relative pt-[40rpx]">
<Dashboard class="absolute right-[38rpx] top-0" :score="score" :color="rules[level].color" />
<view class="bg-[#fff] rounded-[24rpx] p-[22rpx]">
<view class="mt-[30rpx]">
<text class="text-[48rpx] text-[#000] font-600">{{ tagName }}</text>
<!-- <view class="flex items-center text-[#999] text-[24rpx] mt-[20rpx]">
<view class="i-carbon-warning w-[24rpx] h-[24rpx]"></view>
<text class="text-[#999] ml-[10rpx]">状态很好哦继续保持轻松迎接高考吧</text>
</view> -->
<view class="mt-[58rpx]">
<ScoreCard :current-position="level" :rules="rules" />
</view>
<view class="relative mt-[68rpx] bg-[#F5FAFF]">
<image
src="https://api.static.ycymedu.com/src/images/home/test-icon.png"
mode="scaleToFill"
class="w-[180rpx] h-[52rpx] absolute top-[-9rpx] left-[20rpx]"
/>
<view class="px-[20rpx] pb-[20rpx] pt-[58rpx] text-[#333] text-[26rpx]">
{{ description }}
</view>
</view>
<view class="text-[#999] text-[24rpx] mt-[10rpx]">
{{ tip }}
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import Dashboard from '@/pages-evaluation-sub/evaluate/components/psychologicalReportItem/Dashboard.vue'
import ScoreCard from './psychologicalReportItem/ScoreCard.vue'
defineProps({
score: {
type: Number,
default: 0,
},
rules: {
type: Array<{ label: string; range: string; color: string }>,
default: () => [],
},
tip: {
type: String,
default: '结果只做参考,不能准确判断是否有焦虑症。',
},
level: {
type: Number,
default: 0,
},
description: {
type: String,
default: '',
},
tagName: {
type: String,
default: '',
},
})
</script>

View File

@ -0,0 +1,46 @@
<template>
<view class="flex flex-col bg-white rounded-[24rpx] p-[30rpx] gap-[40rpx]">
<view class="flex flex-col gap-[12rpx]">
<view class="flex items-center gap-[10rpx]">
<image
src="https://api.static.ycymedu.com/src/images/home/life-icon.png"
mode="scaleToFill"
class="w-[38rpx] h-[38rpx]"
/>
<text class="text-[32rpx] text-[#000] font-700">生活建议</text>
</view>
<view class="text-[26rpx] text-[#666] font-400">
保持规律作息早睡早起避免熬夜 每天适当运动如散步跑步保持精力充沛
避免过度放松保持适度的学习节奏
</view>
</view>
<view class="flex flex-col gap-[12rpx]">
<view class="flex items-center gap-[10rpx]">
<image
src="https://api.static.ycymedu.com/src/images/home/diet-icon.png"
mode="scaleToFill"
class="w-[38rpx] h-[38rpx]"
/>
<text class="text-[32rpx] text-[#000] font-700">饮食建议</text>
</view>
<view class="text-[26rpx] text-[#666] font-400">
保持规律作息早睡早起避免熬夜 每天适当运动如散步跑步保持精力充沛
避免过度放松保持适度的学习节奏
</view>
</view>
<view class="flex flex-col gap-[12rpx]">
<view class="flex items-center gap-[10rpx]">
<image
src="https://api.static.ycymedu.com/src/images/home/learn-icon.png"
mode="scaleToFill"
class="w-[38rpx] h-[38rpx]"
/>
<text class="text-[32rpx] text-[#000] font-700">学习建议</text>
</view>
<view class="text-[26rpx] text-[#666] font-400">
保持规律作息早睡早起避免熬夜 每天适当运动如散步跑步保持精力充沛
避免过度放松保持适度的学习节奏
</view>
</view>
</view>
</template>

View File

@ -0,0 +1,25 @@
<template>
<view class="text-center mb-[24rpx]">
<view class="text-[32rpx] font-bold relative inline-flex pb-[10rpx] items-center">
<view class="i-carbon-flash-filled h-[23rpx] w-[17rpx] text-[#1880FC]"></view>
<text class="text-[#1880FC] text-[36rpx] font-bold">{{ title }}</text>
<view class="i-carbon-flash-filled h-[23rpx] w-[17rpx] text-[#1880FC]"></view>
<view
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-full h-[10rpx] bg-[#cce3fc] rounded-full title-bar"
></view>
</view>
</view>
</template>
<script lang="ts" setup>
defineProps({
title: {
type: String,
default: '',
},
})
</script>
<style lang="scss" scoped>
@import '@/pages-evaluation-sub/styles/parallelogram.scss';
</style>

View File

@ -0,0 +1,39 @@
<template>
<view
class="text-[26rpx] text-[#000] mx-[24rpx] bg-[#fff] flex flex-col px-[20rpx] gap-[10rpx] pb-[20rpx] custom-class"
>
<view class="flex gap-[10rpx] w-full">
<view class="w-[94rpx] py-[12rpx] bg-color text-center">类型</view>
<view class="py-[12rpx] text-center bg-color flex-1">性格特点</view>
</view>
<view class="flex gap-[10rpx] w-full" v-for="(item, index) in reportItems" :key="index">
<view
class="w-[94rpx] py-[12rpx] bg-color text-center flex flex-col justify-center items-center text-[#333] font-700"
>
<view>{{ item.tag }}</view>
<view>{{ item.title }}</view>
</view>
<view class="py-[22rpx] px-[14rpx] text-start bg-color flex-1">{{ item.resolving }}</view>
</view>
</view>
</template>
<script setup lang="ts">
const props = defineProps({
reportItems: {
type: Array<any>,
default: () => [],
},
})
</script>
<style lang="scss" scoped>
.bg-color {
background-color: #f5faff;
border-radius: 8rpx;
}
.custom-class {
border-radius: 0 0 20rpx 20rpx;
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<view
class="radar-chart-placeholder mx-[24rpx] bg-white chart-class h-[630rpx] flex flex-col justify-center px-[24rpx] pt-[62rpx] pb-[20rpx]"
>
<view class="w-full z-1 flex flex-col gap-[40rpx]">
<view
class="w-full flex items-center gap-[16rpx] text-[30rpx] text-[#A8A8A8]"
v-for="(item, index) in characterData"
:style="`--start-color:${colors[index].startColor};--end-color:${colors[index].endColor};--active-color:${colors[index].activeColor}`"
:key="index"
>
<view
:class="`w-110rpx ${item.leftData.value > item.rightData.value ? 'left-active' : ''}`"
>
{{ item.leftData.name }}
</view>
<view class="flex-1 h-[28rpx] bg-[#F1F2F4] rounded-[16rpx]">
<view
class="w-[60%] rounded-[16rpx] bar-color h-[28rpx]"
:style="`transform:translateX(${(item.rightData.value / (item.rightData.value + item.leftData.value)) * 100}%);`"
></view>
</view>
<view
:class="`w-110rpx ${item.leftData.value > item.rightData.value ? '' : 'right-active'}`"
>
{{ item.rightData.name }}
</view>
</view>
</view>
<view class="px-[16rpx] py-[20rpx] flex flex-col bg-[#F5FAFF] mt-[32rpx]">
<text class="text-[30rpx] text-[#000] mb-[10rpx] font-700">性格介绍</text>
<text class="text-[26rpx] text-[#333]">{{ description }}</text>
</view>
</view>
</template>
<script lang="ts" setup>
const props = defineProps({
linChart: {
type: Object,
default: () => ({
name: [],
value: [],
}),
},
description: {
type: String,
default: '',
},
})
const colors = [
{ startColor: '#97FBCD', endColor: '#00BAAD', activeColor: '#00BAAD' },
{ startColor: '#FF5940', endColor: '#FAA896', activeColor: '#FF4117' },
{ startColor: '#FFBF5E', endColor: '#F59300', activeColor: '#EB8D00' },
{ startColor: '#96E4FA', endColor: '#117CFC', activeColor: '#117CFC' },
]
const transformData = (names: string[], values: number[]) => {
const result = []
const pairMap = new Map()
const pairData = { E: 'I', S: 'N', T: 'F', J: 'P' }
//
names.forEach((name, index) => {
const match = name.match(/\((.*?)\)(\w+)/)
if (match) {
const letter = match[2]
pairMap.set(letter, { name, value: values[index] })
}
})
// pairData
Object.entries(pairData).forEach(([left, right]) => {
const leftData = pairMap.get(left)
const rightData = pairMap.get(right)
if (leftData && rightData) {
result.push({
leftData,
rightData,
})
}
})
return result
}
const characterData = ref([])
watch(
() => props.linChart,
(newV) => {
let data = transformData(newV.name, newV.value)
characterData.value = data
},
)
</script>
<style lang="scss" scoped>
.chart-class {
border-radius: 20rpx;
}
.bar-color {
background: linear-gradient(90deg, var(--start-color) 0%, var(--end-color) 100%);
}
.left-active {
color: var(--active-color);
font-weight: 700;
}
.right-active {
color: var(--active-color);
font-weight: 700;
}
</style>

View File

@ -0,0 +1,181 @@
<template>
<view
class="suitable-positions mx-[30rpx] mt-[30rpx] bg-white rounded-[20rpx] p-[30rpx] flex flex-col"
>
<TitleBar title="适合的岗位领域" />
<view class="flex justify-center items-center">
<view class="h-[146px] w-[265px] z-1">
<LEchart ref="echart" :is-disable-scroll="true"></LEchart>
</view>
</view>
<view class="flex items-center gap-[10rpx] mb-[10rpx]">
<view class="w-[5rpx] h-[30rpx] bg-[#1580FF]"></view>
<text class="text-[#1580FF] text-[32rpx] font-bold">适合职业</text>
<text class="text-[#999] text-[24rpx]">以下数据仅作为参考</text>
</view>
<text class="text-[#666] text-[28rpx]">{{ major }}</text>
<view class="flex items-center gap-[10rpx] mb-[10rpx] mt-[40rpx]">
<view class="w-[5rpx] h-[30rpx] bg-[#1580FF]"></view>
<text class="text-[#1580FF] text-[32rpx] font-bold">适合专业</text>
<text class="text-[#999] text-[24rpx]">以下数据仅作为参考</text>
</view>
<text class="text-[#666] text-[28rpx]">{{ occupation }}</text>
</view>
</template>
<script lang="ts" setup>
import LEchart from '@/pages-evaluation-sub/uni_modules/lime-echart/components/l-echart/l-echart.vue'
import TitleBar from '../TitleBar.vue'
const echarts = require('../../../uni_modules/lime-echart/static/echarts.min')
const echart = ref(null)
const props = defineProps({
mainDomain: {
type: String,
default: '',
},
major: {
type: String,
default: '',
},
occupation: {
type: String,
default: '',
},
})
//
const styleConfig = [
{ bg: '#FDF0F0', text: '#F58C8C', size: 45, x: 2, y: 38 },
{ bg: '#ECF5FF', text: '#3B9DFF', size: 77, x: 52, y: 64 },
{ bg: '#F6F4FD', text: '#7E5EFF', size: 63, x: 136, y: 48 },
{ bg: '#FFF8E5', text: '#FFC832', size: 62, x: 116, y: 104 },
{ bg: '#E6FBF0', text: '#37D480', size: 45, x: 0, y: 110 },
{ bg: '#FDF0F0', text: '#F58C8C', size: 45, x: 166, y: 118 },
]
const chartData = ref([])
//
const selectStylesByLength = (items: string[]) => {
const usedIndices = new Set<number>()
const result: Array<{ style: any; text: string }> = []
//
const processItems = (items: string[], targetSize: number) => {
items.forEach((item) => {
// 使
const availableIndices = styleConfig
.map((style, index) => ({ index, style }))
.filter(({ style, index }) => style.size === targetSize && !usedIndices.has(index))
if (availableIndices.length > 0) {
const { index, style } = availableIndices[0]
usedIndices.add(index)
result.push({ style, text: item })
}
})
}
//
const bigItems = items.filter((item) => item.length >= 4)
const mediumItems = items.filter((item) => item.length === 3)
const smallItems = items.filter((item) => item.length <= 2)
//
processItems(bigItems, 77)
processItems(mediumItems, 63)
processItems(smallItems, 45)
// 使
const remainingItems = items.filter((item) => !result.some((r) => r.text === item))
remainingItems.forEach((item) => {
// 使
for (let i = 0; i < styleConfig.length; i++) {
if (!usedIndices.has(i)) {
usedIndices.add(i)
result.push({ style: styleConfig[i], text: item })
break
}
}
})
return result
}
watch(
() => props.mainDomain,
(newV) => {
if (!newV) return
chartData.value = newV.split('、')
echart.value.init(echarts, (chart) => {
//
const selectedStyles = selectStylesByLength(chartData.value)
//
const data = selectedStyles.map(({ style, text }) => {
return {
name: text,
x: style.x,
y: style.y,
symbolSize: style.size,
itemStyle: {
color: style.bg,
},
label: {
show: true,
color: style.text,
fontSize: 14,
formatter: (params: any) => {
const text = params.name
if (text.length >= 5) {
const lines = []
for (let i = 0; i < text.length; i += 3) {
lines.push(text.slice(i, i + 3))
}
return lines.join('\n\n')
}
return text
},
rich: {
lineHeight: 20,
},
align: 'center',
verticalAlign: 'middle',
position: 'inside',
},
}
})
let option = {
tooltip: {},
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
series: [
{
type: 'graph',
layout: 'none',
roam: true,
data,
links: [],
lineStyle: {
opacity: 0.9,
width: 2,
curveness: 0,
},
},
],
}
chart.setOption(option)
})
},
)
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,108 @@
<template>
<view class="mx-[24rpx] bg-white chart-class h-[500rpx] flex items-center justify-center">
<view class="h-[415rpx] w-full z-1">
<LEchart ref="echart"></LEchart>
</view>
</view>
</template>
<script lang="ts" setup>
import LEchart from '@/pages-evaluation-sub/uni_modules/lime-echart/components/l-echart/l-echart.vue'
const echarts = require('../../../uni_modules/lime-echart/static/echarts.min')
const echart = ref(null)
const props = defineProps({
picData: {
type: Object,
default: () => ({
indicator: [],
radars: [],
}),
},
})
//
const nameMap: Record<string, string> = {
A: '艺术型(A)',
R: '现实型(R)',
I: '研究型(I)',
S: '社会型(S)',
E: '企业型(E)',
C: '常规型(C)',
}
// indicator
const formatIndicator = computed(() => {
return props.picData.indicator.map((item) => ({
name: nameMap[item.name] || item.name,
max: item.max,
}))
})
watch(
() => props.picData,
(newV) => {
if (newV.radars.length > 0) {
echart.value.init(echarts, (chart) => {
let option = {
radar: {
center: ['50%', '50%'],
radius: '70%',
indicator: formatIndicator.value,
splitArea: {
show: false,
},
axisLine: {
lineStyle: {
color: '#e5e6eb',
},
},
splitLine: {
lineStyle: {
color: '#e5e6eb',
},
},
axisName: {
color: '#333',
},
},
series: [
{
name: '职业兴趣评测',
type: 'radar',
data: [
{
value: props.picData.radars,
name: '测评结果',
itemStyle: {
color: '#1580FF',
},
areaStyle: {
color: 'rgba(21,128,255,0.2)',
},
lineStyle: {
width: 2,
},
},
],
},
],
}
chart.setOption(option)
})
}
},
)
onBeforeMount(() => {
if (echart.value) {
echart.value.dispose()
}
})
</script>
<style lang="scss" scoped>
.chart-class {
border-radius: 20rpx 20rpx 0 0;
}
</style>

View File

@ -0,0 +1,183 @@
<template>
<view class="bg-white mx-[24rpx] rounded-[20rpx] pb-[20rpx]">
<view class="px-[24rpx] h-[368rpx] z-1">
<LEchart ref="echart" :customStyle="`z-index:1;`"></LEchart>
</view>
<view class="bg-[#F5FAFF] px-[16rpx] pt-[20rpx] mx-[34rpx] pb-[26rpx]">
<view class="text-[#000] text-[30rpx] mb-[10rpx] font-700">能力评测建议</view>
<view class="text-[26rpx] text-[#333]">{{ description }}</view>
</view>
</view>
</template>
<script lang="ts" setup>
import LEchart from '@/pages-evaluation-sub/uni_modules/lime-echart/components/l-echart/l-echart.vue'
const echarts = require('../../../uni_modules/lime-echart/static/echarts.min')
const echart = ref(null)
const props = defineProps({
echartData: {
type: Array,
default: () => [
{
name: [],
value: [],
},
{
name: [],
value: [],
},
],
},
description: {
type: String,
default: '',
},
})
onMounted(() => {
updateChart()
})
watch(
() => props.echartData,
(newVal) => {
if (newVal && newVal.length > 0) {
updateChart()
}
},
{ deep: true },
)
const updateChart = () => {
echart.value.init(echarts, (chart) => {
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
formatter: function (params) {
let result = params[0].name.split('\n').join('') + '\n'
params.forEach((param) => {
result += `${param.marker} ${param.seriesName}: ${param.value}\n`
})
return result
},
textStyle: {
fontSize: 12,
},
position: function (point, params, dom, rect, size) {
const tooltipWidth = dom._rect.width
const canvasWidth = size.viewSize[0]
if (point[0] + tooltipWidth > canvasWidth) {
return [point[0] - tooltipWidth, '10%']
}
return [point[0], '10%']
},
},
grid: {
top: 40,
left: 10,
right: 20,
bottom: 10,
containLabel: true,
},
legend: {
data: ['我的数据', '平均水平'],
right: 'auto',
left: 'center',
top: 0,
itemWidth: 15,
itemHeight: 10,
textStyle: {
fontSize: 12,
},
},
xAxis: [
{
type: 'category',
data: (props.echartData[1] as { name: string[] })?.name.map((item) =>
item.replace('智能', '').replace(/(.{2})/g, '$1\n'),
),
axisLine: {
lineStyle: {
color: '#E0E0E0',
},
},
axisLabel: {
color: '#666',
fontSize: 12,
interval: 0,
},
axisTick: {
show: false,
},
},
],
yAxis: [
{
type: 'value',
name: '分数',
min: 0,
max: 50,
interval: 10,
axisLine: {
show: true,
lineStyle: {
color: '#E0E0E0',
},
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: '#E8E8E8',
type: 'solid',
},
},
axisLabel: {
color: '#999',
},
},
],
series: [
{
name: '我的数据',
type: 'bar',
barWidth: '20',
itemStyle: {
color: '#1580FF',
borderRadius: [4, 4, 0, 0],
},
data: (props.echartData[0] as { value: number[] })?.value,
},
{
name: '平均水平',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
color: '#F9AA5B',
width: 2,
},
itemStyle: {
color: '#F9AA5B',
borderWidth: 2,
borderColor: '#fff',
},
data: (props.echartData[1] as { value: number[] })?.value,
},
],
}
chart.setOption(option)
})
}
</script>
<style scoped></style>

View File

@ -0,0 +1,162 @@
<template>
<view class="bg-white mx-[24rpx] rounded-[20rpx] pb-[20rpx]">
<view class="px-[24rpx] h-[368rpx] z-1">
<LEchart ref="echart" :customStyle="`z-index:1;`"></LEchart>
</view>
</view>
</template>
<script lang="ts" setup>
import LEchart from '@/pages-evaluation-sub/uni_modules/lime-echart/components/l-echart/l-echart.vue'
const echarts = require('../../../uni_modules/lime-echart/static/echarts.min')
const echart = ref(null)
interface IndicatorItem {
name: string
max: number
}
interface PicChartsData {
indicator: IndicatorItem[]
radars: number[]
}
const props = defineProps({
picCharts: {
type: Object as () => PicChartsData,
default: () => ({
indicator: [],
radars: [],
}),
},
description: {
type: String,
default: '',
},
})
onMounted(() => {
updateChart()
})
watch(
() => props.picCharts,
(newVal) => {
if (newVal && newVal.indicator && newVal.indicator.length > 0) {
updateChart()
}
},
{ deep: true },
)
const updateChart = () => {
echart.value.init(echarts, (chart) => {
//
const categories = props.picCharts.indicator.map((item) => {
const match = item.name.match(/(.*?)\([^)]*\)/)
return match ? match[1].trim() : item.name
})
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
grid: {
top: 60,
left: 0,
right: 0,
bottom: 0,
containLabel: true,
},
xAxis: {
type: 'category',
data: categories,
axisLine: {
lineStyle: {
color: '#E0E0E0',
},
},
axisLabel: {
color: '#666',
fontSize: 12,
interval: 0,
rotate: 45,
formatter: function (value) {
//
if (value.length > 4) {
return value.substring(0, 4) + '...'
}
return value
},
},
axisTick: {
show: false,
},
},
yAxis: {
type: 'value',
name: '分数',
min: 0,
max: 30,
interval: 5,
axisLine: {
show: true,
lineStyle: {
color: '#E0E0E0',
},
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: '#E8E8E8',
type: 'solid',
},
},
axisLabel: {
color: '#999',
},
},
series: [
{
name: '得分',
type: 'bar',
barWidth: '20',
itemStyle: {
color: function (params) {
//
const colorList = [
'#1580FF',
'#F9AA5B',
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
]
return colorList[params.dataIndex % colorList.length]
},
borderRadius: [4, 4, 0, 0],
},
label: {
show: true,
position: 'top',
color: '#666',
fontSize: 12,
},
data: props.picCharts.radars,
},
],
}
chart.setOption(option)
})
}
</script>
<style scoped></style>

View File

@ -0,0 +1,58 @@
<template>
<view
class="rounded-full w-[162rpx] h-[162rpx] bg-[#fff] dashboard-wrapper flex items-center justify-center"
:style="{ '--current-color': color }"
>
<view class="w-[139rpx] h-[139rpx] custom-style flex items-center justify-center">
<view
v-for="i in 39"
:key="i"
class="tick"
:style="{
transform: `rotate(${i * 8 - 90}deg)`,
opacity: `${1 - Math.abs(i * 8 - 90) / 90}`,
}"
></view>
<view class="font-500 text-color w-full h-full flex items-center justify-center">
<view :class="`${score > 99 ? 'text-[50rpx]' : 'text-[62rpx]'}`">{{ score }}</view>
<view class="text-[28rpx] mt-[8rpx]"></view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
defineProps({
color: {
type: String,
default: '#00b281',
},
score: {
type: Number,
default: 0,
},
})
</script>
<style lang="scss" scoped>
.custom-style {
border-radius: 50%;
position: relative;
}
.text-color {
color: var(--current-color);
container-type: inline-size;
white-space: nowrap;
}
.tick {
position: absolute;
top: 0;
left: 50%;
width: 4rpx;
height: 10rpx;
background-color: var(--current-color);
transform-origin: 0 67rpx;
}
</style>

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