feat:注册功能
This commit is contained in:
3
.env.development
Normal file
3
.env.development
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
VITE_API_BASE_URL=http://192.168.1.187:8080
|
||||||
|
# 后台「系统管理 -> 客户端管理」中 APP 端的 clientId
|
||||||
|
VITE_AUTH_CLIENT_ID=
|
||||||
1
api-docs.json
Normal file
1
api-docs.json
Normal file
File diff suppressed because one or more lines are too long
38
src/api/auth.ts
Normal file
38
src/api/auth.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { LoginResult, PasswordLoginParams, RegisterParams, RegisterResult, SendSmsCodeParams } from '@/types/auth'
|
||||||
|
import { AUTH_CLIENT_ID } from '@/constants/config'
|
||||||
|
import { post } from '@/utils/request'
|
||||||
|
|
||||||
|
const AUTH_API = {
|
||||||
|
register: '/app/auth/register',
|
||||||
|
sendSmsCode: '/app/auth/sms/code',
|
||||||
|
loginByPassword: '/app/auth/login/password',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendSmsCode(data: SendSmsCodeParams) {
|
||||||
|
return post<void>(AUTH_API.sendSmsCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(data: RegisterParams) {
|
||||||
|
return post<RegisterResult>(AUTH_API.register, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loginByPassword(
|
||||||
|
data: PasswordLoginParams,
|
||||||
|
options?: { showErrorToast?: boolean },
|
||||||
|
) {
|
||||||
|
if (!AUTH_CLIENT_ID) {
|
||||||
|
const message = '未配置认证客户端 ID,请联系后端获取 clientId 并写入 .env.development'
|
||||||
|
uni.showToast({ title: message, icon: 'none', duration: 3000 })
|
||||||
|
return Promise.reject(new Error(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
return post<LoginResult>(
|
||||||
|
AUTH_API.loginByPassword,
|
||||||
|
{
|
||||||
|
clientId: AUTH_CLIENT_ID,
|
||||||
|
phoneNumber: data.phoneNumber,
|
||||||
|
password: data.password,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
}
|
||||||
12
src/constants/config.ts
Normal file
12
src/constants/config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/** 后端服务地址 */
|
||||||
|
export const API_HOST = '192.168.1.187'
|
||||||
|
|
||||||
|
export const API_PORT = 8080
|
||||||
|
|
||||||
|
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || `http://${API_HOST}:${API_PORT}`
|
||||||
|
|
||||||
|
/** 接口请求超时(毫秒) */
|
||||||
|
export const API_TIMEOUT = 15000
|
||||||
|
|
||||||
|
/** APP 端 OAuth 客户端 ID(sys_client 表,需与后端「客户端管理」一致) */
|
||||||
|
export const AUTH_CLIENT_ID = import.meta.env.VITE_AUTH_CLIENT_ID || ''
|
||||||
62
src/hooks/use-sms-code.ts
Normal file
62
src/hooks/use-sms-code.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { onUnmounted, ref } from 'vue'
|
||||||
|
import { sendSmsCode } from '@/api/auth'
|
||||||
|
import type { SmsScene } from '@/types/auth'
|
||||||
|
const PHONE_REG = /^1\d{10}$/
|
||||||
|
const COUNTDOWN_SECONDS = 60
|
||||||
|
|
||||||
|
export function useSmsCode(getPhone: () => string, scene: SmsScene) {
|
||||||
|
const countdown = ref(0)
|
||||||
|
const sending = ref(false)
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
function clearTimer() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown() {
|
||||||
|
countdown.value = COUNTDOWN_SECONDS
|
||||||
|
clearTimer()
|
||||||
|
timer = setInterval(() => {
|
||||||
|
countdown.value -= 1
|
||||||
|
if (countdown.value <= 0)
|
||||||
|
clearTimer()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSendCode() {
|
||||||
|
if (countdown.value > 0 || sending.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
const phone = getPhone().trim()
|
||||||
|
if (!PHONE_REG.test(phone)) {
|
||||||
|
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sending.value = true
|
||||||
|
try {
|
||||||
|
await sendSmsCode({ phoneNumber: phone, scene })
|
||||||
|
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// 错误提示由 request 统一处理
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
countdown,
|
||||||
|
sending,
|
||||||
|
handleSendCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"modules": {},
|
"modules": {},
|
||||||
"distribute": {
|
"distribute": {
|
||||||
"android": {
|
"android": {
|
||||||
|
"usesCleartextTraffic": true,
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { onUnmounted, ref } from 'vue'
|
||||||
|
import type { SmsScene } from '@/types/auth'
|
||||||
|
import { requestSmsCode } from '@/utils/sms-code'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
options: {
|
options: {
|
||||||
@@ -7,8 +9,10 @@ defineOptions({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
|
phone: string
|
||||||
|
scene: SmsScene
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -16,17 +20,14 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const countdown = ref(0)
|
const countdown = ref(0)
|
||||||
|
const sending = ref(false)
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
function onInput(e: { detail: { value: string } }) {
|
function onInput(e: { detail: { value: string } }) {
|
||||||
emit('update:modelValue', e.detail.value)
|
emit('update:modelValue', e.detail.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGetCode() {
|
function startCountdown() {
|
||||||
if (countdown.value > 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
|
||||||
countdown.value = 60
|
countdown.value = 60
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
countdown.value -= 1
|
countdown.value -= 1
|
||||||
@@ -36,6 +37,31 @@ function handleGetCode() {
|
|||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleGetCode() {
|
||||||
|
if (countdown.value > 0 || sending.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
sending.value = true
|
||||||
|
try {
|
||||||
|
const success = await requestSmsCode(props.phone, props.scene)
|
||||||
|
if (success)
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// 错误提示由 request 统一处理
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -50,11 +76,11 @@ function handleGetCode() {
|
|||||||
>
|
>
|
||||||
<view
|
<view
|
||||||
class="code-input__btn"
|
class="code-input__btn"
|
||||||
:class="{ 'code-input__btn--disabled': countdown > 0 }"
|
:class="{ 'code-input__btn--disabled': countdown > 0 || sending }"
|
||||||
@tap="handleGetCode"
|
@tap="handleGetCode"
|
||||||
>
|
>
|
||||||
<text class="code-input__btn-text">
|
<text class="code-input__btn-text">
|
||||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
{{ sending ? '发送中...' : countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -63,7 +63,11 @@ function handleUserAgreement() {
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="forgot-page__field">
|
<view class="forgot-page__field">
|
||||||
<CodeInput v-model="code" />
|
<CodeInput
|
||||||
|
v-model="code"
|
||||||
|
:phone="phone"
|
||||||
|
scene="resetPwd"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="forgot-page__field">
|
<view class="forgot-page__field">
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { loginByPassword } from '@/api/auth'
|
||||||
|
import { setAccessToken } from '@/utils/auth-storage'
|
||||||
|
import { navigateToResultPage } from '@/utils/navigate-result'
|
||||||
|
import { validatePhone } from '@/utils/sms-code'
|
||||||
import LoginInput from './components/LoginInput.vue'
|
import LoginInput from './components/LoginInput.vue'
|
||||||
|
|
||||||
const phone = ref('')
|
const phone = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const agreed = ref(false)
|
const agreed = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
function toggleAgreed() {
|
function toggleAgreed() {
|
||||||
agreed.value = !agreed.value
|
agreed.value = !agreed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogin() {
|
function shouldNavigateToRegister(msg: string) {
|
||||||
|
return /未注册|不存在|请先注册|创建账号/.test(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
if (submitting.value)
|
||||||
|
return
|
||||||
|
|
||||||
if (!agreed.value) {
|
if (!agreed.value) {
|
||||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||||
return
|
return
|
||||||
@@ -19,10 +31,38 @@ function handleLogin() {
|
|||||||
uni.showToast({ title: '请输入手机号和密码', icon: 'none' })
|
uni.showToast({ title: '请输入手机号和密码', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!validatePhone(phone.value))
|
||||||
|
return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
uni.showLoading({ title: '登录中...', mask: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await loginByPassword({
|
||||||
|
phoneNumber: phone.value.trim(),
|
||||||
|
password: password.value,
|
||||||
|
}, { showErrorToast: false })
|
||||||
|
|
||||||
|
if (result.access_token)
|
||||||
|
setAccessToken(result.access_token)
|
||||||
|
|
||||||
// TODO: 接入登录接口,失败时调用 navigateToResultPage('loginFailed')
|
|
||||||
uni.redirectTo({ url: '/pages/home-index/index' })
|
uni.redirectTo({ url: '/pages/home-index/index' })
|
||||||
}
|
}
|
||||||
|
catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : '登录失败'
|
||||||
|
if (message.includes('未配置认证客户端'))
|
||||||
|
return
|
||||||
|
if (shouldNavigateToRegister(message)) {
|
||||||
|
navigateToResultPage('loginFailed')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({ title: message, icon: 'none' })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
submitting.value = false
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleForgotPassword() {
|
function handleForgotPassword() {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { onUnmounted, ref } from 'vue'
|
||||||
|
import type { SmsScene } from '@/types/auth'
|
||||||
|
import { requestSmsCode } from '@/utils/sms-code'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
options: {
|
options: {
|
||||||
@@ -7,8 +9,10 @@ defineOptions({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
|
phone: string
|
||||||
|
scene: SmsScene
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -16,17 +20,14 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const countdown = ref(0)
|
const countdown = ref(0)
|
||||||
|
const sending = ref(false)
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
function onInput(e: { detail: { value: string } }) {
|
function onInput(e: { detail: { value: string } }) {
|
||||||
emit('update:modelValue', e.detail.value)
|
emit('update:modelValue', e.detail.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGetCode() {
|
function startCountdown() {
|
||||||
if (countdown.value > 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
|
||||||
countdown.value = 60
|
countdown.value = 60
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
countdown.value -= 1
|
countdown.value -= 1
|
||||||
@@ -36,6 +37,31 @@ function handleGetCode() {
|
|||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleGetCode() {
|
||||||
|
if (countdown.value > 0 || sending.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
sending.value = true
|
||||||
|
try {
|
||||||
|
const success = await requestSmsCode(props.phone, props.scene)
|
||||||
|
if (success)
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// 错误提示由 request 统一处理
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -55,11 +81,11 @@ function handleGetCode() {
|
|||||||
>
|
>
|
||||||
<view
|
<view
|
||||||
class="code-input__btn"
|
class="code-input__btn"
|
||||||
:class="{ 'code-input__btn--disabled': countdown > 0 }"
|
:class="{ 'code-input__btn--disabled': countdown > 0 || sending }"
|
||||||
@tap="handleGetCode"
|
@tap="handleGetCode"
|
||||||
>
|
>
|
||||||
<text class="code-input__btn-text">
|
<text class="code-input__btn-text">
|
||||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
{{ sending ? '发送中...' : countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { register } from '@/api/auth'
|
||||||
import { getPasswordStrength } from '@/utils/password-strength'
|
import { getPasswordStrength } from '@/utils/password-strength'
|
||||||
import { navigateToResultPage } from '@/utils/navigate-result'
|
import { navigateToResultPage } from '@/utils/navigate-result'
|
||||||
import CodeInput from './components/CodeInput.vue'
|
import CodeInput from './components/CodeInput.vue'
|
||||||
@@ -12,12 +13,16 @@ const code = ref('')
|
|||||||
const password = ref('')
|
const password = ref('')
|
||||||
const confirmPassword = ref('')
|
const confirmPassword = ref('')
|
||||||
const agreed = ref(false)
|
const agreed = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
function toggleAgreed() {
|
function toggleAgreed() {
|
||||||
agreed.value = !agreed.value
|
agreed.value = !agreed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRegister() {
|
async function handleRegister() {
|
||||||
|
if (submitting.value)
|
||||||
|
return
|
||||||
|
|
||||||
if (!agreed.value) {
|
if (!agreed.value) {
|
||||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||||
return
|
return
|
||||||
@@ -34,8 +39,27 @@ function handleRegister() {
|
|||||||
uni.showToast({ title: '密码等级过低,请重新输入', icon: 'none' })
|
uni.showToast({ title: '密码等级过低,请重新输入', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
uni.showLoading({ title: '注册中...', mask: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
await register({
|
||||||
|
phoneNumber: phone.value.trim(),
|
||||||
|
smsCode: code.value.trim(),
|
||||||
|
password: password.value,
|
||||||
|
confirmPassword: confirmPassword.value,
|
||||||
|
})
|
||||||
navigateToResultPage('register')
|
navigateToResultPage('register')
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
// 错误提示由 request 统一处理
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
submitting.value = false
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleLogin() {
|
function handleLogin() {
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
@@ -70,7 +94,11 @@ function handleUserAgreement() {
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="register-page__field">
|
<view class="register-page__field">
|
||||||
<CodeInput v-model="code" />
|
<CodeInput
|
||||||
|
v-model="code"
|
||||||
|
:phone="phone"
|
||||||
|
scene="register"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="register-page__field">
|
<view class="register-page__field">
|
||||||
@@ -130,9 +158,10 @@ function handleUserAgreement() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="register-page__submit"
|
class="register-page__submit"
|
||||||
|
:disabled="submitting"
|
||||||
@tap="handleRegister"
|
@tap="handleRegister"
|
||||||
>
|
>
|
||||||
注册
|
{{ submitting ? '注册中...' : '注册' }}
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { onUnmounted, ref } from 'vue'
|
||||||
|
import type { SmsScene } from '@/types/auth'
|
||||||
|
import { requestSmsCode } from '@/utils/sms-code'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
options: {
|
options: {
|
||||||
@@ -7,8 +9,10 @@ defineOptions({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
|
phone: string
|
||||||
|
scene: SmsScene
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -16,17 +20,14 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const countdown = ref(0)
|
const countdown = ref(0)
|
||||||
|
const sending = ref(false)
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
function onInput(e: { detail: { value: string } }) {
|
function onInput(e: { detail: { value: string } }) {
|
||||||
emit('update:modelValue', e.detail.value)
|
emit('update:modelValue', e.detail.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGetCode() {
|
function startCountdown() {
|
||||||
if (countdown.value > 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
|
||||||
countdown.value = 60
|
countdown.value = 60
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
countdown.value -= 1
|
countdown.value -= 1
|
||||||
@@ -36,6 +37,31 @@ function handleGetCode() {
|
|||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleGetCode() {
|
||||||
|
if (countdown.value > 0 || sending.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
sending.value = true
|
||||||
|
try {
|
||||||
|
const success = await requestSmsCode(props.phone, props.scene)
|
||||||
|
if (success)
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// 错误提示由 request 统一处理
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -55,11 +81,11 @@ function handleGetCode() {
|
|||||||
>
|
>
|
||||||
<view
|
<view
|
||||||
class="code-input__btn"
|
class="code-input__btn"
|
||||||
:class="{ 'code-input__btn--disabled': countdown > 0 }"
|
:class="{ 'code-input__btn--disabled': countdown > 0 || sending }"
|
||||||
@tap="handleGetCode"
|
@tap="handleGetCode"
|
||||||
>
|
>
|
||||||
<text class="code-input__btn-text">
|
<text class="code-input__btn-text">
|
||||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
{{ sending ? '发送中...' : countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ function handleUserAgreement() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<view class="sms-login-page__code-group">
|
<view class="sms-login-page__code-group">
|
||||||
<CodeInput v-model="code" />
|
<CodeInput
|
||||||
|
v-model="code"
|
||||||
|
:phone="phone"
|
||||||
|
scene="login"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
|
|||||||
@@ -73,3 +73,45 @@
|
|||||||
color: $color-text-primary;
|
color: $color-text-primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-message__image-wrap {
|
||||||
|
max-width: 360rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message__image {
|
||||||
|
display: block;
|
||||||
|
width: 360rpx;
|
||||||
|
height: 360rpx;
|
||||||
|
background: #f2f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message__bubble--file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
min-width: 280rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message__file-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message__file-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
.chat-message__bubble--user & {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message__bubble--service & {
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import type { ChatMessage } from '@/types/customer-service'
|
import type { ChatMessage } from '@/types/customer-service'
|
||||||
|
import fileIcon from '@/static/icons/PDF.png'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
message: ChatMessage
|
message: ChatMessage
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const messageType = computed(() => props.message.type || 'text')
|
||||||
|
|
||||||
|
function handlePreviewImage() {
|
||||||
|
if (!props.message.imageUrl)
|
||||||
|
return
|
||||||
|
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [props.message.imageUrl],
|
||||||
|
current: props.message.imageUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenFile() {
|
||||||
|
if (!props.message.filePath) {
|
||||||
|
uni.showToast({ title: props.message.fileName || '文件', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.openDocument({
|
||||||
|
filePath: props.message.filePath,
|
||||||
|
showMenu: true,
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({ title: `已发送${props.message.fileName || '文件'}`, icon: 'none' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -21,7 +50,37 @@ defineProps<{
|
|||||||
<text class="chat-message__nickname">
|
<text class="chat-message__nickname">
|
||||||
{{ message.nickname }}
|
{{ message.nickname }}
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
|
v-if="messageType === 'image' && message.imageUrl"
|
||||||
|
class="chat-message__image-wrap"
|
||||||
|
@tap="handlePreviewImage"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="chat-message__image"
|
||||||
|
:src="message.imageUrl"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-else-if="messageType === 'file'"
|
||||||
|
class="chat-message__bubble chat-message__bubble--file"
|
||||||
|
:class="`chat-message__bubble--${message.role}`"
|
||||||
|
@tap="handleOpenFile"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="chat-message__file-icon"
|
||||||
|
:src="fileIcon"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<text class="chat-message__file-name">
|
||||||
|
{{ message.fileName || message.content }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
class="chat-message__bubble"
|
class="chat-message__bubble"
|
||||||
:class="`chat-message__bubble--${message.role}`"
|
:class="`chat-message__bubble--${message.role}`"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import type { ChatMessage } from '@/types/customer-service'
|
|||||||
import { navigateBackWithFallback } from '@/utils/navigate-back'
|
import { navigateBackWithFallback } from '@/utils/navigate-back'
|
||||||
import ChatMessageItem from './components/ChatMessage.vue'
|
import ChatMessageItem from './components/ChatMessage.vue'
|
||||||
|
|
||||||
|
const USER_AVATAR = '/static/icons/hi.png'
|
||||||
|
const USER_NICKNAME = '昵称'
|
||||||
|
|
||||||
const messages = ref<ChatMessage[]>(CUSTOMER_SERVICE_MESSAGES.map(item => ({ ...item })))
|
const messages = ref<ChatMessage[]>(CUSTOMER_SERVICE_MESSAGES.map(item => ({ ...item })))
|
||||||
const inputValue = ref('')
|
const inputValue = ref('')
|
||||||
const panelVisible = ref(false)
|
const panelVisible = ref(false)
|
||||||
@@ -22,24 +25,137 @@ function handleSend() {
|
|||||||
if (!content)
|
if (!content)
|
||||||
return
|
return
|
||||||
|
|
||||||
messages.value.push({
|
appendTextMessage(content)
|
||||||
id: String(Date.now()),
|
|
||||||
role: 'user',
|
|
||||||
nickname: '昵称',
|
|
||||||
content,
|
|
||||||
avatar: '/static/icons/hi.png',
|
|
||||||
})
|
|
||||||
inputValue.value = ''
|
inputValue.value = ''
|
||||||
panelVisible.value = false
|
panelVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAttachment(type: 'camera' | 'photo' | 'file') {
|
function appendTextMessage(content: string) {
|
||||||
const labelMap = {
|
messages.value.push({
|
||||||
camera: '拍摄',
|
id: `${Date.now()}-${messages.value.length}`,
|
||||||
photo: '照片',
|
role: 'user',
|
||||||
file: '文件',
|
nickname: USER_NICKNAME,
|
||||||
|
content,
|
||||||
|
type: 'text',
|
||||||
|
avatar: USER_AVATAR,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
uni.showToast({ title: `${labelMap[type]}功能开发中`, icon: 'none' })
|
|
||||||
|
function appendImageMessage(imageUrl: string) {
|
||||||
|
messages.value.push({
|
||||||
|
id: `${Date.now()}-${messages.value.length}`,
|
||||||
|
role: 'user',
|
||||||
|
nickname: USER_NICKNAME,
|
||||||
|
content: '[图片]',
|
||||||
|
type: 'image',
|
||||||
|
imageUrl,
|
||||||
|
avatar: USER_AVATAR,
|
||||||
|
})
|
||||||
|
panelVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendFileMessage(fileName: string, filePath: string) {
|
||||||
|
messages.value.push({
|
||||||
|
id: `${Date.now()}-${messages.value.length}`,
|
||||||
|
role: 'user',
|
||||||
|
nickname: USER_NICKNAME,
|
||||||
|
content: fileName,
|
||||||
|
type: 'file',
|
||||||
|
fileName,
|
||||||
|
filePath,
|
||||||
|
avatar: USER_AVATAR,
|
||||||
|
})
|
||||||
|
panelVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChooseCanceled(errMsg?: string) {
|
||||||
|
return Boolean(errMsg?.includes('cancel') || errMsg?.includes('取消'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChooseImageSuccess(tempFilePaths: string[]) {
|
||||||
|
if (!tempFilePaths.length) {
|
||||||
|
uni.showToast({ title: '未选择图片', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFilePaths.forEach((path) => {
|
||||||
|
appendImageMessage(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChooseImages(sourceType: Array<'album' | 'camera'>) {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 9,
|
||||||
|
sourceType,
|
||||||
|
success: (res) => {
|
||||||
|
handleChooseImageSuccess(res.tempFilePaths)
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
if (isChooseCanceled(error.errMsg))
|
||||||
|
return
|
||||||
|
uni.showToast({ title: '选择图片失败', icon: 'none' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChooseFilesSuccess(files: Array<{ name: string, path: string }>) {
|
||||||
|
if (!files.length) {
|
||||||
|
uni.showToast({ title: '未选择文件', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
appendFileMessage(file.name, file.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChooseFiles() {
|
||||||
|
uni.chooseFile({
|
||||||
|
count: 5,
|
||||||
|
extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.zip', '.rar', '.png', '.jpg', '.jpeg'],
|
||||||
|
success: (res) => {
|
||||||
|
handleChooseFilesSuccess(res.tempFiles.map(file => ({
|
||||||
|
name: file.name,
|
||||||
|
path: file.path,
|
||||||
|
})))
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
if (typeof uni.chooseMessageFile === 'function') {
|
||||||
|
uni.chooseMessageFile({
|
||||||
|
count: 5,
|
||||||
|
type: 'all',
|
||||||
|
success: (res) => {
|
||||||
|
handleChooseFilesSuccess(res.tempFiles.map(file => ({
|
||||||
|
name: file.name,
|
||||||
|
path: file.path,
|
||||||
|
})))
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
if (isChooseCanceled(error.errMsg))
|
||||||
|
return
|
||||||
|
uni.showToast({ title: '选择文件失败', icon: 'none' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({ title: '选择文件失败', icon: 'none' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAttachment(type: 'camera' | 'photo' | 'file') {
|
||||||
|
if (type === 'camera') {
|
||||||
|
handleChooseImages(['camera'])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'photo') {
|
||||||
|
handleChooseImages(['album'])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChooseFiles()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
29
src/types/auth.ts
Normal file
29
src/types/auth.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export type SmsScene = 'register' | 'login' | 'resetPwd'
|
||||||
|
|
||||||
|
export interface SendSmsCodeParams {
|
||||||
|
phoneNumber: string
|
||||||
|
scene: SmsScene
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterParams {
|
||||||
|
phoneNumber: string
|
||||||
|
smsCode: string
|
||||||
|
password: string
|
||||||
|
confirmPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterResult {
|
||||||
|
token?: string
|
||||||
|
userId?: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordLoginParams {
|
||||||
|
phoneNumber: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResult {
|
||||||
|
access_token: string
|
||||||
|
expire_in?: number
|
||||||
|
client_id?: string
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
export type ChatRole = 'user' | 'service'
|
export type ChatRole = 'user' | 'service'
|
||||||
|
|
||||||
|
export type ChatMessageType = 'text' | 'image' | 'file'
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
id: string
|
id: string
|
||||||
role: ChatRole
|
role: ChatRole
|
||||||
nickname: string
|
nickname: string
|
||||||
content: string
|
content: string
|
||||||
avatar: string
|
avatar: string
|
||||||
|
type?: ChatMessageType
|
||||||
|
imageUrl?: string
|
||||||
|
fileName?: string
|
||||||
|
filePath?: string
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/utils/auth-storage.ts
Normal file
13
src/utils/auth-storage.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const ACCESS_TOKEN_KEY = 'access_token'
|
||||||
|
|
||||||
|
export function setAccessToken(token: string) {
|
||||||
|
uni.setStorageSync(ACCESS_TOKEN_KEY, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccessToken() {
|
||||||
|
return uni.getStorageSync(ACCESS_TOKEN_KEY) as string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAccessToken() {
|
||||||
|
uni.removeStorageSync(ACCESS_TOKEN_KEY)
|
||||||
|
}
|
||||||
98
src/utils/request.ts
Normal file
98
src/utils/request.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { API_BASE_URL, API_TIMEOUT } from '@/constants/config'
|
||||||
|
import { getAccessToken } from '@/utils/auth-storage'
|
||||||
|
|
||||||
|
export interface RequestOptions {
|
||||||
|
url: string
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
|
data?: object | string
|
||||||
|
header?: Record<string, string>
|
||||||
|
timeout?: number
|
||||||
|
showErrorToast?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T = unknown> {
|
||||||
|
code?: number
|
||||||
|
message?: string
|
||||||
|
msg?: string
|
||||||
|
data?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUrl(url: string) {
|
||||||
|
if (/^https?:\/\//.test(url))
|
||||||
|
return url
|
||||||
|
|
||||||
|
const path = url.startsWith('/') ? url : `/${url}`
|
||||||
|
return `${API_BASE_URL}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(response: ApiResponse, fallback: string) {
|
||||||
|
return response.message || response.msg || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export function request<T = unknown>(options: RequestOptions): Promise<T> {
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
method = 'GET',
|
||||||
|
data,
|
||||||
|
header,
|
||||||
|
timeout = API_TIMEOUT,
|
||||||
|
showErrorToast = true,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const token = getAccessToken()
|
||||||
|
uni.request({
|
||||||
|
url: resolveUrl(url),
|
||||||
|
method,
|
||||||
|
data,
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
...header,
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
success: (res) => {
|
||||||
|
const statusCode = res.statusCode
|
||||||
|
const body = res.data as ApiResponse<T> | T
|
||||||
|
|
||||||
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
|
if (body && typeof body === 'object' && 'code' in body) {
|
||||||
|
const apiBody = body as ApiResponse<T>
|
||||||
|
if (apiBody.code === 0 || apiBody.code === 200) {
|
||||||
|
resolve((apiBody.data ?? apiBody) as T)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = getErrorMessage(apiBody, '请求失败')
|
||||||
|
if (showErrorToast)
|
||||||
|
uni.showToast({ title: message, icon: 'none' })
|
||||||
|
reject(new Error(message))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(body as T)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = `请求失败(${statusCode})`
|
||||||
|
if (showErrorToast)
|
||||||
|
uni.showToast({ title: message, icon: 'none' })
|
||||||
|
reject(new Error(message))
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
const message = error.errMsg || '网络异常,请稍后重试'
|
||||||
|
if (showErrorToast)
|
||||||
|
uni.showToast({ title: message, icon: 'none' })
|
||||||
|
reject(new Error(message))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get<T = unknown>(url: string, data?: object, options?: Omit<RequestOptions, 'url' | 'method' | 'data'>) {
|
||||||
|
return request<T>({ url, method: 'GET', data, ...options })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function post<T = unknown>(url: string, data?: object, options?: Omit<RequestOptions, 'url' | 'method' | 'data'>) {
|
||||||
|
return request<T>({ url, method: 'POST', data, ...options })
|
||||||
|
}
|
||||||
29
src/utils/sms-code.ts
Normal file
29
src/utils/sms-code.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { sendSmsCode } from '@/api/auth'
|
||||||
|
import type { SmsScene } from '@/types/auth'
|
||||||
|
|
||||||
|
const PHONE_REG = /^1\d{10}$/
|
||||||
|
|
||||||
|
export function validatePhone(phone: string) {
|
||||||
|
const value = phone.trim()
|
||||||
|
if (!value) {
|
||||||
|
uni.showToast({ title: '请输入手机号', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!PHONE_REG.test(value)) {
|
||||||
|
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestSmsCode(phone: string, scene: SmsScene) {
|
||||||
|
if (!validatePhone(phone))
|
||||||
|
return false
|
||||||
|
|
||||||
|
await sendSmsCode({
|
||||||
|
phoneNumber: phone.trim(),
|
||||||
|
scene,
|
||||||
|
})
|
||||||
|
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -13,7 +13,14 @@ export default defineConfig({
|
|||||||
// https://uni-helper.js.org/plugin-uni
|
// https://uni-helper.js.org/plugin-uni
|
||||||
Uni(),
|
Uni(),
|
||||||
],
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://192.168.1.187:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user