初始化
This commit is contained in:
commit
dbf485e0d5
3
.env.development.example
Normal file
3
.env.development.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
# 请求域名
|
||||||
|
VITE_APP_BASE_URL=''
|
||||||
3
.env.production.example
Normal file
3
.env.production.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
# 请求域名
|
||||||
|
VITE_APP_BASE_URL=''
|
||||||
39
.eslintrc.js
Normal file
39
.eslintrc.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
ignorePatterns: ['src/uni_modules/'],
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/eslint-config-typescript/recommended',
|
||||||
|
'@vue/eslint-config-prettier'
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
semi: false,
|
||||||
|
singleQuote: true,
|
||||||
|
printWidth: 100,
|
||||||
|
proseWrap: 'preserve',
|
||||||
|
bracketSameLine: false,
|
||||||
|
endOfLine: 'lf',
|
||||||
|
tabWidth: 4,
|
||||||
|
useTabs: false,
|
||||||
|
trailingComma: 'none'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'no-undef': 'off',
|
||||||
|
'vue/prefer-import-from-vue': 'off',
|
||||||
|
'no-prototype-builtins': 'off',
|
||||||
|
'prefer-spread': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off'
|
||||||
|
},
|
||||||
|
globals: {}
|
||||||
|
}
|
||||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.hbuilderx
|
||||||
|
# .env
|
||||||
|
.env.development
|
||||||
|
.env.production
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
|
}
|
||||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
"css.validate": false,
|
||||||
|
"less.validate": false,
|
||||||
|
"scss.validate": false
|
||||||
|
}
|
||||||
21
index.html
Normal file
21
index.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
initialize.js
Normal file
1
initialize.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
const fs = require('fs')
const { spawn } = require('child_process')
class InitializeItem {
static instance = null
constructor() {
if (InitializeItem.instance) {
return InitializeItem.instance
}
InitializeItem.instance = this
}
async promptUser(question) {
return new Promise((resolve, reject) => {
const readline = require('readline')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question(question, (res) => {
resolve(res)
rl.close()
})
})
}
async shouldInstallDependencies() {
const isInstall = await this.promptUser(
'是否需要自动帮您安装依赖(y/n):'
)
if (isInstall.toLowerCase() === 'y') {
return true
} else if (isInstall.toLowerCase() === 'n') {
return false
} else {
return this.shouldInstallDependencies()
}
}
async installDependencies() {
return new Promise((resolve, reject) => {
console.log('开始安装相关依赖...')
const command = process.platform === 'win32' ? 'cmd.exe' : 'npm'
const args =
process.platform === 'win32'
? ['/c', 'npm', 'install']
: ['install']
const installProcess = spawn(command, args)
installProcess.stdout.on('data', (data) => {
console.log(data.toString())
})
installProcess.stderr.on('data', (data) => {
console.error(data.toString())
})
installProcess.on('close', (code) => {
if (code !== 0) {
reject(
new Error(
`运行安装依赖命令错误,请查看以下报错信息寻找解决方法: ${error.message}`
)
)
} else {
console.log('安装依赖成功!')
resolve()
}
})
})
}
async copyFile(sourceDir, targetDir) {
return new Promise((resolve, reject) => {
fs.copyFile(sourceDir, targetDir, (error) => {
if (error) {
reject(error)
throw new Error(`复制文件失败: ${error.message}`)
}
resolve()
})
})
}
async writeToFile(filePath, { sourceData, targetData }) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('读取文件失败:', err)
return
}
const modifiedData = data.replace(sourceData, targetData)
fs.writeFile(filePath, modifiedData, 'utf8', (err) => {
if (err) {
console.error('写入文件错误:', err)
return
}
resolve()
})
})
})
}
async initialize(targetVersion) {
const currentVersion = process.versions.node
if (currentVersion < targetVersion) {
throw new Error(
`你的当前node版本为(${currentVersion}),需要安装目标版本为 ${targetVersion} 以上!!`
)
}
const shouldInstall = await this.shouldInstallDependencies()
if (shouldInstall) {
await this.installDependencies()
}
await this.copyFile('.env.development.example', '.env.development')
await this.copyFile('.env.production.example', '.env.production')
const domain = await this.promptUser('请输入您的服务器域名地址:')
await this.writeToFile('.env.development', {
sourceData: `VITE_APP_BASE_URL=''`,
targetData: `VITE_APP_BASE_URL='${domain}'`
})
await this.writeToFile('.env.production', {
sourceData: `VITE_APP_BASE_URL=''`,
targetData: `VITE_APP_BASE_URL='${domain}'`
})
require('./scripts/develop');
}
static getInstance() {
if (!InitializeItem.instance) {
InitializeItem.instance = new InitializeItem()
}
return InitializeItem.instance
}
}
;(async () => {
const initializeItem = InitializeItem.getInstance()
try {
await initializeItem.initialize('16.16.0')
} catch (error) {
console.error(error.message)
}
})()
|
||||||
BIN
my-release-key.keystore
Normal file
BIN
my-release-key.keystore
Normal file
Binary file not shown.
108
package.json
Normal file
108
package.json
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"name": "uni-preset-vue",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"init": "node initialize.js",
|
||||||
|
"dev": "node scripts/develop.js",
|
||||||
|
"dev:app": "uni -p app",
|
||||||
|
"dev:custom": "uni -p",
|
||||||
|
"dev:h5": "uni",
|
||||||
|
"dev:h5:ssr": "uni --ssr",
|
||||||
|
"dev:mp-alipay": "uni -p mp-alipay",
|
||||||
|
"dev:mp-baidu": "uni -p mp-baidu",
|
||||||
|
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||||
|
"dev:mp-lark": "uni -p mp-lark",
|
||||||
|
"dev:mp-qq": "uni -p mp-qq",
|
||||||
|
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||||
|
"dev:mp-weixin": "uni -p mp-weixin",
|
||||||
|
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||||
|
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||||
|
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||||
|
"build": "node scripts/publish.js",
|
||||||
|
"build:app": "uni build -p app",
|
||||||
|
"build:custom": "uni build -p",
|
||||||
|
"build:h5": "uni build && node scripts/release.mjs -t h5 -o mobile",
|
||||||
|
"build:h5:ssr": "uni build --ssr",
|
||||||
|
"build:mp-alipay": "uni build -p mp-alipay",
|
||||||
|
"build:mp-baidu": "uni build -p mp-baidu",
|
||||||
|
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||||
|
"build:mp-lark": "uni build -p mp-lark",
|
||||||
|
"build:mp-qq": "uni build -p mp-qq",
|
||||||
|
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||||
|
"build:mp-weixin": "uni build -p mp-weixin && node scripts/release.mjs -t mp-weixin -o weapp",
|
||||||
|
"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",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dcloudio/uni-app": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-app-plus": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-components": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-h5": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-mp-alipay": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-mp-baidu": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-mp-jd": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-mp-kuaishou": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-mp-lark": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-mp-qq": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-mp-toutiao": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-mp-weixin": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-quickapp-webview": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-webview-js": "0.0.3",
|
||||||
|
"@iktakahiro/markdown-it-katex": "4.0.1",
|
||||||
|
"@vueuse/core": "9.8.2",
|
||||||
|
"css-color-function": "1.3.3",
|
||||||
|
"github-markdown-css": "5.2.0",
|
||||||
|
"highlight.js": "11.0.0",
|
||||||
|
"howler": "2.2.4",
|
||||||
|
"js-mp3": "0.1.0",
|
||||||
|
"jsonc-parser": "3.2.1",
|
||||||
|
"js-base64": "^3.7.5",
|
||||||
|
"lodash-es": "4.17.21",
|
||||||
|
"markdown-it": "^13.0.1",
|
||||||
|
"markdown-it-math": "4.1.1",
|
||||||
|
"markmap-common": "0.15.3",
|
||||||
|
"markmap-lib": "0.15.4",
|
||||||
|
"markmap-view": "0.15.4",
|
||||||
|
"mathjs": "11.8.0",
|
||||||
|
"pinia": "2.0.20",
|
||||||
|
"recorder-core": "1.3.23122400",
|
||||||
|
"uniapp-router-next": "1.2.7",
|
||||||
|
"uniapp-router-next-zm": "^1.0.1",
|
||||||
|
"vconsole": "3.14.6",
|
||||||
|
"vue": "3.2.45",
|
||||||
|
"vue-i18n": "9.1.9",
|
||||||
|
"weixin-js-sdk": "1.6.0",
|
||||||
|
"z-paging": "2.7.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dcloudio/types": "^3.3.2",
|
||||||
|
"@dcloudio/uni-automator": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-cli-shared": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/uni-stacktracey": "3.0.0-3070920230324001",
|
||||||
|
"@dcloudio/vite-plugin-uni": "3.0.0-3070920230324001",
|
||||||
|
"@rushstack/eslint-patch": "1.1.4",
|
||||||
|
"@types/howler": "2.2.11",
|
||||||
|
"@types/lodash-es": "4.17.6",
|
||||||
|
"@types/markdown-it": "12.2.3",
|
||||||
|
"@types/node": "18.7.16",
|
||||||
|
"@vue/eslint-config-prettier": "7.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "11.0.0",
|
||||||
|
"autoprefixer": "10.4.8",
|
||||||
|
"eslint": "8.22.0",
|
||||||
|
"eslint-plugin-vue": "9.4.0",
|
||||||
|
"execa": "6.1.0",
|
||||||
|
"fs-extra": "10.1.0",
|
||||||
|
"minimist": "1.2.8",
|
||||||
|
"postcss": "8.4.16",
|
||||||
|
"postcss-rem-to-responsive-pixel": "5.1.3",
|
||||||
|
"prettier": "2.7.1",
|
||||||
|
"sass": "1.54.5",
|
||||||
|
"tailwindcss": "3.3.2",
|
||||||
|
"typescript": "4.7.4",
|
||||||
|
"unplugin-uni-router": "1.2.7",
|
||||||
|
"vite": "4.1.4",
|
||||||
|
"weapp-tailwindcss-webpack-plugin": "1.12.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
105
scripts/develop.js
Normal file
105
scripts/develop.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
const { spawn } = require('child_process')
|
||||||
|
const readline = require('readline')
|
||||||
|
|
||||||
|
class DevelopClientScript {
|
||||||
|
constructor() {
|
||||||
|
if (DevelopClientScript.instance) {
|
||||||
|
return DevelopClientScript.instance
|
||||||
|
}
|
||||||
|
DevelopClientScript.instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
promptUser(question) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
})
|
||||||
|
rl.question(question, (res) => {
|
||||||
|
resolve(res)
|
||||||
|
rl.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async runClient() {
|
||||||
|
console.error('请选择你需要运行的客户端(回复数字后回车)')
|
||||||
|
console.error('0.取消')
|
||||||
|
console.error('1.微信小程序')
|
||||||
|
console.error('2.公众号或者H5')
|
||||||
|
const runClientRes = await this.promptUser('请输入运行的客户端:')
|
||||||
|
switch (runClientRes) {
|
||||||
|
case '0':
|
||||||
|
break
|
||||||
|
case '1':
|
||||||
|
await this.runNpmScript('dev:mp-weixin')
|
||||||
|
break
|
||||||
|
case '2':
|
||||||
|
await this.runNpmScript('dev:h5')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
await this.runClient()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runNpmScript(scriptName) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const isWindows = process.platform === 'win32'
|
||||||
|
const command = isWindows ? 'cmd.exe' : 'npm'
|
||||||
|
const args = isWindows
|
||||||
|
? ['/c', 'npm', 'run', scriptName]
|
||||||
|
: ['run', scriptName]
|
||||||
|
|
||||||
|
const runProcess = spawn(command, args)
|
||||||
|
|
||||||
|
runProcess.stdout.on('data', (data) => {
|
||||||
|
console.log(data.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
runProcess.stderr.on('data', (data) => {
|
||||||
|
console.error(data.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
runProcess.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`运行错误,请查看以下报错信息寻找解决方法: ${error.message}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(targetVersion) {
|
||||||
|
const currentVersion = process.versions.node
|
||||||
|
|
||||||
|
if (currentVersion < targetVersion) {
|
||||||
|
throw new Error(
|
||||||
|
`你的当前node版本为(${currentVersion}),需要安装目标版本为 ${targetVersion} 以上!!`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.runClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance() {
|
||||||
|
if (!DevelopClientScript.instance) {
|
||||||
|
DevelopClientScript.instance = new DevelopClientScript()
|
||||||
|
}
|
||||||
|
return DevelopClientScript.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
const develop = DevelopClientScript.getInstance()
|
||||||
|
try {
|
||||||
|
await develop.run('16.16.0')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message)
|
||||||
|
}
|
||||||
|
})()
|
||||||
1
scripts/publish.js
Normal file
1
scripts/publish.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
const { spawn } = require('child_process')
const readline = require('readline')
class PublishClientScript {
constructor() {
if (PublishClientScript.instance) {
return PublishClientScript.instance
}
PublishClientScript.instance = this
}
promptUser(question) {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question(question, (res) => {
resolve(res)
rl.close()
})
})
}
async runClient() {
console.error('请选择你需要打包的客户端(回复数字后回车) ')
console.error('0.取消')
console.error('1.微信小程序')
console.error('2.公众号或者H5')
const runClientRes = await this.promptUser('请输入打包的客户端:')
switch (runClientRes) {
case '0':
break
case '1':
await this.runNpmScript('build:mp-weixin')
break
case '2':
await this.runNpmScript('build:h5')
break
default:
await this.runClient()
break
}
}
runNpmScript(scriptName) {
return new Promise((resolve, reject) => {
const isWindows = process.platform === 'win32'
const command = isWindows ? 'cmd.exe' : 'npm'
const args = isWindows
? ['/c', 'npm', 'run', scriptName]
: ['run', scriptName]
const runProcess = spawn(command, args)
runProcess.stdout.on('data', (data) => {
console.log(data.toString())
})
runProcess.stderr.on('data', (data) => {
console.error(data.toString())
})
runProcess.on('close', (code) => {
if (code !== 0) {
reject(
new Error(
`运行错误,请查看以下报错信息寻找解决方法: ${error.message}`
)
)
} else {
resolve()
}
})
})
}
async run(targetVersion) {
const currentVersion = process.versions.node
if (currentVersion < targetVersion) {
throw new Error(
`你的当前node版本为(${currentVersion}),需要安装目标版本为 ${targetVersion} 以上!!`
)
}
await this.runClient()
}
static getInstance() {
if (!PublishClientScript.instance) {
PublishClientScript.instance = new PublishClientScript()
}
return PublishClientScript.instance
}
}
;(async () => {
const publish = PublishClientScript.getInstance()
try {
await publish.run('16.16.0')
} catch (error) {
console.error(error.message)
}
})()
|
||||||
46
scripts/release.mjs
Normal file
46
scripts/release.mjs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import fsExtra from 'fs-extra'
|
||||||
|
import minimist from 'minimist'
|
||||||
|
const { existsSync, remove, copy } = fsExtra
|
||||||
|
const cwd = process.cwd()
|
||||||
|
const argv = minimist(process.argv.slice(2), {
|
||||||
|
alias: {
|
||||||
|
target: 't',
|
||||||
|
output: 'o'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//打包发布路径,谨慎改动
|
||||||
|
const releaseRelativePath = `../public/${argv.output}`
|
||||||
|
const distPath = path.resolve(cwd, `dist/build/${argv.target}`)
|
||||||
|
const releasePath = path.resolve(cwd, releaseRelativePath)
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
if (existsSync(releasePath)) {
|
||||||
|
await remove(releasePath)
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`文件正在复制:dist/build/${argv.target} ==> ${releaseRelativePath}`
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
await copyFile(distPath, releasePath)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`\n ${error}`)
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`文件已复制:dist/build/${argv.target} ==> ${releaseRelativePath}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFile(sourceDir, targetDir) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
copy(sourceDir, targetDir, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
build()
|
||||||
76
src/App.vue
Normal file
76
src/App.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onLaunch, onShow } from '@dcloudio/uni-app'
|
||||||
|
import { useAppStore } from './stores/app'
|
||||||
|
import { useUserStore } from './stores/user'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import { addVisit } from './api/shop'
|
||||||
|
import { useSharedId } from './hooks/useShareMessage'
|
||||||
|
import { SHARE_ID, USER_SN } from './enums/constantEnums'
|
||||||
|
import { strToParams } from './utils/util'
|
||||||
|
import cache from './utils/cache'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { getUser } = useUserStore()
|
||||||
|
const { getTheme } = useThemeStore()
|
||||||
|
|
||||||
|
const cacheInvite = (query: any = {}) => {
|
||||||
|
const { share_id } = query
|
||||||
|
const user_sn =
|
||||||
|
query.user_sn ||
|
||||||
|
strToParams(decodeURIComponent(query['scene']))['user_sn'];
|
||||||
|
console.log(query)
|
||||||
|
console.log(user_sn)
|
||||||
|
if (share_id) {
|
||||||
|
cache.set(SHARE_ID, share_id)
|
||||||
|
}
|
||||||
|
if (user_sn) {
|
||||||
|
cache.set(USER_SN, user_sn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#ifdef H5
|
||||||
|
const setH5WebIcon = () => {
|
||||||
|
const config = appStore.getWebsiteConfig
|
||||||
|
let favicon: HTMLLinkElement = document.querySelector('link[rel="icon"]')!
|
||||||
|
if (favicon) {
|
||||||
|
favicon.href = config.logo
|
||||||
|
return
|
||||||
|
}
|
||||||
|
favicon = document.createElement('link')
|
||||||
|
favicon.rel = 'icon'
|
||||||
|
favicon.href = config.logo
|
||||||
|
document.head.appendChild(favicon)
|
||||||
|
}
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
const getConfig = async () => {
|
||||||
|
await appStore.getConfig()
|
||||||
|
//处理关闭h5渠道
|
||||||
|
// #ifdef H5
|
||||||
|
const { status, close, url } = appStore.getH5Config
|
||||||
|
if (status == 0) {
|
||||||
|
if (close == 1) return (location.href = url)
|
||||||
|
uni.reLaunch({ url: '/pages/empty/empty' })
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
onLaunch(async (opinion) => {
|
||||||
|
getConfig()
|
||||||
|
getTheme()
|
||||||
|
await addVisit()
|
||||||
|
getUser()
|
||||||
|
useSharedId()
|
||||||
|
cacheInvite(opinion?.query)
|
||||||
|
//#ifdef H5
|
||||||
|
setH5WebIcon()
|
||||||
|
//#endif
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
uni.hideTabBar()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
//
|
||||||
|
</style>
|
||||||
40
src/androidPrivacy.json
Normal file
40
src/androidPrivacy.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"version" : "1",
|
||||||
|
"prompt" : "template",
|
||||||
|
"title" : "用户协议及隐私政策",
|
||||||
|
"message" : " 为了更好保证您的合法权益,请仔细阅读并同意以下协议。<br/><a href=\"https://chat.mddai.cn/mobile/packages/pages/agreement/agreement?type=service\">《用户协议》</a>和<a href=\"https://chat.mddai.cn/mobile/packages/pages/agreement/agreement?type=privacy\">《隐私政策》</a>",
|
||||||
|
"buttonAccept" : "同意并接受",
|
||||||
|
"buttonRefuse" : "暂不同意",
|
||||||
|
"hrefLoader" : "system|default",
|
||||||
|
"backToExit" : "false",
|
||||||
|
"second" : {
|
||||||
|
"title" : "确认提示",
|
||||||
|
"message" : " 进入应用前,你需先同意<a href=\"https://chat.mddai.cn/mobile/packages/pages/agreement/agreement?type=service\">《用户协议》</a>和<a href=\"https://chat.mddai.cn/mobile/packages/pages/agreement/agreement?type=privacy\">《隐私政策》</a>,否则将退出应用。",
|
||||||
|
"buttonAccept" : "同意并继续",
|
||||||
|
"buttonRefuse" : "退出应用"
|
||||||
|
},
|
||||||
|
"disagreeMode" : {
|
||||||
|
"support" : false,
|
||||||
|
"loadNativePlugins" : false,
|
||||||
|
"visitorEntry" : true,
|
||||||
|
"showAlways" : false
|
||||||
|
},
|
||||||
|
"styles" : {
|
||||||
|
"backgroundColor" : "#ffffff",
|
||||||
|
"borderRadius" : "5px",
|
||||||
|
"title" : {
|
||||||
|
"color" : "#000000",
|
||||||
|
"fontWeight" : "bolder"
|
||||||
|
},
|
||||||
|
"buttonAccept" : {
|
||||||
|
"color" : "#000000"
|
||||||
|
},
|
||||||
|
"buttonRefuse" : {
|
||||||
|
"color" : "#000000",
|
||||||
|
"backgroundColor" : "#FFB529"
|
||||||
|
},
|
||||||
|
"buttonVisitor" : {
|
||||||
|
"color" : "#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/api/account.ts
Normal file
74
src/api/account.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
import { getClient } from '@/utils/client'
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
export function mobileLogin(data: Record<string, any>) {
|
||||||
|
return request.post({ url: '/login/mobileLogin', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账号登录
|
||||||
|
export function accountLogin(data: Record<string, any>) {
|
||||||
|
return request.post({ url: '/login/accountLogin', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 邮箱登录
|
||||||
|
export function emailLogin(data: any) {
|
||||||
|
return request.post({
|
||||||
|
url: '/login/emailLogin',
|
||||||
|
data: { ...data, terminal: getClient() }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//邮箱注册
|
||||||
|
export function emailRegister(data: any) {
|
||||||
|
return request.post({
|
||||||
|
url: '/login/email/register',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//发送邮箱验证码
|
||||||
|
export function sendEmailCode(data: any) {
|
||||||
|
return request.post({ url: '/index/sendEmail', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//注册
|
||||||
|
export function register(data: Record<string, any>) {
|
||||||
|
return request.post({ url: '/login/register', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//忘记密码
|
||||||
|
export function forgotPassword(data: Record<string, any>) {
|
||||||
|
return request.post({ url: '/user/forgotPwd', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//邮箱忘记密码
|
||||||
|
export function emailForgotPassword(data: Record<string, any>) {
|
||||||
|
return request.post({ url: '/user/email/forgotPwd', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//向微信请求code的链接
|
||||||
|
export function getWxCodeUrl(data: Record<string, any>) {
|
||||||
|
return request.get({ url: '/login/oaCodeUrl', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信小程序登录
|
||||||
|
|
||||||
|
export function mnpLogin(data: Record<string, any>) {
|
||||||
|
return request.post({ url: '/login/mnpLogin', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// APP登录
|
||||||
|
export function uninAppLogin(data: Record<string, any>) {
|
||||||
|
return request.post({ url: '/login/appLogin', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公众号登录
|
||||||
|
export function OALogin(data: Record<string, any>) {
|
||||||
|
return request.post({ url: '/login/oaLogin', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取图形验证码
|
||||||
|
export function captcha() {
|
||||||
|
return request.get({ url: '/login/captcha' })
|
||||||
|
}
|
||||||
48
src/api/app.ts
Normal file
48
src/api/app.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import wechatOa from '@/utils/wechat'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
//发送短信
|
||||||
|
export function smsSend(data: any) {
|
||||||
|
return request.post({ url: '/index/sendSms', data: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfig(data: any) {
|
||||||
|
return request.get({ url: '/index/config', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPolicy(data: any) {
|
||||||
|
return request.get({ url: '/index/policy', data: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadImage(file: any, token?: string) {
|
||||||
|
return request.uploadFile({
|
||||||
|
url: '/upload/image',
|
||||||
|
filePath: file,
|
||||||
|
name: 'file',
|
||||||
|
header: {
|
||||||
|
token
|
||||||
|
},
|
||||||
|
fileType: 'image'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadFile(
|
||||||
|
type: 'image' | 'file' | 'video',
|
||||||
|
options: Omit<UniApp.UploadFileOption, 'url'>,
|
||||||
|
onProgress?: (progress: number) => void
|
||||||
|
) {
|
||||||
|
return request.uploadFile(
|
||||||
|
{ ...options, url: `/upload/${type}`, name: 'file' },
|
||||||
|
{
|
||||||
|
onProgress
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wxJsConfig(data: any) {
|
||||||
|
return request.get({ url: '/wechat/jsConfig', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMnpQrCode(data: any) {
|
||||||
|
return request.post({ url: '/wechat/getMnpQrCode', data: data })
|
||||||
|
}
|
||||||
130
src/api/chat.ts
Normal file
130
src/api/chat.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import request, { RequestEventStreamConfig } from '@/utils/request'
|
||||||
|
|
||||||
|
//获取技能列表
|
||||||
|
export function getSkillLists(data: any) {
|
||||||
|
return request.get({ url: '/ai/skill', data })
|
||||||
|
}
|
||||||
|
//获取创作列表
|
||||||
|
export function getCreationLists(data: { keyword: string }) {
|
||||||
|
return request.get({ url: '/ai/creation', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSamplesLists() {
|
||||||
|
return request.get({ url: '/ai/question' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function questionChat(data: any) {
|
||||||
|
return request.post({ url: '/chat_records/chat', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话记录
|
||||||
|
export function getChatRecord(data: any) {
|
||||||
|
return request.get({
|
||||||
|
url: '/chats/chatRecord',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//清空会话
|
||||||
|
export function cleanChatRecord(data: any) {
|
||||||
|
return request.post(
|
||||||
|
{
|
||||||
|
url: '/chats/chatClean',
|
||||||
|
data
|
||||||
|
},
|
||||||
|
{ isAuth: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//清空会话
|
||||||
|
export function delChatRecord(data: any) {
|
||||||
|
return request.post(
|
||||||
|
{
|
||||||
|
url: '/chats/record/del',
|
||||||
|
data
|
||||||
|
},
|
||||||
|
{ isAuth: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//收藏
|
||||||
|
export function collectChatRecord(data: any) {
|
||||||
|
return request.post({ url: '/chats/addCollect', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
//取消收藏
|
||||||
|
export function cancelCollectChatRecord(data: any) {
|
||||||
|
return request.post({ url: '/chats/cancelCollect', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
//收藏列表
|
||||||
|
export function getCollectChatRecordLists(data: any) {
|
||||||
|
return request.get({ url: '/chats/listCollect', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCreationDetail(data: any) {
|
||||||
|
return request.get({ url: '/ai/creation/detail', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSkillDetail(data: any) {
|
||||||
|
return request.get({ url: '/ai/skill/detail', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chatSendText(data: any, config: RequestEventStreamConfig) {
|
||||||
|
return request.eventStream({ url: '/chats/chatSend', data, method: 'POST' }, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话分类列表
|
||||||
|
export function getChatCategoryLists(data: any) {
|
||||||
|
return request.get({ url: '/ai/category', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话分类新增
|
||||||
|
export function chatCategoryAdd(data: any) {
|
||||||
|
return request.post({ url: '/ai/category/add', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话分类编辑
|
||||||
|
export function chatCategoryEdit(data: any) {
|
||||||
|
return request.post({ url: '/ai/category/update', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话分类删除
|
||||||
|
export function chatCategoryDelete(data: any) {
|
||||||
|
return request.post({ url: '/ai/category/del', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话分类清空
|
||||||
|
export function chatCategoryClear() {
|
||||||
|
return request.post({ url: '/ai/category/delAll' }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创作
|
||||||
|
export function creationChat(data: any, config: RequestEventStreamConfig) {
|
||||||
|
return request.eventStream({ url: '/chat/creationChat', data, method: 'POST' }, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取聊天模型接口
|
||||||
|
export function getChatModelApi() {
|
||||||
|
return request.get({ url: '/chats/getChatBillingConfigList' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChatBroadcast(data: any) {
|
||||||
|
return request.post({ url: '/voice/execute', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChatVoiceGenerate(data: any) {
|
||||||
|
return request.post({ url: '/chats/voiceGenerate', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function audioTransfer(filePath: string, formData: Record<string, any>) {
|
||||||
|
return request.uploadFile({
|
||||||
|
url: '/voice/voiceTransfer',
|
||||||
|
filePath,
|
||||||
|
formData,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlugLists() {
|
||||||
|
return request.get({ url: '/chats/plugLists' })
|
||||||
|
}
|
||||||
1
src/api/drawing.ts
Normal file
1
src/api/drawing.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import request, { RequestEventStreamConfig } from '@/utils/request'
export type DrawingFormType = {
prompt: string // 是 关键词
action: string // 是 操作 generate=生成图片 upsample{index}=放大 variation{index}=变换
imageBase: string // 否 图片地址 图生成图时必填
imageId: string // 否 图片id 图片放大或变换时必填
model: string // 是 绘画的模型
version: string // 是 版本
scale: string // 否 图片比例
no_content: string // 否 忽略的关键词
other: string // 否 其它参数
style: string // 否 风格 动漫-default 可爱-cute 丰富-expressive 风景-scenic
engine: string // 否 意间sd-绘画引擎
quality: string // 否 DALLE-3 画质
}
// 生成图片
export function drawing(data: DrawingFormType) {
return request.post({ url: '/draw/execute', data, method: 'POST' })
}
// 生成图片详情
export function drawingDetail(data: number[]) {
return request.post({ url: '/draw/records/detail', data })
}
// 生成图片记录
export function drawingRecord(data: any) {
return request.get({
url: '/draw/records/list',
data
})
}
// 删除
export function drawingDelete(data: number[]) {
return request.post({ url: '/draw/records/del', data, method: 'POST' })
}
// 关键词分类
export function keywordCate() {
return request.get({
url: '/draw/category/get'
})
}
// 关键词
export function keywordPrompt(data: any) {
return request.get({
url: '/draw/prompt/get',
data
})
}
// 绘画示例
export function drawingExample() {
return request.get({ url: '/draw/prompt/example/get' })
}
// 关键词翻译
export function keywordPromptTranslate(data: any) {
return request.post({
url: '/draw/translate',
data, method: 'POST'
})
}
// 绘画模型
export function drawingModel() {
return request.get({ url: '/draw/getDrawBillingConfig' })
}
// 意间绘画风格选择
export function yjStyleSelector() {
return request.get({ url: '/draw/getSelector' })
}
// SD
export function sdModelList() {
return request.get({ url: '/draw/sd/getModel' })
}
|
||||||
17
src/api/member.ts
Normal file
17
src/api/member.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getMemberLists() {
|
||||||
|
return request.get({ url: '/member/packageList' }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommentLists(data: any) {
|
||||||
|
return request.get({ url: '/member/commentList', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function memberBuy(data: any) {
|
||||||
|
return request.post({ url: '/member/buy', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMemberBuyLog(data?: any) {
|
||||||
|
return request.get({ url: '/member/buyLog', data }, { isAuth: true })
|
||||||
|
}
|
||||||
52
src/api/news.ts
Normal file
52
src/api/news.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取文章分类
|
||||||
|
* @return { Promise }
|
||||||
|
*/
|
||||||
|
export function getArticleCate() {
|
||||||
|
return request.get({ url: '/article/category' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取文章列表
|
||||||
|
* @return { Promise }
|
||||||
|
*/
|
||||||
|
export function getArticleList(data: Record<string, any>) {
|
||||||
|
return request.get({ url: '/article/list', data: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取文章详情
|
||||||
|
* @param { number } id
|
||||||
|
* @return { Promise }
|
||||||
|
*/
|
||||||
|
export function getArticleDetail(data: { id: number }) {
|
||||||
|
return request.get({ url: '/article/detail', data: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 加入收藏
|
||||||
|
* @param { number } articleId
|
||||||
|
* @return { Promise }
|
||||||
|
*/
|
||||||
|
export function addCollect(data: { articleId: number }) {
|
||||||
|
return request.post({ url: '/article/collectAdd', data: data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 取消收藏
|
||||||
|
* @param { number } id
|
||||||
|
* @return { Promise }
|
||||||
|
*/
|
||||||
|
export function cancelCollect(data: { articleId: number }) {
|
||||||
|
return request.post({ url: '/article/collectCancel', data: data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取收藏列表
|
||||||
|
* @return { Promise }
|
||||||
|
*/
|
||||||
|
export function getCollect() {
|
||||||
|
return request.get({ url: '/article/collectList' })
|
||||||
|
}
|
||||||
16
src/api/pay.ts
Normal file
16
src/api/pay.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
//支付方式
|
||||||
|
export function getPayWay(data: any) {
|
||||||
|
return request.get({ url: '/pay/payWay', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预支付
|
||||||
|
export function prepay(data: any) {
|
||||||
|
return request.post({ url: '/pay/prepay', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预支付
|
||||||
|
export function getPayResult(data: any) {
|
||||||
|
return request.get({ url: '/pay/payStatus', data }, { isAuth: true })
|
||||||
|
}
|
||||||
55
src/api/promotion.ts
Normal file
55
src/api/promotion.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 分销中心
|
||||||
|
export function getDistributionIndex(data?: any) {
|
||||||
|
return request.get({ url: '/distribution/center', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分销申请
|
||||||
|
export function distributionApply(data: any) {
|
||||||
|
return request.post(
|
||||||
|
{ url: '/distribution/apply', data },
|
||||||
|
{ isAuth: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分销订单
|
||||||
|
export function distributionOrder(data?: any) {
|
||||||
|
return request.get(
|
||||||
|
{ url: '/distribution/order/list', data },
|
||||||
|
{ isAuth: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 粉丝列表
|
||||||
|
export function distributionFans(data?: any) {
|
||||||
|
return request.get(
|
||||||
|
{ url: '/distribution/user/invite/list', data },
|
||||||
|
{ isAuth: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提现 ---------------------------------------------------------------------------------
|
||||||
|
export type WithdrawApplyType = {
|
||||||
|
money: string | number // 提现金额
|
||||||
|
account: string | number // 支付宝账号
|
||||||
|
realName: string | number // 支付宝姓名
|
||||||
|
type: string | number
|
||||||
|
moneyQrcode: string[] | string
|
||||||
|
}
|
||||||
|
// 提现申请
|
||||||
|
export function withdrawApply(data: WithdrawApplyType) {
|
||||||
|
return request.post({ url: '/distribution/withdrawal/apply', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提现记录
|
||||||
|
export function withdrawList(data?: { pageNo: number; pageSize: number }) {
|
||||||
|
return request.get({ url: '/distribution/pc/withdrawal/list', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function accountLog(data?: any) {
|
||||||
|
return request.get(
|
||||||
|
{ url: '/distribution/getRevenueDetails', data },
|
||||||
|
{ isAuth: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
60
src/api/qrcode.ts
Normal file
60
src/api/qrcode.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export type QrcodeFormType = {
|
||||||
|
model: string // 是 绘画模型
|
||||||
|
type: number // 是 生成模式 1-文本模式 2-图片模式
|
||||||
|
way: number // 是 生成模式 1-自定义(模型) 2-模板
|
||||||
|
qr_content: string // 否 二维码内容 (文本模式时必填)
|
||||||
|
qr_image: string // 否 二维码图片 (图片模式时必填)
|
||||||
|
prompt: string // 否 关键词
|
||||||
|
prompt_params: PromptParams | string // 否 其他参数
|
||||||
|
model_id: string | number // 否 模型id
|
||||||
|
template_name: string // 否 模板名称
|
||||||
|
template_id: string | number // 否 模板id
|
||||||
|
aspect_ratio: string | number // 否 比例 (知数云)
|
||||||
|
pixel_style: string | number // 否 码点形状(知数云)
|
||||||
|
marker_shape: string | number // 否 码眼选择(知数云)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PromptParams = {
|
||||||
|
v: string // 版本取值枚举 2 1.1 1) 示例:--v 2 --v 1.1
|
||||||
|
iw: number // (明显程序取值范围 0 - 1, 保留两位小数) 示例: --iw 0.45
|
||||||
|
seed: string // (取值范围1 - 999999999 ) 示例: --seed 123
|
||||||
|
shape: string // (码眼选择范围) ["square", "circle", "plus", "box", "octagon", "random", "tiny-plus"], 示例 --shape random ,默认为 random
|
||||||
|
ar: string // (尺寸选择) 范围 ["1:1", "9:16", "16:9", "3:4","4:3"] 示例 --ar 1:1 ,默认为 1:1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取艺术二维码配置
|
||||||
|
export function qrcodeConfig() {
|
||||||
|
return request.get({ url: '/qrcode/config' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成艺术二维码
|
||||||
|
export function qrcodeImagine(data: QrcodeFormType) {
|
||||||
|
return request.post({ url: '/qrcode/execute', data, method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成艺术二维码详情
|
||||||
|
export function qrcodeDetail(data: { records_id: number[] }) {
|
||||||
|
return request.post({ url: '/qrcode/records/detail', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成艺术二维码记录
|
||||||
|
export function qrcodeRecord(data: any) {
|
||||||
|
return request.get(
|
||||||
|
{
|
||||||
|
url: '/qrcode/records/list',
|
||||||
|
data
|
||||||
|
},
|
||||||
|
{ ignoreCancel: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
export function qrcodeDelete(data: number[]) {
|
||||||
|
return request.post({
|
||||||
|
url: '/qrcode/records/del',
|
||||||
|
data,
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
}
|
||||||
21
src/api/recharge.ts
Normal file
21
src/api/recharge.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
//充值
|
||||||
|
export function recharge(data: any) {
|
||||||
|
return request.post({ url: '/recharge/placeOrder', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
//充值记录
|
||||||
|
export function rechargeRecord(data: any) {
|
||||||
|
return request.get({ url: '/recharge/record', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 充值配置
|
||||||
|
export function rechargeConfig() {
|
||||||
|
return request.get({ url: '/recharge/config' }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 充值配置
|
||||||
|
export function getRechargeConfig() {
|
||||||
|
return request.get({ url: '/recharge/rechargePackage' }, { isAuth: true })
|
||||||
|
}
|
||||||
1
src/api/redeem_code.ts
Normal file
1
src/api/redeem_code.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import request from '@/utils/request'
export type RedeemCodeResponse = {
content: string // 卡密内容
failure_time: string // 失效时间
id: string | number // 卡密ID
sn: string | number // 卡密编号
type: string // 卡密类型
type_desc: string // 卡密类型说明
}
/**
* @description 卡密查询
* @return { RedeemCodeResponse }
* @param data
*/
export function checkRedeemCode(data: {
sn: number | string
}): Promise<RedeemCodeResponse> {
return request.get({ url: '/card_code/record/get', data: data })
}
/**
* @description 卡密兑换
* @param data
*/
export function useRedeemCode(data: { sn: number | string }) {
return request.post({ url: '/card_code/record/use', data: data })
}
|
||||||
16
src/api/shop.ts
Normal file
16
src/api/shop.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { client } from '@/utils/client'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
//首页数据
|
||||||
|
export function getIndex() {
|
||||||
|
return request.get({ url: '/index/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 装修页面
|
||||||
|
export function getDecorate(data: any) {
|
||||||
|
return request.get({ url: '/index/decorate', data }, { ignoreCancel: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addVisit() {
|
||||||
|
return request.post({ url: '/index/visit/add', data: { terminal: client } })
|
||||||
|
}
|
||||||
1
src/api/square.ts
Normal file
1
src/api/square.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import request from '@/utils/request'
/**
* @description 绘画广场列表
* @return { Promise }
* @param params
*/
export function getDrawSquareLists(data?: any) {
return request.get({ url: '/draw/square/list', data })
}
/**
* @description 绘画广场列表
* @return { Promise }
*/
export function getDrawSquareCateLists() {
return request.get({ url: '/draw/square/category/list' })
}
/**
* @description 分享至绘画广场
* @return { Promise }
*/
export function shareDrawSquare(data: any) {
return request.post({ url: '/draw/square/add', data })
}
/**
* @description 喜欢绘画
* @return { Promise }
*/
export function collectDraw(data: any) {
return request.post({ url: '/draw/square/collect', data })
}
/**
* @description 取消喜欢绘画
* @return { Promise }
*/
export function cancelCollectDraw(data: any) {
return request.post({ url: '/draw/square/cancelCollect', data })
}
|
||||||
25
src/api/task.ts
Normal file
25
src/api/task.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getShareId() {
|
||||||
|
return request.post({ url: '/share/share' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shareClick(data: any) {
|
||||||
|
return request.post({ url: '/share/click', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindInvite(data: any, token: string) {
|
||||||
|
return request.post({ url: '/share/invite', data, header: { token } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindRegisterInvite(data: any) {
|
||||||
|
return request.post({ url: '/share/register/invite', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTask() {
|
||||||
|
return request.get({ url: '/rewards/task' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signClick() {
|
||||||
|
return request.post({ url: '/sign/click' })
|
||||||
|
}
|
||||||
58
src/api/user.ts
Normal file
58
src/api/user.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getUserCenter(header?: any) {
|
||||||
|
return request.get({ url: '/user/center', header })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 个人信息
|
||||||
|
export function getUserInfo() {
|
||||||
|
return request.get({ url: '/user/info' }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 个人编辑
|
||||||
|
export function userEdit(data: any) {
|
||||||
|
return request.post({ url: '/user/edit', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定手机
|
||||||
|
export function userBindMobile(data: any, header?: any) {
|
||||||
|
return request.post({ url: '/login/bindMobile', data, header }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信电话
|
||||||
|
export function userMnpMobile(data: any, header?: any) {
|
||||||
|
return request.post({ url: '/user/mnpMobile', data, header }, { isAuth: true })
|
||||||
|
}
|
||||||
|
export function userChangePwd(data: any) {
|
||||||
|
return request.post({ url: '/user/changePwd', data }, { isAuth: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定小程序
|
||||||
|
export function mnpAuthBind(data: any) {
|
||||||
|
return request.post({ url: '/user/bindMnp', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定公众号
|
||||||
|
export function oaAuthBind(data: any) {
|
||||||
|
return request.post({ url: '/user/bindOa', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//更新微信小程序头像昵称
|
||||||
|
export function updateUser(data: Record<string, any>, header: any) {
|
||||||
|
return request.post({ url: '/user/updateUser', data, header })
|
||||||
|
}
|
||||||
|
|
||||||
|
//余额明细
|
||||||
|
export function accountLog(data: any) {
|
||||||
|
return request.get({ url: '/logs/userMoney', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//一键反馈
|
||||||
|
export function feedbackPost(data: any) {
|
||||||
|
return request.post({ url: '/feedback/add', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
//注销账号
|
||||||
|
export function cancelled(data?: any) {
|
||||||
|
return request.post({ url: '/login/cancelled', data })
|
||||||
|
}
|
||||||
1
src/components/agreement/agreement.vue
Normal file
1
src/components/agreement/agreement.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template>
<view class="agreement" v-if="isOpenAgreement" :class="{ shake: isShake }">
<view>
<u-checkbox v-model="isActive" shape="circle">
<view class="text-xs flex">
已阅读并同意
<view @click.stop>
<router-navigate
class="text-primary"
to="/packages/pages/agreement/agreement?type=service"
>
《服务协议》
</router-navigate>
</view>
和
<view @click.stop>
<router-navigate
class="text-primary"
to="/packages/pages/agreement/agreement?type=privacy"
>
《隐私协议》
</router-navigate>
</view>
</view>
</u-checkbox>
</view>
</view>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/stores/app'
import { computed, ref } from 'vue'
const appStore = useAppStore()
const isActive = ref(false)
const isShake = ref(false)
const isOpenAgreement = computed(
() => appStore.getLoginConfig.openAgreement == 1
)
const checkAgreement = () => {
if (!isActive.value && isOpenAgreement.value) {
uni.$u.toast('请勾选已阅读并同意《服务协议》和《隐私协议》')
isShake.value = true
setTimeout(() => {
isShake.value = false
}, 1000)
} else if (!isOpenAgreement.value) {
return true
}
return isActive.value
}
defineExpose({
checkAgreement
})
</script>
<style lang="scss">
.shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
40%,
60% {
transform: translate3d(4px, 0, 0);
}
}
</style>
|
||||||
135
src/components/audio-play/audio-play.vue
Normal file
135
src/components/audio-play/audio-play.vue
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<view class="audio" @click="togglePlay" :class="{ reverse }">
|
||||||
|
<view
|
||||||
|
:style="{
|
||||||
|
transform: `rotate(${reverse ? '135deg' : '-45deg'})`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="wifi-symbol"
|
||||||
|
:class="{
|
||||||
|
playing: isPlaying
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<view class="wifi-circle first"></view>
|
||||||
|
<view class="wifi-circle second"></view>
|
||||||
|
<view class="wifi-circle third"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="duration">{{
|
||||||
|
duration ? Math.round(duration) + '"' : ''
|
||||||
|
}}</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, shallowRef, watch } from 'vue'
|
||||||
|
import { useAudio } from '@/hooks/useAudio'
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
url: string
|
||||||
|
color?: string
|
||||||
|
bgColor?: string
|
||||||
|
reverse?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
color: '#000',
|
||||||
|
bgColor: '#fff',
|
||||||
|
reverse: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { pause, play, duration, isPlaying, setUrl } = useAudio()
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (isPlaying.value) {
|
||||||
|
pause()
|
||||||
|
} else {
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => props.url,
|
||||||
|
(value) => {
|
||||||
|
if (value) {
|
||||||
|
setUrl(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.audio {
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background-color: v-bind(bgColor);
|
||||||
|
display: inline-flex;
|
||||||
|
height: 58rpx;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 100rpx;
|
||||||
|
min-width: 120rpx;
|
||||||
|
&.reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
// . {
|
||||||
|
// transform: rotateZ(135deg);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
.wifi-symbol {
|
||||||
|
margin: 10rpx;
|
||||||
|
position: relative;
|
||||||
|
width: 30rpx;
|
||||||
|
height: 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-circle {
|
||||||
|
border: 4rpx solid v-bind(color);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
transform: scale(0.9) rotate(45deg) translateX(-50%);
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
.first {
|
||||||
|
width: 6rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: v-bind(color);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.second {
|
||||||
|
width: 25rpx;
|
||||||
|
height: 25rpx;
|
||||||
|
}
|
||||||
|
.third {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
}
|
||||||
|
.duration {
|
||||||
|
color: v-bind(color);
|
||||||
|
}
|
||||||
|
.playing {
|
||||||
|
.second {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInOut 1s infinite 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInOut 1s infinite 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInOut {
|
||||||
|
0% {
|
||||||
|
opacity: 0; /*初始状态 透明度为0*/
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1; /*结尾状态 透明度为1*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
111
src/components/avatar-upload/avatar-upload.vue
Normal file
111
src/components/avatar-upload/avatar-upload.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="avatar-upload p-0 m-0 rounded inline-flex flex-col items-center"
|
||||||
|
hover-class="none"
|
||||||
|
open-type="chooseAvatar"
|
||||||
|
@click="chooseAvatar"
|
||||||
|
@chooseavatar="chooseAvatar"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
:style="styles"
|
||||||
|
class="w-full h-full"
|
||||||
|
mode="heightFix"
|
||||||
|
:src="modelValue"
|
||||||
|
v-if="modelValue"
|
||||||
|
/>
|
||||||
|
<slot v-else>
|
||||||
|
<view
|
||||||
|
:style="styles"
|
||||||
|
class="border border-dotted border-light flex w-full h-full flex-col items-center justify-center text-muted text-xs box-border rounded"
|
||||||
|
>
|
||||||
|
<u-icon name="plus" :size="36" />
|
||||||
|
添加图片
|
||||||
|
</view>
|
||||||
|
</slot>
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { addUnit } from '@/utils/util'
|
||||||
|
import { isBoolean } from 'lodash'
|
||||||
|
import { computed, CSSProperties, onUnmounted } from 'vue'
|
||||||
|
import { useRouter } from 'uniapp-router-next'
|
||||||
|
import { client } from '@/utils/client'
|
||||||
|
import { usePermissionsStore } from '@/stores/androidPermissions'
|
||||||
|
import { ClientEnum } from '@/enums/appEnums'
|
||||||
|
const router = useRouter()
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 140
|
||||||
|
},
|
||||||
|
round: {
|
||||||
|
type: [Boolean, String, Number],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string): void
|
||||||
|
(event: 'upload', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const styles = computed<CSSProperties>(() => {
|
||||||
|
const size = addUnit(props.size)
|
||||||
|
return {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: isBoolean(props.round) ? (props.round ? '50%' : '') : addUnit(props.round)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const chooseAvatar = async (e: any) => {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
if (client == ClientEnum['ANDROID']) {
|
||||||
|
const { requestPermissions } = usePermissionsStore()
|
||||||
|
const result = await requestPermissions('WRITE_EXTERNAL_STORAGE')
|
||||||
|
if (result !== 1) return
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
router.navigateTo({
|
||||||
|
path: '/uni_modules/vk-uview-ui/components/u-avatar-cropper/u-avatar-cropper?destWidth=300&rectWidth=200&fileType=jpg'
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const path = e.detail?.avatarUrl
|
||||||
|
if (path) {
|
||||||
|
uploadImageIng(path)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadImageIng = async (file: string) => {
|
||||||
|
emit('update:modelValue', file)
|
||||||
|
emit('upload', file)
|
||||||
|
}
|
||||||
|
// 监听从裁剪页发布的事件,获得裁剪结果
|
||||||
|
uni.$on('uAvatarCropper', (path) => {
|
||||||
|
// console.log(path)
|
||||||
|
uploadImageIng(path)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
uni.$off('uAvatarCropper')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.avatar-upload {
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
166
src/components/chat-plugins/chat-plugins.vue
Normal file
166
src/components/chat-plugins/chat-plugins.vue
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<view class="chat-plugins mr-2" v-if="plugins.length">
|
||||||
|
<view
|
||||||
|
class="flex items-center"
|
||||||
|
:class="{ 'text-primary': show }"
|
||||||
|
@click.stop="togglePopup"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
src="@/static/images/chat_plugins/plugins.png"
|
||||||
|
class="w-[30rpx] h-[30rpx] flex-none"
|
||||||
|
/>
|
||||||
|
<text class="ml-1 line-clamp-1">
|
||||||
|
{{
|
||||||
|
currentPlugin.key ? currentPlugin.name : '插件管理'
|
||||||
|
}}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<u-popup
|
||||||
|
v-model="show"
|
||||||
|
mode="bottom"
|
||||||
|
height="900"
|
||||||
|
:closeable="true"
|
||||||
|
border-radius="20"
|
||||||
|
closeIconColor="#5b5b5b"
|
||||||
|
closeIconSize="28"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="text-xl font-medium text-black py-[26rpx] mx-[30rpx]"
|
||||||
|
style="border-bottom: 1px solid #f3f3f3"
|
||||||
|
>
|
||||||
|
插件管理
|
||||||
|
</view>
|
||||||
|
<view class="px-[30rpx] pt-[30rpx]">
|
||||||
|
<view
|
||||||
|
v-for="item in plugins"
|
||||||
|
:key="item.key"
|
||||||
|
class="flex items-center justify-between mb-[30rpx] rounded-lg bg-page-base p-[30rpx] border border-solid border-page-base"
|
||||||
|
:class="{
|
||||||
|
'bg-primary-light-9 !border-primary': item.key === currentId
|
||||||
|
}"
|
||||||
|
@click="handleClick(item)"
|
||||||
|
>
|
||||||
|
<view>
|
||||||
|
<view class="flex items-center">
|
||||||
|
<image
|
||||||
|
:src="item.icon"
|
||||||
|
class="w-[34rpx] h-[34rpx] mr-2 flex-none"
|
||||||
|
/>
|
||||||
|
<text class="text-base">{{ item.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-if="item.key === currentId"
|
||||||
|
class="text-primary"
|
||||||
|
>
|
||||||
|
<u-icon
|
||||||
|
name="checkmark-circle-fill"
|
||||||
|
size="40rpx"
|
||||||
|
></u-icon>
|
||||||
|
</view>
|
||||||
|
<image
|
||||||
|
v-else
|
||||||
|
:src="iconUnSelect"
|
||||||
|
class="w-[32rpx] h-[32rpx] mr-[2px] flex-none"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import cache from '@/utils/cache'
|
||||||
|
// import { PLUGIN_MODEL_KEY } from '@/enums/constantEnums'
|
||||||
|
|
||||||
|
// icon
|
||||||
|
import iconUnUse from '@/static/images/chat_plugins/unuse.png'
|
||||||
|
import iconUnSelect from '@/static/images/icon/icon_unselect.png'
|
||||||
|
|
||||||
|
// com
|
||||||
|
import { getPlugLists } from '@/api/chat'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: number | string
|
||||||
|
current?: Record<string, any>
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: '',
|
||||||
|
current: () => ({})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', id: number | string): void
|
||||||
|
(event: 'update:current', value: any): void
|
||||||
|
(event: 'change', value: any): void
|
||||||
|
}>()
|
||||||
|
const show = ref<boolean>(false)
|
||||||
|
const plugins = ref<any[]>([])
|
||||||
|
const currentId = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
const currentPlugin = computed(() => {
|
||||||
|
return plugins.value.find((item) => item.key === currentId.value) || {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const togglePopup = () => {
|
||||||
|
show.value = !show.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (row: { key: string }) => {
|
||||||
|
currentId.value = row.key
|
||||||
|
togglePopup()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getData = async () => {
|
||||||
|
const data = await getPlugLists()
|
||||||
|
if (data.length) {
|
||||||
|
plugins.value = [
|
||||||
|
{
|
||||||
|
name: '不使用插件',
|
||||||
|
key: '',
|
||||||
|
icon: iconUnUse,
|
||||||
|
balance: 0
|
||||||
|
},
|
||||||
|
...data
|
||||||
|
]
|
||||||
|
// const pluginId = cache.get(PLUGIN_MODEL_KEY) || ''
|
||||||
|
// const res = data.filter((item: any) => item.key == pluginId.value)
|
||||||
|
// if (res.length) {
|
||||||
|
// currentId.value = pluginId.value || ''
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onShow(() => {
|
||||||
|
getData()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(currentId, (value) => {
|
||||||
|
emit('change', value)
|
||||||
|
emit('update:current', currentPlugin.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chat-plugins {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
inset: auto auto 130% 0%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
flex: 1;
|
||||||
|
width: 100vw;
|
||||||
|
white-space: pre;
|
||||||
|
z-index: 999;
|
||||||
|
@apply bg-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
src/components/chat-plugins/vip-use.vue
Normal file
61
src/components/chat-plugins/vip-use.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="px-[20rpx] h-[90rpx] text-sm flex items-center justify-between bg-white"
|
||||||
|
>
|
||||||
|
<view class="flex">
|
||||||
|
<u-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="medium"
|
||||||
|
:custom-style="{
|
||||||
|
background: 'transparent !important'
|
||||||
|
}"
|
||||||
|
v-if="appStore.getIsShowVip"
|
||||||
|
>
|
||||||
|
<router-navigate
|
||||||
|
class="text-primary"
|
||||||
|
to="/packages/pages/open_vip/open_vip"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
userInfo.is_member && userInfo.member_expired !== 1
|
||||||
|
? userInfo.member_package_name
|
||||||
|
: '开通会员'
|
||||||
|
}}
|
||||||
|
</router-navigate>
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view>
|
||||||
|
<view v-if="plugin.key" class="text-muted mr-2 text-xs">
|
||||||
|
<text v-if="!plugin.member_free">
|
||||||
|
<template v-if="plugin.balance > 0">
|
||||||
|
<text>消耗</text>
|
||||||
|
<text class="text-primary">
|
||||||
|
{{ plugin.balance }}
|
||||||
|
</text>
|
||||||
|
<text>条对话次数</text>
|
||||||
|
</template>
|
||||||
|
<template v-else> 免费 </template>
|
||||||
|
</text>
|
||||||
|
<text v-else> 会员免费 </text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
plugin: Record<string, any>
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
plugin: () => ({})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { userInfo } = storeToRefs(userStore)
|
||||||
|
</script>
|
||||||
305
src/components/chat-record-item/chat-record-item.vue
Normal file
305
src/components/chat-record-item/chat-record-item.vue
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
<template>
|
||||||
|
<view class="chat-record-item">
|
||||||
|
<view :class="`chat-record-item__${type}`">
|
||||||
|
<view>
|
||||||
|
<u-icon
|
||||||
|
class="rounded-full overflow-hidden"
|
||||||
|
:name="type == 'left' ? avatar : userStore.userInfo.avatar"
|
||||||
|
:size="60"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="min-w-0 flex flex-col"
|
||||||
|
:class="{ 'justify-end': type == 'right' }"
|
||||||
|
>
|
||||||
|
<div v-if="time" class="ml-[25rpx] mb-[20rpx] text-muted">
|
||||||
|
{{ time }}
|
||||||
|
<u-tag
|
||||||
|
v-if="modelName"
|
||||||
|
class="ml-2"
|
||||||
|
size="mini"
|
||||||
|
:text="modelName"
|
||||||
|
style="
|
||||||
|
--color-success: #67c23a;
|
||||||
|
--color-success-light-3: transparent;
|
||||||
|
--color-success-light-9: #f0f9eb;
|
||||||
|
"
|
||||||
|
type="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<view :class="`chat-record-item__${type}-content`">
|
||||||
|
<!-- <slot name="content-header"> </slot> -->
|
||||||
|
<template v-if="file.type">
|
||||||
|
<RecordImage
|
||||||
|
v-if="file.type == 1"
|
||||||
|
:url="file.url"
|
||||||
|
:name="file.name"
|
||||||
|
/>
|
||||||
|
<RecordFile
|
||||||
|
v-else-if="file.type == 2"
|
||||||
|
:url="file.url"
|
||||||
|
:name="file.name"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<view>
|
||||||
|
<view
|
||||||
|
class="mb-[20rpx] flex"
|
||||||
|
:class="{
|
||||||
|
'justify-end': type === 'right'
|
||||||
|
}"
|
||||||
|
v-if="audio"
|
||||||
|
>
|
||||||
|
<audio-play
|
||||||
|
:url="audio"
|
||||||
|
:reverse="type === 'right'"
|
||||||
|
:bg-color="'#fff'"
|
||||||
|
></audio-play>
|
||||||
|
</view>
|
||||||
|
<template v-if="isArray(content)">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in content"
|
||||||
|
:key="index"
|
||||||
|
class="mb-[20rpx] last-of-type:mb-0"
|
||||||
|
:class="{
|
||||||
|
'pt-[20rpx] border-t border-solid border-light border-0':
|
||||||
|
index > 0
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<text-item
|
||||||
|
:is-markdown="isMarkdown"
|
||||||
|
:content="item"
|
||||||
|
:loading="loading"
|
||||||
|
:index="index"
|
||||||
|
:record-id="recordId"
|
||||||
|
:show-copy-btn="
|
||||||
|
showCopyBtn && type === 'left'
|
||||||
|
"
|
||||||
|
:show-voice-btn="appStore.getIsVoiceOpen"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<text-item
|
||||||
|
:is-markdown="isMarkdown"
|
||||||
|
:content="content"
|
||||||
|
:loading="loading"
|
||||||
|
:show-copy-btn="showCopyBtn && type === 'left'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view
|
||||||
|
class="flex items-center text-muted text-sm mt-[16rpx]"
|
||||||
|
v-if="loading"
|
||||||
|
>
|
||||||
|
<u-loading mode="flower"></u-loading>
|
||||||
|
<view class="ml-[10rpx]">加载中,请稍等</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-if="type == 'right'"
|
||||||
|
class="flex items-center justify-end pr-[20rpx] pt-[10rpx]"
|
||||||
|
@click="copy(content)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="w-[26rpx] h-[26rpx]"
|
||||||
|
src="@/static/images/icon/icon_copy.png"
|
||||||
|
></image>
|
||||||
|
<text class="text-xs text-muted ml-[8rpx]">复制</text>
|
||||||
|
</view>
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
<view v-if="!loading && type === 'left'">
|
||||||
|
<view class="my-[16rpx] flex justify-end text-muted">
|
||||||
|
<view
|
||||||
|
v-if="showRewriteBtn"
|
||||||
|
class="text-xs flex items-center rounded-full ml-[20rpx]"
|
||||||
|
@click="emit('rewrite')"
|
||||||
|
>
|
||||||
|
<u-icon
|
||||||
|
name="reload"
|
||||||
|
class="mr-[8rpx]"
|
||||||
|
:size="30"
|
||||||
|
/>
|
||||||
|
重写
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="showCollectBtn && recordId"
|
||||||
|
class="text-xs flex items-center rounded-full ml-[20rpx]"
|
||||||
|
:class="{
|
||||||
|
'text-primary': isCollect,
|
||||||
|
'text-muted': !isCollect
|
||||||
|
}"
|
||||||
|
@click="handleCollect(recordId)"
|
||||||
|
>
|
||||||
|
<u-icon
|
||||||
|
:name="isCollect ? 'star-fill' : 'star'"
|
||||||
|
class="mr-[8rpx]"
|
||||||
|
:size="30"
|
||||||
|
/>
|
||||||
|
<text class="text-muted">收藏</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="showPosterBtn && recordId"
|
||||||
|
class="text-xs flex items-center rounded-full ml-[20rpx]"
|
||||||
|
@click="emit('click-poster', recordId)"
|
||||||
|
>
|
||||||
|
<u-icon
|
||||||
|
name="photo"
|
||||||
|
class="mr-[8rpx]"
|
||||||
|
:size="30"
|
||||||
|
></u-icon>
|
||||||
|
生成海报
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 生成海报 -->
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { cancelCollectChatRecord, collectChatRecord } from '@/api/chat'
|
||||||
|
import { useCopy } from '@/hooks/useCopy'
|
||||||
|
import { useLockFn } from '@/hooks/useLockFn'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import TextItem from './text-item.vue'
|
||||||
|
import { isArray } from 'lodash-es'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import RecordImage from './record-image.vue'
|
||||||
|
import RecordFile from './record-file.vue'
|
||||||
|
// import
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
recordId?: number | string
|
||||||
|
type: 'left' | 'right'
|
||||||
|
content: string
|
||||||
|
showCopyBtn?: boolean
|
||||||
|
showCollectBtn?: boolean
|
||||||
|
showRewriteBtn?: boolean
|
||||||
|
showPosterBtn?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
index?: number
|
||||||
|
isCollect?: number
|
||||||
|
audio?: string
|
||||||
|
modelName?: string
|
||||||
|
avatar: string
|
||||||
|
time?: string
|
||||||
|
file?: Record<string, any>
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
showCollectBtn: true,
|
||||||
|
showCopyBtn: true,
|
||||||
|
showRewriteBtn: false,
|
||||||
|
showPosterBtn: false,
|
||||||
|
content: '',
|
||||||
|
loading: false,
|
||||||
|
modelName: '',
|
||||||
|
time: '',
|
||||||
|
file: () => ({})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'close'): void
|
||||||
|
(event: 'rewrite'): void
|
||||||
|
(event: 'update', value: any): void
|
||||||
|
(event: 'click-poster', value?: number | string): void
|
||||||
|
}>()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { copy } = useCopy()
|
||||||
|
|
||||||
|
const { lockFn: handleCollect } = useLockFn(async (id: number | string) => {
|
||||||
|
if (props.isCollect) {
|
||||||
|
await cancelCollectChatRecord({
|
||||||
|
id: id
|
||||||
|
})
|
||||||
|
uni.$u.toast('取消收藏')
|
||||||
|
emit('update', { index: props.index, value: 0 })
|
||||||
|
} else {
|
||||||
|
await collectChatRecord({
|
||||||
|
id: id
|
||||||
|
})
|
||||||
|
uni.$u.toast('收藏成功')
|
||||||
|
emit('update', { index: props.index, value: 1 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isMarkdown = computed(() => {
|
||||||
|
return appStore.getChatConfig.isMarkdown && props.type == 'left'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@keyframes typingFade {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chat-record-item {
|
||||||
|
padding: 0 20rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
&__left,
|
||||||
|
&__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 80rpx;
|
||||||
|
&-content {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 20rpx;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
position: relative;
|
||||||
|
min-width: 70rpx;
|
||||||
|
min-height: 80rpx;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 24rpx;
|
||||||
|
border: 16rpx solid transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text-typing {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: -8rpx;
|
||||||
|
height: 34rpx;
|
||||||
|
width: 6rpx;
|
||||||
|
background-color: $u-type-primary;
|
||||||
|
animation: typingFade 0.4s infinite alternate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__right {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
&__left-content {
|
||||||
|
margin-left: 25rpx;
|
||||||
|
background-color: $u-bg-color;
|
||||||
|
&::before {
|
||||||
|
left: -30rpx;
|
||||||
|
border-right-color: $u-bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__right-content {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #4073fa;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
&::before {
|
||||||
|
right: -30rpx;
|
||||||
|
border-left-color: #4073fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
src/components/chat-record-item/record-file.vue
Normal file
49
src/components/chat-record-item/record-file.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
v-if="url"
|
||||||
|
class="flex mb-[12rpx] bg-white rounded-[10rpx] p-[20rpx] max-w-[450rpx] items-center"
|
||||||
|
@click="onPreview"
|
||||||
|
>
|
||||||
|
<u-image class="flex-none" :src="icon_doc" width="80" height="80" />
|
||||||
|
<view
|
||||||
|
class="line-clamp-2 text-main flex-1 min-w-0 ml-[12rpx]"
|
||||||
|
:style="{
|
||||||
|
'word-break': 'break-word'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import icon_doc from '@/static/images/icon/icon_doc.png'
|
||||||
|
const props = defineProps<{
|
||||||
|
url: string
|
||||||
|
name: string
|
||||||
|
}>()
|
||||||
|
const onPreview = async () => {
|
||||||
|
//#ifdef H5
|
||||||
|
window.open(props.url, '_blank')
|
||||||
|
//#endif
|
||||||
|
//#ifndef H5
|
||||||
|
try {
|
||||||
|
uni.showLoading({
|
||||||
|
title: '请稍等...'
|
||||||
|
})
|
||||||
|
const { tempFilePath } = await uni.downloadFile({
|
||||||
|
url: props.url
|
||||||
|
})
|
||||||
|
await uni.openDocument({
|
||||||
|
filePath: tempFilePath,
|
||||||
|
showMenu: true
|
||||||
|
})
|
||||||
|
uni.hideLoading()
|
||||||
|
} catch (error) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.$u.toast(`文件${props.name}打开失败,请重试`)
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
</script>
|
||||||
34
src/components/chat-record-item/record-image.vue
Normal file
34
src/components/chat-record-item/record-image.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
v-if="url"
|
||||||
|
class="flex mb-[12rpx] bg-white rounded-[10rpx] p-[20rpx] max-w-[450rpx] items-center"
|
||||||
|
>
|
||||||
|
<u-image
|
||||||
|
class="flex-none"
|
||||||
|
:src="url"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
@click="onPreview([url])"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
class="line-clamp-2 text-main flex-1 min-w-0 ml-[12rpx]"
|
||||||
|
:style="{
|
||||||
|
'word-break': 'break-word'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps<{
|
||||||
|
url: string
|
||||||
|
name: string
|
||||||
|
}>()
|
||||||
|
const onPreview = (picture: string[]) => {
|
||||||
|
uni.previewImage({
|
||||||
|
urls: picture
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
91
src/components/chat-record-item/text-item.vue
Normal file
91
src/components/chat-record-item/text-item.vue
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<template v-if="isMarkdown">
|
||||||
|
<ua-markdown :content="content"></ua-markdown>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<text
|
||||||
|
user-select
|
||||||
|
class="whitespace-pre-line leading-[40rpx] select-text"
|
||||||
|
>
|
||||||
|
{{ content }}
|
||||||
|
</text>
|
||||||
|
</template>
|
||||||
|
<view class="flex items-center" v-if="!loading">
|
||||||
|
<view
|
||||||
|
v-if="showCopyBtn"
|
||||||
|
class="text-content text-sm flex items-center mr-[20rpx] mt-[16rpx]"
|
||||||
|
@click="copy(content)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="w-[26rpx] h-[26rpx] mr-[8rpx]"
|
||||||
|
src="@/static/images/common/icon_copy.png"
|
||||||
|
alt="复制"
|
||||||
|
/>
|
||||||
|
复制
|
||||||
|
</view>
|
||||||
|
<template v-if="showVoiceBtn">
|
||||||
|
<view
|
||||||
|
v-if="!audioPlaying"
|
||||||
|
class="text-content text-sm flex items-center mt-[16rpx]"
|
||||||
|
@click="play"
|
||||||
|
>
|
||||||
|
<u-loading
|
||||||
|
v-if="audioLoading"
|
||||||
|
mode="flower"
|
||||||
|
class="mr-[8rpx]"
|
||||||
|
:size="26"
|
||||||
|
></u-loading>
|
||||||
|
<u-icon v-else name="volume" class="mr-[8rpx]" />
|
||||||
|
朗读
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
|
class="text-content text-sm flex items-center mt-[16rpx]"
|
||||||
|
@click="pause"
|
||||||
|
>
|
||||||
|
<u-icon name="volume" class="mr-[8rpx]" />
|
||||||
|
停止
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: true
|
||||||
|
},
|
||||||
|
externalClasses: ['class']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getChatBroadcast } from '@/api/chat'
|
||||||
|
import { useAudioPlay } from '@/hooks/useAudioPlay'
|
||||||
|
import { useCopy } from '@/hooks/useCopy'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
content: string
|
||||||
|
isMarkdown: boolean
|
||||||
|
loading?: boolean
|
||||||
|
showCopyBtn?: boolean
|
||||||
|
showVoiceBtn?: boolean
|
||||||
|
recordId?: number | string
|
||||||
|
index?: number
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
showCopyBtn: false,
|
||||||
|
loading: false,
|
||||||
|
showVoiceBtn: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const { copy } = useCopy()
|
||||||
|
const { play, audioPlaying, pause, audioLoading } = useAudioPlay({
|
||||||
|
api: getChatBroadcast,
|
||||||
|
dataTransform(data) {
|
||||||
|
return data.path
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
text: props.content.toString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
865
src/components/chat-scroll-view/chat-scroll-view.vue
Normal file
865
src/components/chat-scroll-view/chat-scroll-view.vue
Normal file
@ -0,0 +1,865 @@
|
|||||||
|
<template>
|
||||||
|
<view class="chat-scroll-view h-full flex flex-col">
|
||||||
|
<view class="flex-1 min-h-0">
|
||||||
|
<z-paging
|
||||||
|
ref="pagingRef"
|
||||||
|
v-model="chatList"
|
||||||
|
use-chat-record-mode
|
||||||
|
:auto="false"
|
||||||
|
:refresher-enabled="false"
|
||||||
|
:safe-area-inset-bottom="true"
|
||||||
|
:auto-clean-list-when-reload="false"
|
||||||
|
:show-chat-loading-when-reload="true"
|
||||||
|
:paging-style="{ bottom: keyboardIsShow ? 0 : bottom }"
|
||||||
|
:default-page-size="20"
|
||||||
|
@query="queryList"
|
||||||
|
@keyboardHeightChange="keyboardHeightChange"
|
||||||
|
@hidedKeyboard="hidedKeyboard"
|
||||||
|
>
|
||||||
|
<!-- 顶部提示文字 -->
|
||||||
|
|
||||||
|
<!-- style="transform: scaleY(-1)"必须写,否则会导致列表倒置(必须写在for循环标签上,不得写在容器上)!!! -->
|
||||||
|
<!-- 注意不要直接在chat-item组件标签上设置style,因为在微信小程序中是无效的,请包一层view -->
|
||||||
|
|
||||||
|
<template #top>
|
||||||
|
<slot name="top" />
|
||||||
|
<model-picker
|
||||||
|
v-if="!pluginOptions.pluginId"
|
||||||
|
v-model:chatKey="chatKey"
|
||||||
|
v-model:modelKey="modelKey"
|
||||||
|
/>
|
||||||
|
<VipUse
|
||||||
|
v-if="pluginOptions.pluginId"
|
||||||
|
:plugin="pluginOptions.current"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<view class="scroll-view-content pb-[20rpx]" ref="contentRef">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in chatList"
|
||||||
|
:key="`${item.id} + ${index} + ''`"
|
||||||
|
style="transform: scaleY(-1)"
|
||||||
|
>
|
||||||
|
<view class="chat-record mt-[20rpx] pb-[40rpx]">
|
||||||
|
<chat-record-item
|
||||||
|
:record-id="item.id"
|
||||||
|
:type="item.type == 1 ? 'right' : 'left'"
|
||||||
|
:content="item.content"
|
||||||
|
:loading="item.loading"
|
||||||
|
:audio="item.voiceFile"
|
||||||
|
:index="index"
|
||||||
|
:time="item.type == 2 ? item.createTime : ''"
|
||||||
|
:model-name="appStore.getChatConfig.show_model ? item.model : ''"
|
||||||
|
:is-collect="item.is_collect"
|
||||||
|
:avatar="avatar"
|
||||||
|
:showRewriteBtn="index === 0"
|
||||||
|
:showPosterBtn="true"
|
||||||
|
:showCopyBtn="true"
|
||||||
|
:file="{
|
||||||
|
url: item.fileUrl,
|
||||||
|
name: item.fileName,
|
||||||
|
type: item.fileType
|
||||||
|
}"
|
||||||
|
@rewrite="rewrite(index)"
|
||||||
|
@update="(e: any) => chatList[e.index].is_collect = e.value"
|
||||||
|
@click-poster="handleDrawPoster"
|
||||||
|
>
|
||||||
|
</chat-record-item>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<template #empty>
|
||||||
|
<slot name="empty" />
|
||||||
|
</template>
|
||||||
|
<template #bottom>
|
||||||
|
<view class="send-area">
|
||||||
|
<view class="float-btn">
|
||||||
|
<view
|
||||||
|
v-if="chatList.length && !isReceiving"
|
||||||
|
class="px-[20rpx] py-[10rpx] text-xs flex items-center"
|
||||||
|
@click="sendLock('继续')"
|
||||||
|
>
|
||||||
|
<u-icon name="play-circle" class="mr-[8rpx]" size="36" />
|
||||||
|
继续
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="isReceiving"
|
||||||
|
class="px-[20rpx] py-[10rpx] text-xs flex items-center"
|
||||||
|
@click="chatClose()"
|
||||||
|
>
|
||||||
|
<u-icon name="pause-circle" class="mr-[8rpx]" size="36" />
|
||||||
|
停止
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="mb-[20rpx]" v-if="isShowFileUpload">
|
||||||
|
<file-upload
|
||||||
|
v-model="pluginOptions.file"
|
||||||
|
return-type="object"
|
||||||
|
:file-type="pluginOptions.type"
|
||||||
|
:file-extname="getFileExtname"
|
||||||
|
:data="{
|
||||||
|
type:
|
||||||
|
pluginOptions.type == 'file'
|
||||||
|
? 'docs'
|
||||||
|
: '',
|
||||||
|
key: pluginOptions.type != 'image' ? pluginOptions.pluginId : ''
|
||||||
|
}"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="mb-[20rpx] flex items-center">
|
||||||
|
<view class="flex items-center mr-auto">
|
||||||
|
<chat-plugins
|
||||||
|
v-if="type === 1"
|
||||||
|
v-model="pluginOptions.pluginId"
|
||||||
|
v-model:current="pluginOptions.current"
|
||||||
|
@change="pluginChange"
|
||||||
|
>
|
||||||
|
</chat-plugins>
|
||||||
|
<network-switch
|
||||||
|
v-if="!pluginOptions.pluginId"
|
||||||
|
v-model="network"
|
||||||
|
></network-switch>
|
||||||
|
</view>
|
||||||
|
<view class="flex text-content items-center flex-none">
|
||||||
|
<view class="text-xs flex items-center" @click="cleanChatLock">
|
||||||
|
<u-icon name="trash" class="mr-[4rpx]" size="28" />
|
||||||
|
清空
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="send-area__content bg-page-base"
|
||||||
|
:class="[safeAreaInsetBottom ? 'safe-area-inset-bottom' : '']"
|
||||||
|
>
|
||||||
|
<view class="flex-1 min-w-0 relative">
|
||||||
|
<view
|
||||||
|
v-if="showPressBtn"
|
||||||
|
class="absolute left-[-10rpx] top-[-15rpx] bottom-[-15rpx] bg-primary text-btn-text right-[0] z-[9999] flex items-center justify-center rounded-[12rpx]"
|
||||||
|
@longpress="handleLongpress"
|
||||||
|
@touchend="touchEnd"
|
||||||
|
@touchcancel="touchEnd"
|
||||||
|
>
|
||||||
|
按住说话
|
||||||
|
</view>
|
||||||
|
<u-input
|
||||||
|
type="textarea"
|
||||||
|
v-model="userInput"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
maxlength="-1"
|
||||||
|
:auto-height="true"
|
||||||
|
confirm-type="send"
|
||||||
|
:adjust-position="false"
|
||||||
|
:fixed="false"
|
||||||
|
adjust-keyboard-to="bottom"
|
||||||
|
@click="handleClick"
|
||||||
|
@focus="scrollToBottom"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="ml-[20rpx] my-[-12rpx]">
|
||||||
|
<view v-if="userInput">
|
||||||
|
<u-button
|
||||||
|
type="primary"
|
||||||
|
:custom-style="{
|
||||||
|
width: '100rpx',
|
||||||
|
height: '52rpx',
|
||||||
|
margin: '0'
|
||||||
|
}"
|
||||||
|
size="mini"
|
||||||
|
:disabled="isReceiving"
|
||||||
|
@click.stop="sendLock()"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="appStore.getIsVoiceTransfer">
|
||||||
|
<view
|
||||||
|
v-if="showPressBtn"
|
||||||
|
class="text-content"
|
||||||
|
@click="triggerRecordShow"
|
||||||
|
>
|
||||||
|
<u-icon name="more-circle" :size="52" />
|
||||||
|
</view>
|
||||||
|
<view v-else class="text-content" @click="triggerRecordShow">
|
||||||
|
<u-icon name="mic" :size="52" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="type == 1 && appStore.getIsVoiceChat">
|
||||||
|
<dragon-button :size="184" :yEdge="160">
|
||||||
|
<view class="p-[20rpx]" @click="triggerVoiceShow">
|
||||||
|
<view class="flex justify-center mb-[-20rpx] relative z-[99]">
|
||||||
|
<image
|
||||||
|
class="w-[70rpx] h-[70rpx] mb-[10rpx]"
|
||||||
|
:src="loadingPath"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<u-button
|
||||||
|
hover-class="none"
|
||||||
|
:custom-style="{
|
||||||
|
width: '100%',
|
||||||
|
height: '58rpx',
|
||||||
|
fontSize: '24rpx',
|
||||||
|
background: '#28C840',
|
||||||
|
'box-shadow': '0 3px 10px #00000033'
|
||||||
|
}"
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
>
|
||||||
|
在线语音
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</dragon-button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</z-paging>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<guided-popup ref="guidedPopupRef" />
|
||||||
|
<!--#ifdef APP-PLUS-->
|
||||||
|
<appChat
|
||||||
|
@onmessage="appOnmessage"
|
||||||
|
@onclose="appOnclose"
|
||||||
|
@onstart="appOnstart"
|
||||||
|
ref="appChatRef"
|
||||||
|
></appChat>
|
||||||
|
<!--#endif-->
|
||||||
|
<recorder ref="recorderRef" v-model:show="showRecorder" @success="sendLock" />
|
||||||
|
</view>
|
||||||
|
<online-voice
|
||||||
|
v-model:show="showOnlineVoice"
|
||||||
|
:data="{
|
||||||
|
chatKey: chatKey,
|
||||||
|
modelKey: modelKey,
|
||||||
|
type: type,
|
||||||
|
categoryId: otherId,
|
||||||
|
network: network
|
||||||
|
}"
|
||||||
|
@update="pagingRef?.reload()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 生产对话海报 -->
|
||||||
|
<dialog-poster ref="posterRef"></dialog-poster>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: true,
|
||||||
|
styleIsolation: 'shared'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { chatSendText, cleanChatRecord, getChatRecord, delChatRecord } from '@/api/chat'
|
||||||
|
import { useLockFn } from '@/hooks/useLockFn'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
|
import { useRouter } from 'uniapp-router-next'
|
||||||
|
import { onUnmounted, watch, ref, shallowRef, reactive, computed } from 'vue'
|
||||||
|
|
||||||
|
import { onHide, onShow } from '@dcloudio/uni-app'
|
||||||
|
import { useSessionList } from '@/pages/index/components/useSessionList'
|
||||||
|
import { RequestErrMsgEnum } from '@/enums/requestEnums'
|
||||||
|
import { useAudioPlay } from '@/hooks/useAudioPlay'
|
||||||
|
import { CHAT_LIMIT_KEY } from '@/enums/constantEnums'
|
||||||
|
import { isNewDay } from '@/utils/validate'
|
||||||
|
import { useRecorder } from '@/hooks/useRecorder'
|
||||||
|
import OnlineVoice from './components/online-voice.vue'
|
||||||
|
import { useAudio } from '@/hooks/useAudio'
|
||||||
|
import { vibrate } from '@/utils/device/vibrate'
|
||||||
|
// import cache from '@/utils/cache'
|
||||||
|
import config from '@/config'
|
||||||
|
import VipUse from '../chat-plugins/vip-use.vue'
|
||||||
|
import ModelPicker from '../model-picker/model-picker.vue'
|
||||||
|
//#ifdef APP-PLUS
|
||||||
|
import appChat from './components/app-chat'
|
||||||
|
const appChatRef = shallowRef()
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
const loadingUrl = '/api/static/bubble.gif'
|
||||||
|
let loadingPath = `${config.baseUrl}${loadingUrl}`
|
||||||
|
//#ifdef H5
|
||||||
|
loadingPath = `${config.baseUrl === '/' ? `${location.origin}/` : config.baseUrl}${loadingUrl}`
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: any[]): void
|
||||||
|
(event: 'reader', value: any): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
type: number
|
||||||
|
otherId?: number
|
||||||
|
tips?: string
|
||||||
|
bottom?: string
|
||||||
|
placeholder?: string
|
||||||
|
safeAreaInsetBottom: boolean
|
||||||
|
showAdd?: boolean
|
||||||
|
avatar?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
tips: '',
|
||||||
|
placeholder: '请输入内容',
|
||||||
|
safeAreaInsetBottom: false,
|
||||||
|
showAdd: false,
|
||||||
|
bottom: '0'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const chatKey = ref('')
|
||||||
|
const modelKey = ref('')
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const pagingRef = shallowRef()
|
||||||
|
const contentRef = shallowRef()
|
||||||
|
const guidedPopupRef = shallowRef()
|
||||||
|
const network = ref(false)
|
||||||
|
const showPressBtn = ref(false)
|
||||||
|
const showRecorder = ref(false)
|
||||||
|
const showOnlineVoice = ref(false)
|
||||||
|
|
||||||
|
const { sessionActive, sessionAdd, currentSession, sessionEdit } = useSessionList()
|
||||||
|
const chatList = ref<any[]>([])
|
||||||
|
const pluginOptions = reactive({
|
||||||
|
type: 'file',
|
||||||
|
file: { url: '' } as any,
|
||||||
|
pluginId: '',
|
||||||
|
current: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const userInput = ref('')
|
||||||
|
const newUserInput = ref('')
|
||||||
|
const { authorize } = useRecorder({})
|
||||||
|
const { pauseAll } = useAudio()
|
||||||
|
|
||||||
|
const getFileExtname = computed(() => {
|
||||||
|
switch (pluginOptions.type) {
|
||||||
|
case 'image':
|
||||||
|
return ['jpg', 'png', 'gif', 'jpeg']
|
||||||
|
default:
|
||||||
|
return ['doc', 'docx', 'pdf', 'md', 'txt']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isShowFileUpload = computed(() => {
|
||||||
|
return (
|
||||||
|
pluginOptions.pluginId === 'xinghuo-chatdoc' ||
|
||||||
|
pluginOptions.pluginId === 'gpt-4-all' ||
|
||||||
|
pluginOptions.pluginId === 'kimi-chatdoc'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (!userStore.isLogin) {
|
||||||
|
return toLogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// action 是后端判断延迟返回数据的处理--因为停止对话时数据没入库就返回导致数据丢失问题
|
||||||
|
const actions = ref('')
|
||||||
|
const queryList = async (pageNo: number, pageSize: number) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// { lists = [], count }
|
||||||
|
const lists = await getChatRecord({
|
||||||
|
type: props.type,
|
||||||
|
action: actions.value,
|
||||||
|
otherId: props.type !== 1 ? props.otherId : '',
|
||||||
|
categoryId: props.type === 1 ? props.otherId : '',
|
||||||
|
// pageSize: pageSize / 2,
|
||||||
|
// pageNo: pageNo
|
||||||
|
})
|
||||||
|
|
||||||
|
// pagingRef.value?.complete(lists.reverse())
|
||||||
|
// pagingRef.value?.complete(lists)
|
||||||
|
chatList.value = lists.reverse()
|
||||||
|
if (lists.length === 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
pagingRef.value?.scrollToTop(false)
|
||||||
|
}, 200)
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
actions.value = 'stop'
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
if (!lists.length) {
|
||||||
|
pagingRef.value?.complete([])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
pagingRef.value?.complete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboardIsShow = ref(false)
|
||||||
|
|
||||||
|
const keyboardHeightChange = (res: any) => {
|
||||||
|
if (res.height > 0) {
|
||||||
|
keyboardIsShow.value = true
|
||||||
|
} else {
|
||||||
|
keyboardIsShow.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidedKeyboard = () => {
|
||||||
|
keyboardIsShow.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLongpress = async () => {
|
||||||
|
await recorderRef.value.startRecord()
|
||||||
|
showRecorder.value = true
|
||||||
|
vibrate(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginChange = (value: string) => {
|
||||||
|
console.log(value)
|
||||||
|
if (value === 'xinghuo-chatdoc') {
|
||||||
|
pluginOptions.type = 'file'
|
||||||
|
} else if (value === 'gpt-4-all') {
|
||||||
|
pluginOptions.type = 'image'
|
||||||
|
} else if (value === 'kimi-chatdoc') {
|
||||||
|
pluginOptions.type = 'file'
|
||||||
|
} else {
|
||||||
|
pluginOptions.type = ''
|
||||||
|
}
|
||||||
|
resetPluginData()
|
||||||
|
}
|
||||||
|
const resetPluginData = () => {
|
||||||
|
pluginOptions.file = { url: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.otherId,
|
||||||
|
(value) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (value) {
|
||||||
|
actions.value = ''
|
||||||
|
pagingRef.value?.reload()
|
||||||
|
} else {
|
||||||
|
pagingRef.value?.complete([])
|
||||||
|
setTimeout(() => {
|
||||||
|
pagingRef.value?.scrollToTop(false)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}, 10)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modelKey.value,
|
||||||
|
(value) => {
|
||||||
|
pluginOptions.pluginId = ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const triggerRecordShow = async () => {
|
||||||
|
//#ifdef APP-PLUS
|
||||||
|
uni.$u.toast('相关功能正在开发中')
|
||||||
|
return Promise.reject()
|
||||||
|
//#endif
|
||||||
|
if (showPressBtn.value) {
|
||||||
|
showPressBtn.value = false
|
||||||
|
} else {
|
||||||
|
await getRecordAuth()
|
||||||
|
pauseAll()
|
||||||
|
showPressBtn.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerVoiceShow = async () => {
|
||||||
|
//#ifdef APP-PLUS
|
||||||
|
uni.$u.toast('相关功能正在开发中')
|
||||||
|
return Promise.reject()
|
||||||
|
//#endif
|
||||||
|
await getRecordAuth()
|
||||||
|
pauseAll()
|
||||||
|
showOnlineVoice.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRecordAuth = async () => {
|
||||||
|
if (!userStore.isLogin) {
|
||||||
|
toLogin()
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await authorize()
|
||||||
|
} catch (error) {
|
||||||
|
uni.$u.toast(error)
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const recorderRef = shallowRef()
|
||||||
|
const touchEnd = () => {
|
||||||
|
recorderRef.value?.stopRecord()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 海报实力
|
||||||
|
const posterRef = shallowRef()
|
||||||
|
const handleDrawPoster = async (recordId: number) => {
|
||||||
|
const result = chatList.value.filter((item: any) => {
|
||||||
|
return item.id == recordId
|
||||||
|
})
|
||||||
|
if (result.length != 2) {
|
||||||
|
uni.$u.toast('上下文数据不对~')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
posterRef.value.initPosterData({
|
||||||
|
title: result[1].content,
|
||||||
|
content: result[0].content
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lockFn: rewrite } = useLockFn(async (index: number) => {
|
||||||
|
if (isReceiving.value) return
|
||||||
|
const last = chatList.value[index]
|
||||||
|
const userInput = chatList.value.findLast(({ id }) => id === last.id)
|
||||||
|
|
||||||
|
if (userInput) {
|
||||||
|
await delChatRecord({
|
||||||
|
id: last.id
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
chatList.value.splice(index, 2)
|
||||||
|
sendLock(userInput.content)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { lockFn: cleanChatLock } = useLockFn(async () => {
|
||||||
|
if (!userStore.isLogin) return toLogin()
|
||||||
|
|
||||||
|
const modal = await uni.showModal({
|
||||||
|
title: '温馨提示',
|
||||||
|
content: '确定清空对话?'
|
||||||
|
})
|
||||||
|
if (modal.cancel) return
|
||||||
|
chatClose()
|
||||||
|
await cleanChatRecord({
|
||||||
|
type: props.type,
|
||||||
|
otherId: props.type !== 1 ? props.otherId : '',
|
||||||
|
categoryId: props.type == 1 ? props.otherId : ''
|
||||||
|
})
|
||||||
|
pagingRef.value?.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
const scrollToBottom = async () => {
|
||||||
|
pagingRef.value?.scrollToBottom(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isReceiving = ref(false)
|
||||||
|
let streamReader: any = null
|
||||||
|
|
||||||
|
const chatClose = () => {
|
||||||
|
//#ifdef H5
|
||||||
|
streamReader?.abort()
|
||||||
|
//#endif
|
||||||
|
//#ifdef MP-WEIXIN
|
||||||
|
streamReader?.abort()
|
||||||
|
//#endif
|
||||||
|
//#ifdef APP-PLUS
|
||||||
|
appChatRef.value.stop()
|
||||||
|
//#endif
|
||||||
|
setTimeout(() => {
|
||||||
|
userInput.value = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const chatContent = ref<any>({})
|
||||||
|
const { pauseAll: pauseAllVoice } = useAudioPlay()
|
||||||
|
const { lockFn: sendLock } = useLockFn(async (text: string) => {
|
||||||
|
showRecorder.value = false
|
||||||
|
if (!userStore.isLogin) {
|
||||||
|
return toLogin()
|
||||||
|
}
|
||||||
|
if (isReceiving.value) return
|
||||||
|
if (userStore.userInfo.chatLimit && isNewDay(true, CHAT_LIMIT_KEY)) {
|
||||||
|
const res = await uni.showModal({
|
||||||
|
title: '对话上限提示',
|
||||||
|
content: '已超过会员对话上限次数,继续对话将会消耗账户的对话余额',
|
||||||
|
confirmText: '继续',
|
||||||
|
cancelText: '关闭'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.cancel) return (isReceiving.value = false)
|
||||||
|
}
|
||||||
|
const inputValue = text || userInput.value
|
||||||
|
if (!inputValue) {
|
||||||
|
uni.$u.toast(props.placeholder)
|
||||||
|
isReceiving.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (props.type == 1) {
|
||||||
|
if (sessionActive.value === 0) {
|
||||||
|
await sessionAdd()
|
||||||
|
}
|
||||||
|
if (currentSession.value === '新的会话') {
|
||||||
|
await sessionEdit(sessionActive.value, inputValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newUserInput.value = userInput.value
|
||||||
|
userInput.value = ''
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
chatKey: chatKey.value,
|
||||||
|
modelKey: pluginOptions.pluginId ? pluginOptions.pluginId : modelKey.value,
|
||||||
|
question: inputValue,
|
||||||
|
type: props.type,
|
||||||
|
categoryId: props.otherId,
|
||||||
|
otherId: props.otherId,
|
||||||
|
network: network.value,
|
||||||
|
file: pluginOptions.file.url
|
||||||
|
}
|
||||||
|
pagingRef.value.addChatRecordData({
|
||||||
|
type: 1,
|
||||||
|
content: inputValue,
|
||||||
|
fileUrl: pluginOptions.file.url,
|
||||||
|
fileName: pluginOptions.file.name,
|
||||||
|
fileType:
|
||||||
|
pluginOptions.type == '' ? 0 : pluginOptions.type == 'image' ? 1 : 2
|
||||||
|
})
|
||||||
|
chatContent.value = {
|
||||||
|
type: 2,
|
||||||
|
loading: true,
|
||||||
|
content: [] as string[]
|
||||||
|
}
|
||||||
|
pagingRef.value.addChatRecordData(chatContent.value)
|
||||||
|
//#ifdef APP-PLUS
|
||||||
|
appChatRef.value.getParamsData(options)
|
||||||
|
//#endif
|
||||||
|
//#ifndef APP-PLUS
|
||||||
|
try {
|
||||||
|
isReceiving.value = true
|
||||||
|
await chatSendText(options, {
|
||||||
|
onstart(reader) {
|
||||||
|
streamReader = reader
|
||||||
|
pauseAllVoice()
|
||||||
|
userInput.value = ''
|
||||||
|
emit('reader', reader)
|
||||||
|
},
|
||||||
|
onmessage(value) {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.split('data:')
|
||||||
|
.forEach(async (text) => {
|
||||||
|
if (text !== '') {
|
||||||
|
try {
|
||||||
|
const dataJson = JSON.parse(text)
|
||||||
|
|
||||||
|
console.log(dataJson)
|
||||||
|
|
||||||
|
const { id, event, data, error, index } = dataJson
|
||||||
|
|
||||||
|
if (error && error?.errCode == 336) {
|
||||||
|
userInput.value = newUserInput.value
|
||||||
|
guidedPopupRef.value?.open()
|
||||||
|
return
|
||||||
|
} else if (error) {
|
||||||
|
uni.$u.toast(error.errMsg)
|
||||||
|
isReceiving.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// let chatIndex = chatList.value.findIndex(
|
||||||
|
// (item) => item.id === chatId
|
||||||
|
// )
|
||||||
|
// if (chatIndex === -1) {
|
||||||
|
// chatIndex = chatList.value.length - 1
|
||||||
|
// chatList.value[chatIndex].id = chatId
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (data) {
|
||||||
|
// if (!chatList.value[chatIndex].content[index]) {
|
||||||
|
// chatList.value[chatIndex].content[index] = ''
|
||||||
|
// }
|
||||||
|
// chatList.value[chatIndex].content[index] += data
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
if (!chatContent.value.content[index]) {
|
||||||
|
chatContent.value.content[index] = ''
|
||||||
|
}
|
||||||
|
chatContent.value.content[index] += data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'finish') {
|
||||||
|
chatContent.value.loading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('转换失败=>', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
isReceiving.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
pagingRef.value?.reload()
|
||||||
|
}, 600)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('发送消息失败=>', error)
|
||||||
|
if (error.errMsg !== RequestErrMsgEnum.ABORT) {
|
||||||
|
chatList.value.splice(chatList.value.length - 2, 2)
|
||||||
|
}
|
||||||
|
userInput.value = newUserInput.value
|
||||||
|
isReceiving.value = false
|
||||||
|
}
|
||||||
|
//#endif
|
||||||
|
})
|
||||||
|
|
||||||
|
//#ifdef APP-PLUS
|
||||||
|
const appOnmessage = (value: any) => {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.split('data:')
|
||||||
|
.forEach(async (text: any) => {
|
||||||
|
if (text !== '') {
|
||||||
|
try {
|
||||||
|
const dataJson = JSON.parse(text)
|
||||||
|
|
||||||
|
console.log(dataJson)
|
||||||
|
|
||||||
|
const { id, event, data, error, index } = dataJson
|
||||||
|
|
||||||
|
if (error && error?.errCode == 336) {
|
||||||
|
userInput.value = newUserInput.value
|
||||||
|
guidedPopupRef.value?.open()
|
||||||
|
return
|
||||||
|
} else if (error) {
|
||||||
|
uni.$u.toast(error.errMsg)
|
||||||
|
isReceiving.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
if (!chatContent.value.content[index]) {
|
||||||
|
chatContent.value.content[index] = ''
|
||||||
|
}
|
||||||
|
chatContent.value.content[index] += data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'finish') {
|
||||||
|
chatContent.value.loading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const appOnclose = (value: any) => {
|
||||||
|
isReceiving.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
pagingRef.value?.reload()
|
||||||
|
}, 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
const appOnstart = (value: any) => {
|
||||||
|
// console.log(value)
|
||||||
|
streamReader = value
|
||||||
|
emit('reader', value)
|
||||||
|
}
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
const toLogin = () => {
|
||||||
|
router.navigateTo({ path: '/pages/login/login' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUserInput = (value = '') => {
|
||||||
|
userInput.value = value
|
||||||
|
}
|
||||||
|
onUnmounted(() => {
|
||||||
|
chatClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(sessionActive, async (value) => {
|
||||||
|
if (value) {
|
||||||
|
chatClose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
//修复从技能页面回到首页时input框无法正常上弹(可能是微信小程序offKeyboardHeightChange的bug)
|
||||||
|
setTimeout(() => {
|
||||||
|
// 监听键盘高度变化(H5、百度小程序、抖音小程序、飞书小程序不支持)
|
||||||
|
// #ifndef H5 || MP-BAIDU || MP-TOUTIAO
|
||||||
|
uni.onKeyboardHeightChange(pagingRef.value?._handleKeyboardHeightChange)
|
||||||
|
// #endif
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
scrollToBottom,
|
||||||
|
setUserInput,
|
||||||
|
sendLock,
|
||||||
|
rewrite
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chat-scroll-view {
|
||||||
|
.send-area {
|
||||||
|
position: relative;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
.float-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: -10rpx;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
z-index: 100;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
@apply bg-white border-light;
|
||||||
|
}
|
||||||
|
&__content {
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 25rpx 20rpx;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep() {
|
||||||
|
.u-input__textarea {
|
||||||
|
--line-height: 40rpx;
|
||||||
|
--line-num: 4;
|
||||||
|
height: auto;
|
||||||
|
min-height: var(--line-height) !important;
|
||||||
|
max-height: calc(var(--line-height) * var(--line-num));
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
line-height: var(--line-height);
|
||||||
|
.uni-textarea-textarea {
|
||||||
|
max-height: calc(var(--line-height) * var(--line-num));
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.send-btn {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
right: 0rpx;
|
||||||
|
bottom: 10rpx;
|
||||||
|
z-index: 99;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chat-bubble {
|
||||||
|
width: 70rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
148
src/components/chat-scroll-view/components/app-chat.vue
Normal file
148
src/components/chat-scroll-view/components/app-chat.vue
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<text
|
||||||
|
:paramsData="paramsData"
|
||||||
|
:change:paramsData="appChatRender.getData"
|
||||||
|
></text>
|
||||||
|
<text
|
||||||
|
:requestData="requestData"
|
||||||
|
:change:requestData="appChatRender.getRequestData"
|
||||||
|
></text>
|
||||||
|
<!--暂停-->
|
||||||
|
<text
|
||||||
|
:stopCont="stopCont"
|
||||||
|
:change:stopCont="appChatRender.stopMid"
|
||||||
|
></text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { nextTick, ref, shallowRef } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import config from '@/config'
|
||||||
|
import { client } from '@/utils/client'
|
||||||
|
|
||||||
|
interface IParamsData {
|
||||||
|
model: string
|
||||||
|
question: string
|
||||||
|
type: number
|
||||||
|
other_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup(props, context) {
|
||||||
|
let { token } = useUserStore()
|
||||||
|
let baseUrl = ''
|
||||||
|
|
||||||
|
const requestData: any = ref({})
|
||||||
|
const paramsData = ref({})
|
||||||
|
//暂停
|
||||||
|
const stopCont: any = ref(0)
|
||||||
|
const stop = () => {
|
||||||
|
stopCont.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
const onmessage = (value: any) => {
|
||||||
|
context.emit('onmessage', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onclose = () => {
|
||||||
|
context.emit('onclose')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onstart = (reader: any) => {
|
||||||
|
console.log(reader.method)
|
||||||
|
context.emit('onstart', reader)
|
||||||
|
paramsData.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getParamsData = async (data: any) => {
|
||||||
|
token = useUserStore().token
|
||||||
|
baseUrl = config.baseUrl
|
||||||
|
requestData.value.baseUrl = baseUrl
|
||||||
|
requestData.value.token = token
|
||||||
|
requestData.value.terminal = client
|
||||||
|
setTimeout(() => {
|
||||||
|
Object.keys(data).map((item) => {
|
||||||
|
//@ts-ignore
|
||||||
|
paramsData.value[item] = data[item]
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
getParamsData,
|
||||||
|
stop,
|
||||||
|
paramsData,
|
||||||
|
requestData,
|
||||||
|
stopCont,
|
||||||
|
onmessage,
|
||||||
|
onclose,
|
||||||
|
onstart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script module="appChatRender" lang="renderjs">
|
||||||
|
// import { chatSendText } from '@/api/chat'
|
||||||
|
import { getChat } from "./request/index";
|
||||||
|
export default {
|
||||||
|
data(){
|
||||||
|
return{
|
||||||
|
paramData:{},
|
||||||
|
requestData:{},
|
||||||
|
count:0,
|
||||||
|
stopMethod:{},
|
||||||
|
evn:{}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods:{
|
||||||
|
async getData(newValue, oldValue, ownerInstance, instance){
|
||||||
|
this.paramData = newValue
|
||||||
|
if(this.count>0&&Object.keys(this.paramData).length!=0){
|
||||||
|
await this.sendChat({},ownerInstance)
|
||||||
|
}
|
||||||
|
this.count++
|
||||||
|
},
|
||||||
|
async getRequestData(newValue, oldValue, ownerInstance, instance){
|
||||||
|
this.requestData.token = newValue.token
|
||||||
|
this.requestData.baseUrl = newValue.baseUrl
|
||||||
|
this.requestData.terminal = newValue.terminal
|
||||||
|
},
|
||||||
|
async stopMid(newValue, oldValue, ownerInstance, instance){
|
||||||
|
this.stop()
|
||||||
|
},
|
||||||
|
sendChat(event,ownerInstance){
|
||||||
|
console.log(JSON.stringify(this.paramData))
|
||||||
|
|
||||||
|
try{
|
||||||
|
let that = this
|
||||||
|
getChat({...this.paramData},{
|
||||||
|
onstart(reader) {
|
||||||
|
console.log('开始对话')
|
||||||
|
that.stopMethod = reader.cancel.bind(reader)
|
||||||
|
ownerInstance.callMethod('onstart', {method:{}})
|
||||||
|
},
|
||||||
|
onmessage(value) {
|
||||||
|
ownerInstance.callMethod('onmessage', value)
|
||||||
|
},
|
||||||
|
onclose(value) {
|
||||||
|
console.log('对话结束', value)
|
||||||
|
ownerInstance.callMethod('onclose', value)
|
||||||
|
},
|
||||||
|
baseUrl:this.requestData.baseUrl,
|
||||||
|
token:this.requestData.token,
|
||||||
|
terminal: this.requestData.terminal
|
||||||
|
})
|
||||||
|
}catch(e){
|
||||||
|
console.log('错误了', e)
|
||||||
|
//TODO handle the exception
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stop(){
|
||||||
|
this.stopMethod()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
431
src/components/chat-scroll-view/components/online-voice.vue
Normal file
431
src/components/chat-scroll-view/components/online-voice.vue
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
<template>
|
||||||
|
<u-popup
|
||||||
|
v-model="showModel"
|
||||||
|
mode="bottom"
|
||||||
|
safe-area-inset-bottom
|
||||||
|
:mask="false"
|
||||||
|
height="100%"
|
||||||
|
z-index="99999"
|
||||||
|
:custom-style="{
|
||||||
|
background: '#191820'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<view class="h-full flex flex-col py-[120rpx] text-white">
|
||||||
|
<view class="mt-[80rpx] flex flex-col justify-center items-center">
|
||||||
|
<view class="relative">
|
||||||
|
<u-image
|
||||||
|
shape="circle"
|
||||||
|
width="220"
|
||||||
|
height="220"
|
||||||
|
:src="appStore.getChatConfig.chatLogo"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-if="chatStatus == ChatStatus.PLAYING"
|
||||||
|
class="bubble-loading flex justify-center absolute"
|
||||||
|
>
|
||||||
|
<loading size="9rpx" class="mt-[24rpx]" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="text-3xl mt-[20rpx]">{{
|
||||||
|
appStore.getChatConfig.chatTitle
|
||||||
|
}}</view>
|
||||||
|
</view>
|
||||||
|
<view class="flex-1 flex flex-col justify-center items-center">
|
||||||
|
<canvas
|
||||||
|
v-show="isRecording"
|
||||||
|
:style="{
|
||||||
|
width: `${canvasOptions.width}px`,
|
||||||
|
height: `${canvasOptions.height}px`
|
||||||
|
}"
|
||||||
|
:canvas-id="canvasOptions.id"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-if="
|
||||||
|
[
|
||||||
|
ChatStatus.TRANSFER,
|
||||||
|
ChatStatus.PLAYING,
|
||||||
|
ChatStatus.THINKING
|
||||||
|
].includes(chatStatus)
|
||||||
|
"
|
||||||
|
:src="loadingPath"
|
||||||
|
class="w-[250rpx] h-[250rpx]"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<view class="flex justify-center relative">
|
||||||
|
<view>
|
||||||
|
<view class="action-btn" @click="showModel = false">
|
||||||
|
<u-icon name="close" :size="32"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="relative flex justify-center items-center">
|
||||||
|
<view
|
||||||
|
class="w-[170rpx] h-[170rpx] rounded-[50%] bg-primary opacity-30 absolute"
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<button
|
||||||
|
class="flex justify-center items-center w-[140rpx] h-[140rpx] rounded-[50%] bg-primary text-btn-text relative z-10"
|
||||||
|
@click="startRecord"
|
||||||
|
hover-class="none"
|
||||||
|
>
|
||||||
|
<u-icon v-if="isCanRecord" name="mic" :size="60" />
|
||||||
|
<view v-if="isRecording" class="stop"> </view>
|
||||||
|
<loading
|
||||||
|
v-if="
|
||||||
|
[
|
||||||
|
ChatStatus.TRANSFER,
|
||||||
|
ChatStatus.THINKING,
|
||||||
|
ChatStatus.INITIALING
|
||||||
|
].includes(chatStatus)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<u-icon
|
||||||
|
v-if="chatStatus == ChatStatus.PLAYING"
|
||||||
|
name="pause-circle-fill"
|
||||||
|
:size="60"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="flex flex-col justify-center items-center mt-[42rpx] text-xs"
|
||||||
|
>
|
||||||
|
<view>
|
||||||
|
{{ statusToTextMap[chatStatus] }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<guided-popup ref="guidedPopupRef" />
|
||||||
|
</u-popup>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
styleIsolation: 'shared'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
audioTransfer,
|
||||||
|
chatSendText,
|
||||||
|
getChatVoiceGenerate,
|
||||||
|
getChatRecord
|
||||||
|
} from '@/api/chat'
|
||||||
|
import { useAudioPlay } from '@/hooks/useAudioPlay'
|
||||||
|
import {
|
||||||
|
AudioGraphUserOptions,
|
||||||
|
useRecorder,
|
||||||
|
useRenderAudioGraph
|
||||||
|
} from '@/hooks/useRecorder'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import { shallowRef } from 'vue'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { watch } from 'vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { parse } from 'jsonc-parser'
|
||||||
|
import config from '@/config'
|
||||||
|
|
||||||
|
enum ChatStatus {
|
||||||
|
INITIALING,
|
||||||
|
//默认
|
||||||
|
DEFAULT,
|
||||||
|
//录音中
|
||||||
|
RECORDING,
|
||||||
|
//无音频输入
|
||||||
|
RECORDING_EMPTY,
|
||||||
|
//录音转文字中
|
||||||
|
TRANSFER,
|
||||||
|
// 转换的文字为空
|
||||||
|
TRANSFER_EMPTY,
|
||||||
|
// 转文字出错
|
||||||
|
TRANSFER_ERROR,
|
||||||
|
// 正在思考中
|
||||||
|
THINKING,
|
||||||
|
// 思考错误
|
||||||
|
THINKING_ERROR,
|
||||||
|
// 正在回复中
|
||||||
|
PLAYING
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
data: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:show', show: boolean): void
|
||||||
|
(event: 'update'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const statusToTextMap = reactive({
|
||||||
|
[ChatStatus.INITIALING]: '正在初始化对话...',
|
||||||
|
[ChatStatus.DEFAULT]: '点击开始说话',
|
||||||
|
[ChatStatus.RECORDING]: '我在听,您请说...',
|
||||||
|
[ChatStatus.RECORDING_EMPTY]: '我好像没太听清,你可以再说一遍',
|
||||||
|
[ChatStatus.TRANSFER]: '正在加载中,请稍后...',
|
||||||
|
[ChatStatus.TRANSFER_EMPTY]: '您好像没有说话,点击按钮重试',
|
||||||
|
[ChatStatus.TRANSFER_ERROR]: '出错了,请重试',
|
||||||
|
[ChatStatus.THINKING]: '稍等,让我想一想',
|
||||||
|
[ChatStatus.THINKING_ERROR]: '出错了,请重试',
|
||||||
|
[ChatStatus.PLAYING]: '正在回复,点击按钮打断'
|
||||||
|
})
|
||||||
|
const loadingUrl = '/api/static/voice.gif'
|
||||||
|
let loadingPath = `${config.baseUrl}${loadingUrl}`
|
||||||
|
//#ifdef H5
|
||||||
|
loadingPath = `${
|
||||||
|
config.baseUrl === '/' ? `${location.origin}/` : config.baseUrl
|
||||||
|
}${loadingUrl}`
|
||||||
|
//#endif
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const guidedPopupRef = shallowRef()
|
||||||
|
const showModel = computed({
|
||||||
|
get() {
|
||||||
|
return props.show
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:show', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const isCanRecord = computed(() => {
|
||||||
|
return [
|
||||||
|
ChatStatus.DEFAULT,
|
||||||
|
ChatStatus.TRANSFER_ERROR,
|
||||||
|
ChatStatus.TRANSFER_EMPTY,
|
||||||
|
ChatStatus.TRANSFER_ERROR,
|
||||||
|
ChatStatus.THINKING_ERROR
|
||||||
|
].includes(chatStatus.value)
|
||||||
|
})
|
||||||
|
const isAutoStop = ref(false)
|
||||||
|
const chatStatus = ref(ChatStatus.INITIALING)
|
||||||
|
const startTimer = ref(0)
|
||||||
|
const hasVoice = ref(false)
|
||||||
|
const recordTimer = ref(0)
|
||||||
|
const { start, stop, isRecording, authorize, close } = useRecorder({
|
||||||
|
onstart() {
|
||||||
|
hasVoice.value = false
|
||||||
|
startTimer.value = Date.now()
|
||||||
|
chatStatus.value = ChatStatus.RECORDING
|
||||||
|
},
|
||||||
|
async onstop(result) {
|
||||||
|
draw(null, 0)
|
||||||
|
stopRender()
|
||||||
|
if (!isAutoStop.value) {
|
||||||
|
if (!isCanRecord.value) {
|
||||||
|
chatStatus.value = ChatStatus.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isAutoStop.value = false
|
||||||
|
chatStatus.value = ChatStatus.TRANSFER
|
||||||
|
try {
|
||||||
|
const res: any = await audioTransfer(result.tempFilePath, {
|
||||||
|
type: 3
|
||||||
|
})
|
||||||
|
if (!res.text) {
|
||||||
|
chatStatus.value = ChatStatus.TRANSFER_EMPTY
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chat(res.text, res.file)
|
||||||
|
} catch (error) {
|
||||||
|
chatStatus.value = ChatStatus.TRANSFER_ERROR
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ondata(result) {
|
||||||
|
render(result)
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
// 过滤掉杂音
|
||||||
|
if (result.powerLevel > 6) {
|
||||||
|
clearTimeout(recordTimer.value)
|
||||||
|
chatStatus.value = ChatStatus.RECORDING
|
||||||
|
hasVoice.value = true
|
||||||
|
startTimer.value = now
|
||||||
|
//有音频输入,延时1s停止录音
|
||||||
|
recordTimer.value = setTimeout(() => {
|
||||||
|
isAutoStop.value = true
|
||||||
|
stop()
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
// 5s内没有语音输入
|
||||||
|
if (now - startTimer.value >= 5000) {
|
||||||
|
if (!hasVoice.value) {
|
||||||
|
if (chatStatus.value == ChatStatus.RECORDING_EMPTY) {
|
||||||
|
//第二个5s无声音输入,停止录音
|
||||||
|
stop()
|
||||||
|
chatStatus.value = ChatStatus.TRANSFER_EMPTY
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chatStatus.value = ChatStatus.RECORDING_EMPTY
|
||||||
|
startTimer.value = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const canvasOptions = reactive<AudioGraphUserOptions>({
|
||||||
|
id: 'audio-canvas',
|
||||||
|
width: 100,
|
||||||
|
height: 40,
|
||||||
|
minHeight: 5
|
||||||
|
})
|
||||||
|
const { render, stopRender, draw } = useRenderAudioGraph(canvasOptions)
|
||||||
|
|
||||||
|
let streamReader: any = null
|
||||||
|
const recordId = ref(0)
|
||||||
|
|
||||||
|
const { play, pause, destroy } = useAudioPlay({
|
||||||
|
api: getChatVoiceGenerate,
|
||||||
|
dataTransform(data) {
|
||||||
|
return data.file_url
|
||||||
|
},
|
||||||
|
params: reactive({
|
||||||
|
records_id: recordId
|
||||||
|
}),
|
||||||
|
onstart() {
|
||||||
|
chatStatus.value = ChatStatus.PLAYING
|
||||||
|
},
|
||||||
|
onstop() {
|
||||||
|
destroy()
|
||||||
|
showModel.value && start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const chat = (text: string, filePath: string) => {
|
||||||
|
chatStatus.value = ChatStatus.THINKING
|
||||||
|
try {
|
||||||
|
chatSendText(
|
||||||
|
{
|
||||||
|
...props.data,
|
||||||
|
voice_file: filePath,
|
||||||
|
question: text
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onstart(reader) {
|
||||||
|
streamReader = reader
|
||||||
|
},
|
||||||
|
onmessage(value) {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.split('data:')
|
||||||
|
.forEach(async (text) => {
|
||||||
|
const dataJson = parse(text)
|
||||||
|
if (!dataJson) return
|
||||||
|
const { event, data, error, code } = dataJson
|
||||||
|
|
||||||
|
if (error && error?.errCode == 336) {
|
||||||
|
chatStatus.value = ChatStatus.DEFAULT
|
||||||
|
guidedPopupRef.value?.open()
|
||||||
|
} else if (error) {
|
||||||
|
uni.$u.toast(error.errMsg)
|
||||||
|
console.log(dataJson)
|
||||||
|
chatStatus.value = ChatStatus.THINKING_ERROR
|
||||||
|
}
|
||||||
|
if (event === 'finish') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const list = await getChatList()
|
||||||
|
recordId.value =
|
||||||
|
list[list.length - 1].id
|
||||||
|
|
||||||
|
showModel.value && (await play())
|
||||||
|
emit('update')
|
||||||
|
} catch (error) {
|
||||||
|
chatStatus.value =
|
||||||
|
ChatStatus.THINKING_ERROR
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onclose() {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
chatStatus.value = ChatStatus.THINKING_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChatList = async () => {
|
||||||
|
const data = await getChatRecord(props.data)
|
||||||
|
return data || []
|
||||||
|
}
|
||||||
|
const chatClose = () => {
|
||||||
|
//#ifdef H5
|
||||||
|
streamReader?.cancel()
|
||||||
|
//#endif
|
||||||
|
//#ifdef MP-WEIXIN
|
||||||
|
streamReader?.abort()
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
const startRecord = async () => {
|
||||||
|
if (isRecording.value) {
|
||||||
|
stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isCanRecord.value) {
|
||||||
|
start()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatStatus.value == ChatStatus.PLAYING) {
|
||||||
|
pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(showModel, async (value) => {
|
||||||
|
if (!value) {
|
||||||
|
if (isRecording.value) {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
chatClose()
|
||||||
|
// close()
|
||||||
|
pause()
|
||||||
|
emit('update')
|
||||||
|
destroy()
|
||||||
|
} else {
|
||||||
|
chatStatus.value = ChatStatus.INITIALING
|
||||||
|
// #ifdef H5
|
||||||
|
await authorize()
|
||||||
|
// #endif
|
||||||
|
chatStatus.value = ChatStatus.DEFAULT
|
||||||
|
draw(null, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.action-btn {
|
||||||
|
position: absolute;
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #999;
|
||||||
|
top: 50%;
|
||||||
|
left: 64rpx;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.stop {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
@apply bg-btn-text;
|
||||||
|
}
|
||||||
|
.bubble-loading {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
background: url(../../../static/images/common/bubble_bg.png) no-repeat;
|
||||||
|
|
||||||
|
background-size: cover;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
transform: translate(20%, -40%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
src/components/chat-scroll-view/components/request/http.ts
Normal file
202
src/components/chat-scroll-view/components/request/http.ts
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import { merge } from 'lodash-es'
|
||||||
|
import {Base64} from 'js-base64'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import type {
|
||||||
|
HttpRequestOptions,
|
||||||
|
RequestConfig,
|
||||||
|
RequestEventStreamConfig,
|
||||||
|
RequestHooks,
|
||||||
|
RequestOptions
|
||||||
|
} from './type'
|
||||||
|
import { RequestCodeEnum, RequestMethodsEnum } from '@/enums/requestEnums'
|
||||||
|
|
||||||
|
function isStreamResponse(contentType?: string) {
|
||||||
|
if (typeof contentType !== 'string') return false
|
||||||
|
return contentType.includes('text/event-stream')
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestHooks: RequestHooks = {
|
||||||
|
requestInterceptorsHook(options, config) {
|
||||||
|
const { urlPrefix, baseUrl, withToken } = config
|
||||||
|
options.header = options.header || {}
|
||||||
|
if (urlPrefix) {
|
||||||
|
options.url = `${urlPrefix}${options.url}`
|
||||||
|
}
|
||||||
|
if (baseUrl) {
|
||||||
|
options.url = `${baseUrl}${options.url}`
|
||||||
|
}
|
||||||
|
const token = config.token
|
||||||
|
|
||||||
|
options.header['terminal'] = config!.terminal
|
||||||
|
// 添加token
|
||||||
|
if (withToken && !options.header.token) {
|
||||||
|
options.header['ai-token'] = token
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
},
|
||||||
|
async responseInterceptorsHook(response, config, options) {
|
||||||
|
const { isTransformResponse, isReturnDefaultResponse, isAuth } = config
|
||||||
|
|
||||||
|
//返回默认响应,当需要获取响应头及其他数据时可使用
|
||||||
|
if (isReturnDefaultResponse) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
// 是否需要对数据进行处理
|
||||||
|
if (!isTransformResponse) {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
// const { logout } = useUserStore()
|
||||||
|
const { code, data, msg, show } = response.data as any
|
||||||
|
switch (code) {
|
||||||
|
case RequestCodeEnum.SUCCESS:
|
||||||
|
// msg && show && uni.$u.toast(msg)
|
||||||
|
return data
|
||||||
|
case RequestCodeEnum.FAILED:
|
||||||
|
// msg && uni.$u.toast(msg)
|
||||||
|
return Promise.reject(msg)
|
||||||
|
|
||||||
|
case RequestCodeEnum.TOKEN_INVALID:
|
||||||
|
// logout()
|
||||||
|
if (isAuth && options.method?.toUpperCase() !== 'GET') {
|
||||||
|
// router.navigateTo({ path: '/pages/login/login' })
|
||||||
|
}
|
||||||
|
return Promise.reject(msg)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async responseInterceptorsCatchHook(options, error) {
|
||||||
|
if (options.method?.toUpperCase() == RequestMethodsEnum.POST) {
|
||||||
|
// uni.$u.toast('请求失败,请重试')
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultOptions: HttpRequestOptions = {
|
||||||
|
requestOptions: {
|
||||||
|
timeout: 30 * 1000
|
||||||
|
},
|
||||||
|
// baseUrl: `${import.meta.env.VITE_APP_BASE_URL || ''}/`,
|
||||||
|
baseUrl: '',
|
||||||
|
|
||||||
|
//是否返回默认的响应
|
||||||
|
isReturnDefaultResponse: false,
|
||||||
|
// 需要对返回数据进行处理
|
||||||
|
isTransformResponse: true,
|
||||||
|
// 接口拼接地址
|
||||||
|
urlPrefix: 'api',
|
||||||
|
// 忽略重复请求
|
||||||
|
ignoreCancel: false,
|
||||||
|
// 是否携带token
|
||||||
|
withToken: true,
|
||||||
|
isAuth: false,
|
||||||
|
retryCount: 2,
|
||||||
|
retryTimeout: 1000,
|
||||||
|
requestHooks: requestHooks,
|
||||||
|
token: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventStream = (
|
||||||
|
options: RequestOptions,
|
||||||
|
config: RequestEventStreamConfig
|
||||||
|
) => {
|
||||||
|
let mergeOptions = merge({}, defaultOptions.requestOptions, options)
|
||||||
|
|
||||||
|
const mergeConfig: RequestEventStreamConfig = merge(
|
||||||
|
{},
|
||||||
|
defaultOptions,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
const { requestInterceptorsHook, responseInterceptorsHook } =
|
||||||
|
mergeConfig.requestHooks || {}
|
||||||
|
|
||||||
|
// if (requestInterceptorsHook && isFunction(requestInterceptorsHook)) {
|
||||||
|
mergeOptions = requestInterceptorsHook(
|
||||||
|
mergeOptions,
|
||||||
|
mergeConfig as RequestConfig
|
||||||
|
)
|
||||||
|
// }
|
||||||
|
|
||||||
|
let body: any = undefined
|
||||||
|
body = JSON.stringify(mergeOptions.data)
|
||||||
|
|
||||||
|
const { onmessage, onclose, onstart } = config
|
||||||
|
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
const push = async (controller: any, reader: any) => {
|
||||||
|
try {
|
||||||
|
const { value, done } = await reader.read()
|
||||||
|
if (done) {
|
||||||
|
controller.close()
|
||||||
|
onclose?.()
|
||||||
|
} else {
|
||||||
|
const tempval = Base64.decode(decoder.decode(value))
|
||||||
|
onmessage?.(tempval)
|
||||||
|
controller.enqueue(value)
|
||||||
|
push(controller, reader)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
onclose?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log('asdas-----------------d', JSON.stringify(mergeOptions))
|
||||||
|
fetch(mergeOptions.url, {
|
||||||
|
...mergeOptions,
|
||||||
|
body,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json; charset=utf-8',
|
||||||
|
Accept: 'text/event-stream',
|
||||||
|
...mergeOptions.header
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.status == 200) {
|
||||||
|
if (
|
||||||
|
isStreamResponse(response.headers?.get('content-type')!)
|
||||||
|
) {
|
||||||
|
const reader = response.body!.getReader()
|
||||||
|
// console.log(reader)
|
||||||
|
onstart?.(reader)
|
||||||
|
new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
push(controller, reader)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
//@ts-ignore
|
||||||
|
response.data = await response.json()
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(response.statusText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (response: any) => {
|
||||||
|
if (!response) {
|
||||||
|
resolve(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (responseInterceptorsHook) {
|
||||||
|
try {
|
||||||
|
response = await responseInterceptorsHook(
|
||||||
|
response,
|
||||||
|
mergeConfig as RequestConfig,
|
||||||
|
mergeOptions
|
||||||
|
)
|
||||||
|
resolve(response)
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(response)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
13
src/components/chat-scroll-view/components/request/index.ts
Normal file
13
src/components/chat-scroll-view/components/request/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { eventStream } from './http'
|
||||||
|
|
||||||
|
export const getChat = (data: any, config: any) => {
|
||||||
|
console.log('qweqwe----------------', config)
|
||||||
|
return eventStream(
|
||||||
|
{
|
||||||
|
url: '/chats/chatSend',
|
||||||
|
data,
|
||||||
|
method: 'POST'
|
||||||
|
},
|
||||||
|
config
|
||||||
|
)
|
||||||
|
}
|
||||||
44
src/components/chat-scroll-view/components/request/type.d.ts
vendored
Normal file
44
src/components/chat-scroll-view/components/request/type.d.ts
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export type RequestOptions = UniApp.RequestOptions
|
||||||
|
export type ResponseResult =
|
||||||
|
| UniApp.RequestSuccessCallbackResult
|
||||||
|
| UniApp.UploadFileSuccessCallbackResult
|
||||||
|
export type RequestOptionsResponseError = UniApp.GeneralCallbackResult
|
||||||
|
export type RequestTask = UniApp.RequestTask
|
||||||
|
export type UploadFileOption = UniApp.UploadFileOption
|
||||||
|
export interface HttpRequestOptions extends RequestConfig {
|
||||||
|
requestOptions: Partial<RequestOptions>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestConfig {
|
||||||
|
baseUrl: string
|
||||||
|
requestHooks: RequestHooks
|
||||||
|
isReturnDefaultResponse: boolean
|
||||||
|
isTransformResponse: boolean
|
||||||
|
urlPrefix: string
|
||||||
|
ignoreCancel: boolean
|
||||||
|
withToken: boolean
|
||||||
|
isAuth: boolean
|
||||||
|
retryCount: number
|
||||||
|
retryTimeout: number
|
||||||
|
hasRetryCount?: number
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestEventStreamConfig extends Partial<RequestConfig> {
|
||||||
|
onstart?: (event: ReadableStreamDefaultReader | UniApp.RequestTask) => void
|
||||||
|
onmessage?: (value: string) => void
|
||||||
|
onclose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestHooks {
|
||||||
|
requestInterceptorsHook?(
|
||||||
|
options: RequestOptions,
|
||||||
|
config: RequestConfig
|
||||||
|
): RequestOptions
|
||||||
|
responseInterceptorsHook?(
|
||||||
|
response: ResponseResult,
|
||||||
|
config: RequestConfig,
|
||||||
|
options: RequestOptions
|
||||||
|
): any
|
||||||
|
responseInterceptorsCatchHook?(options: RequestOptions, error: any): any
|
||||||
|
}
|
||||||
385
src/components/dialog-poster/dialog-poster.vue
Normal file
385
src/components/dialog-poster/dialog-poster.vue
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
<template>
|
||||||
|
<l-painter
|
||||||
|
ref="painterRef"
|
||||||
|
:isCanvasToTempFilePath="false"
|
||||||
|
:css="`width: 640rpx;`"
|
||||||
|
custom-style="position: fixed; left: 200%;"
|
||||||
|
>
|
||||||
|
<l-painter-view
|
||||||
|
:css="`
|
||||||
|
border-radius: 14rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: ${getPosterBgColor};
|
||||||
|
`"
|
||||||
|
>
|
||||||
|
<l-painter-view
|
||||||
|
:css="`
|
||||||
|
display: block;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
`"
|
||||||
|
>
|
||||||
|
<l-painter-image :src="posterBg" css="width: 100%;" />
|
||||||
|
</l-painter-view>
|
||||||
|
<!-- 主要视图 -->
|
||||||
|
<l-painter-view
|
||||||
|
:css="`
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 40rpx;
|
||||||
|
`"
|
||||||
|
>
|
||||||
|
<!-- 中部内容区域 -->
|
||||||
|
<l-painter-view
|
||||||
|
:css="`
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 240rpx;
|
||||||
|
display: block;
|
||||||
|
`"
|
||||||
|
>
|
||||||
|
<l-painter-view
|
||||||
|
:css="`
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
`"
|
||||||
|
>
|
||||||
|
<l-painter-text
|
||||||
|
:text="state?.title"
|
||||||
|
:css="`
|
||||||
|
line-clamp: 2;
|
||||||
|
background-color: #066cff;
|
||||||
|
border-radius: 16rpx 0 16rpx 16rpx;
|
||||||
|
padding: 20rpx 20rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 26rpx;
|
||||||
|
`"
|
||||||
|
/>
|
||||||
|
</l-painter-view>
|
||||||
|
<l-painter-view
|
||||||
|
:css="`
|
||||||
|
box-sizing: border-box;
|
||||||
|
`"
|
||||||
|
>
|
||||||
|
<l-painter-text
|
||||||
|
:text="state?.content"
|
||||||
|
:css="`
|
||||||
|
color: #333333;
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
background-color: #f0f5fe;
|
||||||
|
border-radius: 0 16rpx 16rpx 16rpx;
|
||||||
|
${
|
||||||
|
state?.options?.showContentType == 1
|
||||||
|
? `line-clamp: ${state?.options?.contentNum}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`"
|
||||||
|
/>
|
||||||
|
</l-painter-view>
|
||||||
|
</l-painter-view>
|
||||||
|
|
||||||
|
<!-- 底部信息区域 -->
|
||||||
|
<l-painter-view
|
||||||
|
:css="`
|
||||||
|
display: block;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
`"
|
||||||
|
>
|
||||||
|
<!-- 个人信息区域 -->
|
||||||
|
<l-painter-view
|
||||||
|
:css="`display: inline-block; width: 380rpx; margin-top: 40rpx;`"
|
||||||
|
>
|
||||||
|
<l-painter-image
|
||||||
|
:src="userInfo?.avatar"
|
||||||
|
css="width: 110rpx; height: 110rpx; border-radius: 50%;display: inline-block;"
|
||||||
|
/>
|
||||||
|
<l-painter-view
|
||||||
|
:css="`
|
||||||
|
width: 270rpx;
|
||||||
|
height: 110rpx;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
padding-left: 20rpx;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`"
|
||||||
|
>
|
||||||
|
<l-painter-text
|
||||||
|
:text="userInfo?.nickname"
|
||||||
|
:css="`color: ${state?.options?.textColor}; font-size: 26rpx;line-clamp:1;width: 200rpx;`"
|
||||||
|
/>
|
||||||
|
<l-painter-text
|
||||||
|
v-if="state?.options?.showData"
|
||||||
|
:text="state?.options?.data"
|
||||||
|
:css="`color: ${state?.options?.textColor}; font-size: 26rpx;line-clamp:1;width: 200rpx;`"
|
||||||
|
/>
|
||||||
|
</l-painter-view>
|
||||||
|
</l-painter-view>
|
||||||
|
|
||||||
|
<!-- 扫码区域 -->
|
||||||
|
<l-painter-view
|
||||||
|
:css="`display: inline-block;width: 180rpx; text-align: center;`"
|
||||||
|
>
|
||||||
|
<!-- H5二维码 -->
|
||||||
|
<!-- #ifdef H5 || APP-PLUS -->
|
||||||
|
<l-painter-qrcode
|
||||||
|
:css="`
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 170rpx;
|
||||||
|
height: 170rpx;
|
||||||
|
padding: 10rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
`"
|
||||||
|
:text="state.invite_link"
|
||||||
|
>
|
||||||
|
</l-painter-qrcode>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- 小程序二维码 -->
|
||||||
|
<!-- #ifdef MP -->
|
||||||
|
<l-painter-image
|
||||||
|
:src="state.mp_qr_code"
|
||||||
|
:css="`
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 170rpx;
|
||||||
|
height: 170rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
`"
|
||||||
|
/>
|
||||||
|
<!-- 邀请文案 -->
|
||||||
|
<!-- #endif -->
|
||||||
|
<l-painter-text
|
||||||
|
text="长按识别二维码"
|
||||||
|
:css="`
|
||||||
|
display: block;
|
||||||
|
color: ${state?.options?.textColor};
|
||||||
|
font-size: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
`"
|
||||||
|
/>
|
||||||
|
</l-painter-view>
|
||||||
|
</l-painter-view>
|
||||||
|
</l-painter-view>
|
||||||
|
</l-painter-view>
|
||||||
|
</l-painter>
|
||||||
|
|
||||||
|
<!-- 弹窗海报 -->
|
||||||
|
<u-popup
|
||||||
|
v-model="state.showPoster"
|
||||||
|
mode="center"
|
||||||
|
:closeable="true"
|
||||||
|
closeIconColor="#FFFFFF"
|
||||||
|
:safe-area-inset-bottom="true"
|
||||||
|
:customStyle="{
|
||||||
|
background: 'none'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<view class="pb-[30rpx]">
|
||||||
|
<!-- #ifndef H5 -->
|
||||||
|
<image
|
||||||
|
style="width: 640rpx"
|
||||||
|
mode="widthFix"
|
||||||
|
:src="state.poster_url"
|
||||||
|
></image>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifdef H5 -->
|
||||||
|
<img style="width: 640rpx" :src="state.poster_url" />
|
||||||
|
<!-- #endif -->
|
||||||
|
<view class="mt-[20rpx]">
|
||||||
|
<u-button
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
size="default"
|
||||||
|
:customStyle="{
|
||||||
|
padding: '0 30rpx',
|
||||||
|
height: '82rpx'
|
||||||
|
}"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
<!-- #ifndef H5 -->
|
||||||
|
保存图片到相册
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifdef H5 -->
|
||||||
|
长按保存图片到相册
|
||||||
|
<!-- #endif -->
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { nextTick, reactive, ref, computed } from 'vue'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { getMnpQrCode } from '@/api/app'
|
||||||
|
import { getDecorate } from '@/api/shop'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import appConfig from '@/config'
|
||||||
|
|
||||||
|
const { getImageUrl } = useAppStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const { userInfo } = storeToRefs(userStore)
|
||||||
|
|
||||||
|
const painterRef = ref()
|
||||||
|
const state = reactive<{
|
||||||
|
showPoster: boolean
|
||||||
|
title: string // 标题
|
||||||
|
content: string // 内容
|
||||||
|
mp_qr_code: string // 小程序码
|
||||||
|
invite_link: string // H5海报邀请链接
|
||||||
|
options: any // 海报装修参数
|
||||||
|
poster_url: string // 最终合成的海报露肩
|
||||||
|
}>({
|
||||||
|
showPoster: false,
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
mp_qr_code: '',
|
||||||
|
invite_link: '',
|
||||||
|
options: {
|
||||||
|
data: '邀请您前来体验',
|
||||||
|
default: 1,
|
||||||
|
poster: 1,
|
||||||
|
defaultUrl1: '',
|
||||||
|
defaultUrl2: '',
|
||||||
|
posterUrl: '',
|
||||||
|
showData: '1'
|
||||||
|
},
|
||||||
|
poster_url: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const posterBg = ref('')
|
||||||
|
|
||||||
|
const getPosterBg = () => {
|
||||||
|
const data = state?.options
|
||||||
|
if (data.default == 1 && data.poster == 1) {
|
||||||
|
// 默认海报1
|
||||||
|
posterBg.value = getImageUrl(data.defaultUrl1)
|
||||||
|
} else if (data.default == 1 && data.poster == 2) {
|
||||||
|
// 默认海报2
|
||||||
|
posterBg.value = getImageUrl(state?.options.defaultUrl2)
|
||||||
|
} else if (data.default == 2) {
|
||||||
|
// 自定义海报
|
||||||
|
posterBg.value = getImageUrl(data.posterUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPosterBgColor = computed(() => {
|
||||||
|
const data = state?.options
|
||||||
|
if (!data) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
console.log(data.bgColor)
|
||||||
|
return data.bgColor
|
||||||
|
})
|
||||||
|
|
||||||
|
const initPosterData = async (row: { title: string; content: string }) => {
|
||||||
|
await uni.showLoading({
|
||||||
|
title: '生成中'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
// 如果已经有数据了就不重新请求
|
||||||
|
if (!userInfo?.value?.sn) {
|
||||||
|
await userStore.getUser()
|
||||||
|
}
|
||||||
|
const { pages } = await getDecorate({ type: 6 })
|
||||||
|
// 获取小程序码 如果已经有数据了就不重新请求
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
if (!state.mp_qr_code) {
|
||||||
|
const { qrcode } = await getMnpQrCode({
|
||||||
|
user_sn: userInfo.value.sn,
|
||||||
|
page: 'pages/index/index'
|
||||||
|
})
|
||||||
|
state.mp_qr_code = 'data:image/png;base64,' + qrcode
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
const domain = window.origin
|
||||||
|
state.invite_link = `${domain}/mobile/pages/index/index?user_sn=${userInfo.value.sn}`
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
let domain = appConfig.baseUrl
|
||||||
|
if (domain.charAt(domain.length - 1) === '/') {
|
||||||
|
domain = domain.slice(0, -1)
|
||||||
|
}
|
||||||
|
state.invite_link = `${domain}/mobile/pages/index/index?user_sn=${userInfo.value.sn}`
|
||||||
|
// #endif
|
||||||
|
state.title = row.title.replace(/\n/g, '')
|
||||||
|
state.content = row.content
|
||||||
|
// .replace(/\n/g, '')
|
||||||
|
state.options = JSON.parse(pages)[0]?.content
|
||||||
|
getPosterBg()
|
||||||
|
await nextTick()
|
||||||
|
await handleDrawCanvas()
|
||||||
|
} catch (error) {
|
||||||
|
uni.$u.toast(error)
|
||||||
|
uni.hideLoading()
|
||||||
|
console.log('请求生产海报失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// #ifndef H5
|
||||||
|
uni.saveImageToPhotosAlbum({
|
||||||
|
filePath: state.poster_url,
|
||||||
|
success: () => {
|
||||||
|
uni.$u.toast('保存成功')
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
uni.$u.toast('保存失败')
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
uni.$u.toast('请长按图片保存')
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrawCanvas = async () => {
|
||||||
|
try {
|
||||||
|
// 生成图片
|
||||||
|
painterRef.value?.canvasToTempFilePathSync({
|
||||||
|
fileType: 'png',
|
||||||
|
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum,需要设置 pathType为url
|
||||||
|
// #ifdef MP
|
||||||
|
pathType: 'url',
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
pathType: 'base64',
|
||||||
|
// #endif
|
||||||
|
quality: 1,
|
||||||
|
success: (res: any) => {
|
||||||
|
console.log('生产结果', res)
|
||||||
|
uni.hideLoading()
|
||||||
|
state.showPoster = true
|
||||||
|
state.poster_url = res.tempFilePath
|
||||||
|
},
|
||||||
|
fail: (error: any) => {
|
||||||
|
console.log(error)
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.$u.toast('调用海报错误', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.$u.toast('调用海报错误', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ handleDrawCanvas, initPosterData })
|
||||||
|
</script>
|
||||||
150
src/components/dragon-button/dragon-button.vue
Normal file
150
src/components/dragon-button/dragon-button.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
id="drag-button"
|
||||||
|
class="drag-container"
|
||||||
|
:class="{ transition: autoDocking && !moving }"
|
||||||
|
:style="{
|
||||||
|
left: `${left}px`,
|
||||||
|
top: `${top}px`,
|
||||||
|
width: `${size}rpx`,
|
||||||
|
height: `${size}rpx`,
|
||||||
|
zIndex: zIndex
|
||||||
|
}"
|
||||||
|
@touchend="touchend"
|
||||||
|
@touchmove.stop.prevent="touchmove"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DragButton',
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* 按钮大小
|
||||||
|
*/
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 200
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 层级
|
||||||
|
*/
|
||||||
|
zIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 999
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* x轴边界限制
|
||||||
|
*/
|
||||||
|
xEdge: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* y轴边界限制
|
||||||
|
*/
|
||||||
|
yEdge: {
|
||||||
|
type: Number,
|
||||||
|
default: 50
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 自动停靠
|
||||||
|
*/
|
||||||
|
autoDocking: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
top: 500,
|
||||||
|
left: 300,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
moving: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
// 获取窗口尺寸
|
||||||
|
const { windowWidth, windowHeight, windowTop } =
|
||||||
|
uni.getSystemInfoSync()
|
||||||
|
this._windowWidth = windowWidth
|
||||||
|
this._windowHeight = windowHeight
|
||||||
|
if (windowTop) {
|
||||||
|
this._windowHeight += windowTop
|
||||||
|
}
|
||||||
|
// 计算按钮初始位置
|
||||||
|
const query = uni.createSelectorQuery().in(this)
|
||||||
|
query
|
||||||
|
.select('#drag-button')
|
||||||
|
.boundingClientRect((data) => {
|
||||||
|
if (!data) return
|
||||||
|
const { width, height } = data
|
||||||
|
this.width = width
|
||||||
|
this.height = height
|
||||||
|
this._offsetWidth = width / 2
|
||||||
|
this._offsetHeight = height / 2
|
||||||
|
this.left = this._windowWidth - this.width - this.xEdge
|
||||||
|
this.top = this._windowHeight - this.height - this.yEdge
|
||||||
|
})
|
||||||
|
.exec()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 拖动
|
||||||
|
touchmove({ touches }) {
|
||||||
|
if (touches.length !== 1) return false
|
||||||
|
|
||||||
|
this.moving = true
|
||||||
|
const { clientX, clientY } = touches[0]
|
||||||
|
this.left = clientX - this._offsetWidth
|
||||||
|
|
||||||
|
const _clientY = clientY - this._offsetHeight
|
||||||
|
|
||||||
|
this.top = _clientY
|
||||||
|
},
|
||||||
|
|
||||||
|
// 松手
|
||||||
|
touchend() {
|
||||||
|
// 左右边界,松手自动停靠
|
||||||
|
if (this.autoDocking) {
|
||||||
|
const rigthEdge = this._windowWidth - this.width - this.xEdge
|
||||||
|
if (this.left < this._windowWidth / 2 - this._offsetWidth) {
|
||||||
|
this.left = this.xEdge
|
||||||
|
} else {
|
||||||
|
this.left = rigthEdge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上下边界
|
||||||
|
const bottomEdge = this._windowHeight - this.height - this.yEdge
|
||||||
|
if (this.top < 50) {
|
||||||
|
this.top = 50
|
||||||
|
} else if (this.top > bottomEdge) {
|
||||||
|
this.top = bottomEdge
|
||||||
|
}
|
||||||
|
|
||||||
|
this.moving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.drag-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
&.transition {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/components/drop-down/drop-down.vue
Normal file
1
src/components/drop-down/drop-down.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template>
<view
v-if="showDropdown"
class="dropdown-mask"
@click="showDropdown = !showDropdown"
></view>
<view class="dropdown">
<view @click="showDropdown = !showDropdown">
<slot></slot>
</view>
<view
class="dropdown-menu"
v-show="showDropdown"
:class="menuClass"
@click="showDropdown = !showDropdown"
>
<!-- 下拉菜单的内容 -->
<slot name="menu"></slot>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
const props = defineProps({
mode: 'down' // 默认向下展开
})
const showDropdown = ref<boolean>(false)
const menuClass = computed(() => {
return {
'dropdown-menu-up': props.mode === 'up',
'dropdown-menu-right': props.mode === 'right',
'dropdown-menu-down': props.mode === 'down',
'dropdown-menu-left': props.mode === 'left'
}
})
</script>
<style scoped>
.dropdown {
position: relative;
flex: 1;
width: auto;
}
.dropdown-mask {
position: absolute;
z-index: 3;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
flex: 1;
width: auto;
white-space: pre;
border-radius: 12rpx;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 9999;
}
.dropdown-menu-up {
width: 100%;
inset: auto auto 130% 0%;
/*transform: translateX(-50%);*/
}
.dropdown-menu-right {
inset: auto auto 50% -130%;
transform: translateY(-50%);
}
.dropdown-menu-down {
inset: auto auto 0% 50%;
transform: translateX(-50%);
}
.dropdown-menu-left {
inset: auto auto 50% 130%;
transform: translateY(-50%);
}
</style>
|
||||||
247
src/components/file-upload/choose-file.ts
Normal file
247
src/components/file-upload/choose-file.ts
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
const ERR_MSG_FAIL = 'chooseFile:fail'
|
||||||
|
interface BaseOptions {
|
||||||
|
type: 'file' | 'image' | 'video'
|
||||||
|
}
|
||||||
|
type ChooseFileOptions = UniApp.ChooseFileOptions
|
||||||
|
type ChooseImageOptions = UniApp.ChooseImageOptions
|
||||||
|
type ChooseVideoOptions = UniApp.ChooseVideoOptions
|
||||||
|
type ChooseOptions = BaseOptions &
|
||||||
|
(ChooseFileOptions | ChooseImageOptions | ChooseVideoOptions)
|
||||||
|
|
||||||
|
export interface ChooseResult {
|
||||||
|
tempFilePaths: string[] | string
|
||||||
|
tempFiles: any[]
|
||||||
|
errMsg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileData {
|
||||||
|
name: string
|
||||||
|
uuid: number
|
||||||
|
extname: string
|
||||||
|
fileType: string
|
||||||
|
url: string
|
||||||
|
path: string
|
||||||
|
size: string
|
||||||
|
progress: number
|
||||||
|
status: 'ready' | 'success' | 'error'
|
||||||
|
errMsg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseImage(opts: ChooseImageOptions) {
|
||||||
|
const {
|
||||||
|
count,
|
||||||
|
sizeType = ['original', 'compressed'],
|
||||||
|
sourceType,
|
||||||
|
extension
|
||||||
|
} = opts
|
||||||
|
return new Promise<ChooseResult>((resolve, reject) => {
|
||||||
|
uni.chooseImage({
|
||||||
|
count,
|
||||||
|
sizeType,
|
||||||
|
sourceType,
|
||||||
|
extension,
|
||||||
|
success(res) {
|
||||||
|
resolve(normalizeFileRes(res as ChooseResult, 'image'))
|
||||||
|
},
|
||||||
|
fail(res) {
|
||||||
|
reject({
|
||||||
|
errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseVideo(opts: ChooseVideoOptions) {
|
||||||
|
const { camera, compressed, maxDuration, sourceType, extension } = opts
|
||||||
|
return new Promise<ChooseResult>((resolve, reject) => {
|
||||||
|
uni.chooseVideo({
|
||||||
|
camera,
|
||||||
|
compressed,
|
||||||
|
maxDuration,
|
||||||
|
sourceType,
|
||||||
|
extension,
|
||||||
|
success(res) {
|
||||||
|
const { tempFilePath, duration, size, height, width } = res
|
||||||
|
resolve(
|
||||||
|
normalizeFileRes(
|
||||||
|
{
|
||||||
|
errMsg: 'chooseVideo:ok',
|
||||||
|
tempFilePaths: [tempFilePath],
|
||||||
|
tempFiles: [
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
(res.tempFile && res.tempFile.name) ||
|
||||||
|
'',
|
||||||
|
path: tempFilePath,
|
||||||
|
size,
|
||||||
|
type:
|
||||||
|
(res.tempFile && res.tempFile.type) ||
|
||||||
|
'',
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
duration,
|
||||||
|
fileType: 'video'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'video'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fail(res) {
|
||||||
|
reject({
|
||||||
|
errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseAll(opts: ChooseFileOptions) {
|
||||||
|
const { count, extension } = opts
|
||||||
|
return new Promise<ChooseResult>((resolve, reject) => {
|
||||||
|
let chooseFile = uni.chooseFile
|
||||||
|
if (
|
||||||
|
typeof wx !== 'undefined' &&
|
||||||
|
typeof wx.chooseMessageFile === 'function'
|
||||||
|
) {
|
||||||
|
chooseFile = wx.chooseMessageFile
|
||||||
|
}
|
||||||
|
if (typeof chooseFile !== 'function') {
|
||||||
|
return reject({
|
||||||
|
errMsg:
|
||||||
|
ERR_MSG_FAIL +
|
||||||
|
' 请指定 type 类型,该平台仅支持选择 image 或 video。'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
chooseFile({
|
||||||
|
type: 'all',
|
||||||
|
count,
|
||||||
|
extension,
|
||||||
|
success(res) {
|
||||||
|
resolve(normalizeFileRes(res as ChooseResult))
|
||||||
|
},
|
||||||
|
fail(res) {
|
||||||
|
reject({
|
||||||
|
errMsg: res.errMsg.replace('chooseFile:fail', ERR_MSG_FAIL)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFileRes(res: ChooseResult, fileType?: BaseOptions['type']) {
|
||||||
|
res.tempFiles.forEach((item) => {
|
||||||
|
if (!item.name) {
|
||||||
|
item.name = item.path.substring(item.path.lastIndexOf('/') + 1)
|
||||||
|
}
|
||||||
|
if (fileType) {
|
||||||
|
item.fileType = fileType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!res.tempFilePaths) {
|
||||||
|
res.tempFilePaths = res.tempFiles.map((file) => file.path)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseFile(
|
||||||
|
opts: ChooseOptions = {
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
): Promise<ChooseResult> {
|
||||||
|
if (opts.type === 'image') {
|
||||||
|
return chooseImage(opts as ChooseImageOptions)
|
||||||
|
} else if (opts.type === 'video') {
|
||||||
|
return chooseVideo(opts as ChooseVideoOptions)
|
||||||
|
}
|
||||||
|
return chooseAll(opts as ChooseFileOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取文件扩展名和文件名
|
||||||
|
* @param name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getFileExt = (name: string) => {
|
||||||
|
const lastLen = name.lastIndexOf('.')
|
||||||
|
const len = name.length
|
||||||
|
const fileName = name.substring(0, lastLen)
|
||||||
|
const ext = name.substring(lastLen + 1, len)
|
||||||
|
return {
|
||||||
|
name: fileName,
|
||||||
|
ext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @description 路径中获取文件名称
|
||||||
|
* @param name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getFileName = (path: string) => {
|
||||||
|
const lastLen = path.lastIndexOf('.')
|
||||||
|
const lastPath = path.lastIndexOf('/')
|
||||||
|
// 不是文件
|
||||||
|
if (lastLen === -1) return path
|
||||||
|
return path.substring(lastPath + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeFileData = (files: any) => {
|
||||||
|
const fileFullName = getFileExt(files.name)
|
||||||
|
const extname = fileFullName.ext.toLowerCase()
|
||||||
|
const filedata: FileData = {
|
||||||
|
name: files.name,
|
||||||
|
uuid: files.uuid,
|
||||||
|
extname: extname || '',
|
||||||
|
fileType: files.fileType,
|
||||||
|
path: files.path,
|
||||||
|
url: files.path,
|
||||||
|
size: files.size,
|
||||||
|
progress: 0,
|
||||||
|
status: 'ready',
|
||||||
|
errMsg: ''
|
||||||
|
}
|
||||||
|
return filedata
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检验扩展名是否正确
|
||||||
|
const getFilesByExtname = (res: ChooseResult, extname: string[] = []) => {
|
||||||
|
const filePaths: any[] = []
|
||||||
|
const files: any[] = []
|
||||||
|
if (!extname.length) {
|
||||||
|
return {
|
||||||
|
filePaths: res.tempFilePaths,
|
||||||
|
files: res.tempFiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.tempFiles.forEach((v) => {
|
||||||
|
const fileFullName = getFileExt(v.name)
|
||||||
|
const ext = fileFullName.ext.toLowerCase()
|
||||||
|
if (extname.indexOf(ext) !== -1) {
|
||||||
|
files.push(v)
|
||||||
|
filePaths.push(v.path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (files.length !== res.tempFiles.length) {
|
||||||
|
uni.showToast({
|
||||||
|
title: `当前选择了${res.tempFiles.length}个文件 ,${
|
||||||
|
res.tempFiles.length - files.length
|
||||||
|
} 个文件格式不正确`,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 5000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
filePaths,
|
||||||
|
files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
chooseFile,
|
||||||
|
getFileExt,
|
||||||
|
getFilesByExtname,
|
||||||
|
normalizeFileData,
|
||||||
|
getFileName
|
||||||
|
}
|
||||||
394
src/components/file-upload/file-upload.vue
Normal file
394
src/components/file-upload/file-upload.vue
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="file-upload"
|
||||||
|
:class="{
|
||||||
|
'file-upload--line': limitLength === 1
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<view class="file-picker">
|
||||||
|
<view @click="choose">
|
||||||
|
<slot>
|
||||||
|
<view class="file-button">
|
||||||
|
<u-icon
|
||||||
|
name="arrow-upward"
|
||||||
|
:size="32"
|
||||||
|
v-if="fileType === 'file'"
|
||||||
|
/>
|
||||||
|
<u-icon name="camera" :size="32" v-else />
|
||||||
|
<view class="ml-[10rpx]">
|
||||||
|
{{ getBtnText }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="file-list">
|
||||||
|
<view
|
||||||
|
class="file-item relative"
|
||||||
|
v-for="item in filesLists"
|
||||||
|
:key="item.url"
|
||||||
|
>
|
||||||
|
<view class="text-primary mr-[10rpx] flex">
|
||||||
|
<u-icon
|
||||||
|
v-if="fileType == 'file'"
|
||||||
|
:size="32"
|
||||||
|
name="file-text"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-if="fileType == 'image'"
|
||||||
|
:src="item.path || item.url"
|
||||||
|
class="w-[32rpx] h-[32rpx]"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-if="fileType == 'video'"
|
||||||
|
:src="item.path || item.url"
|
||||||
|
class="w-[32rpx] h-[32rpx]"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="flex-1 min-w-0 mr-[20rpx]">
|
||||||
|
<view class="line-clamp-1">
|
||||||
|
{{ item.name }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="!disabled"
|
||||||
|
@click.stop="removeFile(item.url!)"
|
||||||
|
class="relative flex items-center z-10"
|
||||||
|
>
|
||||||
|
<u-icon name="close" :size="28" />
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="absolute bottom-[0px] w-full left-0 flex"
|
||||||
|
v-if="
|
||||||
|
showProgress
|
||||||
|
&& item.progress! >= 0
|
||||||
|
&& item.progress
|
||||||
|
&& item.status !== 'success'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<u-line-progress
|
||||||
|
class="w-full h-auto"
|
||||||
|
height="6"
|
||||||
|
:show-percent="false"
|
||||||
|
:active-color="$theme.primaryColor"
|
||||||
|
:percent="item.progress"
|
||||||
|
></u-line-progress>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="item.status === 'error'"
|
||||||
|
class="absolute inset-0 bg-[rgba(0,0,0,0.4)] flex items-center justify-center text-white"
|
||||||
|
>
|
||||||
|
<view @click="retry(item)"> 点击重试 </view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType, computed, ref, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
ChooseResult,
|
||||||
|
FileData,
|
||||||
|
chooseFile,
|
||||||
|
getFileName,
|
||||||
|
getFilesByExtname,
|
||||||
|
normalizeFileData
|
||||||
|
} from './choose-file'
|
||||||
|
import { uploadFile } from '@/api/app'
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: [Array, Object],
|
||||||
|
default() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 文件类型
|
||||||
|
fileType: {
|
||||||
|
type: String as PropType<'image' | 'video' | 'file'>,
|
||||||
|
default: 'all'
|
||||||
|
},
|
||||||
|
returnType: {
|
||||||
|
type: String as PropType<'object' | 'array'>,
|
||||||
|
default: 'array'
|
||||||
|
},
|
||||||
|
// 文件类型筛选
|
||||||
|
fileExtname: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sizeType: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default() {
|
||||||
|
return ['original', 'compressed']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sourceType: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default() {
|
||||||
|
return ['album', 'camera']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showProgress: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
//最大上传量
|
||||||
|
maxCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: any): void
|
||||||
|
}>()
|
||||||
|
const filesLists = ref<Partial<FileData>[]>([])
|
||||||
|
const limitLength = computed(() => {
|
||||||
|
if (props.returnType === 'object') {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if (!props.limit) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.limit
|
||||||
|
})
|
||||||
|
|
||||||
|
const choose = async () => {
|
||||||
|
if (props.disabled) return
|
||||||
|
if (
|
||||||
|
filesLists.value.length >= Number(limitLength.value) &&
|
||||||
|
props.returnType === 'array' &&
|
||||||
|
limitLength.value > 1
|
||||||
|
) {
|
||||||
|
uni.showToast({
|
||||||
|
title: `您最多选择 ${limitLength.value} 个文件`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const filesResult = await chooseFile({
|
||||||
|
type: props.fileType,
|
||||||
|
compressed: false,
|
||||||
|
sizeType: props.sizeType,
|
||||||
|
sourceType: props.sourceType,
|
||||||
|
extension: props.fileExtname.length ? props.fileExtname : undefined,
|
||||||
|
count: limitLength.value - filesLists.value.length
|
||||||
|
})
|
||||||
|
chooseFileCallback(filesResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chooseFileCallback = async (filesResult: ChooseResult) => {
|
||||||
|
const isOne = Number(limitLength.value) === 1
|
||||||
|
if (isOne) {
|
||||||
|
filesLists.value = []
|
||||||
|
}
|
||||||
|
const { files } = getFilesByExtname(filesResult, props.fileExtname)
|
||||||
|
const currentData = []
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
if (limitLength.value - filesLists.value.length <= 0) break
|
||||||
|
const filedata = normalizeFileData(files[i])
|
||||||
|
filesLists.value.push(filedata)
|
||||||
|
currentData.push(filedata)
|
||||||
|
}
|
||||||
|
await upload(currentData)
|
||||||
|
emitUpdateValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitUpdateValue = () => {
|
||||||
|
let value: any = {}
|
||||||
|
if (props.returnType === 'object') {
|
||||||
|
const [item] = filesLists.value
|
||||||
|
console.log(item)
|
||||||
|
if (item?.status === 'success') {
|
||||||
|
value = {
|
||||||
|
url: item.url,
|
||||||
|
name: item.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const data = filesLists.value.filter(
|
||||||
|
(item) => item.status === 'success'
|
||||||
|
)
|
||||||
|
value = data.map((item) => ({
|
||||||
|
url: item.url,
|
||||||
|
name: item.name
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
//上传,并处理并发问题
|
||||||
|
const upload = (files: FileData[]): Promise<void> => {
|
||||||
|
const len = files.length
|
||||||
|
let index = 0
|
||||||
|
let count = 0
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const run = async () => {
|
||||||
|
const cur = index++
|
||||||
|
const fileItem = files[cur]
|
||||||
|
const currentIndex = filesLists.value.findIndex(
|
||||||
|
(item) => item.path === fileItem.path
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const fileType: any = props.fileType === 'file' ? 'docs' : props.fileType
|
||||||
|
const { path }: any = await uploadFile(
|
||||||
|
fileType,
|
||||||
|
{
|
||||||
|
filePath: fileItem.url,
|
||||||
|
formData: props.data,
|
||||||
|
header: props.header
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
filesLists.value[currentIndex].progress = progress
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
filesLists.value[currentIndex].status = 'success'
|
||||||
|
filesLists.value[currentIndex].url = path
|
||||||
|
} catch (error) {
|
||||||
|
filesLists.value[currentIndex].errMsg = error as string
|
||||||
|
filesLists.value[currentIndex].status = 'error'
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if (count === len) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (index < len) {
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < Math.min(len, props.maxCount); i++) {
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (url: string) => {
|
||||||
|
const index = filesLists.value.findIndex((item) => item.url === url)
|
||||||
|
if (index > -1) {
|
||||||
|
filesLists.value.splice(index, 1)
|
||||||
|
emitUpdateValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const retry = async (item: any) => {
|
||||||
|
item.status = 'ready'
|
||||||
|
item.progress = 0
|
||||||
|
await upload([{ ...item }])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBtnText = computed(() => {
|
||||||
|
switch (props.fileType) {
|
||||||
|
case 'image':
|
||||||
|
return '上传图片'
|
||||||
|
case 'video':
|
||||||
|
return '上传视频'
|
||||||
|
default:
|
||||||
|
return '上传文件'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const setValueItem = (item: any) => {
|
||||||
|
if (!item.url) return
|
||||||
|
const isInFiles = filesLists.value.some((file) => file.url == item.url)
|
||||||
|
if (!isInFiles) {
|
||||||
|
if (!item.name) {
|
||||||
|
item.name = getFileName(item.url)
|
||||||
|
}
|
||||||
|
item.status = 'success'
|
||||||
|
filesLists.value.push({ ...item })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newVal) => {
|
||||||
|
if (Array.isArray(newVal)) {
|
||||||
|
newVal.forEach((item: any) => {
|
||||||
|
setValueItem(item)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (!newVal.url) {
|
||||||
|
filesLists.value = []
|
||||||
|
}
|
||||||
|
setValueItem(newVal)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
filesLists.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
clear
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.file-upload {
|
||||||
|
// display: flex;
|
||||||
|
&.file-upload--line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.file-list {
|
||||||
|
.file-item {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file-picker {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
flex: none;
|
||||||
|
.file-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15rpx 20rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 10px #e6e9ed;
|
||||||
|
@apply text-primary text-xs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file-list {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
.file-item {
|
||||||
|
padding: 15rpx 20rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font-size: 26rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
286
src/components/floating-menu/floating-menu.vue
Normal file
286
src/components/floating-menu/floating-menu.vue
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifndef MP-WEIXIN -->
|
||||||
|
<view class="floating-menu" @touchstart.stop @touchend.stop @touchmove.stop>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<view class="floating-menu">
|
||||||
|
<!-- #endif -->
|
||||||
|
<movable-area class="movable-area" :scale-area="false">
|
||||||
|
<movable-view
|
||||||
|
class="movable-view"
|
||||||
|
:class="!menuData.isRemove ? 'animation-info' : ''"
|
||||||
|
style="pointer-events: auto"
|
||||||
|
@touchstart="touchstart"
|
||||||
|
@touchend="touchend"
|
||||||
|
@change="onChange"
|
||||||
|
direction="all"
|
||||||
|
inertia="true"
|
||||||
|
:x="menuData.x"
|
||||||
|
:y="menuData.y"
|
||||||
|
:disabled="disabled"
|
||||||
|
:out-of-bounds="true"
|
||||||
|
:damping="200"
|
||||||
|
:friction="100"
|
||||||
|
>
|
||||||
|
<view class="item-main" @click="openMenu">
|
||||||
|
<u-icon :name="UnfoldIcon" size="36" />
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="menuData.showBtn"
|
||||||
|
class="menu-box"
|
||||||
|
:class="menuData.isLeft ? 'leftOut1' : 'rightOut1'"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="index"
|
||||||
|
@click="onJump(item)"
|
||||||
|
class="item-main"
|
||||||
|
>
|
||||||
|
<u-icon :name="item.icon" size="36" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</movable-view>
|
||||||
|
</movable-area>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { getCurrentInstance, nextTick, onMounted, reactive } from 'vue'
|
||||||
|
import HomeIcon from '@/static/images/floating_menu/home.png'
|
||||||
|
import UserIcon from '@/static/images/floating_menu/user.png'
|
||||||
|
import BackIcon from '@/static/images/floating_menu/back.png'
|
||||||
|
import UnfoldIcon from '@/static/images/floating_menu/unfold.png'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
list?: any
|
||||||
|
disabled?: boolean
|
||||||
|
bottom?: number
|
||||||
|
right?: number
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
icon: HomeIcon,
|
||||||
|
pages: '/pages/index/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: UserIcon,
|
||||||
|
pages: '/pages/user/user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BackIcon,
|
||||||
|
pages: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
disabled: false,
|
||||||
|
bottom: 160,
|
||||||
|
right: 10
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const menuData = reactive({
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
isRemove: true,
|
||||||
|
windowWidth: 0,
|
||||||
|
windowHeight: 0,
|
||||||
|
btnWidth: 0,
|
||||||
|
btnHeight: 0,
|
||||||
|
x: 10000,
|
||||||
|
y: 10000,
|
||||||
|
old: {
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
},
|
||||||
|
showBtn: false,
|
||||||
|
isLeft: false
|
||||||
|
})
|
||||||
|
const ctx = getCurrentInstance()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
const sysInfo = uni.getSystemInfoSync()
|
||||||
|
menuData.windowWidth = sysInfo.windowWidth
|
||||||
|
menuData.windowHeight = sysInfo.windowHeight
|
||||||
|
try {
|
||||||
|
uni.createSelectorQuery()
|
||||||
|
.in(ctx)
|
||||||
|
.select('.movable-view')
|
||||||
|
.boundingClientRect((rect: any) => {
|
||||||
|
menuData.btnWidth = rect.width
|
||||||
|
menuData.btnHeight = rect.height
|
||||||
|
menuData.x = menuData.old.x
|
||||||
|
menuData.y = menuData.old.y
|
||||||
|
nextTick(() => {
|
||||||
|
menuData.x =
|
||||||
|
menuData.windowWidth - menuData.btnWidth - props.right
|
||||||
|
menuData.y =
|
||||||
|
menuData.windowHeight - menuData.btnHeight - props.bottom
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.exec()
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//移动按钮
|
||||||
|
const onChange = (e: any) => {
|
||||||
|
menuData.old.x = e.detail.x
|
||||||
|
menuData.old.y = e.detail.y
|
||||||
|
}
|
||||||
|
|
||||||
|
//开始移动
|
||||||
|
const touchstart = (e: any) => {
|
||||||
|
menuData.isRemove = true
|
||||||
|
}
|
||||||
|
//结束移动
|
||||||
|
const touchend = (e: any) => {
|
||||||
|
if (!props.disabled && menuData.old.x) {
|
||||||
|
menuData.x = menuData.old.x
|
||||||
|
menuData.y = menuData.old.y
|
||||||
|
const bWidth = (menuData.windowWidth - menuData.btnWidth) / 2
|
||||||
|
if (menuData.x < 0 || (menuData.x > 0 && menuData.x <= bWidth)) {
|
||||||
|
nextTick(() => {
|
||||||
|
// = 10 是为了结束拖动以后不贴边
|
||||||
|
menuData.x = 10
|
||||||
|
menuData.isLeft = true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
nextTick(() => {
|
||||||
|
// - 10 是为了结束拖动以后不贴边
|
||||||
|
menuData.x = menuData.windowWidth - menuData.btnWidth - 10
|
||||||
|
menuData.isLeft = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
menuData.isRemove = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openMenu = () => {
|
||||||
|
menuData.showBtn = !menuData.showBtn
|
||||||
|
}
|
||||||
|
|
||||||
|
//点击菜单
|
||||||
|
const onJump = (item: any) => {
|
||||||
|
if (item.pages) {
|
||||||
|
router.switchTab(item.pages)
|
||||||
|
} else {
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1,
|
||||||
|
fail: () => {
|
||||||
|
uni.$u.toast('已经是第一个页面了')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.movable-view {
|
||||||
|
width: 90rpx;
|
||||||
|
height: 90rpx;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 50rpx;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
width: 272rpx;
|
||||||
|
height: 90rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 50rpx;
|
||||||
|
}
|
||||||
|
.leftOut1 {
|
||||||
|
left: 110rpx;
|
||||||
|
animation: leftOut 0.4s;
|
||||||
|
}
|
||||||
|
.rightOut1 {
|
||||||
|
right: 110rpx;
|
||||||
|
animation: rightOut 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-main {
|
||||||
|
width: 90rpx;
|
||||||
|
height: 90rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes leftOut {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
left: 50rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
opacity: 0;
|
||||||
|
left: 50rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
left: 110rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
opacity: 1;
|
||||||
|
left: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
left: 110rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rightOut {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
right: 50rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
opacity: 0;
|
||||||
|
right: 50rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
right: 110rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
opacity: 1;
|
||||||
|
right: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
right: 110rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.movable-area {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999999 !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
src/components/guided-popup/guided-popup.vue
Normal file
94
src/components/guided-popup/guided-popup.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<u-popup v-model="showPopup" mode="center" closeable border-radius="14">
|
||||||
|
<view class="w-[670rpx] min-h-[500rpx]">
|
||||||
|
<view class="p-[28rpx] text-center font-medium text-xl"> 对话余额不足 </view>
|
||||||
|
<view class="border-t border-solid border-light border-0 px-[40rpx] py-[30rpx]">
|
||||||
|
<view>你可以通过以下渠道获取对话条数:</view>
|
||||||
|
<view
|
||||||
|
class="mt-[40rpx] flex items-center"
|
||||||
|
v-for="(item, index) in channelList"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<view class="mr-[20rpx] font-medium">
|
||||||
|
{{ item.title }}
|
||||||
|
</view>
|
||||||
|
<view class="ml-auto">
|
||||||
|
<u-button
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
size="medium"
|
||||||
|
:customStyle="{
|
||||||
|
padding: '0 24rpx',
|
||||||
|
height: '56rpx'
|
||||||
|
}"
|
||||||
|
@click="jump(item.path)"
|
||||||
|
>
|
||||||
|
{{ item.btnText }}
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'uniapp-router-next'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { getTask } from '@/api/task'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
const showPopup = ref(false)
|
||||||
|
const open = () => {
|
||||||
|
showPopup.value = true
|
||||||
|
}
|
||||||
|
const close = () => {
|
||||||
|
showPopup.value = false
|
||||||
|
}
|
||||||
|
const router = useRouter()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const jump = (path: string) => {
|
||||||
|
close()
|
||||||
|
router.navigateTo(path)
|
||||||
|
}
|
||||||
|
const taskList = ref([])
|
||||||
|
|
||||||
|
const getTaskList = async () => {
|
||||||
|
taskList.value = await getTask()
|
||||||
|
console.log(taskList.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelList = computed(() => {
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
title: '免费获取任务奖励',
|
||||||
|
btnText: '前往分享',
|
||||||
|
path: '/packages/pages/task_center/task_center',
|
||||||
|
show: taskList?.value?.length > 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '余额充值',
|
||||||
|
btnText: '前往充值',
|
||||||
|
path: '/packages/pages/recharge/recharge',
|
||||||
|
show: appStore.getIsShowRecharge
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '开通会员',
|
||||||
|
btnText: '开通会员',
|
||||||
|
path: '/packages/pages/open_vip/open_vip',
|
||||||
|
show: appStore.getIsShowVip
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return data.filter((item) => item.show)
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
getTaskList()
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
close
|
||||||
|
})
|
||||||
|
</script>
|
||||||
90
src/components/l-textarea/l-textarea.vue
Normal file
90
src/components/l-textarea/l-textarea.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<view class="l-textarea" :data-error="error" :class="{ error: error }">
|
||||||
|
<textarea
|
||||||
|
class="l-textarea__inner"
|
||||||
|
placeholder-class="l-textarea__placeholder"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:rows="rows"
|
||||||
|
v-model="textAreaValue"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:class="{
|
||||||
|
'!border-primary': isFocus
|
||||||
|
}"
|
||||||
|
:style="customClass"
|
||||||
|
@input="getTextAreaValue"
|
||||||
|
@focus="isFocus = true"
|
||||||
|
@blur="isFocus = false"
|
||||||
|
/>
|
||||||
|
<view v-if="showWordLimit" class="l-textarea-length-maxlength">
|
||||||
|
<text>{{ lengthText }}</text> / <text>{{ maxlength }}</text>
|
||||||
|
</view>
|
||||||
|
<slot name="length-suffix"></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { watch, ref, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
placeholder: '',
|
||||||
|
maxlength: '',
|
||||||
|
showWordLimit: false,
|
||||||
|
customClass: {},
|
||||||
|
error: '',
|
||||||
|
rows: '',
|
||||||
|
modelValue: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const textAreaValue = ref(props.modelValue)
|
||||||
|
const lengthText = ref(props.modelValue?.length || 0)
|
||||||
|
const isFocus = ref(false)
|
||||||
|
|
||||||
|
const getTextAreaValue = (e) => {
|
||||||
|
const event = e || window.event
|
||||||
|
const target = event.detail || event.taget
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
textAreaValue.value = val
|
||||||
|
lengthText.value = val.length
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.l-textarea {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-textarea-length-maxlength {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
bottom: 10rpx;
|
||||||
|
right: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #aeaeae;
|
||||||
|
@apply bg-page-base;
|
||||||
|
}
|
||||||
|
.l-textarea__inner {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border 0.3s;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
/** 禁止textarea拉伸 */
|
||||||
|
resize: none;
|
||||||
|
@apply bg-page-base;
|
||||||
|
}
|
||||||
|
.l-textarea__placeholder {
|
||||||
|
color: #9CA3AF;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
66
src/components/loading/loading.vue
Normal file
66
src/components/loading/loading.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<view class="loading">
|
||||||
|
<view class="dot flex items-center">
|
||||||
|
<view class="dot-item"></view>
|
||||||
|
<view class="dot-item"></view>
|
||||||
|
<view class="dot-item"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import Color from 'color'
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: string
|
||||||
|
color?: string
|
||||||
|
alpha?: number
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: '22rpx',
|
||||||
|
color: '#ffffff',
|
||||||
|
alpha: 0.3
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const lightColor = computed(() => {
|
||||||
|
return Color(props.color)!.alpha(props.alpha).rgbaString() || '#fff'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@keyframes dot-loading {
|
||||||
|
0% {
|
||||||
|
background-color: v-bind(color);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background: v-bind(color);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background: v-bind(lightColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
.dot-item {
|
||||||
|
width: v-bind(size);
|
||||||
|
height: v-bind(size);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: dot-loading 1s linear infinite;
|
||||||
|
&:nth-child(1) {
|
||||||
|
background: v-bind(color);
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
background: v-bind(color);
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: calc(v-bind(size) / 2);
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
background: v-bind(lightColor);
|
||||||
|
margin-left: calc(v-bind(size) / 2);
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
285
src/components/model-picker/model-picker.vue
Normal file
285
src/components/model-picker/model-picker.vue
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
v-if="userStore.isLogin && !chatModel.loading"
|
||||||
|
class="px-[20rpx] py-[15rpx] text-sm flex items-center justify-between bg-white"
|
||||||
|
>
|
||||||
|
<view class="flex-none mr-[20rpx]">
|
||||||
|
<view class="flex items-center">
|
||||||
|
<u-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="medium"
|
||||||
|
:custom-style="{
|
||||||
|
background: 'transparent !important'
|
||||||
|
}"
|
||||||
|
v-if="appStore.getIsShowVip"
|
||||||
|
>
|
||||||
|
<router-navigate class="text-primary" to="/packages/pages/open_vip/open_vip">
|
||||||
|
{{
|
||||||
|
userInfo.isMember && userInfo.memberExpired !== 1
|
||||||
|
? userInfo.memberPackageName
|
||||||
|
: '开通会员'
|
||||||
|
}}
|
||||||
|
</router-navigate>
|
||||||
|
</u-button>
|
||||||
|
<view class="ml-[20rpx]" v-if="!chatModel.modelList.length">
|
||||||
|
<text
|
||||||
|
v-if="userInfo.isMember && userInfo.memberExpired !== 1"
|
||||||
|
class="flex-1 min-w-0"
|
||||||
|
>
|
||||||
|
已开通会员,不消耗次数
|
||||||
|
</text>
|
||||||
|
<text v-else>
|
||||||
|
<text>消耗</text>
|
||||||
|
<text class="text-primary"> 1 </text>
|
||||||
|
<text> 条对话次数</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="chatModel.billing_is_open * 1"
|
||||||
|
@click="chatModel.show = true"
|
||||||
|
class="flex ml-auto justify-center items-center rounded-[30px] h-[60rpx]"
|
||||||
|
>
|
||||||
|
<text class="text-[#415058] mr-[6px] flex">
|
||||||
|
<text class="line-clamp-1">
|
||||||
|
{{ chatModel.current.alias }} /
|
||||||
|
|
||||||
|
<text
|
||||||
|
v-if="
|
||||||
|
chatModel?.current?.member_free &&
|
||||||
|
chatModel?.current?.is_member
|
||||||
|
"
|
||||||
|
class="text-muted"
|
||||||
|
>
|
||||||
|
会员免费
|
||||||
|
</text>
|
||||||
|
<text v-else class="flex-1 min-w-0 text-muted">
|
||||||
|
<template v-if="chatModel?.current?.balance * 1">
|
||||||
|
<text>消耗</text>
|
||||||
|
<text class="text-primary">
|
||||||
|
{{ chatModel?.current?.balance }}
|
||||||
|
</text>
|
||||||
|
<text>条对话次数</text>
|
||||||
|
</template>
|
||||||
|
<template v-else> 免费 </template>
|
||||||
|
</text>
|
||||||
|
</text>
|
||||||
|
</text>
|
||||||
|
<u-icon name="arrow-down" size="24rpx"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view v-else class="text-[#415058]">
|
||||||
|
<template v-if="chatModel?.current?.balance * 1">
|
||||||
|
<text>消耗</text>
|
||||||
|
<text class="text-primary">
|
||||||
|
{{ chatModel?.current?.balance }}
|
||||||
|
</text>
|
||||||
|
<text>条对话次数</text>
|
||||||
|
</template>
|
||||||
|
<template v-else> 免费 </template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<u-popup
|
||||||
|
v-model="chatModel.show"
|
||||||
|
mode="bottom"
|
||||||
|
border-radius="14"
|
||||||
|
:safe-area-inset-bottom="true"
|
||||||
|
height="980rpx"
|
||||||
|
closeable
|
||||||
|
>
|
||||||
|
<view class="p-[20rpx] text-xl font-bold"> 切换模型通道 </view>
|
||||||
|
<scroll-view class="h-[890rpx] box-border" scroll-y>
|
||||||
|
<view class="pb-[200rpx] px-[30rpx]">
|
||||||
|
<view
|
||||||
|
class="model-card"
|
||||||
|
v-for="(item, index) in chatModel.modelList"
|
||||||
|
:key="item.key"
|
||||||
|
:class="{
|
||||||
|
'model-card__active': chatModel.currentIndex === index
|
||||||
|
}"
|
||||||
|
@click="chatModel.currentIndex = index"
|
||||||
|
>
|
||||||
|
<view>
|
||||||
|
<view class="flex items-center">
|
||||||
|
<view class="mr-2" v-if="item.image">
|
||||||
|
<u-image :src="item.image" width="40rpx" height="40rpx"></u-image>
|
||||||
|
</view>
|
||||||
|
<view class="text-lg font-bold">{{ item.alias || item.name }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.describe" class="text-base text-muted mt-2">
|
||||||
|
{{ item.describe }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="mt-4">
|
||||||
|
<view
|
||||||
|
class="mt-[20rpx] min-h-[50rpx] flex items-center justify-between"
|
||||||
|
v-for="citem in item.model_list"
|
||||||
|
:key="citem.key"
|
||||||
|
@click="handleChoiceModel(citem)"
|
||||||
|
>
|
||||||
|
<view class="mr-[6px] flex-1 min-w-0">
|
||||||
|
{{ citem.alias }}
|
||||||
|
</view>
|
||||||
|
<view class="ml-[10rpx] flex items-center">
|
||||||
|
<view
|
||||||
|
class="text-muted mr-2"
|
||||||
|
:class="{
|
||||||
|
'!text-primary': modelKey === citem.key && chatModel.currentIndex === index
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
v-if="
|
||||||
|
citem.member_free &&
|
||||||
|
citem.is_member
|
||||||
|
"
|
||||||
|
>
|
||||||
|
会员免费
|
||||||
|
</text>
|
||||||
|
<text v-else>
|
||||||
|
<template v-if="citem.balance * 1">
|
||||||
|
<text>消耗</text>
|
||||||
|
<text class="text-primary">
|
||||||
|
{{ citem.balance }}
|
||||||
|
</text>
|
||||||
|
<text>条对话次数</text>
|
||||||
|
</template>
|
||||||
|
<template v-else> 免费 </template>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="text-muted ml-1 mr-[2px]"
|
||||||
|
v-if="modelKey !== citem.key || chatModel.currentIndex !== index"
|
||||||
|
>
|
||||||
|
<u-image
|
||||||
|
:src="IconUnSelect"
|
||||||
|
width="32rpx"
|
||||||
|
height="32rpx"
|
||||||
|
></u-image>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="text-primary mt-[3px]"
|
||||||
|
v-if="modelKey === citem.key && chatModel.currentIndex === index"
|
||||||
|
>
|
||||||
|
<u-icon name="checkmark-circle-fill" size="40rpx"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</u-popup>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { getChatModelApi } from '@/api/chat'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import IconUnSelect from '@/static/images/icon/icon_unselect.png'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { userInfo } = storeToRefs(userStore)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
chatKey: any
|
||||||
|
modelKey: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:chatKey', value: any): void
|
||||||
|
(event: 'update:modelKey', value: any): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const chatKey = computed({
|
||||||
|
get() {
|
||||||
|
return props.chatKey
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:chatKey', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const modelKey = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelKey
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:modelKey', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 聊天模型数据
|
||||||
|
const chatModel = reactive({
|
||||||
|
billing_is_open: 0,
|
||||||
|
loading: true,
|
||||||
|
show: false,
|
||||||
|
current: {
|
||||||
|
balance: 1,
|
||||||
|
key: '',
|
||||||
|
member_free: true,
|
||||||
|
model: '',
|
||||||
|
default: false
|
||||||
|
} as any,
|
||||||
|
currentIndex: 0,
|
||||||
|
modelList: [] as any[]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取聊天模型数据
|
||||||
|
const getChatModelFunc = async () => {
|
||||||
|
try {
|
||||||
|
const { billing_is_open, list: data } = await getChatModelApi()
|
||||||
|
chatModel.billing_is_open = billing_is_open
|
||||||
|
chatModel.modelList = data
|
||||||
|
const first = data.find((item: any) => item.default) || data[0]
|
||||||
|
chatModel.currentIndex = data.findIndex((item: any) => item.default) || 0
|
||||||
|
chatModel.current =
|
||||||
|
first.model_list.find((item: any) => item.default) || first.model_list[0]
|
||||||
|
} catch (error) {
|
||||||
|
console.log('获取聊天模型数据错误=>', error)
|
||||||
|
} finally {
|
||||||
|
chatModel.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
getChatModelFunc()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选择聊天模型
|
||||||
|
const handleChoiceModel = (item: any) => {
|
||||||
|
chatModel.current = item
|
||||||
|
chatModel.show = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => chatModel.current,
|
||||||
|
(value) => {
|
||||||
|
console.log('选择聊天模型=>', value)
|
||||||
|
chatKey.value = value?.chat_key
|
||||||
|
modelKey.value = value?.key
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.model-card {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0px 1px 6px 0px rgba(230, 233, 237, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-card__active {
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
@apply border-primary bg-primary-light-9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
69
src/components/network-switch/network-switch.vue
Normal file
69
src/components/network-switch/network-switch.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="flex items-center text-sm"
|
||||||
|
v-if="appStore.config?.network?.network_is_open"
|
||||||
|
>
|
||||||
|
<u-switch v-model="switchModel" size="30" @change="showTips"></u-switch>
|
||||||
|
<span class="ml-[10rpx]">联网模式</span>
|
||||||
|
<view class="flex px-[10rpx]" @click="showNetworkTips">
|
||||||
|
<u-icon name="info-circle" size="30" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { computed, onMounted, watch } from 'vue'
|
||||||
|
import cache from '@/utils/cache'
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: boolean): void
|
||||||
|
}>()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const switchModel = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const showNetworkTips = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '温馨提示',
|
||||||
|
showCancel: false,
|
||||||
|
content: `1、开启联网模式后,AI机器人将可实时获取联网数据,由于网络搜索总结目前可能不太稳定,有时联网搜索出来的答案可能不太准确。
|
||||||
|
2、联网模式不支持连续对话,请在单次提问中描述清楚问题。
|
||||||
|
${
|
||||||
|
appStore.config?.network?.network_balance
|
||||||
|
? `3、开启联网后,每次对话将多消耗${appStore.config?.network?.network_balance}条对话(会员不消耗,大模型内置支持联网时也不消耗条数)`
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const showTips = (open: boolean) => {
|
||||||
|
uni.$u.toast(open ? '联网功能已开启' : '联网功能已关闭')
|
||||||
|
}
|
||||||
|
watch(switchModel, (value) => {
|
||||||
|
cache.set('isOpenNetwork', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => appStore.config?.network?.network_is_open,
|
||||||
|
(value) => {
|
||||||
|
if (value) {
|
||||||
|
const isOpenNetwork = cache.get('isOpenNetwork')
|
||||||
|
switchModel.value = Boolean(isOpenNetwork)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
1
src/components/notice-popup/notice-popup.vue
Normal file
1
src/components/notice-popup/notice-popup.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template>
<u-popup v-model="showNotice" mode="center" border-radius="24" zIndex="10">
<view class="w-[90vw] px-[24rpx]">
<view class="p-[30rpx] text-lg text-center font-medium">
{{ richTextTitle }}
</view>
<scroll-view class="max-h-[768rpx]" scroll-y>
<mp-html :content="richTextContent"></mp-html>
</scroll-view>
<view class="py-[30rpx] bg-white">
<button
:style="{
height: '82rpx',
lineHeight: '82rpx',
fontSize: '30rpx',
border: 'none',
borderRadius: '60px',
background: 'var(--color-primary)'
}"
@click="showNotice = false"
>
我知道了
</button>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { useAppStore } from '@/stores/app'
import cache from '@/utils/cache'
import { NOTICE } from '@/enums/constantEnums'
const appStore = useAppStore()
const showNotice = ref<boolean>(false)
const richTextTitle = computed(
() => appStore.getBulletinConfig.title
)
const richTextContent = computed(
() => appStore.getBulletinConfig.content
)
const isBulletin = computed(
() => appStore.getBulletinConfig.status == 1
)
const shouldShowNotice = (value: boolean) => {
const lastVisitTime = cache.get(NOTICE)
const currentTime = new Date().toDateString()
const isNewDay = !lastVisitTime || lastVisitTime !== currentTime
if (isNewDay && value) {
cache.set(NOTICE, currentTime)
}
return isNewDay
}
watch(
() => [isBulletin.value],
() => {
if (
isBulletin.value &&
shouldShowNotice(isBulletin.value)
) {
showNotice.value = true
}
},
{
deep: true,
immediate: true
}
)
</script>
|
||||||
68
src/components/page-status/page-status.vue
Normal file
68
src/components/page-status/page-status.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="page-status"
|
||||||
|
v-if="status !== PageStatusEnum['NORMAL']"
|
||||||
|
:class="{ 'page-status--fixed': fixed }"
|
||||||
|
>
|
||||||
|
<!-- Loading -->
|
||||||
|
<template v-if="status === PageStatusEnum['LOADING']">
|
||||||
|
<slot name="loading">
|
||||||
|
<u-loading :size="60" mode="flower" />
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<!-- Error -->
|
||||||
|
<template v-if="status === PageStatusEnum['ERROR']">
|
||||||
|
<slot name="error"></slot>
|
||||||
|
</template>
|
||||||
|
<!-- Empty -->
|
||||||
|
<template v-if="status === PageStatusEnum['EMPTY']">
|
||||||
|
<slot name="empty"></slot>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
<template v-else>
|
||||||
|
<slot> </slot>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
virtualHost: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PageStatusEnum } from '@/enums/appEnums'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
default: PageStatusEnum['LOADING']
|
||||||
|
},
|
||||||
|
fixed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-status {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ffffff;
|
||||||
|
&--fixed {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/components/payment/check.vue
Normal file
1
src/components/payment/check.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template>
<u-popup
class="pay-popup"
v-model="showCheckPay"
round
mode="center"
borderRadius="10"
:maskCloseAble="false"
>
<view class="content bg-white w-[560rpx] p-[40rpx]">
<view class="text-2xl font-medium text-center"> 支付确认 </view>
<view class="pt-[30rpx] pb-[40rpx]">
<view>
请在微信内完成支付,如果您已支付成功,请点击`已完成支付`按钮
</view>
</view>
<view class="flex">
<view class="flex-1 mr-[20rpx]">
<u-button
shape="circle"
type="primary"
plain
size="medium"
hover-class="none"
:customStyle="{ width: '100%' }"
@click="queryPayResult(false)"
>
重新支付
</u-button>
</view>
<view class="flex-1">
<u-button
shape="circle"
type="primary"
size="medium"
hover-class="none"
:customStyle="{ width: '100%' }"
@click="queryPayResult()"
>
已完成支付
</u-button>
</view>
</view>
</view>
</u-popup>
</template>
<script setup lang="ts">
import { getPayResult } from '@/api/pay'
import { computed } from 'vue'
const props = defineProps({
show: {
type: Boolean,
required: true
},
// 订单id
orderId: {
type: Number,
required: true
},
//订单来源
from: {
type: String,
required: true
}
})
const emit = defineEmits(['update:show', 'success', 'fail'])
const showCheckPay = computed({
get() {
return props.show
},
set(value) {
emit('update:show', value)
}
})
const queryPayResult = async (confirm = true) => {
const res = await getPayResult({
order_id: props.orderId,
from: props.from
})
if (res.pay_status === 0) {
if (confirm == true) {
uni.$u.toast('您的订单还未支付,请重新支付')
}
emit('fail')
// showPay.value = true
// handlePayResult(PayStatusEnum.FAIL)
} else {
if (confirm == false) {
uni.$u.toast('您的订单已经支付,请勿重新支付')
}
// handlePayResult(PayStatusEnum.SUCCESS)
emit('success')
}
showCheckPay.value = false
}
</script>
|
||||||
327
src/components/payment/payment.vue
Normal file
327
src/components/payment/payment.vue
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
<template>
|
||||||
|
<u-popup
|
||||||
|
v-model="showPay"
|
||||||
|
mode="bottom"
|
||||||
|
:safe-area-inset-bottom="safeArea"
|
||||||
|
:mask-close-able="false"
|
||||||
|
border-radius="14"
|
||||||
|
closeable
|
||||||
|
:z-index="899"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<view class>
|
||||||
|
<page-status :status="popupStatus" :fixed="false">
|
||||||
|
<template #error>
|
||||||
|
<u-empty text="订单信息错误,无法查询到订单信息" mode="order"></u-empty>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<view class="payment w-full pb-[20rpx]">
|
||||||
|
<view class="header py-[50rpx] flex flex-col items-center">
|
||||||
|
<view class="text-[34rpx]">选择支付方式</view>
|
||||||
|
</view>
|
||||||
|
<view class="main min-h-[300rpx] mx-[20rpx]">
|
||||||
|
<view>
|
||||||
|
<view class="payway-lists">
|
||||||
|
<u-radio-group
|
||||||
|
v-model="payWay"
|
||||||
|
class="w-full"
|
||||||
|
:active-color="$theme.primaryColor"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="p-[20rpx] flex items-center w-full payway-item"
|
||||||
|
v-for="(item, index) in payData.list"
|
||||||
|
:key="index"
|
||||||
|
@click="selectPayWay(item.id)"
|
||||||
|
>
|
||||||
|
<u-icon
|
||||||
|
class="flex-none"
|
||||||
|
:size="48"
|
||||||
|
:name="item.icon"
|
||||||
|
></u-icon>
|
||||||
|
<view class="mx-[16rpx] flex-1">
|
||||||
|
<view class="payway-item--name flex-1">
|
||||||
|
{{ item.name }}
|
||||||
|
</view>
|
||||||
|
<view class="text-muted text-xs">{{
|
||||||
|
item.extra
|
||||||
|
}}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<u-radio class="mr-[-20rpx]" :name="item.id"> </u-radio>
|
||||||
|
</view>
|
||||||
|
</u-radio-group>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="submit-btn p-[20rpx] mt-[50rpx]">
|
||||||
|
<u-button
|
||||||
|
@click="handlePay"
|
||||||
|
shape="circle"
|
||||||
|
type="primary"
|
||||||
|
:loading="isLock"
|
||||||
|
:disabled="!paySetup.is_open"
|
||||||
|
>
|
||||||
|
{{ paySetup.tips }}
|
||||||
|
<price
|
||||||
|
v-if="orderAmount && paySetup.is_open"
|
||||||
|
:content="orderAmount"
|
||||||
|
mainSize="34rpx"
|
||||||
|
minorSize="34rpx"
|
||||||
|
fontWeight="500"
|
||||||
|
color="var(--color-btn-text)"
|
||||||
|
></price>
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</page-status>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
<payment-check
|
||||||
|
v-model:show="showCheckPay"
|
||||||
|
:from="from"
|
||||||
|
:order-id="orderId"
|
||||||
|
@success="checkSuccess"
|
||||||
|
@fail="checkFail"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { pay, PayWayEnum } from '@/utils/pay'
|
||||||
|
import { getPayWay, prepay } from '@/api/pay'
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useLockFn } from '@/hooks/useLockFn'
|
||||||
|
import { series } from '@/utils/util'
|
||||||
|
import { ClientEnum, PageStatusEnum, PayStatusEnum } from '@/enums/appEnums'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { client } from '@/utils/client'
|
||||||
|
import { useRouter } from 'uniapp-router-next'
|
||||||
|
// #ifdef H5
|
||||||
|
import wechatOa, { UrlScene } from '@/utils/wechat'
|
||||||
|
// #endif
|
||||||
|
import PaymentCheck from './check.vue'
|
||||||
|
|
||||||
|
/*
|
||||||
|
页面参数 orderId:订单id,from:订单来源
|
||||||
|
*/
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showCheck: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
// 订单id
|
||||||
|
orderId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
//订单来源
|
||||||
|
from: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
//h5微信支付回跳路径,一般为拉起支付的页面路径
|
||||||
|
redirect: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
orderAmount: {
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
safeArea: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:showCheck', 'update:show', 'close', 'success', 'fail'])
|
||||||
|
const router = useRouter()
|
||||||
|
const payWay = ref()
|
||||||
|
|
||||||
|
const popupStatus = ref(PageStatusEnum.LOADING)
|
||||||
|
const payData = ref<any>({
|
||||||
|
lists: []
|
||||||
|
})
|
||||||
|
// const system = uni.getSystemInfoSync()
|
||||||
|
const paySetup = ref({
|
||||||
|
is_open: 1,
|
||||||
|
tips: '立即支付'
|
||||||
|
})
|
||||||
|
|
||||||
|
const showCheckPay = computed({
|
||||||
|
get() {
|
||||||
|
return props.showCheck
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:showCheck', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const showPay = computed({
|
||||||
|
get() {
|
||||||
|
return props.show
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:show', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
showPay.value = false
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
const getPayData = async () => {
|
||||||
|
popupStatus.value = PageStatusEnum.LOADING
|
||||||
|
try {
|
||||||
|
payData.value = await getPayWay({
|
||||||
|
orderId: props.orderId,
|
||||||
|
from: props.from
|
||||||
|
})
|
||||||
|
popupStatus.value = PageStatusEnum.NORMAL
|
||||||
|
const checkPay =
|
||||||
|
payData.value.list.find((item: any) => item.isDefault) || payData.value.list[0]
|
||||||
|
payWay.value = checkPay?.id
|
||||||
|
} catch (error) {
|
||||||
|
console.log('获取支付方式错误=>', error)
|
||||||
|
popupStatus.value = PageStatusEnum.ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const selectPayWay = (pay: number) => {
|
||||||
|
payWay.value = pay
|
||||||
|
}
|
||||||
|
const checkIsBindWx = async () => {
|
||||||
|
if (
|
||||||
|
userStore.userInfo.isBindWechat == false &&
|
||||||
|
[ClientEnum.OA_WEIXIN, ClientEnum.MP_WEIXIN].includes(client) &&
|
||||||
|
payWay.value == PayWayEnum.WECHAT
|
||||||
|
) {
|
||||||
|
switch (client) {
|
||||||
|
case ClientEnum.OA_WEIXIN: {
|
||||||
|
wechatOa.getUrl(UrlScene.BASE, 'snsapi_base', {
|
||||||
|
id: props.orderId,
|
||||||
|
from: props.from
|
||||||
|
})
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
case ClientEnum.MP_WEIXIN: {
|
||||||
|
const data = await uni.login()
|
||||||
|
return data.code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payment = async (code: string | undefined) => {
|
||||||
|
// 调用预支付
|
||||||
|
try {
|
||||||
|
uni.showLoading({
|
||||||
|
title: '正在支付中'
|
||||||
|
})
|
||||||
|
const data = await prepay({
|
||||||
|
orderId: props.orderId,
|
||||||
|
scene: props.from,
|
||||||
|
payWay: payWay.value,
|
||||||
|
redirect: props.redirect,
|
||||||
|
code
|
||||||
|
})
|
||||||
|
console.log('支付信息', data)
|
||||||
|
const res = await pay.payment(payWay.value, data)
|
||||||
|
return res
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { isLock, lockFn: handlePay } = useLockFn(async () => {
|
||||||
|
try {
|
||||||
|
const code = await checkIsBindWx()
|
||||||
|
const res: PayStatusEnum = await payment(code)
|
||||||
|
handlePayResult(res)
|
||||||
|
uni.hideLoading()
|
||||||
|
} catch (error) {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handlePayResult = (status: PayStatusEnum) => {
|
||||||
|
switch (status) {
|
||||||
|
case PayStatusEnum.SUCCESS:
|
||||||
|
emit('success')
|
||||||
|
break
|
||||||
|
case PayStatusEnum.FAIL:
|
||||||
|
emit('fail')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const checkSuccess = () => {
|
||||||
|
handlePayResult(PayStatusEnum.SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkFail = () => {
|
||||||
|
showPay.value = true
|
||||||
|
handlePayResult(PayStatusEnum.FAIL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取支付配置
|
||||||
|
// const getPaySetup = async () => {
|
||||||
|
// try {
|
||||||
|
// paySetup.value = await getIosPayConfig()
|
||||||
|
// } catch (error) {
|
||||||
|
// console.log('获取支付设置错误=>', error)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
async (value) => {
|
||||||
|
if (value) {
|
||||||
|
if (!props.orderId) {
|
||||||
|
popupStatus.value = PageStatusEnum.ERROR
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await getPayData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
onMounted(async () => {
|
||||||
|
// #ifdef H5
|
||||||
|
const options = wechatOa.getAuthData()
|
||||||
|
if (options.code && options.scene === UrlScene.BASE) {
|
||||||
|
payWay.value = PayWayEnum.WECHAT
|
||||||
|
showPay.value = true
|
||||||
|
try {
|
||||||
|
const res: PayStatusEnum = await payment(options.code)
|
||||||
|
handlePayResult(res)
|
||||||
|
uni.hideLoading()
|
||||||
|
} catch (error) {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
wechatOa.setAuthData({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP
|
||||||
|
// if (system.system.indexOf('iOS') !== -1) {
|
||||||
|
// await getPaySetup()
|
||||||
|
// }
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.payway-lists {
|
||||||
|
.payway-item {
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
@apply border-page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
src/components/price/price.vue
Normal file
126
src/components/price/price.vue
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<view class="price-container">
|
||||||
|
<view
|
||||||
|
:class="['price-wrap', { 'price-wrap--disabled': lineThrough }]"
|
||||||
|
:style="{ color: color }"
|
||||||
|
>
|
||||||
|
<!-- Prefix -->
|
||||||
|
<view class="fix-pre" :style="{ fontSize: minorSize }">
|
||||||
|
<slot name="prefix">{{ prefix }}</slot>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<view :style="{ 'font-weight': fontWeight }">
|
||||||
|
<!-- Integer -->
|
||||||
|
<text :style="{ fontSize: mainSize }">{{ integer }}</text>
|
||||||
|
<!-- Decimals -->
|
||||||
|
<text :style="{ fontSize: minorSize }">{{ decimals }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Suffix -->
|
||||||
|
<view class="fix-suf" :style="{ fontSize: minorSize }">
|
||||||
|
<slot name="suffix">{{ suffix }}</slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* @description 价格展示,适用于有前后缀,小数样式不一
|
||||||
|
* @property {String|Number} content 价格 (必填项)
|
||||||
|
* @property {Number} prec 小数位 (默认: 2)
|
||||||
|
* @property {Boolean} autoPrec 自动小数位【注:以prec为最大小数位】 (默认: true)
|
||||||
|
* @property {String} color 颜色 (默认: 'unset')
|
||||||
|
* @property {String} mainSize 主要内容字体大小 (默认: 46rpx)
|
||||||
|
* @property {String} minorSize 主要内容字体大小 (默认: 32rpx)
|
||||||
|
* @property {Boolean} lineThrough 贯穿线 (默认: false)
|
||||||
|
* @property {String|Number} fontWeight 字重 (默认: normal)
|
||||||
|
* @property {String} prefix 前缀 (默认: ¥)
|
||||||
|
* @property {String} suffix 后缀
|
||||||
|
* @example <price content="100" suffix="\/元" />
|
||||||
|
*/
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { formatPrice } from '@/utils/util'
|
||||||
|
|
||||||
|
/** Props Start **/
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
content: string | number // 标题
|
||||||
|
prec?: number // 小数数量
|
||||||
|
autoPrec?: boolean // 动态小数
|
||||||
|
color?: string // 颜色
|
||||||
|
mainSize?: string // 主要内容字体大小
|
||||||
|
minorSize?: string // 次要内容字体大小
|
||||||
|
lineThrough?: boolean // 贯穿线
|
||||||
|
fontWeight?: string // 字重
|
||||||
|
prefix?: string // 前缀
|
||||||
|
suffix?: string // 后缀
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
content: '',
|
||||||
|
prec: 2,
|
||||||
|
autoPrec: true,
|
||||||
|
color: '#FA8919',
|
||||||
|
mainSize: '36rpx',
|
||||||
|
minorSize: '28rpx',
|
||||||
|
lineThrough: false,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
prefix: '¥',
|
||||||
|
suffix: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
/** Props End **/
|
||||||
|
|
||||||
|
/** Computed Start **/
|
||||||
|
/**
|
||||||
|
* @description 金额主体部分
|
||||||
|
*/
|
||||||
|
const integer = computed(() => {
|
||||||
|
return formatPrice({
|
||||||
|
price: props.content,
|
||||||
|
take: 'int'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* @description 金额小数部分
|
||||||
|
*/
|
||||||
|
const decimals = computed(() => {
|
||||||
|
let decimals = formatPrice({
|
||||||
|
price: props.content,
|
||||||
|
take: 'dec',
|
||||||
|
prec: props.prec
|
||||||
|
})
|
||||||
|
// 小数余十不能是 .10||.20||.30以此类推,
|
||||||
|
decimals = decimals % 10 == 0 ? decimals.substr(0, decimals.length - 1) : decimals
|
||||||
|
return props.autoPrec ? (decimals * 1 ? '.' + decimals : '') : props.prec ? '.' + decimals : ''
|
||||||
|
})
|
||||||
|
/** Computed End **/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.price-container {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: block;
|
||||||
|
content: '';
|
||||||
|
height: 0.05em;
|
||||||
|
background-color: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
293
src/components/recorder/recorder.vue
Normal file
293
src/components/recorder/recorder.vue
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<u-popup
|
||||||
|
v-model="showModel"
|
||||||
|
mode="bottom"
|
||||||
|
safe-area-inset-bottom
|
||||||
|
:mask="false"
|
||||||
|
height="100%"
|
||||||
|
:custom-style="{
|
||||||
|
background:
|
||||||
|
'linear-gradient(180deg,rgba(18,19,23,.08),rgba(18,19,23,.98) 96%)'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="h-full flex flex-col-reverse recorder"
|
||||||
|
@touchend="stopRecord"
|
||||||
|
@touchcancel="stopRecord"
|
||||||
|
>
|
||||||
|
<view class="px-[60rpx] pb-[140rpx]">
|
||||||
|
<view class="px-[60rpx]">
|
||||||
|
<view class="bubble-text relative">
|
||||||
|
<u-input
|
||||||
|
class="flex-1"
|
||||||
|
v-show="userInput || (!isTransfer && !isRecording)"
|
||||||
|
type="textarea"
|
||||||
|
v-model="userInput"
|
||||||
|
placeholder="请输入您的问题"
|
||||||
|
maxlength="-1"
|
||||||
|
:cursor-spacing="120"
|
||||||
|
:auto-height="true"
|
||||||
|
confirm-type="send"
|
||||||
|
:fixed="true"
|
||||||
|
/>
|
||||||
|
<view class="wave" v-if="!userInput && isRecording">
|
||||||
|
<view
|
||||||
|
class="wave-item"
|
||||||
|
v-for="(item, index) in 10"
|
||||||
|
:key="item"
|
||||||
|
:style="{
|
||||||
|
'--delay': `${index / 10}s`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="isTransfer" class="loading">
|
||||||
|
<u-loading
|
||||||
|
mode="flower"
|
||||||
|
color="#fff"
|
||||||
|
:size="40"
|
||||||
|
></u-loading>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="text-white mt-[40rpx] text-center text-xs">
|
||||||
|
<template v-if="isTransfer"> 文字转换中... </template>
|
||||||
|
<template v-else>
|
||||||
|
{{
|
||||||
|
isRecording
|
||||||
|
? '松开手指,转换文字'
|
||||||
|
: '点击气泡可编辑文字'
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
<view class="flex justify-between text-white">
|
||||||
|
<view
|
||||||
|
class="action-btn bg-content"
|
||||||
|
@click="showModel = false"
|
||||||
|
>
|
||||||
|
<u-icon name="close" :size="32"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="action-btn bg-success"
|
||||||
|
@click="emit('success', userInput)"
|
||||||
|
>
|
||||||
|
<u-icon name="checkmark" :size="34"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="flex justify-center">
|
||||||
|
<view class="relative flex justify-center items-center">
|
||||||
|
<view
|
||||||
|
class="w-[170rpx] h-[170rpx] rounded-[50%] bg-primary opacity-30 absolute"
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<button
|
||||||
|
class="flex justify-center items-center w-[130rpx] h-[130rpx] rounded-[50%] bg-primary text-btn-text relative z-10"
|
||||||
|
@longpress="startRecord"
|
||||||
|
hover-class="none"
|
||||||
|
>
|
||||||
|
<!-- 添加空格的目的是:touchend事件有时候不触发 -->
|
||||||
|
|
||||||
|
<u-icon v-if="!isRecording" name="mic" :size="60" />
|
||||||
|
<loading v-else> </loading>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
styleIsolation: 'shared'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { audioTransfer } from '@/api/chat'
|
||||||
|
import { useRecorder } from '@/hooks/useRecorder'
|
||||||
|
import { onUnmounted } from 'vue'
|
||||||
|
import { shallowRef } from 'vue'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { watch } from 'vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:show', show: boolean): void
|
||||||
|
(event: 'success', text: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const showModel = computed({
|
||||||
|
get() {
|
||||||
|
return props.show
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:show', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const userInput = ref('')
|
||||||
|
const isTransfer = ref(false)
|
||||||
|
const { start, isRecording, stop, authorize, close } = useRecorder({
|
||||||
|
async onstop(result) {
|
||||||
|
if (result.duration < 1000) {
|
||||||
|
uni.$u.toast('说话时间太短')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isTransfer.value = true
|
||||||
|
try {
|
||||||
|
const data: any = await audioTransfer(result.tempFilePath, {
|
||||||
|
type: 2
|
||||||
|
})
|
||||||
|
userInput.value += data.text
|
||||||
|
} finally {
|
||||||
|
isTransfer.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const startRecord = async () => {
|
||||||
|
if (isRecording.value || isTransfer.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await start()
|
||||||
|
} catch (error) {
|
||||||
|
uni.$u.toast(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopRecord = () => {
|
||||||
|
if (isRecording.value) {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(showModel, (value) => {
|
||||||
|
if (!value) {
|
||||||
|
stopRecord()
|
||||||
|
} else {
|
||||||
|
userInput.value = ''
|
||||||
|
// startRecord()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
authorize: authorize,
|
||||||
|
startRecord,
|
||||||
|
stopRecord,
|
||||||
|
closeRecord: close
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@keyframes dot-loading {
|
||||||
|
0% {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background: #fff;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background: rgba(256, 256, 256, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes wave {
|
||||||
|
0%,
|
||||||
|
40%,
|
||||||
|
100% {
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: scaleY(0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.recorder {
|
||||||
|
.bubble-text {
|
||||||
|
position: relative;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 120rpx;
|
||||||
|
@apply bg-primary;
|
||||||
|
&::before {
|
||||||
|
content: ' ';
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -5px;
|
||||||
|
-webkit-transform: translateX(-50%) rotate(45deg);
|
||||||
|
transform: translateX(-50%) rotate(45deg);
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
:deep() {
|
||||||
|
.u-input__textarea {
|
||||||
|
--line-height: 40rpx;
|
||||||
|
--line-num: 6;
|
||||||
|
height: auto;
|
||||||
|
min-height: var(--line-height) !important;
|
||||||
|
max-height: calc(var(--line-height) * var(--line-num));
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
line-height: var(--line-height);
|
||||||
|
@apply text-white;
|
||||||
|
.uni-textarea-textarea {
|
||||||
|
max-height: calc(var(--line-height) * var(--line-num));
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
.dot-item {
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: dot-loading 1s linear infinite;
|
||||||
|
&:nth-child(1) {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
background: #fff;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-left: 6px;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
background: rgba(256, 256, 256, 0.3);
|
||||||
|
margin-left: 6px;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.wave {
|
||||||
|
display: flex;
|
||||||
|
.wave-item {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6rpx;
|
||||||
|
height: 30rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 0 5rpx;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
animation: wave 1.2s infinite ease-in-out;
|
||||||
|
animation-delay: var(--delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
src/components/tabbar/tabbar.vue
Normal file
92
src/components/tabbar/tabbar.vue
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<u-tabbar
|
||||||
|
v-if="showTabbar"
|
||||||
|
v-model="current"
|
||||||
|
v-bind="tabbarStyle"
|
||||||
|
:list="tabbarList"
|
||||||
|
@change="handleChange"
|
||||||
|
:hide-tab-bar="true"
|
||||||
|
></u-tabbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useNavigationBarTitleStore } from '@/stores/navigationBarTitle'
|
||||||
|
import { navigateTo } from '@/utils/util'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
const current = ref()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const navigationBarTitleStore = useNavigationBarTitleStore()
|
||||||
|
const tabbarList = computed(() => {
|
||||||
|
return (
|
||||||
|
appStore.getTabbarConfig.list
|
||||||
|
?.filter((item: any) => item.is_show == '1')
|
||||||
|
.map((item: any) => {
|
||||||
|
const link = item.link
|
||||||
|
return {
|
||||||
|
iconPath: appStore.getImageUrl(item.unselected),
|
||||||
|
selectedIconPath: appStore.getImageUrl(item.selected),
|
||||||
|
text: item.name,
|
||||||
|
link,
|
||||||
|
pagePath: link.path
|
||||||
|
}
|
||||||
|
}) || []
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 原生菜单列表
|
||||||
|
const nativeTabList = [
|
||||||
|
'/pages/index/index',
|
||||||
|
'/pages/ai_creation/ai_creation',
|
||||||
|
'/pages/skills/skills',
|
||||||
|
'/pages/app/app',
|
||||||
|
'/pages/user/user'
|
||||||
|
]
|
||||||
|
|
||||||
|
const getCurrentIndex = () => {
|
||||||
|
const currentPages = getCurrentPages()
|
||||||
|
const currentPage = currentPages[currentPages.length - 1]
|
||||||
|
const current = tabbarList.value.findIndex((item: any) => {
|
||||||
|
return item.pagePath === '/' + currentPage.route
|
||||||
|
})
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
const showTabbar = computed(() => {
|
||||||
|
const currentPages = getCurrentPages()
|
||||||
|
const currentPage = currentPages[currentPages.length - 1]
|
||||||
|
const current = tabbarList.value.findIndex((item: any) => {
|
||||||
|
return item.pagePath === '/' + currentPage.route
|
||||||
|
})
|
||||||
|
return current >= 0
|
||||||
|
})
|
||||||
|
const tabbarStyle = computed(() => ({
|
||||||
|
activeColor: appStore.getTabbarConfig?.style?.selected_color,
|
||||||
|
inactiveColor: appStore.getTabbarConfig?.style?.default_color
|
||||||
|
}))
|
||||||
|
const handleChange = (index: number) => {
|
||||||
|
const selectTab = tabbarList.value[index]
|
||||||
|
selectTab.link.name = selectTab.text
|
||||||
|
const navigateType = nativeTabList.includes(selectTab.pagePath)
|
||||||
|
? 'switchTab'
|
||||||
|
: 'reLaunch'
|
||||||
|
navigateTo(selectTab.link, false, navigateType)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
tabbarList,
|
||||||
|
() => {
|
||||||
|
const current = getCurrentIndex()
|
||||||
|
if (current >= 0) {
|
||||||
|
const currentTab = tabbarList.value[current]
|
||||||
|
navigationBarTitleStore.add({
|
||||||
|
path: currentTab.pagePath,
|
||||||
|
title: currentTab.text
|
||||||
|
})
|
||||||
|
navigationBarTitleStore.setTitle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
367
src/components/ua-markdown/css/markdown-style.css
Normal file
367
src/components/ua-markdown/css/markdown-style.css
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px}
|
||||||
|
.h1,
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
.h2,
|
||||||
|
h2 {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
.h3,
|
||||||
|
h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.h4,
|
||||||
|
h4 {
|
||||||
|
font-size: px;
|
||||||
|
}
|
||||||
|
.h5,
|
||||||
|
h5 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.h6,
|
||||||
|
h6 {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #2196f3;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
hr,
|
||||||
|
::v-deep .hr {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
img, ._img {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
em {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
ol ol,
|
||||||
|
ol ul,
|
||||||
|
ul ol,
|
||||||
|
ul ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
ol ol,
|
||||||
|
ul ol {
|
||||||
|
list-style-type: lower-roman;
|
||||||
|
}
|
||||||
|
ol ol ol,
|
||||||
|
ul ul ol {
|
||||||
|
list-style-type: lower-alpha;
|
||||||
|
}
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
dt {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
dt,
|
||||||
|
dd {
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.task-list-item {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
.task-list-item input {
|
||||||
|
margin: 0 0.2em 0.25em -1.6em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
position: relative;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
codekbd,
|
||||||
|
pre,
|
||||||
|
samp {
|
||||||
|
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
code:not(.hljs) {
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
code:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
pre code.hljs {
|
||||||
|
color: var(--vg__text-1);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--vg__bg-1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.markdown-wrap {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: #24272e;
|
||||||
|
}
|
||||||
|
._a {
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
.copy-line {
|
||||||
|
background-color: #595b63;
|
||||||
|
color: #e0e0e0;
|
||||||
|
height: 38px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 12px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.copy-line .code-copy-btn {
|
||||||
|
color: #e0e0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
pre.code-block-wrapper {
|
||||||
|
background: #2b2b2b;
|
||||||
|
color: #f8f8f2;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
pre.code-block-wrapper code {
|
||||||
|
padding: auto;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
background-color: inherit;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.code-block-header__copy {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
abbr[data-original-title],
|
||||||
|
abbr[title] {
|
||||||
|
cursor: help;
|
||||||
|
border-bottom: 1px dotted #777;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 17.5px;
|
||||||
|
border-left: 5px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
blockquote ol:last-child,
|
||||||
|
blockquote p:last-child,
|
||||||
|
blockquote ul:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
blockquote .small,
|
||||||
|
blockquote footer,
|
||||||
|
blockquote small {
|
||||||
|
display: block;
|
||||||
|
font-size: 80%;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
blockquote .small:before,
|
||||||
|
blockquote footer:before,
|
||||||
|
blockquote small:before {
|
||||||
|
content: '\2014 \00A0';
|
||||||
|
}
|
||||||
|
.blockquote-reverse,
|
||||||
|
blockquote.pull-right {
|
||||||
|
padding-right: 15px;
|
||||||
|
padding-left: 0;
|
||||||
|
text-align: right;
|
||||||
|
border-right: 5px solid #eee;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
.blockquote-reverse .small:before,
|
||||||
|
.blockquote-reverse footer:before,
|
||||||
|
.blockquote-reverse small:before,
|
||||||
|
blockquote.pull-right .small:before,
|
||||||
|
blockquote.pull-right footer:before,
|
||||||
|
blockquote.pull-right small:before {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
.blockquote-reverse .small:after,
|
||||||
|
.blockquote-reverse footer:after,
|
||||||
|
.blockquote-reverse small:after,
|
||||||
|
blockquote.pull-right .small:after,
|
||||||
|
blockquote.pull-right footer:after,
|
||||||
|
blockquote.pull-right small:after {
|
||||||
|
content: '\00A0 \2014';
|
||||||
|
}
|
||||||
|
.footnotes {
|
||||||
|
-moz-column-count: 2;
|
||||||
|
-webkit-column-count: 2;
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
.footnotes-list {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
table,
|
||||||
|
::v-deep .table {
|
||||||
|
border-spacing: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 65em;
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
table tr,
|
||||||
|
::v-deep .table .tr {
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
table th,
|
||||||
|
table td,
|
||||||
|
::v-deep .table .th,
|
||||||
|
::v-deep .table .td {
|
||||||
|
padding: 6px 13px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
table th,
|
||||||
|
::v-deep .table .th {
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
.hljs[class*='language-']:before {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 0.8em;
|
||||||
|
right: 1em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.hljs[class~='language-js']:before {
|
||||||
|
content: 'js';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-ts']:before {
|
||||||
|
content: 'ts';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-html']:before {
|
||||||
|
content: 'html';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-md']:before {
|
||||||
|
content: 'md';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-vue']:before {
|
||||||
|
content: 'vue';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-css']:before {
|
||||||
|
content: 'css';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-sass']:before {
|
||||||
|
content: 'sass';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-scss']:before {
|
||||||
|
content: 'scss';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-less']:before {
|
||||||
|
content: 'less';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-stylus']:before {
|
||||||
|
content: 'stylus';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-go']:before {
|
||||||
|
content: 'go';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-java']:before {
|
||||||
|
content: 'java';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-c']:before {
|
||||||
|
content: 'c';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-sh']:before {
|
||||||
|
content: 'sh';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-yaml']:before {
|
||||||
|
content: 'yaml';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-py']:before {
|
||||||
|
content: 'py';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-docker']:before {
|
||||||
|
content: 'docker';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-dockerfile']:before {
|
||||||
|
content: 'dockerfile';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-makefile']:before {
|
||||||
|
content: 'makefile';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-javascript']:before {
|
||||||
|
content: 'js';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-typescript']:before {
|
||||||
|
content: 'ts';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-markup']:before {
|
||||||
|
content: 'html';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-markdown']:before {
|
||||||
|
content: 'md';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-json']:before {
|
||||||
|
content: 'json';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-ruby']:before {
|
||||||
|
content: 'rb';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-python']:before {
|
||||||
|
content: 'py';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-bash']:before {
|
||||||
|
content: 'sh';
|
||||||
|
}
|
||||||
|
.hljs[class~='language-php']:before {
|
||||||
|
content: 'php';
|
||||||
|
}
|
||||||
|
|
||||||
|
._abbr,
|
||||||
|
._b,
|
||||||
|
._code,
|
||||||
|
._del,
|
||||||
|
._em,
|
||||||
|
._i,
|
||||||
|
._ins,
|
||||||
|
._label,
|
||||||
|
._q,
|
||||||
|
._span,
|
||||||
|
._strong,
|
||||||
|
._sub,
|
||||||
|
._sup {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
|
||||||
10
src/components/ua-markdown/lib/highlight/github-dark.min.css
vendored
Normal file
10
src/components/ua-markdown/lib/highlight/github-dark.min.css
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||||
|
Theme: GitHub Dark
|
||||||
|
Description: Dark theme as seen on github.com
|
||||||
|
Author: github.com
|
||||||
|
Maintainer: @Hirse
|
||||||
|
Updated: 2021-05-15
|
||||||
|
|
||||||
|
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||||
|
Current colors taken from GitHub's CSS
|
||||||
|
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||||
352
src/components/ua-markdown/lib/html-parser.js
Normal file
352
src/components/ua-markdown/lib/html-parser.js
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
/*
|
||||||
|
* HTML5 Parser By Sam Blowes
|
||||||
|
*
|
||||||
|
* Designed for HTML5 documents
|
||||||
|
*
|
||||||
|
* Original code by John Resig (ejohn.org)
|
||||||
|
* http://ejohn.org/blog/pure-javascript-html-parser/
|
||||||
|
* Original code by Erik Arvidsson, Mozilla Public License
|
||||||
|
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
||||||
|
*
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
* License
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* This code is triple licensed using Apache Software License 2.0,
|
||||||
|
* Mozilla Public License or GNU Public License
|
||||||
|
*
|
||||||
|
* ////////////////////////////////////////////////////////////////////////////
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy
|
||||||
|
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* ////////////////////////////////////////////////////////////////////////////
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License
|
||||||
|
* Version 1.1 (the "License"); you may not use this file except in
|
||||||
|
* compliance with the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS"
|
||||||
|
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing rights and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* The Original Code is Simple HTML Parser.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Erik Arvidsson.
|
||||||
|
* Portions created by Erik Arvidssson are Copyright (C) 2004. All Rights
|
||||||
|
* Reserved.
|
||||||
|
*
|
||||||
|
* ////////////////////////////////////////////////////////////////////////////
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
* Usage
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* // Use like so:
|
||||||
|
* HTMLParser(htmlString, {
|
||||||
|
* start: function(tag, attrs, unary) {},
|
||||||
|
* end: function(tag) {},
|
||||||
|
* chars: function(text) {},
|
||||||
|
* comment: function(text) {}
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // or to get an XML string:
|
||||||
|
* HTMLtoXML(htmlString);
|
||||||
|
*
|
||||||
|
* // or to get an XML DOM Document
|
||||||
|
* HTMLtoDOM(htmlString);
|
||||||
|
*
|
||||||
|
* // or to inject into an existing document/DOM node
|
||||||
|
* HTMLtoDOM(htmlString, document);
|
||||||
|
* HTMLtoDOM(htmlString, document.body);
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
// Regular Expressions for parsing tags and attributes
|
||||||
|
var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
|
||||||
|
var endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/;
|
||||||
|
var attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; // Empty Elements - HTML 5
|
||||||
|
|
||||||
|
var empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr'); // Block Elements - HTML 5
|
||||||
|
// fixed by xxx 将 ins 标签从块级名单中移除
|
||||||
|
|
||||||
|
var block = makeMap('a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video'); // Inline Elements - HTML 5
|
||||||
|
|
||||||
|
var inline = makeMap('abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var'); // Elements that you can, intentionally, leave open
|
||||||
|
// (and which close themselves)
|
||||||
|
|
||||||
|
var closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'); // Attributes that have their values filled in disabled="disabled"
|
||||||
|
|
||||||
|
var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected'); // Special Elements (can contain anything)
|
||||||
|
|
||||||
|
var special = makeMap('script,style');
|
||||||
|
function HTMLParser(html, handler) {
|
||||||
|
var index;
|
||||||
|
var chars;
|
||||||
|
var match;
|
||||||
|
var stack = [];
|
||||||
|
var last = html;
|
||||||
|
|
||||||
|
stack.last = function () {
|
||||||
|
return this[this.length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
while (html) {
|
||||||
|
chars = true; // Make sure we're not in a script or style element
|
||||||
|
|
||||||
|
if (!stack.last() || !special[stack.last()]) {
|
||||||
|
// Comment
|
||||||
|
if (html.indexOf('<!--') == 0) {
|
||||||
|
index = html.indexOf('-->');
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
if (handler.comment) {
|
||||||
|
handler.comment(html.substring(4, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
html = html.substring(index + 3);
|
||||||
|
chars = false;
|
||||||
|
} // end tag
|
||||||
|
|
||||||
|
} else if (html.indexOf('</') == 0) {
|
||||||
|
match = html.match(endTag);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
html = html.substring(match[0].length);
|
||||||
|
match[0].replace(endTag, parseEndTag);
|
||||||
|
chars = false;
|
||||||
|
} // start tag
|
||||||
|
|
||||||
|
} else if (html.indexOf('<') == 0) {
|
||||||
|
match = html.match(startTag);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
html = html.substring(match[0].length);
|
||||||
|
match[0].replace(startTag, parseStartTag);
|
||||||
|
chars = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chars) {
|
||||||
|
index = html.indexOf('<');
|
||||||
|
var text = index < 0 ? html : html.substring(0, index);
|
||||||
|
html = index < 0 ? '' : html.substring(index);
|
||||||
|
|
||||||
|
if (handler.chars) {
|
||||||
|
handler.chars(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html = html.replace(new RegExp('([\\s\\S]*?)<\/' + stack.last() + '[^>]*>'), function (all, text) {
|
||||||
|
text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, '$1$2');
|
||||||
|
|
||||||
|
if (handler.chars) {
|
||||||
|
handler.chars(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
parseEndTag('', stack.last());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (html == last) {
|
||||||
|
throw 'Parse Error: ' + html;
|
||||||
|
}
|
||||||
|
|
||||||
|
last = html;
|
||||||
|
} // Clean up any remaining tags
|
||||||
|
|
||||||
|
|
||||||
|
parseEndTag();
|
||||||
|
|
||||||
|
function parseStartTag(tag, tagName, rest, unary) {
|
||||||
|
tagName = tagName.toLowerCase();
|
||||||
|
|
||||||
|
if (block[tagName]) {
|
||||||
|
while (stack.last() && inline[stack.last()]) {
|
||||||
|
parseEndTag('', stack.last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeSelf[tagName] && stack.last() == tagName) {
|
||||||
|
parseEndTag('', tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
unary = empty[tagName] || !!unary;
|
||||||
|
|
||||||
|
if (!unary) {
|
||||||
|
stack.push(tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handler.start) {
|
||||||
|
var attrs = [];
|
||||||
|
rest.replace(attr, function (match, name) {
|
||||||
|
var value = arguments[2] ? arguments[2] : arguments[3] ? arguments[3] : arguments[4] ? arguments[4] : fillAttrs[name] ? name : '';
|
||||||
|
attrs.push({
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') // "
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (handler.start) {
|
||||||
|
handler.start(tagName, attrs, unary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEndTag(tag, tagName) {
|
||||||
|
// If no tag name is provided, clean shop
|
||||||
|
if (!tagName) {
|
||||||
|
var pos = 0;
|
||||||
|
} // Find the closest opened tag of the same type
|
||||||
|
else {
|
||||||
|
for (var pos = stack.length - 1; pos >= 0; pos--) {
|
||||||
|
if (stack[pos] == tagName) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos >= 0) {
|
||||||
|
// Close all the open elements, up the stack
|
||||||
|
for (var i = stack.length - 1; i >= pos; i--) {
|
||||||
|
if (handler.end) {
|
||||||
|
handler.end(stack[i]);
|
||||||
|
}
|
||||||
|
} // Remove the open elements from the stack
|
||||||
|
|
||||||
|
|
||||||
|
stack.length = pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMap(str) {
|
||||||
|
var obj = {};
|
||||||
|
var items = str.split(',');
|
||||||
|
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
obj[items[i]] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDOCTYPE(html) {
|
||||||
|
return html.replace(/<\?xml.*\?>\n/, '').replace(/<!doctype.*>\n/, '').replace(/<!DOCTYPE.*>\n/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAttrs(attrs) {
|
||||||
|
return attrs.reduce(function (pre, attr) {
|
||||||
|
var value = attr.value;
|
||||||
|
var name = attr.name;
|
||||||
|
|
||||||
|
if (pre[name]) {
|
||||||
|
pre[name] = pre[name] + " " + value;
|
||||||
|
} else {
|
||||||
|
pre[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pre;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHtml(html) {
|
||||||
|
html = removeDOCTYPE(html);
|
||||||
|
var stacks = [];
|
||||||
|
var results = {
|
||||||
|
node: 'root',
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
HTMLParser(html, {
|
||||||
|
start: function start(tag, attrs, unary) {
|
||||||
|
var node = {
|
||||||
|
name: tag
|
||||||
|
};
|
||||||
|
|
||||||
|
if (attrs.length !== 0) {
|
||||||
|
node.attrs = parseAttrs(attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unary) {
|
||||||
|
var parent = stacks[0] || results;
|
||||||
|
|
||||||
|
if (!parent.children) {
|
||||||
|
parent.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.push(node);
|
||||||
|
} else {
|
||||||
|
stacks.unshift(node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
end: function end(tag) {
|
||||||
|
var node = stacks.shift();
|
||||||
|
if (node.name !== tag) console.error('invalid state: mismatch end tag');
|
||||||
|
|
||||||
|
if (stacks.length === 0) {
|
||||||
|
results.children.push(node);
|
||||||
|
} else {
|
||||||
|
var parent = stacks[0];
|
||||||
|
|
||||||
|
if (!parent.children) {
|
||||||
|
parent.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.push(node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chars: function chars(text) {
|
||||||
|
var node = {
|
||||||
|
type: 'text',
|
||||||
|
text: text
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stacks.length === 0) {
|
||||||
|
results.children.push(node);
|
||||||
|
} else {
|
||||||
|
var parent = stacks[0];
|
||||||
|
|
||||||
|
if (!parent.children) {
|
||||||
|
parent.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.push(node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
comment: function comment(text) {
|
||||||
|
var node = {
|
||||||
|
node: 'comment',
|
||||||
|
text: text
|
||||||
|
};
|
||||||
|
var parent = stacks[0];
|
||||||
|
|
||||||
|
if (!parent.children) {
|
||||||
|
parent.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default parseHtml;
|
||||||
2
src/components/ua-markdown/lib/markdown-it.min.js
vendored
Normal file
2
src/components/ua-markdown/lib/markdown-it.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
145
src/components/ua-markdown/ua-markdown.vue
Normal file
145
src/components/ua-markdown/ua-markdown.vue
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<!-- uniapp vue3 markdown解析 -->
|
||||||
|
<template>
|
||||||
|
<view class="ua__markdown">
|
||||||
|
<!-- <rich-text-->
|
||||||
|
<!-- space="nbsp"-->
|
||||||
|
<!-- selectable-->
|
||||||
|
<!-- user-select-->
|
||||||
|
<!-- :nodes="parseNodes(content)"-->
|
||||||
|
<!-- @itemclick="handleItemClick"-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- </rich-text>-->
|
||||||
|
<mp-html
|
||||||
|
:selectable="true"
|
||||||
|
:scrollTable="true"
|
||||||
|
:content="parseNodes(content)"
|
||||||
|
@linktap="handleItemClick"
|
||||||
|
></mp-html>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MarkdownIt from './lib/markdown-it.min.js'
|
||||||
|
import hljs from 'highlight.js/lib/common'
|
||||||
|
import './lib/highlight/atom-one-dark.css'
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
import parseHtml from './lib/html-parser.js'
|
||||||
|
// #endif
|
||||||
|
import markdownItMath from '@iktakahiro/markdown-it-katex'
|
||||||
|
import MpHtml from '@/uni_modules/mp-html/components/mp-html/mp-html.vue'
|
||||||
|
import { useCopy } from '@/hooks/useCopy'
|
||||||
|
const props = defineProps({
|
||||||
|
// 解析内容
|
||||||
|
content: String,
|
||||||
|
showLine: { type: [Boolean, String], default: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
const copyCodeData = []
|
||||||
|
const markdown = MarkdownIt({
|
||||||
|
html: true,
|
||||||
|
breaks: true,
|
||||||
|
typographer: true,
|
||||||
|
linkify: true,
|
||||||
|
lineNumbers: true,
|
||||||
|
highlight: function (str, lang) {
|
||||||
|
let preCode = ''
|
||||||
|
try {
|
||||||
|
preCode = hljs.highlightAuto(str).value
|
||||||
|
} catch (err) {
|
||||||
|
preCode = markdown.utils.escapeHtml(str)
|
||||||
|
}
|
||||||
|
const lines = preCode.split(/\n/).slice(0, -1)
|
||||||
|
// 添加自定义行号
|
||||||
|
let html = lines
|
||||||
|
.map((item, index) => {
|
||||||
|
if (item == '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
'<li><span class="line-num" data-line="' +
|
||||||
|
(index + 1) +
|
||||||
|
'"></span>' +
|
||||||
|
item +
|
||||||
|
'</li>'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
if (props.showLine) {
|
||||||
|
html = '<ol style="padding: 0px 30px;">' + html + '</ol>'
|
||||||
|
} else {
|
||||||
|
html =
|
||||||
|
'<ol style="padding: 0px 7px;list-style:none;">' +
|
||||||
|
html +
|
||||||
|
'</ol>'
|
||||||
|
}
|
||||||
|
copyCodeData.push(str)
|
||||||
|
let htmlCode = `<div class="markdown-wrap">`
|
||||||
|
|
||||||
|
htmlCode += `<div class="copy-line" style="text-align: right;font-size: 12px; margin-bottom: -10px;border-radius: 5px 5px 0 0;">`
|
||||||
|
htmlCode += `${lang}<a class="code-copy-btn" code-data-index="${
|
||||||
|
copyCodeData.length - 1
|
||||||
|
}">复制代码</a>`
|
||||||
|
htmlCode += `</div>`
|
||||||
|
|
||||||
|
htmlCode += `<pre class="hljs" style="padding:10px 8px;margin:5px 0;overflow: auto;display: block;border-radius: 5px;"><code>${html}</code></pre>`
|
||||||
|
htmlCode += '</div>'
|
||||||
|
return htmlCode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
markdown.use(markdownItMath)
|
||||||
|
const parseNodes = (value) => {
|
||||||
|
if (!value) return
|
||||||
|
// 解析<br />到\n
|
||||||
|
value = value.replace(/<br>|<br\/>|<br \/>/g, '\n')
|
||||||
|
value = value.replace(/ /g, ' ')
|
||||||
|
let htmlString = ''
|
||||||
|
if (value.split('```').length % 2) {
|
||||||
|
let mdtext = value
|
||||||
|
if (mdtext[mdtext.length - 1] != '\n') {
|
||||||
|
mdtext += '\n'
|
||||||
|
}
|
||||||
|
htmlString = markdown.render(mdtext)
|
||||||
|
} else {
|
||||||
|
htmlString = markdown.render(value)
|
||||||
|
}
|
||||||
|
// 解决小程序表格边框型失效问题
|
||||||
|
htmlString = htmlString.replace(/<table/g, `<table class="table"`)
|
||||||
|
htmlString = htmlString.replace(/<tr/g, `<tr class="tr"`)
|
||||||
|
htmlString = htmlString.replace(/<th>/g, `<th class="th">`)
|
||||||
|
htmlString = htmlString.replace(/<td/g, `<td class="td"`)
|
||||||
|
htmlString = htmlString.replace(/<hr>|<hr\/>|<hr \/>/g, `<hr class="hr">`)
|
||||||
|
|
||||||
|
// #ifndef APP-NVUE
|
||||||
|
return htmlString
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 将htmlString转成htmlArray,反之使用rich-text解析
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
return parseHtml(htmlString)
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制代码
|
||||||
|
const handleItemClick = (e) => {
|
||||||
|
const { 'code-data-index': codeDataIndex, class: className } = e
|
||||||
|
if (className == 'code-copy-btn') {
|
||||||
|
// #ifdef H5
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: copyCodeData[codeDataIndex],
|
||||||
|
showToast: false,
|
||||||
|
success() {
|
||||||
|
uni.showToast({
|
||||||
|
title: '复制成功',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
const { copy } = useCopy()
|
||||||
|
console.log(copyCodeData[codeDataIndex])
|
||||||
|
copy(copyCodeData[codeDataIndex])
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
48
src/components/widgets/banner/banner.vue
Normal file
48
src/components/widgets/banner/banner.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="banner h-[340rpx] bg-white translate-y-0"
|
||||||
|
v-if="content.data.length && content.enabled"
|
||||||
|
>
|
||||||
|
<swiper
|
||||||
|
class="swiper h-full"
|
||||||
|
:indicator-dots="content.data.length > 1"
|
||||||
|
indicator-active-color="#4173ff"
|
||||||
|
:autoplay="true"
|
||||||
|
>
|
||||||
|
<swiper-item
|
||||||
|
v-for="(item, index) in content.data"
|
||||||
|
:key="index"
|
||||||
|
@click="handleClick(item.link)"
|
||||||
|
>
|
||||||
|
<u-image
|
||||||
|
mode="aspectFit"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
:src="getImageUrl(item.image)"
|
||||||
|
/>
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { navigateTo } from '@/utils/util'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const { getImageUrl } = useAppStore()
|
||||||
|
const handleClick = (link: any) => {
|
||||||
|
navigateTo(link)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
89
src/components/widgets/customer-service/customer-service.vue
Normal file
89
src/components/widgets/customer-service/customer-service.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="customer-service bg-white flex flex-col justify-center items-center mx-[36rpx] mt-[20rpx] rounded-lg px-[110rpx] pt-[100rpx] pb-[160rpx]"
|
||||||
|
>
|
||||||
|
<u-image width="280" height="280" :src="getImageUrl(content.qrcode)" />
|
||||||
|
<view
|
||||||
|
v-if="content.title && +content.title_status"
|
||||||
|
class="text-lg mt-[14rpx] font-medium"
|
||||||
|
>
|
||||||
|
{{ content.title }}
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="content.time && +content.time_status"
|
||||||
|
class="text-content mt-[40rpx]"
|
||||||
|
>
|
||||||
|
服务时间:{{ content.time }}
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="content.mobile && +content.mobile_status"
|
||||||
|
class="text-content mt-[14rpx] flex flex-wrap"
|
||||||
|
>
|
||||||
|
客服电话:{{ content.mobile }}
|
||||||
|
<!-- #ifdef H5 -->
|
||||||
|
<a
|
||||||
|
class="ml-[10rpx] phone text-muted underline"
|
||||||
|
:href="'tel:' + content.mobile"
|
||||||
|
>
|
||||||
|
拨打
|
||||||
|
</a>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifndef H5 -->
|
||||||
|
<view
|
||||||
|
class="ml-[10rpx] phone text-muted underline"
|
||||||
|
@click="handleCall"
|
||||||
|
>
|
||||||
|
拨打
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
</view>
|
||||||
|
<view class="mt-[100rpx] w-full">
|
||||||
|
<u-button
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
@click="onSaveQrcode(getImageUrl(content.qrcode))"
|
||||||
|
>
|
||||||
|
保存二维码图片
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { saveImageToPhotosAlbum } from '@/utils/file'
|
||||||
|
import { client } from '@/utils/client'
|
||||||
|
import { ClientEnum } from '@/enums/appEnums'
|
||||||
|
import { usePermissionsStore } from '@/stores/androidPermissions'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getImageUrl } = useAppStore()
|
||||||
|
|
||||||
|
const handleCall = () => {
|
||||||
|
uni.makePhoneCall({
|
||||||
|
phoneNumber: String(props.content.mobile)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSaveQrcode = async (qrcode: string) => {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
if (client == ClientEnum['ANDROID']) {
|
||||||
|
const { requestPermissions } = usePermissionsStore()
|
||||||
|
const result = await requestPermissions('WRITE_EXTERNAL_STORAGE')
|
||||||
|
if (result !== 1) return
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
saveImageToPhotosAlbum(qrcode)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
79
src/components/widgets/my-service/my-service.vue
Normal file
79
src/components/widgets/my-service/my-service.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="my-service bg-white mx-[20rpx] mb-[20rpx] rounded-lg">
|
||||||
|
<div
|
||||||
|
v-if="content.style == 1"
|
||||||
|
class="flex flex-wrap pt-[40rpx] pb-[20rpx]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in showList"
|
||||||
|
:key="index"
|
||||||
|
class="flex flex-col items-center w-1/4 mb-[15px]"
|
||||||
|
@click="handleClick(item.link, item.name)"
|
||||||
|
>
|
||||||
|
<u-image
|
||||||
|
width="52"
|
||||||
|
height="52"
|
||||||
|
:src="getImageUrl(item.image)"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div class="mt-[7px]">{{ item.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="content.style == 2">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in showList"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center nav-item h-[100rpx] px-[24rpx]"
|
||||||
|
@click="handleClick(item.link, item.name)"
|
||||||
|
>
|
||||||
|
<u-image
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
:src="getImageUrl(item.image)"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div class="ml-[20rpx] flex-1">{{ item.name }}</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<u-icon name="arrow-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { navigateTo } from '@/utils/util'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const { getImageUrl } = useAppStore()
|
||||||
|
|
||||||
|
const showList = computed(() => {
|
||||||
|
return (
|
||||||
|
props.content.data?.filter((item: any) =>
|
||||||
|
item.is_show ? item.is_show == '1' : true
|
||||||
|
) || []
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const handleClick = (link: any, name: string) => {
|
||||||
|
link.name = name
|
||||||
|
navigateTo(link, true)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.nav-item {
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
@apply border-light border-solid border-0 border-b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
38
src/components/widgets/nav/nav.vue
Normal file
38
src/components/widgets/nav/nav.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<view class="nav pt-[30rpx] pb-[16rpx] bg-white" v-if="content.data.length && content.enabled">
|
||||||
|
<view class="nav-item flex flex-wrap">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in content.data"
|
||||||
|
:key="index"
|
||||||
|
class="flex flex-col items-center w-1/5 mb-[30rpx]"
|
||||||
|
@click="handleClick(item.link)"
|
||||||
|
>
|
||||||
|
<u-image width="41px" height="41px" :src="getImageUrl(item.image)" alt="" />
|
||||||
|
<view class="mt-[14rpx]">{{ item.name }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { navigateTo } from '@/utils/util'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = (link: any) => {
|
||||||
|
navigateTo(link)
|
||||||
|
}
|
||||||
|
const { getImageUrl } = useAppStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
13
src/components/widgets/search/search.vue
Normal file
13
src/components/widgets/search/search.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<navigator
|
||||||
|
url="/pages/search/search"
|
||||||
|
class="search px-[24rpx] py-[14rpx] bg-white"
|
||||||
|
hover-class="none"
|
||||||
|
>
|
||||||
|
<u-search placeholder="请输入关键词搜索" disabled :show-action="false"></u-search>
|
||||||
|
</navigator>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
49
src/components/widgets/user-banner/user-banner.vue
Normal file
49
src/components/widgets/user-banner/user-banner.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="banner h-[200rpx] mx-[20rpx] mt-[20rpx] translate-y-0"
|
||||||
|
v-if="content.data.length && content.enabled"
|
||||||
|
>
|
||||||
|
<swiper
|
||||||
|
class="swiper h-full"
|
||||||
|
:indicator-dots="content.data.length > 1"
|
||||||
|
indicator-active-color="#4173ff"
|
||||||
|
:autoplay="true"
|
||||||
|
>
|
||||||
|
<swiper-item
|
||||||
|
v-for="(item, index) in content.data"
|
||||||
|
:key="index"
|
||||||
|
@click="handleClick(item.link)"
|
||||||
|
>
|
||||||
|
<u-image
|
||||||
|
mode="aspectFit"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
:src="getImageUrl(item.image)"
|
||||||
|
:border-radius="14"
|
||||||
|
/>
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { navigateTo } from '@/utils/util'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const handleClick = (link: any) => {
|
||||||
|
navigateTo(link)
|
||||||
|
}
|
||||||
|
const { getImageUrl } = useAppStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
1
src/components/widgets/user-bottom/user-bottom.vue
Normal file
1
src/components/widgets/user-bottom/user-bottom.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template>
<view class="user-bottom flex items-center p-[16px] mt-[20px] text-center">
<view class="text-sm w-full text-content">
<text class="">{{ content.title }}</text>
<text class="mr-[4px]">{{ content.content }}</text>
<image
:src="iconCopy"
class="w-[28rpx] h-[28rpx] align-middle"
v-if="content?.canCopy"
@click="copy(content.content)"
></image>
</view>
</view>
</template>
<script lang="ts" setup>
import { useCopy } from '@/hooks/useCopy'
import { useNavigationBarTitleStore } from '@/stores/navigationBarTitle'
import iconCopy from '@/static/images/icon/icon_copy.png'
const navigationBarTitleStore = useNavigationBarTitleStore()
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
}
})
const { copy } = useCopy()
</script>
<style lang="scss" scoped>
.user-bottom {
}
</style>
|
||||||
106
src/components/widgets/user-info/user-info.vue
Normal file
106
src/components/widgets/user-info/user-info.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<view class="user-info mb-[-70rpx]">
|
||||||
|
<!-- #ifndef H5 -->
|
||||||
|
<u-sticky
|
||||||
|
h5-nav-height="0"
|
||||||
|
bg-color="transparent"
|
||||||
|
@fixed="isFixed = true"
|
||||||
|
@unfixed="isFixed = false"
|
||||||
|
>
|
||||||
|
<u-navbar
|
||||||
|
:is-back="false"
|
||||||
|
:is-fixed="false"
|
||||||
|
:title="navigationBarTitleStore.getTitle"
|
||||||
|
:title-color="$theme.navColor"
|
||||||
|
:background="{ backgroundColor: getNavBg }"
|
||||||
|
:border-bottom="false"
|
||||||
|
:title-bold="true"
|
||||||
|
>
|
||||||
|
</u-navbar>
|
||||||
|
</u-sticky>
|
||||||
|
<!-- #endif -->
|
||||||
|
<view class="flex px-[50rpx] pb-[100rpx] justify-between pt-[40rpx]">
|
||||||
|
<view
|
||||||
|
v-if="isLogin"
|
||||||
|
class="flex items-center"
|
||||||
|
@click="navigateTo('/packages/pages/user_set/user_set')"
|
||||||
|
>
|
||||||
|
<u-avatar :src="user.avatar" :size="120"></u-avatar>
|
||||||
|
<view class="text-btn-text ml-[20rpx]">
|
||||||
|
<view class="flex items-center">
|
||||||
|
<view class="text-2xl font-medium">
|
||||||
|
{{ user.nickname }}
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="flex-none ml-[16rpx] text-xs text-white rounded-[6rpx] px-[10rpx] py-[6rpx]"
|
||||||
|
:class="{
|
||||||
|
'text-[#F8C596]': user.isMember
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
background: user.isMember
|
||||||
|
? 'linear-gradient(90.00deg, #484848 0%, #101010 100%)'
|
||||||
|
: '#4073fa'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ user.isMember ? user.memberPackageName : '普通用户' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="text-xs mt-[18rpx]" @click.stop="copy(user.sn)">
|
||||||
|
用户ID:{{ user.sn }}
|
||||||
|
<text class="underline">复制</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<navigator v-else class="flex items-center" hover-class="none" url="/pages/login/login">
|
||||||
|
<u-avatar src="/static/images/user/default_avatar.png" :size="120"></u-avatar>
|
||||||
|
<view class="text-btn-text text-3xl ml-[20rpx]">未登录</view>
|
||||||
|
</navigator>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useCopy } from '@/hooks/useCopy'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import { useRouter } from 'uniapp-router-next'
|
||||||
|
|
||||||
|
import { useNavigationBarTitleStore } from '@/stores/navigationBarTitle'
|
||||||
|
const navigationBarTitleStore = useNavigationBarTitleStore()
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
isLogin: {
|
||||||
|
type: Boolean
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const router = useRouter()
|
||||||
|
const { copy } = useCopy()
|
||||||
|
const isFixed = ref(false)
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
const getNavBg = computed(() => {
|
||||||
|
return isFixed.value ? themeStore.primaryColor : 'transparent'
|
||||||
|
})
|
||||||
|
const navigateTo = (url: string) => {
|
||||||
|
router.navigateTo(url)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.user-info {
|
||||||
|
background: url('../../../static/images/user/user_bg.png'),
|
||||||
|
linear-gradient(90deg, $u-type-primary, $u-minor-color);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: bottom;
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
96
src/components/widgets/user-vip/user-vip.vue
Normal file
96
src/components/widgets/user-vip/user-vip.vue
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<view class="user-vip mx-[20rpx] rounded-lg mb-[20rpx]">
|
||||||
|
<view class="p-[30rpx] flex" v-if="appStore.getIsShowVip">
|
||||||
|
<u-image width="80" height="80" :src="getImageUrl(content.icon)" alt="" />
|
||||||
|
<view class="flex justify-between w-full">
|
||||||
|
<view class="ml-[20rpx] flex flex-col justify-between">
|
||||||
|
<view class="text-[32rpx] text-[#55300F] font-bold">
|
||||||
|
{{ user.isMember ? '您已成为VIP会员' : content.title }}
|
||||||
|
</view>
|
||||||
|
<view class="text-[24rpx] text-[#6A3D15]">
|
||||||
|
{{
|
||||||
|
user.isMember
|
||||||
|
? user.memberPerpetual
|
||||||
|
? '有效期至:永久'
|
||||||
|
: `有效期至:${user.memberExpire}`
|
||||||
|
: content.sub_title
|
||||||
|
}}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="flex flex-col justify-center" v-if="isLogin">
|
||||||
|
<u-button
|
||||||
|
v-if="appStore.getIsShowVip && !user.memberPerpetual"
|
||||||
|
shape="circle"
|
||||||
|
size="medium"
|
||||||
|
:customStyle="{
|
||||||
|
padding: '0 24rpx',
|
||||||
|
height: '56rpx',
|
||||||
|
background: '#333333',
|
||||||
|
color: '#F8C596'
|
||||||
|
}"
|
||||||
|
hover-class="none"
|
||||||
|
@click="navigateTo('/packages/pages/open_vip/open_vip')"
|
||||||
|
>
|
||||||
|
{{ user.isMember ? '立即续费' : content.btn }}
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<router-navigate to="/packages/pages/recharge/recharge">
|
||||||
|
<view class="bg-white rounded-lg flex p-[30rpx] pr-[24rpx]">
|
||||||
|
<view class="flex-1">
|
||||||
|
<view>对话余额</view>
|
||||||
|
<view class="flex items-center mt-[20rpx]">
|
||||||
|
<text class="text-primary"> {{ user.balance || 0 }}条 </text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="flex-1" v-if="appStore.getDrawConfig.is_open">
|
||||||
|
<view>绘画余额</view>
|
||||||
|
<view class="flex items-center mt-[20rpx]">
|
||||||
|
<text class="text-primary"> {{ user.balanceDraw || 0 }}次 </text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="text-muted flex">
|
||||||
|
<u-icon name="arrow-right" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</router-navigate>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useRouter } from 'uniapp-router-next'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
isLogin: {
|
||||||
|
type: Boolean
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const router = useRouter()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { getImageUrl } = useAppStore()
|
||||||
|
const navigateTo = (path: string) => {
|
||||||
|
// uni.navigateTo({ url: path })
|
||||||
|
router.navigateTo(path)
|
||||||
|
// console.log(path)
|
||||||
|
// uni.switchTab({ url: path })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.user-vip {
|
||||||
|
background: linear-gradient(90deg, #ffe8cf 0%, #ffe8cf 20%, #e1ab7a 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
23
src/config/index.ts
Normal file
23
src/config/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { isDevMode } from "@/utils/env";
|
||||||
|
const envBaseUrl = import.meta.env.VITE_APP_BASE_URL || "";
|
||||||
|
|
||||||
|
let baseUrl = `${envBaseUrl}/`;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 微信小程序在`VITE_APP_BASE_URL`存在或`dev`模式下
|
||||||
|
* 使用`VITE_APP_BASE_URL`的值
|
||||||
|
* 其他情况使用`[baseUrl]`,方便服务端替换
|
||||||
|
*/
|
||||||
|
|
||||||
|
//#ifdef MP-WEIXIN
|
||||||
|
baseUrl = isDevMode() || envBaseUrl ? baseUrl : "[baseUrl]";
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
version: "2.4.0", //版本号
|
||||||
|
baseUrl, //请求接口域名
|
||||||
|
urlPrefix: "api", //请求默认前缀
|
||||||
|
timeout: 30 * 1000, //请求超时时长
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
5
src/enums/agreementEnums.ts
Normal file
5
src/enums/agreementEnums.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//菜单主题类型
|
||||||
|
export enum AgreementEnum {
|
||||||
|
PRIVACY = 'privacy',
|
||||||
|
SERVICE = 'service'
|
||||||
|
}
|
||||||
50
src/enums/appEnums.ts
Normal file
50
src/enums/appEnums.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//菜单主题类型
|
||||||
|
export enum ThemeEnum {
|
||||||
|
LIGHT = 'light',
|
||||||
|
DARK = 'dark'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户端
|
||||||
|
export enum ClientEnum {
|
||||||
|
MP_WEIXIN = 1, // 微信-小程序
|
||||||
|
OA_WEIXIN = 2, // 微信-公众号
|
||||||
|
H5 = 3, // H5
|
||||||
|
IOS = 5, //苹果
|
||||||
|
ANDROID = 6 //安卓
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SMSEnum {
|
||||||
|
LOGIN = 101,
|
||||||
|
BIND_MOBILE = 102,
|
||||||
|
CHANGE_MOBILE = 103,
|
||||||
|
FIND_PASSWORD = 104,
|
||||||
|
REGISTER = 105
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SearchTypeEnum {
|
||||||
|
HISTORY = 'history'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户资料
|
||||||
|
export enum FieldType {
|
||||||
|
NONE = '',
|
||||||
|
AVATAR = 'avatar',
|
||||||
|
USERNAME = 'username',
|
||||||
|
NICKNAME = 'nickname',
|
||||||
|
SEX = 'sex'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付结果
|
||||||
|
export enum PayStatusEnum {
|
||||||
|
SUCCESS = 'success',
|
||||||
|
FAIL = 'fail',
|
||||||
|
PENDING = 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面状态
|
||||||
|
export enum PageStatusEnum {
|
||||||
|
LOADING = 'loading', // 加载中
|
||||||
|
NORMAL = 'normal', // 正常
|
||||||
|
ERROR = 'error', // 异常
|
||||||
|
EMPTY = 'empty' // 为空
|
||||||
|
}
|
||||||
9
src/enums/cacheEnums.ts
Normal file
9
src/enums/cacheEnums.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// 本地缓冲key
|
||||||
|
|
||||||
|
//token
|
||||||
|
export const TOKEN_KEY = 'token'
|
||||||
|
|
||||||
|
// 搜索历史记录
|
||||||
|
export const HISTORY = 'history'
|
||||||
|
|
||||||
|
export const BACK_URL = 'back_url'
|
||||||
27
src/enums/constantEnums.ts
Normal file
27
src/enums/constantEnums.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// 本地缓冲key
|
||||||
|
|
||||||
|
//token
|
||||||
|
export const TOKEN_KEY = 'token'
|
||||||
|
|
||||||
|
export const IS_CLOSE_FOLLOW_OA = 'is_close_follow_oa'
|
||||||
|
|
||||||
|
// 搜索历史记录
|
||||||
|
export const HISTORY = 'history'
|
||||||
|
|
||||||
|
export const BACK_URL = 'back_url'
|
||||||
|
|
||||||
|
export const SHARE_ID = 'share_id'
|
||||||
|
export const USER_SN = 'user_sn'
|
||||||
|
|
||||||
|
export const PAY_STATUS_EVENT = 'event:payStatus'
|
||||||
|
|
||||||
|
// 公告缓存key
|
||||||
|
export const NOTICE = 'notice'
|
||||||
|
|
||||||
|
export const HAS_READ_PRIVACY = 'has_read_privacy'
|
||||||
|
|
||||||
|
export const CHAT_LIMIT_KEY = 'chan_limit'
|
||||||
|
|
||||||
|
export const DRAW_LIMIT_KEY = 'draw_limit'
|
||||||
|
|
||||||
|
export const QRCODE_LIMIT_KEY = 'qrcode_limit'
|
||||||
34
src/enums/requestEnums.ts
Normal file
34
src/enums/requestEnums.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export enum ContentTypeEnum {
|
||||||
|
// json
|
||||||
|
JSON = 'application/json;charset=UTF-8',
|
||||||
|
// form-data 上传资源(图片,视频)
|
||||||
|
FORM_DATA = 'multipart/form-data;charset=UTF-8'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RequestMethodsEnum {
|
||||||
|
GET = 'GET',
|
||||||
|
POST = 'POST'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RequestCodeEnum {
|
||||||
|
SUCCESS = 200, //成功
|
||||||
|
FAILED = 300, // 失败
|
||||||
|
PARAMS_VALID_ERROR = 310, //参数校验错误
|
||||||
|
PARAMS_TYPE_ERROR = 311, //参数类型错误
|
||||||
|
REQUEST_METHOD_ERROR = 312, //请求方法错误
|
||||||
|
ASSERT_ARGUMENT_ERROR = 313, //断言参数错误
|
||||||
|
ASSERT_MYBATIS_ERROR = 314, //断言mybatis错误
|
||||||
|
LOGIN_ACCOUNT_ERROR = 330, //登陆账号或密码错误
|
||||||
|
LOGIN_DISABLE_ERROR = 331, //登陆账号已被禁用
|
||||||
|
TOKEN_EMPTY = 332, // TOKEN参数为空
|
||||||
|
TOKEN_INVALID = 333, // TOKEN参数无效
|
||||||
|
DRAW_ERROR = 336, // 绘图失败
|
||||||
|
NO_PERMISSTION = 403, //无相关权限
|
||||||
|
REQUEST_404_ERROR = 404, //请求接口不存在
|
||||||
|
SYSTEM_ERROR = 500 //系统错误
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RequestErrMsgEnum {
|
||||||
|
ABORT = 'request:fail abort',
|
||||||
|
TIMEOUT = 'request:fail timeout'
|
||||||
|
}
|
||||||
87
src/hooks/useAudio.ts
Normal file
87
src/hooks/useAudio.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
|
const audioCtxs = new Set<UniApp.InnerAudioContext>()
|
||||||
|
export const useAudio = () => {
|
||||||
|
const audioCtx = shallowRef<UniApp.InnerAudioContext | null>(null)
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
const duration = ref(0)
|
||||||
|
const onPlay = () => {
|
||||||
|
isPlaying.value = true
|
||||||
|
}
|
||||||
|
const onStop = () => {
|
||||||
|
isPlaying.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onError = (e: any) => {
|
||||||
|
console.error(e)
|
||||||
|
isPlaying.value = false
|
||||||
|
}
|
||||||
|
const onCanplay = () => {
|
||||||
|
duration.value = audioCtx.value?.duration || 0
|
||||||
|
if (duration.value == 0) {
|
||||||
|
//处理微信小程序获取不到时长的bug
|
||||||
|
setTimeout(() => {
|
||||||
|
duration.value = audioCtx.value?.duration || 0
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
// console.log(audioCtx.value?.buffered)
|
||||||
|
}
|
||||||
|
const createAudio = () => {
|
||||||
|
audioCtx.value = uni.createInnerAudioContext()
|
||||||
|
audioCtxs.add(audioCtx.value)
|
||||||
|
audioCtx.value.onCanplay(onCanplay)
|
||||||
|
audioCtx.value.onPlay(onPlay)
|
||||||
|
audioCtx.value.onEnded(onStop)
|
||||||
|
audioCtx.value.onError(onError)
|
||||||
|
audioCtx.value.onStop(onStop)
|
||||||
|
}
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
if (audioCtx.value) {
|
||||||
|
audioCtx.value.destroy()
|
||||||
|
audioCtxs.delete(audioCtx.value)
|
||||||
|
audioCtx.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const setUrl = (src: string) => {
|
||||||
|
if (!audioCtx.value) {
|
||||||
|
createAudio()
|
||||||
|
}
|
||||||
|
audioCtx.value!.src = src
|
||||||
|
}
|
||||||
|
const play = async (src?: string) => {
|
||||||
|
pauseAll()
|
||||||
|
if (!audioCtx.value) {
|
||||||
|
createAudio()
|
||||||
|
}
|
||||||
|
if (src) {
|
||||||
|
setUrl(src)
|
||||||
|
}
|
||||||
|
audioCtx.value!.play()
|
||||||
|
}
|
||||||
|
const pause = () => {
|
||||||
|
audioCtx.value?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const pauseAll = () => {
|
||||||
|
audioCtxs.forEach((audio) => {
|
||||||
|
if (!audio.paused) {
|
||||||
|
audio.stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (isPlaying.value) {
|
||||||
|
pause()
|
||||||
|
}
|
||||||
|
destroy()
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
pause,
|
||||||
|
pauseAll,
|
||||||
|
play,
|
||||||
|
duration,
|
||||||
|
isPlaying,
|
||||||
|
setUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
189
src/hooks/useAudioPlay.ts
Normal file
189
src/hooks/useAudioPlay.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { isAndroid, isWeixinClient } from '@/utils/client'
|
||||||
|
//#ifdef H5
|
||||||
|
import { Howl, Howler } from 'howler'
|
||||||
|
//#endif
|
||||||
|
import { onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
api(...args: any): Promise<any>
|
||||||
|
params: Record<string, any>
|
||||||
|
dataTransform(data: any): string
|
||||||
|
onstart?(): void
|
||||||
|
onstop?(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioSet = new Set<UniApp.InnerAudioContext>()
|
||||||
|
|
||||||
|
export const useAudioPlay = (options?: Options) => {
|
||||||
|
const { api, dataTransform, params, onstart, onstop } = options || {}
|
||||||
|
const audio = shallowRef<UniApp.InnerAudioContext | null>(null)
|
||||||
|
const audioLoading = ref(false)
|
||||||
|
const audioPlaying = ref(false)
|
||||||
|
|
||||||
|
const _paly = async () => {
|
||||||
|
//#ifdef H5
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.load()
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.once('load', () => {
|
||||||
|
if (isWeixinClient()) {
|
||||||
|
window.WeixinJSBridge.invoke(
|
||||||
|
'getNetworkType',
|
||||||
|
{},
|
||||||
|
() => {
|
||||||
|
audio.value!.play()
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
audio.value!.play()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//#endif
|
||||||
|
//#ifndef H5
|
||||||
|
audio.value!.play()
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
const play = async () => {
|
||||||
|
pauseAll()
|
||||||
|
|
||||||
|
if (audio.value?.src || audio.value?._src) {
|
||||||
|
await _paly()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await api?.(params)
|
||||||
|
const audioUrl = dataTransform?.(data)
|
||||||
|
if (!audioUrl) {
|
||||||
|
throw Error('获取的语音url错误')
|
||||||
|
}
|
||||||
|
if (!audio.value) {
|
||||||
|
createAudio(audioUrl)
|
||||||
|
}
|
||||||
|
audio.value!.src = audioUrl
|
||||||
|
await _paly()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
uni.$u.toast('语音播报异常')
|
||||||
|
} finally {
|
||||||
|
audioLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pause = () => {
|
||||||
|
audio.value?.stop()
|
||||||
|
audioPlaying.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const pauseAll = () => {
|
||||||
|
audioSet.forEach((audio) => {
|
||||||
|
audio?.stop()
|
||||||
|
//@ts-ignore
|
||||||
|
audio.audioPlaying.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPlay = () => {
|
||||||
|
onstart?.()
|
||||||
|
audioPlaying.value = true
|
||||||
|
}
|
||||||
|
const onStop = () => {
|
||||||
|
if (audio.value?.src || audio.value?._src) {
|
||||||
|
//有src才算正真的停止
|
||||||
|
onstop?.()
|
||||||
|
}
|
||||||
|
audioPlaying.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onError = () => {
|
||||||
|
// onerror()
|
||||||
|
audioPlaying.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAudio = (src: string) => {
|
||||||
|
// if (isWeixinClient() ) {
|
||||||
|
// #ifdef H5
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value = new Howl({
|
||||||
|
src,
|
||||||
|
autoplay: false,
|
||||||
|
preload: false
|
||||||
|
})
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.audioPlaying = audioPlaying
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.on('play', onPlay)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.on('end', onStop)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.on('loaderror', onError)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.on('playerror', onError)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.on('stop', onStop)
|
||||||
|
// #endif
|
||||||
|
// } else {
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
audio.value = uni.createInnerAudioContext()
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.audioPlaying = audioPlaying
|
||||||
|
audio.value.onPlay(onPlay)
|
||||||
|
audio.value.onEnded(onStop)
|
||||||
|
audio.value.onError(onError)
|
||||||
|
audio.value.onStop(onStop)
|
||||||
|
// #endif
|
||||||
|
// }
|
||||||
|
//@ts-ignore
|
||||||
|
audioSet.add(audio.value)
|
||||||
|
}
|
||||||
|
const destroy = () => {
|
||||||
|
if (!audio.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if (isWeixinClient() && audio.value) {
|
||||||
|
// #ifdef H5
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.off('play', onPlay)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.off('end', onStop)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.off('loaderror', onError)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.off('playerror', onError)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value.off('stop', onStop)
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value?.offPlay(onPlay)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value?.offEnded(onStop)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value?.offError(onError)
|
||||||
|
//@ts-ignore
|
||||||
|
audio.value?.offStop(onStop)
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
audioSet.delete(audio.value!)
|
||||||
|
audio.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (audioPlaying.value) {
|
||||||
|
pause()
|
||||||
|
}
|
||||||
|
destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
play,
|
||||||
|
audioLoading,
|
||||||
|
audioPlaying,
|
||||||
|
pause,
|
||||||
|
pauseAll,
|
||||||
|
destroy
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/hooks/useCaptchaEffect.ts
Normal file
27
src/hooks/useCaptchaEffect.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { captcha } from '@/api/account'
|
||||||
|
|
||||||
|
const useCaptchaEffect = () => {
|
||||||
|
const captchaKey = ref<string>('')
|
||||||
|
const captchaImage = ref<string>('')
|
||||||
|
|
||||||
|
const getCaptchaFn = async () => {
|
||||||
|
try {
|
||||||
|
const data = await captcha()
|
||||||
|
captchaKey.value = data.uuid
|
||||||
|
captchaImage.value = data.img
|
||||||
|
} catch (error) {
|
||||||
|
console.log('获取图形码失败=>', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(getCaptchaFn)
|
||||||
|
|
||||||
|
return {
|
||||||
|
captchaKey,
|
||||||
|
captchaImage,
|
||||||
|
getCaptchaFn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useCaptchaEffect
|
||||||
10
src/hooks/useCopy.ts
Normal file
10
src/hooks/useCopy.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export function useCopy() {
|
||||||
|
const copy = (text: string) => {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: String(text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
copy
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/hooks/useLockFn.ts
Normal file
21
src/hooks/useLockFn.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export function useLockFn(fn: (...args: any[]) => Promise<any>) {
|
||||||
|
const isLock = ref(false)
|
||||||
|
const lockFn = async (...args: any[]) => {
|
||||||
|
if (isLock.value) return
|
||||||
|
isLock.value = true
|
||||||
|
try {
|
||||||
|
const res = await fn(...args)
|
||||||
|
isLock.value = false
|
||||||
|
return res
|
||||||
|
} catch (e) {
|
||||||
|
isLock.value = false
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isLock,
|
||||||
|
lockFn
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/hooks/usePolling.ts
Normal file
1
src/hooks/usePolling.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import { ref } from 'vue'
interface Options {
time?: number
totalTime?: number
count?: number
callback?(): void
}
// @ts-ignore
export default function usePolling(fun: () => Promise<any>, options?: Options) {
const result = ref(null)
const error = ref(null)
const {
time = 2000,
totalTime,
count,
callback = () => false
} = options ?? ({} as Options)
let timer: any = null
let endTime: number | null = null
let totalCount = 0
const run = () => {
console.log('count2:', count)
if (endTime && endTime <= Date.now()) {
end()
callback()
return
}
if (count && totalCount >= count) {
end()
callback()
return
}
totalCount++
timer = setTimeout(() => {
fun()
.then((res) => {
result.value = res
run()
})
.catch((err) => {
error.value = err
})
}, time)
}
const start = () => {
endTime = totalTime ? Date.now() + totalTime : null
run()
}
const end = () => {
setTimeout(() => {
clearTimeout(timer)
timer = null
endTime = null
totalCount = 0
}, 0)
}
return {
start,
end,
error,
result
}
}
|
||||||
672
src/hooks/useRecorder.ts
Normal file
672
src/hooks/useRecorder.ts
Normal file
@ -0,0 +1,672 @@
|
|||||||
|
import {
|
||||||
|
getCurrentInstance,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
shallowRef
|
||||||
|
} from 'vue'
|
||||||
|
//#ifdef H5
|
||||||
|
import Recorder from 'recorder-core/recorder.mp3.min'
|
||||||
|
// #endif
|
||||||
|
import Mp3 from 'js-mp3'
|
||||||
|
//@ts-ignore
|
||||||
|
import genFFT from '@/lib/fft.js'
|
||||||
|
|
||||||
|
interface H5RecordOptions {
|
||||||
|
type: 'mp3' | 'wav'
|
||||||
|
bitRate: number
|
||||||
|
sampleRate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options = H5RecordOptions & UniApp.RecorderManagerStartOptions
|
||||||
|
interface RecorderResult {
|
||||||
|
tempFilePath: string
|
||||||
|
duration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataResult {
|
||||||
|
pcmData: Int16Array
|
||||||
|
powerLevel: number
|
||||||
|
sampleRate: number
|
||||||
|
}
|
||||||
|
interface callbacks {
|
||||||
|
onstart?(): void
|
||||||
|
onstop?(result: RecorderResult): void
|
||||||
|
ondata?(result: DataResult): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRecorder = (callbacks: callbacks, options?: Options) => {
|
||||||
|
options = options || {
|
||||||
|
type: 'mp3',
|
||||||
|
sampleRate: 32000,
|
||||||
|
bitRate: 32,
|
||||||
|
duration: 600000,
|
||||||
|
numberOfChannels: 1, //录音通道数
|
||||||
|
encodeBitRate: 64000,
|
||||||
|
format: 'mp3', //音频格式,有效值 aac/mp3 等
|
||||||
|
frameSize: 1 //指定帧大小,单位 KB
|
||||||
|
}
|
||||||
|
const isRecording = ref(false)
|
||||||
|
const mediaRecorder = shallowRef()
|
||||||
|
// const isAuth = ref(false)
|
||||||
|
const createMediaRecorder = () => {
|
||||||
|
//#ifdef H5
|
||||||
|
mediaRecorder.value = Recorder({
|
||||||
|
...options,
|
||||||
|
async onProcess(
|
||||||
|
pcmdata: Int16Array[],
|
||||||
|
powerLevel: number,
|
||||||
|
duration: number,
|
||||||
|
sampleRate: number
|
||||||
|
) {
|
||||||
|
callbacks?.ondata?.({
|
||||||
|
pcmData: pcmdata[pcmdata.length - 1],
|
||||||
|
powerLevel,
|
||||||
|
sampleRate
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//#endif
|
||||||
|
//#ifndef H5
|
||||||
|
mediaRecorder.value = uni.getRecorderManager()
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @description 获取录音权限
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const authorize = (): Promise<void> => {
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
//优化点:已经获取过权限,不必再获取
|
||||||
|
// if (isAuth.value) {
|
||||||
|
// resolve()
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
if (!mediaRecorder.value) {
|
||||||
|
createMediaRecorder()
|
||||||
|
}
|
||||||
|
//#ifdef H5
|
||||||
|
mediaRecorder.value.open(
|
||||||
|
() => {
|
||||||
|
// isAuth.value = true
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
(msg: string) => {
|
||||||
|
reject('无法录音:' + msg)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
//#ifdef MP
|
||||||
|
try {
|
||||||
|
await uni.authorize({
|
||||||
|
scope: 'scope.record'
|
||||||
|
})
|
||||||
|
resolve()
|
||||||
|
} catch (error: any) {
|
||||||
|
const res = await uni.showModal({
|
||||||
|
title: '开启麦克风权限',
|
||||||
|
content: '为了正常使用语音输入功能,请开启麦克风权限',
|
||||||
|
confirmText: '去设置'
|
||||||
|
})
|
||||||
|
if (res.confirm) {
|
||||||
|
const { authSetting } = await uni.openSetting()
|
||||||
|
if (authSetting['scope.record']) {
|
||||||
|
// isAuth.value = true
|
||||||
|
return resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reject('无法录音:用户拒绝了麦克风权限')
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endif
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* @returns
|
||||||
|
* @description 开始录音
|
||||||
|
*/
|
||||||
|
const start = async () => {
|
||||||
|
// 注册事件
|
||||||
|
try {
|
||||||
|
if (!mediaRecorder.value) {
|
||||||
|
createMediaRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value.start(options)
|
||||||
|
isRecording.value = true
|
||||||
|
callbacks.onstart?.()
|
||||||
|
//#ifndef H5
|
||||||
|
mediaRecorder.value.onStart(() => {
|
||||||
|
isRecording.value = true
|
||||||
|
callbacks.onstart?.()
|
||||||
|
})
|
||||||
|
mediaRecorder.value.onStop((e: any) => {
|
||||||
|
isRecording.value = false
|
||||||
|
callbacks.onstop?.(e)
|
||||||
|
})
|
||||||
|
mediaRecorder.value.onError((e: any) => {
|
||||||
|
isRecording.value = false
|
||||||
|
})
|
||||||
|
// 被打断结束,恢复录音
|
||||||
|
mediaRecorder.value.onInterruptionEnd(() => {
|
||||||
|
mediaRecorder.value.resume()
|
||||||
|
})
|
||||||
|
|
||||||
|
mediaRecorder.value.onFrameRecorded(
|
||||||
|
async ({ frameBuffer }: any) => {
|
||||||
|
const decoder = Mp3.newDecoder(frameBuffer)
|
||||||
|
if (decoder != null) {
|
||||||
|
const pcmArrayBuffer = decoder.decode()
|
||||||
|
|
||||||
|
const pcmArr = new Int16Array(pcmArrayBuffer)
|
||||||
|
const size = pcmArr.length
|
||||||
|
|
||||||
|
let sum = 0
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
sum += Math.abs(pcmArr[i])
|
||||||
|
}
|
||||||
|
let powerLevel = (sum * 500.0) / (size * 16383)
|
||||||
|
if (powerLevel >= 100) {
|
||||||
|
powerLevel = 100
|
||||||
|
}
|
||||||
|
if (powerLevel <= 5) {
|
||||||
|
powerLevel = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
powerLevel = parseInt(String(powerLevel))
|
||||||
|
callbacks?.ondata?.({
|
||||||
|
pcmData: pcmArr,
|
||||||
|
powerLevel,
|
||||||
|
sampleRate: options?.sampleRate!
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
//#endif
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stop = () => {
|
||||||
|
if (mediaRecorder.value) {
|
||||||
|
//#ifndef H5
|
||||||
|
mediaRecorder.value?.stop()
|
||||||
|
//#endif
|
||||||
|
//#ifdef H5
|
||||||
|
mediaRecorder.value?.stop(
|
||||||
|
(blob: Blob, duration: number) => {
|
||||||
|
const tempFilePath = window.URL.createObjectURL(blob)
|
||||||
|
isRecording.value = false
|
||||||
|
return callbacks.onstop?.({
|
||||||
|
tempFilePath,
|
||||||
|
duration
|
||||||
|
})
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
isRecording.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
//#ifndef H5
|
||||||
|
mediaRecorder.value?.offStart?.()
|
||||||
|
mediaRecorder.value?.offStop?.()
|
||||||
|
mediaRecorder.value?.offError?.()
|
||||||
|
mediaRecorder.value?.offInterruptionEnd?.()
|
||||||
|
mediaRecorder.value?.onFrameRecorded?.()
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
const close = () => {
|
||||||
|
mediaRecorder.value?.close?.(() => {
|
||||||
|
isRecording.value = false
|
||||||
|
})
|
||||||
|
mediaRecorder.value = null
|
||||||
|
// isAuth.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stop()
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
isRecording,
|
||||||
|
mediaRecorder,
|
||||||
|
start,
|
||||||
|
authorize,
|
||||||
|
stop,
|
||||||
|
close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AudioGraphOptions {
|
||||||
|
/**
|
||||||
|
* @description 画布的canvasid
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* @description 画布的宽度
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* @description 画布的高度
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
/**
|
||||||
|
* @description 缩放系数,应为正整数,使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊
|
||||||
|
*/
|
||||||
|
scale: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 绘制帧率,不可过高
|
||||||
|
*/
|
||||||
|
fps: number
|
||||||
|
/**
|
||||||
|
* @description 频率傅立叶变换的大小,越大,也精细
|
||||||
|
*/
|
||||||
|
fftSize: number
|
||||||
|
/**
|
||||||
|
* @description 直方图柱子数量
|
||||||
|
*/
|
||||||
|
lineCount: number
|
||||||
|
/**
|
||||||
|
* @description 柱子最小高度
|
||||||
|
*/
|
||||||
|
minHeight: number
|
||||||
|
/**
|
||||||
|
* @description 柱子线条宽度占比,为所有柱子占用整个视图宽度的比例,剩下的空白区域均匀插入柱子中间;
|
||||||
|
* 默认值也基本相当于一根柱子占0.6,一根空白占0.4;
|
||||||
|
* 设为1不留空白,当视图不足容下所有柱子时也不留空白
|
||||||
|
*/
|
||||||
|
widthRatio: number
|
||||||
|
/**
|
||||||
|
* @description 柱子间空白固定基础宽度,柱子宽度自适应,当不为0时widthRatio无效,当视图不足容下所有柱子时将不会留空白,允许为负数,让柱子发生重叠
|
||||||
|
*/
|
||||||
|
spaceWidth: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description position: -1, //绘制位置,取值-1到1,-1为最底下,0为中间,1为最顶上,支持小数点
|
||||||
|
*/
|
||||||
|
position: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 是否开启镜像
|
||||||
|
*/
|
||||||
|
mirrorEnable: boolean
|
||||||
|
/**
|
||||||
|
* @description 柱子从最顶上下降到最底部最长时间ms
|
||||||
|
*/
|
||||||
|
fallDuration: number
|
||||||
|
/**
|
||||||
|
* @description 柱子的颜色
|
||||||
|
* @example [{pos:0,color:"#fff"},{pos:1,color"#000"}]
|
||||||
|
*/
|
||||||
|
linear: { pos: number; color: string }[]
|
||||||
|
/**
|
||||||
|
* @description 是否开启圆角
|
||||||
|
*/
|
||||||
|
round: boolean
|
||||||
|
/**
|
||||||
|
* @description 是否要绘制所有频率;默认false主要绘制5khz以下的频率,高频部分占比很少,此时不同的采样率对频谱显示几乎没有影响;设为true后不同采样率下显示的频谱是不一样的,因为 最大频率=采样率/2 会有差异
|
||||||
|
//当发生绘制时会回调此方法,参数为当前绘制的频率数据和采样率,可实现多个直方图同时绘制,只消耗一个input输入和计算时间
|
||||||
|
*/
|
||||||
|
fullFreq: boolean
|
||||||
|
onDraw?(
|
||||||
|
ctx: UniApp.CanvasContext,
|
||||||
|
data: {
|
||||||
|
frequencyData: Float64Array | null
|
||||||
|
sampleRate: number
|
||||||
|
options: AudioGraphOptions
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AudioGraphUserOptions = Partial<AudioGraphOptions> &
|
||||||
|
Pick<AudioGraphOptions, 'id' | 'width' | 'height'>
|
||||||
|
const defaultAudioGraphOptions: AudioGraphOptions = {
|
||||||
|
id: '',
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
scale: 2,
|
||||||
|
fps: 30,
|
||||||
|
fftSize: 1024,
|
||||||
|
lineCount: 6,
|
||||||
|
widthRatio: 0.6,
|
||||||
|
spaceWidth: 0,
|
||||||
|
minHeight: 8,
|
||||||
|
position: 0,
|
||||||
|
mirrorEnable: false,
|
||||||
|
fallDuration: 600,
|
||||||
|
linear: [
|
||||||
|
{
|
||||||
|
pos: 0,
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pos: 1,
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
round: true,
|
||||||
|
fullFreq: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRenderAudioGraph = (options: AudioGraphUserOptions) => {
|
||||||
|
const canvasId = options.id
|
||||||
|
if (!canvasId) {
|
||||||
|
console.error('绘制图形前必须指定`canvasId`')
|
||||||
|
}
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
const opt: AudioGraphOptions = Object.assign(
|
||||||
|
{},
|
||||||
|
defaultAudioGraphOptions,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
const canvasCtx = uni.createCanvasContext(canvasId, instance?.proxy)
|
||||||
|
if (!opt.width || !opt.height) {
|
||||||
|
console.error('必须指定画布的宽高')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fft = genFFT(opt.fftSize)
|
||||||
|
let fragment: DataResult | undefined = undefined
|
||||||
|
let pcmPos = 0
|
||||||
|
let inputTime = 0
|
||||||
|
let scheduleTimer = 0
|
||||||
|
let drawTime = 0
|
||||||
|
let lastH: number[] = []
|
||||||
|
const render = (data: DataResult) => {
|
||||||
|
fragment = data
|
||||||
|
pcmPos = 0
|
||||||
|
inputTime = Date.now()
|
||||||
|
schedule()
|
||||||
|
}
|
||||||
|
const genLinear = (
|
||||||
|
ctx: UniApp.CanvasContext,
|
||||||
|
colors: AudioGraphOptions['linear'],
|
||||||
|
from: number,
|
||||||
|
to: number
|
||||||
|
) => {
|
||||||
|
const rtv = ctx.createLinearGradient(0, from, 0, to)
|
||||||
|
for (let i = 0; i < colors.length; i++) {
|
||||||
|
rtv.addColorStop(colors[i].pos, colors[i].color)
|
||||||
|
}
|
||||||
|
return rtv
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawRect = (
|
||||||
|
ctx: UniApp.CanvasContext,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
r: number[]
|
||||||
|
) => {
|
||||||
|
const [r1, r2, r3, r4] = r
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x + r1, y)
|
||||||
|
ctx.lineTo(x + width - r1, y)
|
||||||
|
ctx.arc(x + width - r2, y + r2, r2, Math.PI * 1.5, Math.PI * 2)
|
||||||
|
ctx.lineTo(x + width, y + height - r3)
|
||||||
|
ctx.arc(x + width - r3, y + height - r3, r3, 0, Math.PI * 0.5)
|
||||||
|
ctx.lineTo(x + r4, y + height)
|
||||||
|
ctx.arc(x + r4, y + height - r4, r4, Math.PI * 0.5, Math.PI)
|
||||||
|
ctx.lineTo(x, y + r1)
|
||||||
|
ctx.arc(x + r1, y + r1, r1, Math.PI, Math.PI * 1.5)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDraw: AudioGraphOptions['onDraw'] = opt.onDraw
|
||||||
|
? opt.onDraw
|
||||||
|
: (ctx, { frequencyData, sampleRate, options }) => {
|
||||||
|
const {
|
||||||
|
scale,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
lineCount,
|
||||||
|
round,
|
||||||
|
fftSize,
|
||||||
|
position,
|
||||||
|
fallDuration,
|
||||||
|
fps,
|
||||||
|
fullFreq,
|
||||||
|
linear,
|
||||||
|
mirrorEnable
|
||||||
|
} = options
|
||||||
|
const realWidth = width * scale
|
||||||
|
const realHeight = height * scale
|
||||||
|
//计算高度位置
|
||||||
|
const posAbs = Math.abs(position)
|
||||||
|
let originY = position == 1 ? 0 : realHeight //y轴原点
|
||||||
|
let heightY = realHeight //最高的一边高度
|
||||||
|
if (posAbs < 1) {
|
||||||
|
heightY = heightY / 2
|
||||||
|
originY = heightY
|
||||||
|
heightY = Math.floor(heightY * (1 + posAbs))
|
||||||
|
originY = Math.floor(
|
||||||
|
position > 0
|
||||||
|
? originY * (1 - posAbs)
|
||||||
|
: originY * (1 + posAbs)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const lastHeight = lastH
|
||||||
|
// 计算速度
|
||||||
|
const speed = Math.ceil(heightY / (fallDuration / (1000 / fps)))
|
||||||
|
const Y0 =
|
||||||
|
1 << (Math.round(Math.log(fftSize) / Math.log(2) + 3) << 1)
|
||||||
|
const logY0 = Math.log(Y0) / Math.log(10)
|
||||||
|
const dBmax = (20 * Math.log(0x7fff)) / Math.log(10)
|
||||||
|
|
||||||
|
const fftSizeHalf = fftSize / 2.5
|
||||||
|
let fftSize5k = fftSizeHalf
|
||||||
|
//非绘制所有频率时,计算5khz所在位置,8000采样率及以下最高只有4khz
|
||||||
|
if (!fullFreq) {
|
||||||
|
fftSize5k = Math.min(
|
||||||
|
fftSizeHalf,
|
||||||
|
Math.floor((fftSizeHalf * 5000) / (sampleRate / 2))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFullFreq = fftSize5k == fftSize
|
||||||
|
const line80 = isFullFreq
|
||||||
|
? lineCount
|
||||||
|
: Math.round(lineCount * 0.8) //80%的柱子位置
|
||||||
|
const fftSizeStep1 = fftSize5k / line80
|
||||||
|
const fftSizeStep2 = isFullFreq
|
||||||
|
? 0
|
||||||
|
: (fftSizeHalf - fftSize5k) / (lineCount - line80)
|
||||||
|
let fftIdx = 0
|
||||||
|
for (let i = 0; i < lineCount; i++) {
|
||||||
|
// !fullFreq 时不采用jmp123的非线性划分频段,录音语音并不适用于音乐的频率,应当弱化高频部分
|
||||||
|
//80%关注0-5khz主要人声部分 20%关注剩下的高频,这样不管什么采样率都能做到大部分频率显示一致。
|
||||||
|
const start = Math.ceil(fftIdx)
|
||||||
|
if (i < line80) {
|
||||||
|
//5khz以下
|
||||||
|
fftIdx += fftSizeStep1
|
||||||
|
} else {
|
||||||
|
//5khz以上
|
||||||
|
fftIdx += fftSizeStep2
|
||||||
|
}
|
||||||
|
let end = Math.ceil(fftIdx)
|
||||||
|
if (end == start) end++
|
||||||
|
end = Math.min(end, fftSizeHalf)
|
||||||
|
|
||||||
|
//参考AudioGUI.java .drawHistogram方法
|
||||||
|
|
||||||
|
//查找当前频段的最大"幅值"
|
||||||
|
let maxAmp = 0
|
||||||
|
if (frequencyData) {
|
||||||
|
for (let j = start; j < end; j++) {
|
||||||
|
maxAmp = Math.max(maxAmp, Math.abs(frequencyData[j]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//计算音量
|
||||||
|
const dB =
|
||||||
|
maxAmp > Y0
|
||||||
|
? Math.floor(
|
||||||
|
(Math.log(maxAmp) / Math.log(10) - logY0) * 17
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
let h = heightY * Math.min(dB / dBmax, 1)
|
||||||
|
|
||||||
|
//使柱子匀速下降
|
||||||
|
lastHeight[i] = (lastHeight[i] || 0) - speed
|
||||||
|
if (h < lastHeight[i]) {
|
||||||
|
h = lastHeight[i]
|
||||||
|
}
|
||||||
|
if (h < 0) {
|
||||||
|
h = 0
|
||||||
|
}
|
||||||
|
lastHeight[i] = h
|
||||||
|
}
|
||||||
|
//开始绘制图形
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, realWidth, realHeight)
|
||||||
|
const d = 1 / opt.scale
|
||||||
|
ctx.scale(d, d)
|
||||||
|
const linear1 = genLinear(ctx, linear, originY, originY - heightY) //上半部分的填充
|
||||||
|
const linear2 = genLinear(ctx, linear, originY, originY + heightY) //下半部分的填充
|
||||||
|
const mirrorCount = mirrorEnable ? lineCount * 2 - 1 : lineCount //镜像柱子数量翻一倍-1根
|
||||||
|
|
||||||
|
const spaceWidth = options.spaceWidth * scale
|
||||||
|
let widthRatio = options.widthRatio
|
||||||
|
if (spaceWidth != 0) {
|
||||||
|
widthRatio =
|
||||||
|
(realWidth - spaceWidth * (mirrorCount + 1)) / realWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
let lineWN = 0,
|
||||||
|
spaceFloat = 0,
|
||||||
|
lineWF = 0
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const lineFloat = Math.max(
|
||||||
|
1 * scale,
|
||||||
|
(realWidth * widthRatio) / mirrorCount
|
||||||
|
) //柱子宽度至少1个单位
|
||||||
|
lineWN = Math.floor(lineFloat)
|
||||||
|
lineWF = lineFloat - lineWN //提取出小数部分
|
||||||
|
spaceFloat =
|
||||||
|
(realWidth - mirrorCount * lineFloat) / (mirrorCount + 1) //均匀间隔,首尾都留空,可能为负数,柱子将发生重叠
|
||||||
|
if (spaceFloat > 0 && spaceFloat < 1) {
|
||||||
|
widthRatio = 1
|
||||||
|
spaceFloat = 0 //不够一个像素,丢弃不绘制间隔,重新计算
|
||||||
|
} else break
|
||||||
|
}
|
||||||
|
//绘制
|
||||||
|
const minHeight = options.minHeight * scale
|
||||||
|
const XFloat = mirrorEnable
|
||||||
|
? (realWidth - lineWN) / 2 - spaceFloat
|
||||||
|
: 0 //镜像时,中间柱子位于正中心
|
||||||
|
|
||||||
|
for (let iMirror = 0; iMirror < 2; iMirror++) {
|
||||||
|
if (iMirror) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.scale(-1, 1)
|
||||||
|
}
|
||||||
|
const xMirror = iMirror ? realWidth : 0 //绘制镜像部分,不用drawImage(canvas)进行镜像绘制,提升兼容性(iOS微信小程序bug https://developers.weixin.qq.com/community/develop/doc/000aaca2148dc8a235a0fb8c66b000)
|
||||||
|
|
||||||
|
//绘制柱子
|
||||||
|
|
||||||
|
for (
|
||||||
|
let i = 0, xFloat = XFloat, wFloat = 0, x, y, w, h;
|
||||||
|
i < lineCount;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
xFloat += spaceFloat
|
||||||
|
x = Math.floor(xFloat) - xMirror
|
||||||
|
w = lineWN
|
||||||
|
wFloat += lineWF
|
||||||
|
if (wFloat >= 1) {
|
||||||
|
w++
|
||||||
|
wFloat--
|
||||||
|
} //小数凑够1像素
|
||||||
|
h = Math.max(lastH[i], minHeight)
|
||||||
|
// console.log(h)
|
||||||
|
const radius = round ? w / 2 : 0
|
||||||
|
//绘制上半部分
|
||||||
|
let r = new Array(4).fill(radius)
|
||||||
|
if (originY != 0) {
|
||||||
|
y = originY - h
|
||||||
|
ctx.setFillStyle(linear1)
|
||||||
|
if (originY != realHeight) {
|
||||||
|
r = [radius, radius, 0, 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRect(ctx, x, y, w, h, r)
|
||||||
|
}
|
||||||
|
//绘制下半部分
|
||||||
|
if (originY != realHeight) {
|
||||||
|
ctx.setFillStyle(linear2)
|
||||||
|
if (originY != 0) {
|
||||||
|
r = [0, 0, radius, radius]
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRect(ctx, x, originY, w, h, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
xFloat += w
|
||||||
|
}
|
||||||
|
if (iMirror) {
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
if (!mirrorEnable) break
|
||||||
|
}
|
||||||
|
ctx.draw()
|
||||||
|
}
|
||||||
|
delete opt.onDraw
|
||||||
|
const draw = (frequencyData: Float64Array | null, sampleRate: number) => {
|
||||||
|
onDraw(canvasCtx, {
|
||||||
|
frequencyData,
|
||||||
|
sampleRate: sampleRate,
|
||||||
|
options: opt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const schedule = () => {
|
||||||
|
const interval = Math.floor(1000 / opt.fps)
|
||||||
|
if (!scheduleTimer) {
|
||||||
|
scheduleTimer = setInterval(function () {
|
||||||
|
schedule()
|
||||||
|
}, interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
drawTime = drawTime || 0
|
||||||
|
if (now - inputTime > opt?.fallDuration * 1.5) {
|
||||||
|
//超时没有输入,顶部横条已全部落下,干掉定时器
|
||||||
|
clearInterval(scheduleTimer)
|
||||||
|
lastH = [] //重置高度再绘制一次,避免定时不准没到底就停了
|
||||||
|
draw(null, fragment!.sampleRate)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (now - drawTime < interval) {
|
||||||
|
//没到间隔时间,不绘制
|
||||||
|
return
|
||||||
|
}
|
||||||
|
drawTime = now
|
||||||
|
//调用FFT计算频率数据
|
||||||
|
const bufferSize = fft.bufferSize
|
||||||
|
const pcm = fragment!.pcmData
|
||||||
|
let pos = pcmPos
|
||||||
|
const arr = new Int16Array(bufferSize)
|
||||||
|
for (let i = 0; i < bufferSize && pos < pcm.length; i++, pos++) {
|
||||||
|
arr[i] = pcm[pos]
|
||||||
|
}
|
||||||
|
pcmPos = pos
|
||||||
|
|
||||||
|
const frequencyData = fft.transform(arr)
|
||||||
|
draw(frequencyData, fragment!.sampleRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopRender = () => {
|
||||||
|
clearInterval(scheduleTimer)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
render,
|
||||||
|
draw,
|
||||||
|
stopRender
|
||||||
|
}
|
||||||
|
}
|
||||||
176
src/hooks/useShareMessage.ts
Normal file
176
src/hooks/useShareMessage.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import router from '@/router'
|
||||||
|
import appConfig from '@/config'
|
||||||
|
import { getShareId, shareClick } from '@/api/task'
|
||||||
|
import { paramsToStr } from '@/utils/util'
|
||||||
|
import { useNavigationBarTitleStore } from '@/stores/navigationBarTitle'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import shareMixin from '@/mixins/share'
|
||||||
|
export interface ShareOptions {
|
||||||
|
desc: string
|
||||||
|
title: string
|
||||||
|
imageUrl: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserShareOptions = Partial<ShareOptions>
|
||||||
|
|
||||||
|
//生成分享路径,首页和当前页面两种
|
||||||
|
export async function generateSharePath(isHome = false) {
|
||||||
|
const route = router.currentRoute.value
|
||||||
|
let origin = ''
|
||||||
|
//#ifdef H5
|
||||||
|
origin = `${window.location.origin}/mobile`
|
||||||
|
//#endif
|
||||||
|
//#ifdef APP-PLUS
|
||||||
|
origin = `${appConfig.baseUrl}mobile`
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
path: isHome ? '/pages/index/index' : route.path,
|
||||||
|
query: isHome ? {} : route.query
|
||||||
|
}
|
||||||
|
const path = `${origin}${config.path}`
|
||||||
|
const options: any = config.query
|
||||||
|
try {
|
||||||
|
const { shareId } = await getShareId()
|
||||||
|
if (shareId) {
|
||||||
|
options.share_id = shareId
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
return `${path}${paramsToStr(options)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useShareMessage() {
|
||||||
|
const resolvedH5Options = (options: ShareOptions) => {
|
||||||
|
return {
|
||||||
|
desc: options.desc,
|
||||||
|
image_url: options.imageUrl,
|
||||||
|
link: options.path,
|
||||||
|
title: options.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedMpOptions = (options: ShareOptions) => {
|
||||||
|
return {
|
||||||
|
imageUrl: options.imageUrl,
|
||||||
|
path: options.path,
|
||||||
|
title: options.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @description 解析分享参数,将外部参数与默认参数合并
|
||||||
|
* @param options
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const resolveOptions = async (
|
||||||
|
options: UserShareOptions = {}
|
||||||
|
): Promise<ShareOptions> => {
|
||||||
|
const navigationBarTitleStore = useNavigationBarTitleStore()
|
||||||
|
const route = router.currentRoute.value
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { style } =
|
||||||
|
(router.routeMatcher.getRouteByPath(route.path) as any) || {}
|
||||||
|
uni.showLoading({
|
||||||
|
title: '请稍后...'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { shareTitle, shareImage, shareContent, sharePage } =
|
||||||
|
appStore.getShareConfig
|
||||||
|
const { name, logo } = appStore.getWebsiteConfig
|
||||||
|
|
||||||
|
// 分享为首页
|
||||||
|
const isShareWithHome = sharePage == 2
|
||||||
|
const link = await generateSharePath(isShareWithHome)
|
||||||
|
let resolved = {
|
||||||
|
title: shareTitle,
|
||||||
|
path: link,
|
||||||
|
desc: shareContent,
|
||||||
|
image_url: shareImage
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非首页可以合并外部参数
|
||||||
|
if (!isShareWithHome) {
|
||||||
|
resolved = {
|
||||||
|
...resolved,
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!resolved.title) {
|
||||||
|
if (isShareWithHome) {
|
||||||
|
resolved.title = name
|
||||||
|
} else {
|
||||||
|
// 1. 用户点击进入页面的后台配置标题
|
||||||
|
// 2. 页面内pagesjson组册的页面标题
|
||||||
|
// 3. 网站名称
|
||||||
|
resolved.title =
|
||||||
|
navigationBarTitleStore.getTitle ||
|
||||||
|
style?.navigationBarTitleText ||
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolved.imageUrl) {
|
||||||
|
resolved.imageUrl = logo
|
||||||
|
}
|
||||||
|
console.log(resolved)
|
||||||
|
// #ifdef H5
|
||||||
|
resolved = resolvedH5Options(resolved) as any
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
resolved = resolvedMpOptions(resolved) as any
|
||||||
|
// #endif
|
||||||
|
uni.hideLoading()
|
||||||
|
return resolved as ShareOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用分享,可以单独在页面中使用
|
||||||
|
const useShare = (options: UserShareOptions = {}) => {
|
||||||
|
// #ifdef H5
|
||||||
|
const registerEvent = () => {
|
||||||
|
//@ts-ignore
|
||||||
|
WeixinJSBridge.on('menu:share:appmessage', async function () {
|
||||||
|
const resolved = await resolveOptions(options)
|
||||||
|
//@ts-ignore
|
||||||
|
WeixinJSBridge.invoke('sendAppMessage', resolved)
|
||||||
|
})
|
||||||
|
//@ts-ignore
|
||||||
|
WeixinJSBridge.on('menu:share:timeline', async function () {
|
||||||
|
const resolved = await resolveOptions(options)
|
||||||
|
//@ts-ignore
|
||||||
|
WeixinJSBridge.invoke('shareTimeline', resolved)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//@ts-ignore
|
||||||
|
if (typeof WeixinJSBridge === 'object') {
|
||||||
|
registerEvent()
|
||||||
|
} else {
|
||||||
|
document.addEventListener(
|
||||||
|
'WeixinJSBridgeReady',
|
||||||
|
registerEvent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMixinShare = () => {
|
||||||
|
;(shareMixin as any).onShareAppMessage = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
resolveOptions,
|
||||||
|
useShare,
|
||||||
|
removeMixinShare
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function useSharedId() {
|
||||||
|
const options = uni.getEnterOptionsSync()
|
||||||
|
const share_id = options.query.share_id
|
||||||
|
|
||||||
|
if (share_id) {
|
||||||
|
await shareClick({
|
||||||
|
share_id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/hooks/useTouch.ts
Normal file
72
src/hooks/useTouch.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 触碰屏幕钩子函数
|
||||||
|
* @return { Function } 暴露钩子
|
||||||
|
*/
|
||||||
|
export function useTouch() {
|
||||||
|
// 最小移动距离
|
||||||
|
const MIN_DISTANCE = 10
|
||||||
|
|
||||||
|
const touch = reactive({
|
||||||
|
direction: '',
|
||||||
|
deltaX: 0,
|
||||||
|
deltaY: 0,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 计算距离
|
||||||
|
* @return { string } 空字符串
|
||||||
|
*/
|
||||||
|
const getDirection = (x: number, y: number) => {
|
||||||
|
if (x > y && x > MIN_DISTANCE) {
|
||||||
|
return 'horizontal'
|
||||||
|
}
|
||||||
|
if (y > x && y > MIN_DISTANCE) {
|
||||||
|
return 'vertical'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 重置参数
|
||||||
|
*/
|
||||||
|
const resetTouchStatus = () => {
|
||||||
|
touch.direction = ''
|
||||||
|
touch.deltaX = 0
|
||||||
|
touch.deltaY = 0
|
||||||
|
touch.offsetX = 0
|
||||||
|
touch.offsetY = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 触发
|
||||||
|
*/
|
||||||
|
const touchStart = (event: any) => {
|
||||||
|
resetTouchStatus()
|
||||||
|
const events = event.touches[0]
|
||||||
|
touch.startX = events.clientX
|
||||||
|
touch.startY = events.clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 移动
|
||||||
|
*/
|
||||||
|
const touchMove = (event: any) => {
|
||||||
|
const events = event.touches[0]
|
||||||
|
touch.deltaX = events.clientX - touch.startX
|
||||||
|
touch.deltaY = events.clientY - touch.startY
|
||||||
|
touch.offsetX = Math.abs(touch.deltaX)
|
||||||
|
touch.offsetY = Math.abs(touch.deltaY)
|
||||||
|
touch.direction = touch.direction || getDirection(touch.offsetX, touch.offsetY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
touch,
|
||||||
|
resetTouchStatus,
|
||||||
|
touchStart,
|
||||||
|
touchMove
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/lib/fft.js
Normal file
114
src/lib/fft.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
时域转频域,快速傅里叶变换(FFT)
|
||||||
|
|
||||||
|
var fft = genFFT(bufferSize)
|
||||||
|
bufferSize取值2的n次方
|
||||||
|
fft.bufferSize 实际采用的bufferSize
|
||||||
|
fft.transform(inBuffer)
|
||||||
|
inBuffer:[Int16,...] 数组长度必须是bufferSize
|
||||||
|
返回[Float64(Long),...],长度为bufferSize/2
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
从FFT.java 移植,Java开源库:jmp123 版本0.3
|
||||||
|
https://www.iteye.com/topic/851459
|
||||||
|
*/
|
||||||
|
const genFFT = function (bufferSize) {
|
||||||
|
var FFT_N_LOG, FFT_N, MINY;
|
||||||
|
var real, imag, sintable, costable;
|
||||||
|
var bitReverse;
|
||||||
|
|
||||||
|
var FFT_Fn = function (bufferSize) {
|
||||||
|
//bufferSize只能取值2的n次方
|
||||||
|
FFT_N_LOG = Math.round(Math.log(bufferSize) / Math.log(2));
|
||||||
|
FFT_N = 1 << FFT_N_LOG;
|
||||||
|
MINY = (FFT_N << 2) * Math.sqrt(2);
|
||||||
|
|
||||||
|
real = [];
|
||||||
|
imag = [];
|
||||||
|
sintable = [0];
|
||||||
|
costable = [0];
|
||||||
|
bitReverse = [];
|
||||||
|
|
||||||
|
var i, j, k, reve;
|
||||||
|
for (i = 0; i < FFT_N; i++) {
|
||||||
|
k = i;
|
||||||
|
for (j = 0, reve = 0; j != FFT_N_LOG; j++) {
|
||||||
|
reve <<= 1;
|
||||||
|
reve |= k & 1;
|
||||||
|
k >>>= 1;
|
||||||
|
}
|
||||||
|
bitReverse[i] = reve;
|
||||||
|
}
|
||||||
|
|
||||||
|
var theta,
|
||||||
|
dt = (2 * Math.PI) / FFT_N;
|
||||||
|
for (i = (FFT_N >> 1) - 1; i > 0; i--) {
|
||||||
|
theta = i * dt;
|
||||||
|
costable[i] = Math.cos(theta);
|
||||||
|
sintable[i] = Math.sin(theta);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
用于频谱显示的快速傅里叶变换
|
||||||
|
inBuffer 输入FFT_N个实数,返回 FFT_N/2个输出值(复数模的平方)。
|
||||||
|
*/
|
||||||
|
var getModulus = function (inBuffer) {
|
||||||
|
var i,
|
||||||
|
j,
|
||||||
|
k,
|
||||||
|
ir,
|
||||||
|
j0 = 1,
|
||||||
|
idx = FFT_N_LOG - 1;
|
||||||
|
var cosv, sinv, tmpr, tmpi;
|
||||||
|
for (i = 0; i != FFT_N; i++) {
|
||||||
|
real[i] = inBuffer[bitReverse[i]];
|
||||||
|
imag[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = FFT_N_LOG; i != 0; i--) {
|
||||||
|
for (j = 0; j != j0; j++) {
|
||||||
|
cosv = costable[j << idx];
|
||||||
|
sinv = sintable[j << idx];
|
||||||
|
for (k = j; k < FFT_N; k += j0 << 1) {
|
||||||
|
ir = k + j0;
|
||||||
|
tmpr = cosv * real[ir] - sinv * imag[ir];
|
||||||
|
tmpi = cosv * imag[ir] + sinv * real[ir];
|
||||||
|
real[ir] = real[k] - tmpr;
|
||||||
|
imag[ir] = imag[k] - tmpi;
|
||||||
|
real[k] += tmpr;
|
||||||
|
imag[k] += tmpi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
j0 <<= 1;
|
||||||
|
idx--;
|
||||||
|
}
|
||||||
|
|
||||||
|
j = FFT_N >> 1;
|
||||||
|
var outBuffer = new Float64Array(j);
|
||||||
|
/*
|
||||||
|
* 输出模的平方:
|
||||||
|
* for(i = 1; i <= j; i++)
|
||||||
|
* inBuffer[i-1] = real[i] * real[i] + imag[i] * imag[i];
|
||||||
|
*
|
||||||
|
* 如果FFT只用于频谱显示,可以"淘汰"幅值较小的而减少浮点乘法运算. MINY的值
|
||||||
|
* 和Spectrum.Y0,Spectrum.logY0对应.
|
||||||
|
*/
|
||||||
|
sinv = MINY;
|
||||||
|
cosv = -MINY;
|
||||||
|
for (i = j; i != 0; i--) {
|
||||||
|
tmpr = real[i];
|
||||||
|
tmpi = imag[i];
|
||||||
|
if (tmpr > cosv && tmpr < sinv && tmpi > cosv && tmpi < sinv)
|
||||||
|
outBuffer[i - 1] = 0;
|
||||||
|
else outBuffer[i - 1] = Math.round(tmpr * tmpr + tmpi * tmpi);
|
||||||
|
}
|
||||||
|
return outBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
FFT_Fn(bufferSize);
|
||||||
|
return { transform: getModulus, bufferSize: FFT_N };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default genFFT
|
||||||
10943
src/lib/html2canvas.esm.js
Normal file
10943
src/lib/html2canvas.esm.js
Normal file
File diff suppressed because one or more lines are too long
15
src/main.ts
Normal file
15
src/main.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import plugins from './plugins'
|
||||||
|
import router from './router'
|
||||||
|
import './styles/index.scss'
|
||||||
|
import { setupMixin } from './mixins'
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
setupMixin(app)
|
||||||
|
app.use(plugins)
|
||||||
|
app.use(router)
|
||||||
|
return {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/manifest.json
Normal file
172
src/manifest.json
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
{
|
||||||
|
"name" : "码多多AI",
|
||||||
|
"appid" : "__UNI__2A068A4",
|
||||||
|
"description" : "",
|
||||||
|
"versionName" : "3.2.1",
|
||||||
|
"versionCode" : 201,
|
||||||
|
"transformPx" : false,
|
||||||
|
/* 5+App特有相关 */
|
||||||
|
"app-plus" : {
|
||||||
|
"compatible" : {
|
||||||
|
"ignoreVersion" : true //true表示忽略版本检查提示框,HBuilderX1.9.0及以上版本支持
|
||||||
|
},
|
||||||
|
"usingComponents" : true,
|
||||||
|
"nvueStyleCompiler" : "uni-app",
|
||||||
|
"compilerVersion" : 3,
|
||||||
|
"splashscreen" : {
|
||||||
|
"alwaysShowBeforeRender" : true,
|
||||||
|
"waiting" : true,
|
||||||
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
"modules" : {
|
||||||
|
"Payment" : {},
|
||||||
|
"OAuth" : {},
|
||||||
|
"Share" : {}
|
||||||
|
},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
"distribute" : {
|
||||||
|
/* android打包配置 */
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
],
|
||||||
|
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ]
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
"ios" : {
|
||||||
|
"dSYMs" : false,
|
||||||
|
"privacyDescription" : {
|
||||||
|
"NSCameraUsageDescription" : "您可以拍照设置头像、拍照上传图片",
|
||||||
|
"NSPhotoLibraryAddUsageDescription" : "您可以设置头像、保存图片到相册,还可以上传图片",
|
||||||
|
"NSPhotoLibraryUsageDescription" : "您可以设置头像、保存图片到相册,还可以上传图片",
|
||||||
|
"NSUserTrackingUsageDescription" : "根据您的习惯为您推荐"
|
||||||
|
},
|
||||||
|
"capabilities" : {
|
||||||
|
"entitlements" : {
|
||||||
|
"com.apple.developer.associated-domains" : [
|
||||||
|
"applinks:static-mp-62a38312-a6b8-4502-9a4c-9bb095d26ddd.next.bspapp.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* SDK配置 */
|
||||||
|
"sdkConfigs" : {
|
||||||
|
"payment" : {
|
||||||
|
"weixin" : {
|
||||||
|
"__platform__" : [ "ios", "android" ],
|
||||||
|
"appid" : "wxee7d4a85331633e4",
|
||||||
|
"UniversalLinks" : "https://static-mp-62a38312-a6b8-4502-9a4c-9bb095d26ddd.next.bspapp.com/uni-universallinks/__UNI__1FC79BE/"
|
||||||
|
},
|
||||||
|
"alipay" : {
|
||||||
|
"__platform__" : [ "ios", "android" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ad" : {},
|
||||||
|
"oauth" : {
|
||||||
|
"weixin" : {
|
||||||
|
"appid" : "wxee7d4a85331633e4",
|
||||||
|
"UniversalLinks" : "https://static-mp-62a38312-a6b8-4502-9a4c-9bb095d26ddd.next.bspapp.com/uni-universallinks/__UNI__1FC79BE/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"share" : {
|
||||||
|
"weixin" : {
|
||||||
|
"appid" : "wxee7d4a85331633e4",
|
||||||
|
"UniversalLinks" : "https://static-mp-62a38312-a6b8-4502-9a4c-9bb095d26ddd.next.bspapp.com/uni-universallinks/__UNI__1FC79BE/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons" : {
|
||||||
|
"android" : {
|
||||||
|
"hdpi" : "unpackage/res/icons/72x72.png",
|
||||||
|
"xhdpi" : "unpackage/res/icons/96x96.png",
|
||||||
|
"xxhdpi" : "unpackage/res/icons/144x144.png",
|
||||||
|
"xxxhdpi" : "unpackage/res/icons/192x192.png"
|
||||||
|
},
|
||||||
|
"ios" : {
|
||||||
|
"appstore" : "unpackage/res/icons/1024x1024.png",
|
||||||
|
"ipad" : {
|
||||||
|
"app" : "unpackage/res/icons/76x76.png",
|
||||||
|
"app@2x" : "unpackage/res/icons/152x152.png",
|
||||||
|
"notification" : "unpackage/res/icons/20x20.png",
|
||||||
|
"notification@2x" : "unpackage/res/icons/40x40.png",
|
||||||
|
"proapp@2x" : "unpackage/res/icons/167x167.png",
|
||||||
|
"settings" : "unpackage/res/icons/29x29.png",
|
||||||
|
"settings@2x" : "unpackage/res/icons/58x58.png",
|
||||||
|
"spotlight" : "unpackage/res/icons/40x40.png",
|
||||||
|
"spotlight@2x" : "unpackage/res/icons/80x80.png"
|
||||||
|
},
|
||||||
|
"iphone" : {
|
||||||
|
"app@2x" : "unpackage/res/icons/120x120.png",
|
||||||
|
"app@3x" : "unpackage/res/icons/180x180.png",
|
||||||
|
"notification@2x" : "unpackage/res/icons/40x40.png",
|
||||||
|
"notification@3x" : "unpackage/res/icons/60x60.png",
|
||||||
|
"settings@2x" : "unpackage/res/icons/58x58.png",
|
||||||
|
"settings@3x" : "unpackage/res/icons/87x87.png",
|
||||||
|
"spotlight@2x" : "unpackage/res/icons/80x80.png",
|
||||||
|
"spotlight@3x" : "unpackage/res/icons/120x120.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"splashscreen" : {
|
||||||
|
"useOriginalMsgbox" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* 快应用特有相关 */
|
||||||
|
"quickapp" : {},
|
||||||
|
/* 小程序特有相关 */
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "wx386a75e518b38935",
|
||||||
|
"optimization" : {
|
||||||
|
"subPackages" : true
|
||||||
|
},
|
||||||
|
"setting" : {
|
||||||
|
"urlCheck" : false,
|
||||||
|
"es6" : true,
|
||||||
|
"minified" : true
|
||||||
|
},
|
||||||
|
"__usePrivacyCheck__" : true,
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-alipay" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-baidu" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "3",
|
||||||
|
"h5" : {
|
||||||
|
"router" : {
|
||||||
|
"mode" : "history",
|
||||||
|
"base" : "/mobile/"
|
||||||
|
},
|
||||||
|
"title" : "加载中"
|
||||||
|
},
|
||||||
|
"_spaceID" : "mp-62a38312-a6b8-4502-9a4c-9bb095d26ddd"
|
||||||
|
}
|
||||||
7
src/mixins/index.ts
Normal file
7
src/mixins/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { App } from 'vue'
|
||||||
|
import share from './share'
|
||||||
|
import theme from './theme'
|
||||||
|
import setTitle from './setTitle'
|
||||||
|
export function setupMixin(app: App) {
|
||||||
|
app.mixin(share).mixin(theme).mixin(setTitle)
|
||||||
|
}
|
||||||
8
src/mixins/setTitle.ts
Normal file
8
src/mixins/setTitle.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { useNavigationBarTitleStore } from '@/stores/navigationBarTitle'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
onShow() {
|
||||||
|
const navigationBarTitleStore = useNavigationBarTitleStore()
|
||||||
|
navigationBarTitleStore.setTitle()
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user