feat: init
commit
a8ee21b558
|
|
@ -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: 自定义',
|
||||
},
|
||||
}
|
||||
|
|
@ -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 # 关闭末尾空格修剪
|
||||
|
|
@ -0,0 +1 @@
|
|||
src/uni_modules/
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Run the commit-msg hook
|
||||
npx --no-install commitlint --edit
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Run the pre-commit hook
|
||||
npx --no-install -- lint-staged
|
||||
|
|
@ -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
|
||||
|
|
@ -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/**
|
||||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
src/uni_modules/
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
// 默认格式化工具选择prettier
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
// 保存的时候自动格式化
|
||||
"editor.formatOnSave": true,
|
||||
//开启自动修复
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
// 配置stylelint检查的文件类型范围
|
||||
"stylelint.validate": ["css", "scss", "vue", "html"], // 与package.json的scripts对应
|
||||
"stylelint.enable": true,
|
||||
"css.validate": false,
|
||||
"less.validate": false,
|
||||
"scss.validate": false,
|
||||
"[shellscript]": {
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[dotenv]": {
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
// 配置语言的文件关联
|
||||
"files.associations": {
|
||||
"pages.json": "jsonc", // pages.json 可以写注释
|
||||
"manifest.json": "jsonc" // manifest.json 可以写注释
|
||||
},
|
||||
"cSpell.words": [
|
||||
"Aplipay",
|
||||
"climblee",
|
||||
"commitlint",
|
||||
"dcloudio",
|
||||
"iconfont",
|
||||
"qrcode",
|
||||
"refresherrefresh",
|
||||
"scrolltolower",
|
||||
"tabbar",
|
||||
"Toutiao",
|
||||
"unibest",
|
||||
"uvui",
|
||||
"Wechat",
|
||||
"WechatMiniprogram",
|
||||
"Weixin"
|
||||
],
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
|
||||
".eslintrc.cjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,.stylelintrc.*,.eslintrc-auto-import.json,.editorconfig,.commitlint.cjs"
|
||||
},
|
||||
"vetur.validation.template": false,
|
||||
"vetur.validation.script": false,
|
||||
"vetur.validation.style": false,
|
||||
"vetur.experimental.templateInterpolationService": true
|
||||
}
|
||||
|
|
@ -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"],
|
||||
},
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# 六维生涯
|
||||
|
||||
六维生涯小程序
|
||||
|
||||
## ⚙️ 环境
|
||||
|
||||
- node>=18
|
||||
- pnpm>=7.30
|
||||
- Vue Official>=2.1.10
|
||||
- TypeScript>=5.0
|
||||
|
||||
## 📂 快速开始
|
||||
|
||||
执行 `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云打包。
|
||||
|
|
@ -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'
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'development'
|
||||
# 是否去除console 和 debugger
|
||||
VITE_DELETE_CONSOLE = false
|
||||
# 是否开启sourcemap
|
||||
VITE_SHOW_SOURCEMAP = true
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'development'
|
||||
# 是否去除console 和 debugger
|
||||
VITE_DELETE_CONSOLE = true
|
||||
# 是否开启sourcemap
|
||||
VITE_SHOW_SOURCEMAP = false
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV = 'development'
|
||||
# 是否去除console 和 debugger
|
||||
VITE_DELETE_CONSOLE = false
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -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>
|
||||
|
|
@ -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',
|
||||
})
|
||||
|
|
@ -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[]
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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": {}
|
||||
}
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
|
|
@ -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}`)
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}×tamp=${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,0 +1,165 @@
|
|||
<template>
|
||||
<!-- TabBar占位块 - 与TabBar高度一致 -->
|
||||
<view
|
||||
v-if="showPlaceholder"
|
||||
class="tabbar-placeholder"
|
||||
:style="{ height: `${tabbarTotalHeight}px` }"
|
||||
></view>
|
||||
|
||||
<!-- TabBar组件 -->
|
||||
<view class="custom-tabbar pb-safe">
|
||||
<view class="tabbar-content">
|
||||
<view
|
||||
class="tabbar-item"
|
||||
v-for="item in tabbarList"
|
||||
:key="item.id"
|
||||
:class="[item.centerItem ? 'center-item' : '']"
|
||||
@click="changeItem(item)"
|
||||
>
|
||||
<view class="item-top">
|
||||
<image
|
||||
class="w-full h-full object-contain item-icon"
|
||||
:src="currentPage == item.id ? item.selectIcon : item.icon"
|
||||
></image>
|
||||
</view>
|
||||
<view class="item-bottom" :class="[currentPage == item.id ? 'item-active' : '']">
|
||||
<text>{{ item.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TabesItem } from '@/service/app/types'
|
||||
import { tabbarList } from '@/hooks/useTabbarList'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
defineProps({
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 是否显示占位块
|
||||
showPlaceholder: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 安全区域底部高度
|
||||
const safeAreaBottom = ref(0)
|
||||
|
||||
// 计算TabBar总高度 (TabBar高度 + 安全区域高度)
|
||||
const tabbarTotalHeight = computed(() => {
|
||||
// 100rpx转为px,不同设备可能有差异
|
||||
const tabbarHeight = uni.upx2px(100)
|
||||
return tabbarHeight + safeAreaBottom.value
|
||||
})
|
||||
|
||||
const changeItem = (item: TabesItem) => {
|
||||
if (item.navigatorItem) {
|
||||
uni.navigateTo({
|
||||
url: item.path,
|
||||
})
|
||||
} else {
|
||||
uni.switchTab({
|
||||
url: item.path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
uni.hideTabBar()
|
||||
|
||||
// 获取系统信息以计算安全区域
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
if (res.safeAreaInsets) {
|
||||
safeAreaBottom.value = res.safeAreaInsets.bottom || 0
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// 暴露高度信息给父组件
|
||||
defineExpose({
|
||||
tabbarTotalHeight,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-tabbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.tabbar-content {
|
||||
display: flex;
|
||||
height: 100rpx;
|
||||
padding: 0 20rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tabbar-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
.item-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.item-bottom {
|
||||
font-size: 20rpx;
|
||||
margin-top: 6rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.item-active {
|
||||
color: #3370ff;
|
||||
}
|
||||
|
||||
.center-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.center-item .item-icon {
|
||||
width: 98rpx;
|
||||
height: 98rpx;
|
||||
}
|
||||
|
||||
.center-item .item-bottom {
|
||||
position: absolute;
|
||||
bottom: 5rpx;
|
||||
}
|
||||
|
||||
/* 占位块样式 */
|
||||
.tabbar-placeholder {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
.pb-safe {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
<template>
|
||||
<view class="navbar">
|
||||
<!-- 状态栏占位 -->
|
||||
<view
|
||||
v-if="safeAreaInsetTop"
|
||||
class="status-bar"
|
||||
:style="{ height: statusBarHeight + 'px', backgroundColor: bgColor }"
|
||||
></view>
|
||||
|
||||
<!-- 导航栏主体 -->
|
||||
<view
|
||||
class="navbar-content"
|
||||
:class="[contentClass, fixed ? 'navbar-fixed' : '', bordered ? 'navbar-border' : '']"
|
||||
:style="{
|
||||
backgroundColor: bgColor,
|
||||
height: navHeight + 'px',
|
||||
top: fixed ? (safeAreaInsetTop ? statusBarHeight : 0) + 'px' : '0',
|
||||
}"
|
||||
>
|
||||
<!-- 左侧区域 -->
|
||||
<view class="navbar-left" @click="handleClickLeft">
|
||||
<view v-if="leftArrow" class="back-icon">
|
||||
<view class="i-carbon-chevron-left text-[40rpx] text-[#333] font-semibold" />
|
||||
</view>
|
||||
<slot name="left"></slot>
|
||||
</view>
|
||||
|
||||
<!-- 中间标题区域 -->
|
||||
<view class="navbar-title">
|
||||
<slot name="title">
|
||||
<text class="title-text">{{ title }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<view class="navbar-right">
|
||||
<slot name="right"></slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 占位元素 -->
|
||||
<view
|
||||
v-if="placeholder && fixed"
|
||||
:style="{
|
||||
height: `${navHeight}px`,
|
||||
backgroundColor: bgColor,
|
||||
}"
|
||||
></view>
|
||||
<slot name="background"></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
leftArrow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
safeAreaInsetTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#ffffff',
|
||||
},
|
||||
contentClass: {
|
||||
type: String,
|
||||
default: 'justify-between',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['clickLeft'])
|
||||
|
||||
// 获取系统信息
|
||||
const systemInfo = uni.getWindowInfo()
|
||||
const deviceInfo = uni.getDeviceInfo()
|
||||
|
||||
const statusBarHeight = systemInfo.statusBarHeight || 0
|
||||
|
||||
// 动态计算导航栏高度
|
||||
const navHeight = computed(() => {
|
||||
// 获取屏幕信息
|
||||
const { screenWidth } = systemInfo
|
||||
const { platform } = deviceInfo
|
||||
|
||||
// 将px转换为rpx的比例
|
||||
const ratio = 750 / screenWidth
|
||||
|
||||
// 根据平台设置不同的导航栏高度
|
||||
if (platform === 'ios') {
|
||||
return 88 / ratio // iOS 一般是 44pt,转换为px
|
||||
} else if (platform === 'android') {
|
||||
return 96 / ratio // Android 一般是 48dp,转换为px
|
||||
} else {
|
||||
return 88 / ratio // 默认值
|
||||
}
|
||||
})
|
||||
|
||||
const handleClickLeft = () => {
|
||||
emit('clickLeft')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.navbar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
width: 100%;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* justify-content: space-between; */
|
||||
padding: 0 16rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.navbar-fixed {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.navbar-border {
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
/* flex: 1; */
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 34rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 52rpx;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAKYAAsAAAAABlAAAAJMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACCcApcdgE2AiQDCAsGAAQgBYRnBzYbmQXIHpIkBQQKkYCABBEPz2/t/XN3twEbowBkQTxQEQ1RKaKSxEOi0agkJKF5Qvj/f037IFKwlZ2dWU2tJu0EhPwHkBwgOVAclKcvAQpI/v/fz/08XECy+YBymmPQiwIcSmhAY4uSFcgJ+IaxC1zCYwLtRjWSnZ2rGgQWBowLxCPrVBBYllQqNTQ0VISaBXEHtTRNUwW4jb4f/xYEC0kqMzDx6CGrQuKXxKc6Zf7POYQgQHs5kIwjYwEoxK3G/DpRwbi0dlNwKKjAL4lf6vw/R2zVWvTPIwuiCnp2wCRUZ3yJX5pJFVDfByyAFR2AblMAX/OR3t7+zOJi8GyyfzC1uQXLZvtnk/0zyfTy+PvH0/Xp5OzR98/H797/+/fDu3d/3739+/fd+/+nmxvLc5vrS+sry2vz84tLs9Mzc4vzs9NTM/Ozc1OzM3MzU/Mz0wvTU4vTk0tTE8uTEyuT4yv/G0E3XUxv7wwNbu/s9G8fbO9v7+3sb+3ubW4dbO4dbO3vbu4dbO3JzqPFtRE4gEGAX0NBkL+hpCZALkEp5FKUQqE0NHlXJIGrDNAOcEQBCHU+kXT5QNblC7kEv1EK9Y9SB/8o7YYu2m0YXrJLouNIjQJhH+QbVkVZrUQ+YuqzUJdzxPMHhdIj0+hg4o0D8ogj5r5bSoQUxjADz+A8hBDQFEYwh3mommXTul7Vm5ZtqAqJHIdoKCDYDyQ3mCqUG1YKn5+C0s0yiJ/qKVAQedKAhg6Y3mEHJBQaWKnvLVMiiEIxGAY8Aw6HIAhAJmEIzIIOUjLTTAB1taL1QvNq+fYN7QDjcc2okeioaOmy5LFXt3QAAAAA')
|
||||
format('woff2');
|
||||
}
|
||||
|
||||
.back-text {
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 48rpx;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
<template>
|
||||
<view class="tab-container">
|
||||
<!-- tab标题栏 -->
|
||||
<scroll-view
|
||||
scroll-x
|
||||
class="tab-scroll-view"
|
||||
:scroll-left="scrollLeft"
|
||||
scroll-with-animation
|
||||
show-scrollbar="false"
|
||||
:id="tabScrollId"
|
||||
>
|
||||
<view class="tab-items-container">
|
||||
<view
|
||||
v-for="(item, index) in tabs"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: currentIndex === index }"
|
||||
@click="handleTabClick(index)"
|
||||
:id="`tab-item-${index}`"
|
||||
>
|
||||
<text class="tab-text">{{ item.title }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 独立的滑块元素 -->
|
||||
<view
|
||||
class="tab-line"
|
||||
:style="{
|
||||
transform: `translateX(${lineLeft}px)`,
|
||||
width: `${lineWidth}rpx`,
|
||||
backgroundColor: props.themeColor,
|
||||
}"
|
||||
></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted } from 'vue'
|
||||
|
||||
// 唯一ID,防止多个Tab组件冲突
|
||||
const tabScrollId = `tab-scroll-${Date.now()}`
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
// tabs数据,格式:[{title: '标签1'}, {title: '标签2'}]
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 默认选中的索引
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 主题色
|
||||
themeColor: {
|
||||
type: String,
|
||||
default: '#3C9CFD',
|
||||
},
|
||||
// 滑块宽度
|
||||
lineWidth: {
|
||||
type: [Number, String],
|
||||
default: 48,
|
||||
},
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
// 当前激活的索引
|
||||
const currentIndex = ref(props.modelValue)
|
||||
|
||||
// 滚动位置
|
||||
const scrollLeft = ref(0)
|
||||
|
||||
// 滑块位置
|
||||
const lineLeft = ref(0)
|
||||
|
||||
// 监听props变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (currentIndex.value !== newVal) {
|
||||
currentIndex.value = newVal
|
||||
updateTabPosition()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听currentIndex变化
|
||||
watch(
|
||||
() => currentIndex.value,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
updateTabPosition()
|
||||
// 向父组件同步更新
|
||||
emit('update:modelValue', newVal)
|
||||
emit('change', {
|
||||
index: newVal,
|
||||
item: props.tabs[newVal] || {},
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听tabs变化,重新计算滑块位置
|
||||
watch(
|
||||
() => props.tabs,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
updateTabPosition()
|
||||
})
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 组件挂载后初始化
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
updateTabPosition()
|
||||
})
|
||||
})
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
// 更新标签位置和滑块位置 - 优化版
|
||||
const updateTabPosition = () => {
|
||||
nextTick(() => {
|
||||
// 创建查询对象
|
||||
const query = uni.createSelectorQuery().in(instance)
|
||||
|
||||
// 获取滚动视图和当前选中标签的信息
|
||||
query.select(`#${tabScrollId}`).boundingClientRect()
|
||||
query.select(`#tab-item-${currentIndex.value}`).boundingClientRect()
|
||||
|
||||
query.exec((res) => {
|
||||
if (res && res[0] && res[1]) {
|
||||
const scrollView = res[0]
|
||||
const currentTab = res[1]
|
||||
|
||||
// 1. 计算滑块位置 - 直接使用当前标签的中心位置
|
||||
const tabCenter = currentTab.left + currentTab.width / 2 - scrollView.left
|
||||
|
||||
// 2. 计算滑块左侧应该在的位置(居中)
|
||||
const lineWidthPx = uni.upx2px(Number(props.lineWidth))
|
||||
lineLeft.value = tabCenter - lineWidthPx / 2
|
||||
|
||||
// 3. 计算滚动位置,使选中的标签居中显示在滚动视图中
|
||||
const offsetLeft = currentTab.left - scrollView.left
|
||||
scrollLeft.value = offsetLeft - scrollView.width / 2 + currentTab.width / 2
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 点击标签切换
|
||||
const handleTabClick = (index) => {
|
||||
if (currentIndex.value !== index) {
|
||||
currentIndex.value = index
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-scroll-view {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-items-container {
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 32rpx;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: v-bind('props.themeColor');
|
||||
}
|
||||
|
||||
.tab-line {
|
||||
position: absolute;
|
||||
height: 6rpx;
|
||||
border-radius: 6rpx;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
/* 平滑过渡效果 */
|
||||
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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,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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { routeInterceptor } from './route'
|
||||
export { requestInterceptor } from './request'
|
||||
export { prototypeInterceptor } from './prototype'
|
||||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
<template>
|
||||
<view class="navbar">
|
||||
<!-- 状态栏占位 -->
|
||||
<view
|
||||
v-if="safeAreaInsetTop"
|
||||
class="status-bar"
|
||||
:style="{ height: statusBarHeight + 'px', backgroundColor: bgColor }"
|
||||
></view>
|
||||
|
||||
<!-- 导航栏主体 -->
|
||||
<view
|
||||
class="navbar-content"
|
||||
:class="[contentClass, fixed ? 'navbar-fixed' : '', bordered ? 'navbar-border' : '']"
|
||||
:style="{
|
||||
backgroundColor: bgColor,
|
||||
height: navHeight + 'px',
|
||||
top: fixed ? (safeAreaInsetTop ? statusBarHeight : 0) + 'px' : '0',
|
||||
}"
|
||||
>
|
||||
<!-- 左侧区域 -->
|
||||
<view class="navbar-left" @click="handleClickLeft">
|
||||
<view v-if="leftArrow" class="back-icon">
|
||||
<view class="i-carbon-chevron-left text-[40rpx] text-[#333] font-semibold" />
|
||||
</view>
|
||||
<slot name="left"></slot>
|
||||
</view>
|
||||
|
||||
<!-- 中间标题区域 -->
|
||||
<view class="navbar-title">
|
||||
<slot name="title">
|
||||
<text class="title-text">{{ title }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<view class="navbar-right">
|
||||
<slot name="right"></slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 占位元素 -->
|
||||
<view
|
||||
v-if="placeholder && fixed"
|
||||
:style="{
|
||||
height: `${navHeight}px`,
|
||||
backgroundColor: bgColor,
|
||||
}"
|
||||
></view>
|
||||
<slot name="background"></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
leftArrow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
safeAreaInsetTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#ffffff',
|
||||
},
|
||||
contentClass: {
|
||||
type: String,
|
||||
default: 'justify-between',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['clickLeft'])
|
||||
|
||||
// 获取系统信息
|
||||
const systemInfo = uni.getWindowInfo()
|
||||
const deviceInfo = uni.getDeviceInfo()
|
||||
|
||||
const statusBarHeight = systemInfo.statusBarHeight || 0
|
||||
|
||||
// 动态计算导航栏高度
|
||||
const navHeight = computed(() => {
|
||||
// 获取屏幕信息
|
||||
const { screenWidth } = systemInfo
|
||||
const { platform } = deviceInfo
|
||||
|
||||
// 将px转换为rpx的比例
|
||||
const ratio = 750 / screenWidth
|
||||
|
||||
// 根据平台设置不同的导航栏高度
|
||||
if (platform === 'ios') {
|
||||
return 88 / ratio // iOS 一般是 44pt,转换为px
|
||||
} else if (platform === 'android') {
|
||||
return 96 / ratio // Android 一般是 48dp,转换为px
|
||||
} else {
|
||||
return 88 / ratio // 默认值
|
||||
}
|
||||
})
|
||||
|
||||
const handleClickLeft = () => {
|
||||
emit('clickLeft')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.navbar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
width: 100%;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* justify-content: space-between; */
|
||||
padding: 0 16rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.navbar-fixed {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.navbar-border {
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
/* flex: 1; */
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 34rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 52rpx;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAKYAAsAAAAABlAAAAJMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACCcApcdgE2AiQDCAsGAAQgBYRnBzYbmQXIHpIkBQQKkYCABBEPz2/t/XN3twEbowBkQTxQEQ1RKaKSxEOi0agkJKF5Qvj/f037IFKwlZ2dWU2tJu0EhPwHkBwgOVAclKcvAQpI/v/fz/08XECy+YBymmPQiwIcSmhAY4uSFcgJ+IaxC1zCYwLtRjWSnZ2rGgQWBowLxCPrVBBYllQqNTQ0VISaBXEHtTRNUwW4jb4f/xYEC0kqMzDx6CGrQuKXxKc6Zf7POYQgQHs5kIwjYwEoxK3G/DpRwbi0dlNwKKjAL4lf6vw/R2zVWvTPIwuiCnp2wCRUZ3yJX5pJFVDfByyAFR2AblMAX/OR3t7+zOJi8GyyfzC1uQXLZvtnk/0zyfTy+PvH0/Xp5OzR98/H797/+/fDu3d/3739+/fd+/+nmxvLc5vrS+sry2vz84tLs9Mzc4vzs9NTM/Ozc1OzM3MzU/Mz0wvTU4vTk0tTE8uTEyuT4yv/G0E3XUxv7wwNbu/s9G8fbO9v7+3sb+3ubW4dbO4dbO3vbu4dbO3JzqPFtRE4gEGAX0NBkL+hpCZALkEp5FKUQqE0NHlXJIGrDNAOcEQBCHU+kXT5QNblC7kEv1EK9Y9SB/8o7YYu2m0YXrJLouNIjQJhH+QbVkVZrUQ+YuqzUJdzxPMHhdIj0+hg4o0D8ogj5r5bSoQUxjADz+A8hBDQFEYwh3mommXTul7Vm5ZtqAqJHIdoKCDYDyQ3mCqUG1YKn5+C0s0yiJ/qKVAQedKAhg6Y3mEHJBQaWKnvLVMiiEIxGAY8Aw6HIAhAJmEIzIIOUjLTTAB1taL1QvNq+fYN7QDjcc2okeioaOmy5LFXt3QAAAAA')
|
||||
format('woff2');
|
||||
}
|
||||
|
||||
.back-text {
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 48rpx;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
||||
// 将px转换为rpx的比例
|
||||
const ratio = 750 / screenWidth
|
||||
|
||||
// 根据平台设置不同的导航栏高度
|
||||
if (platform === 'ios') {
|
||||
return 88 / ratio // iOS 一般是 44pt,转换为px
|
||||
} else if (platform === 'android') {
|
||||
return 96 / ratio // Android 一般是 48dp,转换为px
|
||||
} else {
|
||||
return 88 / ratio // 默认值
|
||||
}
|
||||
})
|
||||
|
||||
const handleClickLeft = () => {
|
||||
emit('clickLeft')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.navbar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
width: 100%;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* justify-content: space-between; */
|
||||
padding: 0 16rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.navbar-fixed {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
Loading…
Reference in New Issue