修改Saas登录方式
parent
132d024848
commit
dc2017266d
|
|
@ -10,6 +10,8 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Redis 配置类
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.yanzhu.framework.common.enums.UserTypeEnum;
|
|||
import com.yanzhu.framework.common.pojo.CommonResult;
|
||||
import com.yanzhu.framework.security.config.SecurityProperties;
|
||||
import com.yanzhu.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.yanzhu.framework.tenant.core.aop.TenantIgnore;
|
||||
import com.yanzhu.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.yanzhu.module.system.controller.admin.auth.vo.*;
|
||||
import com.yanzhu.module.system.convert.auth.AuthConvert;
|
||||
import com.yanzhu.module.system.dal.dataobject.permission.MenuDO;
|
||||
|
|
@ -42,6 +44,7 @@ import static com.yanzhu.framework.security.core.util.SecurityFrameworkUtils.get
|
|||
@Tag(name = "管理后台 - 认证")
|
||||
@RestController
|
||||
@RequestMapping("/system/auth")
|
||||
@TenantIgnore
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class AuthController {
|
||||
|
|
@ -66,6 +69,11 @@ public class AuthController {
|
|||
@PermitAll
|
||||
@Operation(summary = "使用账号密码登录")
|
||||
public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
|
||||
// 先根据用户名查询用户,获取租户ID
|
||||
AdminUserDO user = userService.getUserByUsername(reqVO.getUsername());
|
||||
if (user != null && user.getTenantId() != null) {
|
||||
TenantContextHolder.setTenantId(user.getTenantId());
|
||||
}
|
||||
return success(authService.login(reqVO));
|
||||
}
|
||||
|
||||
|
|
@ -127,9 +135,12 @@ public class AuthController {
|
|||
@PostMapping("/sms-login")
|
||||
@PermitAll
|
||||
@Operation(summary = "使用短信验证码登录")
|
||||
// 可按需开启限流:https://github.com/YunaiV/ruoyi-vue-pro/issues/851
|
||||
// @RateLimiter(time = 60, count = 6, keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#reqVO.mobile")
|
||||
public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) {
|
||||
// 先根据手机号查询用户,获取租户ID
|
||||
AdminUserDO user = userService.getUserByMobile(reqVO.getMobile());
|
||||
if (user != null && user.getTenantId() != null) {
|
||||
TenantContextHolder.setTenantId(user.getTenantId());
|
||||
}
|
||||
return success(authService.smsLogin(reqVO));
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +148,11 @@ public class AuthController {
|
|||
@PermitAll
|
||||
@Operation(summary = "发送手机验证码")
|
||||
public CommonResult<Boolean> sendLoginSmsCode(@RequestBody @Valid AuthSmsSendReqVO reqVO) {
|
||||
// 先根据手机号查询用户,获取租户ID
|
||||
AdminUserDO user = userService.getUserByMobile(reqVO.getMobile());
|
||||
if (user != null && user.getTenantId() != null) {
|
||||
TenantContextHolder.setTenantId(user.getTenantId());
|
||||
}
|
||||
authService.sendSmsCode(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import java.time.LocalDateTime;
|
|||
@Builder
|
||||
public class AuthLoginRespVO {
|
||||
|
||||
@Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long userId;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.yanzhu.framework.common.util.object.BeanUtils;
|
|||
import com.yanzhu.framework.common.util.servlet.ServletUtils;
|
||||
import com.yanzhu.framework.common.util.validation.ValidationUtils;
|
||||
import com.yanzhu.framework.datapermission.core.annotation.DataPermission;
|
||||
import com.yanzhu.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.yanzhu.module.system.api.logger.dto.LoginLogCreateReqDTO;
|
||||
import com.yanzhu.module.system.api.sms.SmsCodeApi;
|
||||
import com.yanzhu.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
|
||||
|
|
@ -216,7 +217,10 @@ public class AdminAuthServiceImpl implements AdminAuthService {
|
|||
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
|
||||
OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
|
||||
// 构建返回结果
|
||||
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
|
||||
AuthLoginRespVO respVO = BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
|
||||
// 设置租户编号
|
||||
respVO.setTenantId(TenantContextHolder.getTenantId());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -20,9 +20,13 @@ VITE_APP_DOCALERT_ENABLE=true
|
|||
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
|
||||
|
||||
# 默认账户密码
|
||||
VITE_APP_DEFAULT_LOGIN_TENANT = 研筑科技
|
||||
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
|
||||
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
|
||||
#VITE_APP_DEFAULT_LOGIN_TENANT = 研筑科技
|
||||
#VITE_APP_DEFAULT_LOGIN_USERNAME = admin
|
||||
#VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
|
||||
|
||||
VITE_APP_DEFAULT_LOGIN_TENANT =
|
||||
VITE_APP_DEFAULT_LOGIN_USERNAME =
|
||||
VITE_APP_DEFAULT_LOGIN_PASSWORD =
|
||||
|
||||
# API 加解密
|
||||
VITE_APP_API_ENCRYPT_ENABLE = true
|
||||
|
|
|
|||
|
|
@ -37,9 +37,14 @@ VITE_APP_CAPTCHA_ENABLE=true
|
|||
VITE_APP_TENANT_ENABLE=true
|
||||
|
||||
# 默认账户密码
|
||||
VITE_APP_DEFAULT_LOGIN_TENANT = 研筑科技
|
||||
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
|
||||
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
|
||||
# 默认账户密码
|
||||
#VITE_APP_DEFAULT_LOGIN_TENANT = 研筑科技
|
||||
#VITE_APP_DEFAULT_LOGIN_USERNAME = admin
|
||||
#VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
|
||||
|
||||
VITE_APP_DEFAULT_LOGIN_TENANT =
|
||||
VITE_APP_DEFAULT_LOGIN_USERNAME =
|
||||
VITE_APP_DEFAULT_LOGIN_PASSWORD =
|
||||
|
||||
# GoView域名
|
||||
VITE_GOVIEW_URL='http://127.0.0.1:3000'
|
||||
|
|
|
|||
|
|
@ -35,3 +35,6 @@
|
|||
border-left-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.hidden{
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ export const formatToken = (token: string): string => {
|
|||
// ========== 账号相关 ==========
|
||||
|
||||
export type LoginFormType = {
|
||||
tenantName: string
|
||||
username: string
|
||||
password: string
|
||||
rememberMe: boolean
|
||||
|
|
|
|||
|
|
@ -3,6 +3,5 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'Error403' })
|
||||
|
||||
const { push } = useRouter()
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -4,4 +4,4 @@
|
|||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'Error404' })
|
||||
const { push } = useRouter()
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -4,4 +4,4 @@
|
|||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'Error500' })
|
||||
const { push } = useRouter()
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -55,7 +55,6 @@
|
|||
</el-skeleton>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-row class="mt-8px" :gutter="8" justify="space-between">
|
||||
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
|
||||
<el-card shadow="never">
|
||||
|
|
@ -107,7 +106,6 @@
|
|||
</el-row>
|
||||
</el-skeleton>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mt-8px">
|
||||
<el-skeleton :loading="loading" animated>
|
||||
<el-row :gutter="20" justify="space-between">
|
||||
|
|
@ -184,15 +182,12 @@
|
|||
import { set } from 'lodash-es'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { formatTime } from '@/utils'
|
||||
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
// import { useWatermark } from '@/hooks/web/useWatermark'
|
||||
import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
|
||||
import { pieOptions, barOptions } from './echarts-data'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
|
@ -207,7 +202,6 @@ let totalSate = reactive<WorkplaceTotal>({
|
|||
access: 0,
|
||||
todo: 0
|
||||
})
|
||||
|
||||
const getCount = async () => {
|
||||
const data = {
|
||||
project: 40,
|
||||
|
|
@ -216,7 +210,6 @@ const getCount = async () => {
|
|||
}
|
||||
totalSate = Object.assign(totalSate, data)
|
||||
}
|
||||
|
||||
// 获取项目数
|
||||
let projects = reactive<Project[]>([])
|
||||
const getProject = async () => {
|
||||
|
|
@ -272,7 +265,6 @@ const getProject = async () => {
|
|||
]
|
||||
projects = Object.assign(projects, data)
|
||||
}
|
||||
|
||||
// 获取通知公告
|
||||
let notice = reactive<Notice[]>([])
|
||||
const getNotice = async () => {
|
||||
|
|
@ -304,10 +296,8 @@ const getNotice = async () => {
|
|||
]
|
||||
notice = Object.assign(notice, data)
|
||||
}
|
||||
|
||||
// 获取快捷入口
|
||||
let shortcut = reactive<Shortcut[]>([])
|
||||
|
||||
const getShortcut = async () => {
|
||||
const data = [
|
||||
{
|
||||
|
|
@ -349,7 +339,6 @@ const getShortcut = async () => {
|
|||
]
|
||||
shortcut = Object.assign(shortcut, data)
|
||||
}
|
||||
|
||||
// 用户来源
|
||||
const getUserAccessSource = async () => {
|
||||
const data = [
|
||||
|
|
@ -372,7 +361,6 @@ const getUserAccessSource = async () => {
|
|||
})
|
||||
}
|
||||
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
|
||||
|
||||
// 周活跃量
|
||||
const getWeeklyUserActivity = async () => {
|
||||
const data = [
|
||||
|
|
@ -397,7 +385,6 @@ const getWeeklyUserActivity = async () => {
|
|||
}
|
||||
])
|
||||
}
|
||||
|
||||
const getAllApi = async () => {
|
||||
await Promise.all([
|
||||
getCount(),
|
||||
|
|
@ -409,14 +396,11 @@ const getAllApi = async () => {
|
|||
])
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const handleProjectClick = (message: string) => {
|
||||
window.open(`https://${message}`, '_blank')
|
||||
}
|
||||
|
||||
const handleShortcutClick = (url: string) => {
|
||||
router.push(url)
|
||||
}
|
||||
|
||||
getAllApi()
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -28,7 +28,6 @@
|
|||
</el-skeleton>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" :rows="2" animated>
|
||||
|
|
@ -57,7 +56,6 @@
|
|||
</el-skeleton>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" :rows="2" animated>
|
||||
|
|
@ -86,7 +84,6 @@
|
|||
</el-skeleton>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" :rows="2" animated>
|
||||
|
|
@ -143,26 +140,21 @@
|
|||
<script lang="ts" setup>
|
||||
import { set } from 'lodash-es'
|
||||
import { EChartsOption } from 'echarts'
|
||||
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import type { AnalysisTotalTypes } from './types'
|
||||
import { barOptions, lineOptions, pieOptions } from './echarts-data'
|
||||
|
||||
defineOptions({ name: 'Home2' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(true)
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('panel')
|
||||
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
|
||||
|
||||
let totalState = reactive<AnalysisTotalTypes>({
|
||||
users: 0,
|
||||
messages: 0,
|
||||
moneys: 0,
|
||||
shoppings: 0
|
||||
})
|
||||
|
||||
const getCount = async () => {
|
||||
const data = {
|
||||
users: 102400,
|
||||
|
|
@ -172,7 +164,6 @@ const getCount = async () => {
|
|||
}
|
||||
totalState = Object.assign(totalState, data)
|
||||
}
|
||||
|
||||
// 用户来源
|
||||
const getUserAccessSource = async () => {
|
||||
const data = [
|
||||
|
|
@ -190,7 +181,6 @@ const getUserAccessSource = async () => {
|
|||
set(pieOptionsData, 'series.data', data)
|
||||
}
|
||||
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
|
||||
|
||||
// 周活跃量
|
||||
const getWeeklyUserActivity = async () => {
|
||||
const data = [
|
||||
|
|
@ -215,9 +205,7 @@ const getWeeklyUserActivity = async () => {
|
|||
}
|
||||
])
|
||||
}
|
||||
|
||||
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
|
||||
|
||||
// 每月销售总额
|
||||
const getMonthlySales = async () => {
|
||||
const data = [
|
||||
|
|
@ -259,61 +247,48 @@ const getMonthlySales = async () => {
|
|||
}
|
||||
])
|
||||
}
|
||||
|
||||
const getAllApi = async () => {
|
||||
await Promise.all([getCount(), getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()])
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
getAllApi()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$prefix-cls: #{$namespace}-panel;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
&__item {
|
||||
&--peoples {
|
||||
color: #40c9c6;
|
||||
}
|
||||
|
||||
&--message {
|
||||
color: #36a3f7;
|
||||
}
|
||||
|
||||
&--money {
|
||||
color: #f4516c;
|
||||
}
|
||||
|
||||
&--shopping {
|
||||
color: #34bfa3;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
:deep(.#{$namespace}-icon) {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--icon {
|
||||
transition: all 0.38s ease-out;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--peoples {
|
||||
background: #40c9c6;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--message {
|
||||
background: #36a3f7;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--money {
|
||||
background: #f4516c;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--shopping {
|
||||
background: #34bfa3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -69,28 +69,21 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { underlineToHump } from '@/utils'
|
||||
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
|
||||
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
|
||||
|
||||
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components'
|
||||
|
||||
defineOptions({ name: 'Login' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('login')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$prefix-cls: #{$namespace}-login;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
overflow: auto;
|
||||
|
||||
&__left {
|
||||
&::before {
|
||||
position: absolute;
|
||||
|
|
@ -107,15 +100,13 @@ $prefix-cls: #{$namespace}-login;
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.dark .login-form {
|
||||
.el-divider__text {
|
||||
background-color: var(--login-bg-color);
|
||||
}
|
||||
|
||||
.el-card {
|
||||
background-color: var(--login-bg-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -148,17 +148,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { underlineToHump } from '@/utils'
|
||||
|
||||
import { ElLoading } from 'element-plus'
|
||||
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
|
||||
import * as LoginApi from '@/api/login'
|
||||
import * as authUtil from '@/utils/auth'
|
||||
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
|
||||
|
|
@ -166,12 +162,9 @@ import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
|
|||
import { LoginStateEnum, useFormValid, useLoginState } from './components/useLogin'
|
||||
import LoginFormTitle from './components/LoginFormTitle.vue'
|
||||
import router from '@/router'
|
||||
|
||||
defineOptions({ name: 'SocialLogin' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('login')
|
||||
|
|
@ -186,9 +179,7 @@ const permissionStore = usePermissionStore()
|
|||
const loginLoading = ref(false)
|
||||
const verify = ref()
|
||||
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 pictureWord 文字验证码
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
|
||||
|
||||
const LoginRules = {
|
||||
tenantName: [required],
|
||||
username: [required],
|
||||
|
|
@ -206,7 +197,6 @@ const loginData = reactive({
|
|||
rememberMe: false
|
||||
}
|
||||
})
|
||||
|
||||
// 获取验证码
|
||||
const getCode = async () => {
|
||||
// 情况一,未开启:则直接登录
|
||||
|
|
@ -239,13 +229,11 @@ const getCookie = () => {
|
|||
}
|
||||
}
|
||||
const loading = ref() // ElLoading.service 返回的实例
|
||||
|
||||
// tricky: 配合LoginForm.vue中redirectUri需要对参数进行encode,需要在回调后进行decode
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href))
|
||||
return url.searchParams.get(key) ?? ''
|
||||
}
|
||||
|
||||
// 尝试登录: 当账号已经绑定,socialLogin会直接获得token
|
||||
const tryLogin = async () => {
|
||||
try {
|
||||
|
|
@ -253,14 +241,11 @@ const tryLogin = async () => {
|
|||
const redirect = getUrlValue('redirect')
|
||||
const code = route?.query?.code as string
|
||||
const state = route?.query?.state as string
|
||||
|
||||
const res = await LoginApi.socialLogin(type, code, state)
|
||||
authUtil.setToken(res)
|
||||
|
||||
router.push({ path: redirect || '/' })
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
// 登录
|
||||
const handleLogin = async (params) => {
|
||||
loginLoading.value = true
|
||||
|
|
@ -270,13 +255,10 @@ const handleLogin = async (params) => {
|
|||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
let redirect = getUrlValue('redirect')
|
||||
|
||||
const type = getUrlValue('type')
|
||||
const code = route?.query?.code as string
|
||||
const state = route?.query?.state as string
|
||||
|
||||
const loginDataLoginForm = { ...loginData.loginForm }
|
||||
const res = await LoginApi.login({
|
||||
// 账号密码登录
|
||||
|
|
@ -316,19 +298,15 @@ const handleLogin = async (params) => {
|
|||
loading.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCookie()
|
||||
tryLogin()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$prefix-cls: #{$namespace}-login;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
overflow: auto;
|
||||
|
||||
&__left {
|
||||
&::before {
|
||||
position: absolute;
|
||||
|
|
@ -344,4 +322,4 @@ $prefix-cls: #{$namespace}-login;
|
|||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -120,9 +120,7 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
|
||||
import { sendSmsCode, smsResetPassword } from '@/api/login'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
|
||||
|
|
@ -131,7 +129,6 @@ import * as authUtil from '@/utils/auth'
|
|||
import * as LoginApi from '@/api/login'
|
||||
defineOptions({ name: 'ForgetPasswordForm' })
|
||||
const verify = ref()
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const { currentRoute } = useRouter()
|
||||
|
|
@ -144,7 +141,6 @@ const { validForm } = useFormValid(formSmsResetPassword)
|
|||
const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
|
||||
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 pictureWord 文字验证码
|
||||
|
||||
const validatePass2 = (_rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
|
|
@ -154,7 +150,6 @@ const validatePass2 = (_rule, value, callback) => {
|
|||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
tenantName: [{ required: true, min: 2, max: 20, trigger: 'blur', message: '长度为4到16位' }],
|
||||
mobile: [{ required: true, min: 11, max: 11, trigger: 'blur', message: '手机号长度为11位' }],
|
||||
|
|
@ -171,7 +166,6 @@ const rules = {
|
|||
check_password: [{ required: true, validator: validatePass2, trigger: 'blur' }],
|
||||
code: [required]
|
||||
}
|
||||
|
||||
const resetPasswordData = reactive({
|
||||
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
||||
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
||||
|
|
@ -182,7 +176,6 @@ const resetPasswordData = reactive({
|
|||
mobile: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const smsVO = reactive({
|
||||
tenantName: '',
|
||||
mobile: '',
|
||||
|
|
@ -191,7 +184,6 @@ const smsVO = reactive({
|
|||
})
|
||||
const mobileCodeTimer = ref(0)
|
||||
const redirect = ref<string>('')
|
||||
|
||||
// 获取验证码
|
||||
const getCode = async () => {
|
||||
// 情况一,未开启:则直接发送验证码
|
||||
|
|
@ -203,7 +195,6 @@ const getCode = async () => {
|
|||
verify.value.show()
|
||||
}
|
||||
}
|
||||
|
||||
const getSmsCode = async (params) => {
|
||||
if (resetPasswordData.tenantEnable === 'true') {
|
||||
await getTenantId()
|
||||
|
|
@ -231,7 +222,6 @@ watch(
|
|||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
const getTenantId = async () => {
|
||||
if (resetPasswordData.tenantEnable === 'true') {
|
||||
const res = await LoginApi.getTenantIdByName(resetPasswordData.tenantName)
|
||||
|
|
@ -242,7 +232,6 @@ const getTenantId = async () => {
|
|||
authUtil.setTenantId(res)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const resetPassword = async () => {
|
||||
const data = await validForm()
|
||||
|
|
@ -264,15 +253,13 @@ const resetPassword = async () => {
|
|||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.anticon) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.smsbtn {
|
||||
margin-top: 33px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -15,17 +15,6 @@
|
|||
<LoginFormTitle class="w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="px-10px">
|
||||
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.tenantName"
|
||||
:placeholder="t('login.tenantNamePlaceholder')"
|
||||
:prefix-icon="iconHouse"
|
||||
link
|
||||
type="primary"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="px-10px">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
|
|
@ -86,7 +75,7 @@
|
|||
mode="pop"
|
||||
@success="handleLogin"
|
||||
/>
|
||||
<el-col :span="24" class="px-10px">
|
||||
<el-col :span="24" class="px-10px hidden">
|
||||
<el-form-item>
|
||||
<el-row :gutter="5" justify="space-between" style="width: 100%">
|
||||
<el-col :span="8">
|
||||
|
|
@ -113,8 +102,8 @@
|
|||
</el-row>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
|
||||
<el-col :span="24" class="px-10px">
|
||||
<el-divider content-position="center" class="hidden">{{ t('login.otherLogin') }}</el-divider>
|
||||
<el-col :span="24" class="px-10px hidden">
|
||||
<el-form-item>
|
||||
<div class="w-full flex justify-between">
|
||||
<Icon
|
||||
|
|
@ -129,8 +118,8 @@
|
|||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-divider content-position="center">萌新必读</el-divider>
|
||||
<el-col :span="24" class="px-10px">
|
||||
<el-divider content-position="center" class="hidden">萌新必读</el-divider>
|
||||
<el-col :span="24" class="px-10px hidden">
|
||||
<el-form-item>
|
||||
<div class="w-full flex justify-between">
|
||||
<el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
|
||||
|
|
@ -151,19 +140,14 @@
|
|||
import { ElLoading } from 'element-plus'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
|
||||
import * as authUtil from '@/utils/auth'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import * as LoginApi from '@/api/login'
|
||||
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
|
||||
|
||||
defineOptions({ name: 'LoginForm' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const iconHouse = useIcon({ icon: 'ep:house' })
|
||||
const iconAvatar = useIcon({ icon: 'ep:avatar' })
|
||||
const iconLock = useIcon({ icon: 'ep:lock' })
|
||||
const formLogin = ref()
|
||||
|
|
@ -175,11 +159,8 @@ const redirect = ref<string>('')
|
|||
const loginLoading = ref(false)
|
||||
const verify = ref()
|
||||
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 pictureWord 文字验证码
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
|
||||
|
||||
const LoginRules = {
|
||||
tenantName: [required],
|
||||
username: [required],
|
||||
password: [required]
|
||||
}
|
||||
|
|
@ -188,21 +169,18 @@ const loginData = reactive({
|
|||
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
||||
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
||||
loginForm: {
|
||||
tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
|
||||
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
|
||||
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
|
||||
captchaVerification: '',
|
||||
rememberMe: true // 默认记录我。如果不需要,可手动修改
|
||||
}
|
||||
})
|
||||
|
||||
const socialList = [
|
||||
{ icon: 'ant-design:wechat-filled', type: 30 },
|
||||
{ icon: 'ant-design:dingtalk-circle-filled', type: 20 },
|
||||
{ icon: 'ant-design:github-filled', type: 0 },
|
||||
{ icon: 'ant-design:alipay-circle-filled', type: 0 }
|
||||
]
|
||||
|
||||
// 获取验证码
|
||||
const getCode = async () => {
|
||||
// 情况一,未开启:则直接登录
|
||||
|
|
@ -214,13 +192,6 @@ const getCode = async () => {
|
|||
verify.value.show()
|
||||
}
|
||||
}
|
||||
// 获取租户 ID
|
||||
const getTenantId = async () => {
|
||||
if (loginData.tenantEnable === 'true') {
|
||||
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
|
||||
authUtil.setTenantId(res)
|
||||
}
|
||||
}
|
||||
// 记住我
|
||||
const getLoginFormCache = () => {
|
||||
const loginForm = authUtil.getLoginForm()
|
||||
|
|
@ -229,19 +200,7 @@ const getLoginFormCache = () => {
|
|||
...loginData.loginForm,
|
||||
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
|
||||
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
|
||||
rememberMe: loginForm.rememberMe,
|
||||
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
|
||||
}
|
||||
}
|
||||
}
|
||||
// 根据域名,获得租户信息
|
||||
const getTenantByWebsite = async () => {
|
||||
if (loginData.tenantEnable === 'true') {
|
||||
const website = location.host
|
||||
const res = await LoginApi.getTenantByWebsite(website)
|
||||
if (res) {
|
||||
loginData.loginForm.tenantName = res.name
|
||||
authUtil.setTenantId(res.id)
|
||||
rememberMe: loginForm.rememberMe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -250,7 +209,6 @@ const loading = ref() // ElLoading.service 返回的实例
|
|||
const handleLogin = async (params: any) => {
|
||||
loginLoading.value = true
|
||||
try {
|
||||
await getTenantId()
|
||||
const data = await validForm()
|
||||
if (!data) {
|
||||
return
|
||||
|
|
@ -272,6 +230,10 @@ const handleLogin = async (params: any) => {
|
|||
authUtil.removeLoginForm()
|
||||
}
|
||||
authUtil.setToken(res)
|
||||
// 保存租户ID
|
||||
if (res.tenantId) {
|
||||
authUtil.setTenantId(res.tenantId)
|
||||
}
|
||||
if (!redirect.value) {
|
||||
redirect.value = '/'
|
||||
}
|
||||
|
|
@ -286,30 +248,12 @@ const handleLogin = async (params: any) => {
|
|||
loading.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 社交登录
|
||||
const doSocialLogin = async (type: number) => {
|
||||
if (type === 0) {
|
||||
message.error('此方式未配置')
|
||||
} else {
|
||||
loginLoading.value = true
|
||||
if (loginData.tenantEnable === 'true') {
|
||||
// 尝试先通过 tenantName 获取租户
|
||||
await getTenantId()
|
||||
// 如果获取不到,则需要弹出提示,进行处理
|
||||
if (!authUtil.getTenantId()) {
|
||||
try {
|
||||
const data = await message.prompt('请输入租户名称', t('common.reminder'))
|
||||
if (data?.action !== 'confirm') throw 'cancel'
|
||||
const res = await LoginApi.getTenantIdByName(data.value)
|
||||
authUtil.setTenantId(res)
|
||||
} catch (error) {
|
||||
if (error === 'cancel') return
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// 计算 redirectUri
|
||||
// 注意: type、redirect 需要先 encode 一次,否则钉钉回调会丢失。
|
||||
// 配合 social-login.vue#getUrlValue() 使用
|
||||
|
|
@ -317,7 +261,6 @@ const doSocialLogin = async (type: number) => {
|
|||
location.origin +
|
||||
'/social-login?' +
|
||||
encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`)
|
||||
|
||||
// 进行跳转
|
||||
window.location.href = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
|
||||
}
|
||||
|
|
@ -333,22 +276,18 @@ watch(
|
|||
)
|
||||
onMounted(() => {
|
||||
getLoginFormCache()
|
||||
getTenantByWebsite()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.anticon) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login-code {
|
||||
float: right;
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
|
@ -357,4 +296,4 @@ onMounted(() => {
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -5,13 +5,9 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
|
||||
defineOptions({ name: 'LoginFormTitle' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getLoginState } = useLoginState()
|
||||
|
||||
const getFormTitle = computed(() => {
|
||||
const titleObj = {
|
||||
[LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
|
||||
|
|
@ -23,4 +19,4 @@ const getFormTitle = computed(() => {
|
|||
}
|
||||
return titleObj[unref(getLoginState)]
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -16,17 +16,6 @@
|
|||
<LoginFormTitle class="w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="px-10px">
|
||||
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.tenantName"
|
||||
:placeholder="t('login.tenantNamePlaceholder')"
|
||||
:prefix-icon="iconHouse"
|
||||
type="primary"
|
||||
link
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 手机号 -->
|
||||
<el-col :span="24" class="px-10px">
|
||||
<el-form-item prop="mobileNumber">
|
||||
|
|
@ -94,33 +83,26 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
|
||||
import { setTenantId, setToken } from '@/utils/auth'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { getTenantIdByName, sendSmsCode, smsLogin } from '@/api/login'
|
||||
import { sendSmsCode, smsLogin } from '@/api/login'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
|
||||
import { ElLoading } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'MobileForm' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const permissionStore = usePermissionStore()
|
||||
const { currentRoute, push } = useRouter()
|
||||
const formSmsLogin = ref()
|
||||
const loginLoading = ref(false)
|
||||
const iconHouse = useIcon({ icon: 'ep:house' })
|
||||
const iconCellphone = useIcon({ icon: 'ep:cellphone' })
|
||||
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
|
||||
const { validForm } = useFormValid(formSmsLogin)
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
|
||||
|
||||
const rules = {
|
||||
tenantName: [required],
|
||||
mobileNumber: [required],
|
||||
code: [required]
|
||||
}
|
||||
|
|
@ -133,7 +115,6 @@ const loginData = reactive({
|
|||
},
|
||||
loginForm: {
|
||||
uuid: '',
|
||||
tenantName: '研筑科技',
|
||||
mobileNumber: '',
|
||||
code: ''
|
||||
}
|
||||
|
|
@ -151,7 +132,6 @@ const smsVO = reactive({
|
|||
const mobileCodeTimer = ref(0)
|
||||
const redirect = ref<string>('')
|
||||
const getSmsCode = async () => {
|
||||
await getTenantId()
|
||||
smsVO.smsCode.mobile = loginData.loginForm.mobileNumber
|
||||
await sendSmsCode(smsVO.smsCode).then(async () => {
|
||||
message.success(t('login.SmsSendMsg'))
|
||||
|
|
@ -174,16 +154,8 @@ watch(
|
|||
immediate: true
|
||||
}
|
||||
)
|
||||
// 获取租户 ID
|
||||
const getTenantId = async () => {
|
||||
if (loginData.tenantEnable === 'true') {
|
||||
const res = await getTenantIdByName(loginData.loginForm.tenantName)
|
||||
setTenantId(res)
|
||||
}
|
||||
}
|
||||
// 登录
|
||||
const signIn = async () => {
|
||||
await getTenantId()
|
||||
const data = await validForm()
|
||||
if (!data) return
|
||||
ElLoading.service({
|
||||
|
|
@ -197,6 +169,10 @@ const signIn = async () => {
|
|||
await smsLogin(smsVO.loginSms)
|
||||
.then(async (res) => {
|
||||
setToken(res)
|
||||
// 保存租户ID
|
||||
if (res.tenantId) {
|
||||
setTenantId(res.tenantId)
|
||||
}
|
||||
if (!redirect.value) {
|
||||
redirect.value = '/'
|
||||
}
|
||||
|
|
@ -212,15 +188,13 @@ const signIn = async () => {
|
|||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.anticon) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.smsbtn {
|
||||
margin-top: 33px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -18,13 +18,10 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
import logoImg from '@/assets/imgs/logo.png'
|
||||
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
|
||||
defineOptions({ name: 'QrCodeForm' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE)
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -105,9 +105,7 @@ import * as authUtil from '@/utils/auth'
|
|||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import * as LoginApi from '@/api/login'
|
||||
import { LoginStateEnum, useLoginState, useFormValid } from './useLogin'
|
||||
|
||||
defineOptions({ name: 'RegisterForm' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const iconHouse = useIcon({ icon: 'ep:house' })
|
||||
const iconAvatar = useIcon({ icon: 'ep:avatar' })
|
||||
|
|
@ -121,9 +119,7 @@ const redirect = ref<string>('')
|
|||
const loginLoading = ref(false)
|
||||
const verify = ref()
|
||||
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 pictureWord 文字验证码
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
|
||||
|
||||
const equalToPassword = (_rule, value, callback) => {
|
||||
if (registerData.registerForm.password !== value) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
|
|
@ -131,7 +127,6 @@ const equalToPassword = (_rule, value, callback) => {
|
|||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const registerRules = {
|
||||
tenantName: [
|
||||
{ required: true, trigger: 'blur', message: '请输入您所属的租户' },
|
||||
|
|
@ -155,7 +150,6 @@ const registerRules = {
|
|||
{ required: true, validator: equalToPassword, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const registerData = reactive({
|
||||
isShowPassword: false,
|
||||
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
||||
|
|
@ -170,7 +164,6 @@ const registerData = reactive({
|
|||
captchaVerification: ''
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ref() // ElLoading.service 返回的实例
|
||||
// 提交注册
|
||||
const handleRegister = async (params: any) => {
|
||||
|
|
@ -180,16 +173,13 @@ const handleRegister = async (params: any) => {
|
|||
await getTenantId()
|
||||
registerData.registerForm.tenantId = authUtil.getTenantId()
|
||||
}
|
||||
|
||||
if (registerData.captchaEnable) {
|
||||
registerData.registerForm.captchaVerification = params.captchaVerification
|
||||
}
|
||||
|
||||
const data = await validForm()
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
const res = await LoginApi.register(registerData.registerForm)
|
||||
if (!res) {
|
||||
return
|
||||
|
|
@ -199,9 +189,7 @@ const handleRegister = async (params: any) => {
|
|||
text: '正在加载系统中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
authUtil.removeLoginForm()
|
||||
|
||||
authUtil.setToken(res)
|
||||
if (!redirect.value) {
|
||||
redirect.value = '/'
|
||||
|
|
@ -217,7 +205,6 @@ const handleRegister = async (params: any) => {
|
|||
loading.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
const getCode = async () => {
|
||||
// 情况一,未开启:则直接注册
|
||||
|
|
@ -229,7 +216,6 @@ const getCode = async () => {
|
|||
verify.value.show()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取租户 ID
|
||||
const getTenantId = async () => {
|
||||
if (registerData.tenantEnable === 'true') {
|
||||
|
|
@ -237,7 +223,6 @@ const getTenantId = async () => {
|
|||
authUtil.setTenantId(res)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据域名,获得租户信息
|
||||
const getTenantByWebsite = async () => {
|
||||
if (registerData.tenantEnable === 'true') {
|
||||
|
|
@ -249,7 +234,6 @@ const getTenantByWebsite = async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
(route: RouteLocationNormalizedLoaded) => {
|
||||
|
|
@ -264,19 +248,16 @@ onMounted(() => {
|
|||
getTenantByWebsite()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.anticon) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login-code {
|
||||
float: right;
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
|
@ -285,4 +266,4 @@ onMounted(() => {
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -43,13 +43,10 @@ import LoginFormTitle from './LoginFormTitle.vue'
|
|||
import * as OAuth2Api from '@/api/login/oauth2'
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
defineOptions({ name: 'SSOLogin' })
|
||||
|
||||
const route = useRoute() // 路由
|
||||
const { currentRoute } = useRouter() // 路由
|
||||
const { getLoginState, setLoginState } = useLoginState()
|
||||
|
||||
const client = ref({
|
||||
// 客户端信息
|
||||
name: '',
|
||||
|
|
@ -78,7 +75,6 @@ const formData = reactive<formType>({
|
|||
scopes: [] // 已选中的 scope 数组
|
||||
})
|
||||
const formLoading = ref(false) // 表单是否提交中
|
||||
|
||||
/** 初始化授权信息 */
|
||||
const init = async () => {
|
||||
// 防止在没有登录的情况下循环弹窗
|
||||
|
|
@ -93,7 +89,6 @@ const init = async () => {
|
|||
if (route.query.scope) {
|
||||
queryParams.scopes = (route.query.scope as string).split(' ')
|
||||
}
|
||||
|
||||
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
|
||||
if (queryParams.scopes.length > 0) {
|
||||
const data = await doAuthorize(true, queryParams.scopes, [])
|
||||
|
|
@ -102,7 +97,6 @@ const init = async () => {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取授权页的基本信息
|
||||
const data = await OAuth2Api.getAuthorize(queryParams.clientId)
|
||||
client.value = data.client
|
||||
|
|
@ -130,7 +124,6 @@ const init = async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理授权的提交 */
|
||||
const handleAuthorize = async (approved) => {
|
||||
// 计算 checkedScopes + uncheckedScopes
|
||||
|
|
@ -157,7 +150,6 @@ const handleAuthorize = async (approved) => {
|
|||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 调用授权 API 接口 */
|
||||
const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => {
|
||||
return OAuth2Api.authorize(
|
||||
|
|
@ -170,7 +162,6 @@ const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => {
|
|||
uncheckedScopes
|
||||
)
|
||||
}
|
||||
|
||||
/** 格式化 scope 文本 */
|
||||
const formatScope = (scope) => {
|
||||
// 格式化 scope 授权范围,方便用户理解。
|
||||
|
|
@ -184,7 +175,6 @@ const formatScope = (scope) => {
|
|||
return scope
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听当前路由为 SSOLogin 时,进行数据的初始化 */
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
|
|
@ -196,4 +186,4 @@ watch(
|
|||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -28,12 +28,10 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components'
|
||||
|
||||
const { t } = useI18n()
|
||||
defineOptions({ name: 'Profile' })
|
||||
const activeName = ref('basicInfo')
|
||||
const profileUserRef = ref()
|
||||
|
||||
// 处理基本信息更新成功
|
||||
const handleBasicInfoSuccess = async () => {
|
||||
await profileUserRef.value?.refresh()
|
||||
|
|
@ -44,24 +42,20 @@ const handleBasicInfoSuccess = async () => {
|
|||
max-height: 960px;
|
||||
padding: 15px 20px 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-card .el-card__header, .el-card .el-card__body) {
|
||||
padding: 15px !important;
|
||||
}
|
||||
|
||||
.profile-tabs > .el-tabs__content {
|
||||
padding: 32px;
|
||||
font-weight: 600;
|
||||
color: #6b778c;
|
||||
}
|
||||
|
||||
.el-tabs--left .el-tabs__content {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -22,18 +22,14 @@ import {
|
|||
UserProfileUpdateReqVO
|
||||
} from '@/api/system/user/profile'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'BasicInfo' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage() // 消息弹窗
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
// 表单校验
|
||||
const rules = reactive<FormRules>({
|
||||
nickname: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }],
|
||||
|
|
@ -78,7 +74,6 @@ const schema = reactive<FormSchema[]>([
|
|||
}
|
||||
])
|
||||
const formRef = ref<FormExpose>() // 表单 Ref
|
||||
|
||||
// 监听 userStore 中头像的变化,同步更新表单数据
|
||||
watch(
|
||||
() => userStore.getUser.avatar,
|
||||
|
|
@ -92,7 +87,6 @@ watch(
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
const submit = () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
if (!elForm) return
|
||||
|
|
@ -108,14 +102,12 @@ const submit = () => {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
const res = await getUserProfile()
|
||||
unref(formRef)?.setValues(res)
|
||||
return res
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await init()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -50,20 +50,15 @@
|
|||
import { formatDate } from '@/utils/formatTime'
|
||||
import UserAvatar from './UserAvatar.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
|
||||
|
||||
defineOptions({ name: 'ProfileUser' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
const userInfo = ref({} as ProfileVO)
|
||||
|
||||
const getUserInfo = async () => {
|
||||
const users = await getUserProfile()
|
||||
userInfo.value = users
|
||||
}
|
||||
|
||||
// 监听 userStore 中头像的变化,同步更新本地 userInfo
|
||||
watch(
|
||||
() => userStore.getUser.avatar,
|
||||
|
|
@ -73,24 +68,20 @@ watch(
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 暴露刷新方法
|
||||
defineExpose({
|
||||
refresh: getUserInfo
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getUserInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-center {
|
||||
position: relative;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.list-group-striped > .list-group-item {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
|
|
@ -98,12 +89,10 @@ onMounted(async () => {
|
|||
border-left: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
padding: 11px 0;
|
||||
margin-bottom: -1px;
|
||||
|
|
@ -111,8 +100,7 @@ onMounted(async () => {
|
|||
border-top: 1px solid #e7eaec;
|
||||
border-bottom: 1px solid #e7eaec;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -17,12 +17,9 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
import { InputPassword } from '@/components/InputPassword'
|
||||
import { updateUserPassword } from '@/api/system/user/profile'
|
||||
|
||||
defineOptions({ name: 'ResetPwd' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
|
@ -31,7 +28,6 @@ const password = reactive({
|
|||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 表单校验
|
||||
const equalToPassword = (_rule, value, callback) => {
|
||||
if (password.newPassword !== value) {
|
||||
|
|
@ -40,7 +36,6 @@ const equalToPassword = (_rule, value, callback) => {
|
|||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
oldPassword: [
|
||||
{ required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' },
|
||||
|
|
@ -55,7 +50,6 @@ const rules = reactive<FormRules>({
|
|||
{ required: true, validator: equalToPassword, trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const submit = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate(async (valid) => {
|
||||
|
|
@ -65,9 +59,8 @@ const submit = (formEl: FormInstance | undefined) => {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
const reset = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -17,15 +17,11 @@ import { CropperAvatar } from '@/components/Cropper'
|
|||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useUpload } from '@/components/UploadFile/src/useUpload'
|
||||
import { UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
|
||||
|
||||
defineOptions({ name: 'UserAvatar' })
|
||||
|
||||
defineProps({
|
||||
img: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const cropperRef = ref()
|
||||
const handelUpload = async ({ data }) => {
|
||||
const { httpRequest } = useUpload()
|
||||
|
|
@ -36,13 +32,11 @@ const handelUpload = async ({ data }) => {
|
|||
} as UploadRequestOptions)) as unknown as { data: string }
|
||||
).data
|
||||
await updateUserProfile({ avatar })
|
||||
|
||||
// 关闭弹窗,并更新 userStore
|
||||
cropperRef.value.close()
|
||||
await userStore.setUserAvatarAction(avatar)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.change-avatar {
|
||||
img {
|
||||
|
|
@ -51,4 +45,4 @@ const handelUpload = async ({ data }) => {
|
|||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -25,14 +25,12 @@
|
|||
import { SystemUserSocialTypeEnum } from '@/utils/constants'
|
||||
import { getBindSocialUserList } from '@/api/system/social/user'
|
||||
import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
|
||||
|
||||
defineOptions({ name: 'UserSocial' })
|
||||
defineProps<{
|
||||
activeName: string
|
||||
}>()
|
||||
const message = useMessage()
|
||||
const socialUsers = ref<any[]>([])
|
||||
|
||||
const initSocial = async () => {
|
||||
socialUsers.value = [] // 重置避免无限增长
|
||||
// 获取已绑定的社交用户列表
|
||||
|
|
@ -68,13 +66,11 @@ const bindSocial = () => {
|
|||
emit('update:activeName', 'userSocial')
|
||||
})
|
||||
}
|
||||
|
||||
// 双层 encode 需要在回调后进行 decode
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href))
|
||||
return url.searchParams.get(key) ?? ''
|
||||
}
|
||||
|
||||
const bind = (row) => {
|
||||
// 双层 encode 解决钉钉回调 type 参数丢失的问题
|
||||
const redirectUri = location.origin + '/user/profile?' + encodeURIComponent(`type=${row.type}`)
|
||||
|
|
@ -90,11 +86,9 @@ const unbind = async (row) => {
|
|||
}
|
||||
message.success('解绑成功')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initSocial()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route,
|
||||
() => {
|
||||
|
|
@ -104,4 +98,4 @@ watch(
|
|||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -3,16 +3,12 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'Redirect' })
|
||||
|
||||
const { currentRoute, replace } = useRouter()
|
||||
const { params, query } = unref(currentRoute)
|
||||
const { path, _redirect_type = 'path' } = params
|
||||
|
||||
Reflect.deleteProperty(params, '_redirect_type')
|
||||
Reflect.deleteProperty(params, 'path')
|
||||
|
||||
const _path = Array.isArray(path) ? path.join('/') : path
|
||||
|
||||
if (_redirect_type === 'name') {
|
||||
replace({
|
||||
name: _path,
|
||||
|
|
@ -25,4 +21,4 @@ if (_redirect_type === 'name') {
|
|||
query
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -10,7 +10,6 @@
|
|||
<Icon icon="ep:plus" class="mr-5px" />
|
||||
新建对话
|
||||
</el-button>
|
||||
|
||||
<!-- 左顶部:搜索对话 -->
|
||||
<el-input
|
||||
v-model="searchName"
|
||||
|
|
@ -23,7 +22,6 @@
|
|||
<Icon icon="ep:search" />
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 左中间:对话列表 -->
|
||||
<div class="overflow-auto h-full">
|
||||
<!-- 情况一:加载中 -->
|
||||
|
|
@ -98,7 +96,6 @@
|
|||
<div class="h-160px w-100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左底部:工具栏 -->
|
||||
<div
|
||||
class="absolute bottom-0 left-0 right-0 px-5 leading-8.75 flex justify-between items-center"
|
||||
|
|
@ -125,22 +122,18 @@
|
|||
<el-text class="ml-1.25" size="small">清空未置顶对话</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色仓库抽屉 -->
|
||||
<el-drawer v-model="roleRepositoryOpen" title="角色仓库" size="754px">
|
||||
<RoleRepository />
|
||||
</el-drawer>
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
import RoleRepository from '../role/RoleRepository.vue'
|
||||
import { Bottom, Top } from '@element-plus/icons-vue'
|
||||
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 定义属性
|
||||
const searchName = ref<string>('') // 对话搜索
|
||||
const activeConversationId = ref<number | null>(null) // 选中的对话,默认为 null
|
||||
|
|
@ -149,7 +142,6 @@ const conversationList = ref([] as ChatConversationVO[]) // 对话列表
|
|||
const conversationMap = ref<any>({}) // 对话分组 (置顶、今天、三天前、一星期前、一个月前)
|
||||
const loading = ref<boolean>(false) // 加载中
|
||||
const loadingTime = ref<any>() // 加载中定时器
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
activeId: {
|
||||
|
|
@ -157,7 +149,6 @@ const props = defineProps({
|
|||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 定义钩子
|
||||
const emits = defineEmits([
|
||||
'onConversationCreate',
|
||||
|
|
@ -165,7 +156,6 @@ const emits = defineEmits([
|
|||
'onConversationClear',
|
||||
'onConversationDelete'
|
||||
])
|
||||
|
||||
/** 搜索对话 */
|
||||
const searchConversation = async (e) => {
|
||||
// 恢复数据
|
||||
|
|
@ -179,7 +169,6 @@ const searchConversation = async (e) => {
|
|||
conversationMap.value = await getConversationGroupByCreateTime(filterValues)
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击对话 */
|
||||
const handleConversationClick = async (id: number) => {
|
||||
// 过滤出选中的对话
|
||||
|
|
@ -194,7 +183,6 @@ const handleConversationClick = async (id: number) => {
|
|||
activeConversationId.value = id
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取对话列表 */
|
||||
const getChatConversationList = async () => {
|
||||
try {
|
||||
|
|
@ -202,7 +190,6 @@ const getChatConversationList = async () => {
|
|||
loadingTime.value = setTimeout(() => {
|
||||
loading.value = true
|
||||
}, 50)
|
||||
|
||||
// 1.1 获取 对话数据
|
||||
conversationList.value = await ChatConversationApi.getChatConversationMyList()
|
||||
// 1.2 排序
|
||||
|
|
@ -215,7 +202,6 @@ const getChatConversationList = async () => {
|
|||
conversationMap.value = {}
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 对话根据时间分组(置顶、今天、一天前、三天前、七天前、30 天前)
|
||||
conversationMap.value = await getConversationGroupByCreateTime(conversationList.value)
|
||||
} finally {
|
||||
|
|
@ -227,7 +213,6 @@ const getChatConversationList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 按照 creteTime 创建时间,进行分组 */
|
||||
const getConversationGroupByCreateTime = async (list: ChatConversationVO[]) => {
|
||||
// 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
|
||||
|
|
@ -270,7 +255,6 @@ const getConversationGroupByCreateTime = async (list: ChatConversationVO[]) => {
|
|||
}
|
||||
return groupMap
|
||||
}
|
||||
|
||||
/** 新建对话 */
|
||||
const createConversation = async () => {
|
||||
// 1. 新建对话
|
||||
|
|
@ -284,7 +268,6 @@ const createConversation = async () => {
|
|||
// 4. 回调
|
||||
emits('onConversationCreate')
|
||||
}
|
||||
|
||||
/** 修改对话的标题 */
|
||||
const updateConversationTitle = async (conversation: ChatConversationVO) => {
|
||||
// 1. 二次确认
|
||||
|
|
@ -312,7 +295,6 @@ const updateConversationTitle = async (conversation: ChatConversationVO) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除聊天对话 */
|
||||
const deleteChatConversation = async (conversation: ChatConversationVO) => {
|
||||
try {
|
||||
|
|
@ -327,7 +309,6 @@ const deleteChatConversation = async (conversation: ChatConversationVO) => {
|
|||
emits('onConversationDelete', conversation)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 清空对话 */
|
||||
const handleClearConversation = async () => {
|
||||
try {
|
||||
|
|
@ -345,7 +326,6 @@ const handleClearConversation = async () => {
|
|||
emits('onConversationClear')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 对话置顶 */
|
||||
const handleTop = async (conversation: ChatConversationVO) => {
|
||||
// 更新对话置顶
|
||||
|
|
@ -354,24 +334,19 @@ const handleTop = async (conversation: ChatConversationVO) => {
|
|||
// 刷新对话
|
||||
await getChatConversationList()
|
||||
}
|
||||
|
||||
// ============ 角色仓库 ============
|
||||
|
||||
/** 角色仓库抽屉 */
|
||||
const roleRepositoryOpen = ref<boolean>(false) // 角色仓库是否打开
|
||||
const handleRoleRepository = async () => {
|
||||
roleRepositoryOpen.value = !roleRepositoryOpen.value
|
||||
}
|
||||
|
||||
/** 监听选中的对话 */
|
||||
const { activeId } = toRefs(props)
|
||||
watch(activeId, async (newValue, oldValue) => {
|
||||
activeConversationId.value = newValue as string
|
||||
})
|
||||
|
||||
// 定义 public 方法
|
||||
defineExpose({ createConversation })
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取 对话列表
|
||||
|
|
@ -388,4 +363,4 @@ onMounted(async () => {
|
|||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -64,12 +64,9 @@
|
|||
import { ModelApi, ModelVO } from '@/api/ai/model/model'
|
||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
/** AI 聊天对话的更新表单 */
|
||||
defineOptions({ name: 'ChatConversationUpdateForm' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formData = ref({
|
||||
|
|
@ -89,7 +86,6 @@ const formRules = reactive({
|
|||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
const models = ref([] as ModelVO[]) // 聊天模型列表
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (id: number) => {
|
||||
dialogVisible.value = true
|
||||
|
|
@ -113,7 +109,6 @@ const open = async (id: number) => {
|
|||
models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
|
|
@ -132,7 +127,6 @@ const submitForm = async () => {
|
|||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
|
|
@ -145,4 +139,4 @@ const resetForm = () => {
|
|||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -23,7 +23,6 @@
|
|||
{{ fileList.length }}
|
||||
</span>
|
||||
</el-button>
|
||||
|
||||
<!-- 隐藏的文件输入框 -->
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
|
|
@ -33,7 +32,6 @@
|
|||
:accept="acceptTypes"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
|
||||
<!-- Hover 显示的文件列表 -->
|
||||
<div
|
||||
v-if="fileList.length > 0 && showTooltip"
|
||||
|
|
@ -82,11 +80,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUpload } from '@/components/UploadFile/src/useUpload'
|
||||
import { formatFileSize, getFileIcon } from '@/utils/file'
|
||||
|
||||
export interface FileItem {
|
||||
name: string
|
||||
size: number
|
||||
|
|
@ -95,9 +91,7 @@ export interface FileItem {
|
|||
progress?: number
|
||||
raw?: File
|
||||
}
|
||||
|
||||
defineOptions({ name: 'MessageFileUpload' })
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<string[]>,
|
||||
|
|
@ -120,9 +114,7 @@ const props = defineProps({
|
|||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'upload-success', 'upload-error'])
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
const fileList = ref<FileItem[]>([]) // 内部管理文件列表
|
||||
const uploadedUrls = ref<string[]>([]) // 已上传的 URL 列表
|
||||
|
|
@ -130,7 +122,6 @@ const showTooltip = ref(false) // 控制 tooltip 显示
|
|||
const hideTimer = ref<NodeJS.Timeout | null>(null) // 隐藏延迟定时器
|
||||
const message = useMessage()
|
||||
const { httpRequest } = useUpload()
|
||||
|
||||
/** 监听 v-model 变化 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
|
|
@ -143,12 +134,10 @@ watch(
|
|||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
/** 触发文件选择 */
|
||||
const triggerFileInput = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
/** 显示 tooltip */
|
||||
const showTooltipHandler = () => {
|
||||
if (hideTimer.value) {
|
||||
|
|
@ -157,7 +146,6 @@ const showTooltipHandler = () => {
|
|||
}
|
||||
showTooltip.value = true
|
||||
}
|
||||
|
||||
/** 隐藏 tooltip */
|
||||
const hideTooltipHandler = () => {
|
||||
hideTimer.value = setTimeout(() => {
|
||||
|
|
@ -165,7 +153,6 @@ const hideTooltipHandler = () => {
|
|||
hideTimer.value = null
|
||||
}, 300) // 300ms 延迟隐藏
|
||||
}
|
||||
|
||||
/** 处理文件选择 */
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
|
|
@ -196,11 +183,9 @@ const handleFileSelect = (event: Event) => {
|
|||
// 立即开始上传
|
||||
uploadFile(fileItem)
|
||||
})
|
||||
|
||||
// 清空 input 值,允许重复选择相同文件
|
||||
target.value = ''
|
||||
}
|
||||
|
||||
/** 上传文件 */
|
||||
const uploadFile = async (fileItem: FileItem) => {
|
||||
try {
|
||||
|
|
@ -210,7 +195,6 @@ const uploadFile = async (fileItem: FileItem) => {
|
|||
fileItem.progress = (fileItem.progress || 0) + Math.random() * 10
|
||||
}
|
||||
}, 100)
|
||||
|
||||
// 调用上传接口
|
||||
// const formData = new FormData()
|
||||
// formData.append('file', fileItem.raw!)
|
||||
|
|
@ -223,16 +207,13 @@ const uploadFile = async (fileItem: FileItem) => {
|
|||
fileItem.url = (response as any).data
|
||||
// 添加到 URL 列表
|
||||
uploadedUrls.value.push(fileItem.url!)
|
||||
|
||||
clearInterval(progressInterval)
|
||||
|
||||
emit('upload-success', fileItem)
|
||||
updateModelValue()
|
||||
} catch (error) {
|
||||
fileItem.uploading = false
|
||||
message.error(`文件 ${fileItem.name} 上传失败`)
|
||||
emit('upload-error', error)
|
||||
|
||||
// 移除上传失败的文件
|
||||
const index = fileList.value.indexOf(fileItem)
|
||||
if (index > -1) {
|
||||
|
|
@ -240,7 +221,6 @@ const uploadFile = async (fileItem: FileItem) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除文件 */
|
||||
const removeFile = (index: number) => {
|
||||
// 从 URL 列表中移除
|
||||
|
|
@ -252,15 +232,12 @@ const removeFile = (index: number) => {
|
|||
uploadedUrls.value.splice(urlIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
updateModelValue()
|
||||
}
|
||||
|
||||
/** 更新 v-model */
|
||||
const updateModelValue = () => {
|
||||
emit('update:modelValue', [...uploadedUrls.value])
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
triggerFileInput,
|
||||
|
|
@ -270,7 +247,6 @@ defineExpose({
|
|||
updateModelValue()
|
||||
}
|
||||
})
|
||||
|
||||
// 组件销毁时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (hideTimer.value) {
|
||||
|
|
@ -278,7 +254,6 @@ onUnmounted(() => {
|
|||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 上传按钮样式 */
|
||||
.upload-btn {
|
||||
|
|
@ -288,12 +263,10 @@ onUnmounted(() => {
|
|||
--el-button-hover-border-color: transparent;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.upload-btn.has-files {
|
||||
color: var(--el-color-primary);
|
||||
--el-button-hover-bg-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.file-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
|
|
@ -309,7 +282,6 @@ onUnmounted(() => {
|
|||
padding: 8px;
|
||||
animation: fadeInDown 0.2s ease;
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
|
|
@ -321,7 +293,6 @@ onUnmounted(() => {
|
|||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
/* Tooltip 箭头伪元素 */
|
||||
.tooltip-arrow::after {
|
||||
content: '';
|
||||
|
|
@ -334,7 +305,6 @@ onUnmounted(() => {
|
|||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid white;
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
@ -345,7 +315,6 @@ onUnmounted(() => {
|
|||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
@ -356,21 +325,17 @@ onUnmounted(() => {
|
|||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.file-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.file-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.file-list::-webkit-scrollbar-thumb {
|
||||
background: var(--el-border-color-light);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.file-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--el-border-color);
|
||||
}
|
||||
|
|
@ -378,17 +343,14 @@ onUnmounted(() => {
|
|||
.file-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.file-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.file-list::-webkit-scrollbar-thumb {
|
||||
background: var(--el-border-color-light);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.file-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--el-border-color);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -27,16 +27,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getFileIcon, getFileNameFromUrl, isImage } from '@/utils/file'
|
||||
|
||||
defineOptions({ name: 'MessageFiles' })
|
||||
|
||||
defineProps<{
|
||||
attachmentUrls?: string[]
|
||||
}>()
|
||||
|
||||
/** 获取文件类型样式类 */
|
||||
const getFileTypeClass = (filename: string): string => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase() || ''
|
||||
|
|
@ -58,9 +54,8 @@ const getFileTypeClass = (filename: string): string => {
|
|||
return 'bg-gradient-to-br from-gray-5 to-gray-7'
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击文件 */
|
||||
const handleFileClick = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -19,7 +19,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 知识引用详情弹窗 -->
|
||||
<el-popover
|
||||
v-model:visible="dialogVisible"
|
||||
|
|
@ -53,7 +52,6 @@
|
|||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
segments: {
|
||||
|
|
@ -63,7 +61,6 @@ const props = defineProps<{
|
|||
content: string
|
||||
}[]
|
||||
}>()
|
||||
|
||||
const document = ref<{
|
||||
id: number
|
||||
title: string
|
||||
|
|
@ -74,11 +71,9 @@ const document = ref<{
|
|||
} | null>(null) // 知识库文档列表
|
||||
const dialogVisible = ref(false) // 知识引用详情弹窗
|
||||
const documentRef = ref<HTMLElement>() // 知识引用详情弹窗 Ref
|
||||
|
||||
/** 按照 document 聚合 segments */
|
||||
const documentList = computed(() => {
|
||||
if (!props.segments) return []
|
||||
|
||||
const docMap = new Map()
|
||||
props.segments.forEach((segment) => {
|
||||
if (!docMap.has(segment.documentId)) {
|
||||
|
|
@ -95,10 +90,9 @@ const documentList = computed(() => {
|
|||
})
|
||||
return Array.from(docMap.values())
|
||||
})
|
||||
|
||||
/** 点击 document 处理 */
|
||||
const handleClick = (doc: any) => {
|
||||
document.value = doc
|
||||
dialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -124,18 +124,14 @@ import { ChatConversationVO } from '@/api/ai/chat/conversation'
|
|||
import { useUserStore } from '@/store/modules/user'
|
||||
import userAvatarDefaultImg from '@/assets/imgs/avatar.gif'
|
||||
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { copy } = useClipboard({ legacy: true }) // 初始化 copy 到粘贴板
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方)
|
||||
const messageContainer: any = ref(null)
|
||||
const isScrolling = ref(false) //用于判断用户是否在滚动
|
||||
|
||||
const userAvatar = computed(() => userStore.user.avatar || userAvatarDefaultImg)
|
||||
const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg)
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
conversation: {
|
||||
|
|
@ -147,13 +143,9 @@ const props = defineProps({
|
|||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const { list } = toRefs(props) // 消息列表
|
||||
|
||||
const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']) // 定义 emits
|
||||
|
||||
// ============ 处理对话滚动 ==============
|
||||
|
||||
/** 滚动到底部 */
|
||||
const scrollToBottom = async (isIgnore?: boolean) => {
|
||||
// 注意要使用 nextTick 以免获取不到 dom
|
||||
|
|
@ -163,7 +155,6 @@ const scrollToBottom = async (isIgnore?: boolean) => {
|
|||
messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
const scrollContainer = messageContainer.value
|
||||
const scrollTop = scrollContainer.scrollTop
|
||||
|
|
@ -177,29 +168,23 @@ function handleScroll() {
|
|||
isScrolling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 回到底部 */
|
||||
const handleGoBottom = async () => {
|
||||
const scrollContainer = messageContainer.value
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight
|
||||
}
|
||||
|
||||
/** 回到顶部 */
|
||||
const handlerGoTop = async () => {
|
||||
const scrollContainer = messageContainer.value
|
||||
scrollContainer.scrollTop = 0
|
||||
}
|
||||
|
||||
defineExpose({ scrollToBottom, handlerGoTop }) // 提供方法给 parent 调用
|
||||
|
||||
// ============ 处理消息操作 ==============
|
||||
|
||||
/** 复制 */
|
||||
const copyContent = async (content: string) => {
|
||||
await copy(content)
|
||||
message.success('复制成功!')
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
const onDelete = async (id) => {
|
||||
// 删除 message
|
||||
|
|
@ -208,19 +193,16 @@ const onDelete = async (id) => {
|
|||
// 回调
|
||||
emits('onDeleteSuccess')
|
||||
}
|
||||
|
||||
/** 刷新 */
|
||||
const onRefresh = async (message: ChatMessageVO) => {
|
||||
emits('onRefresh', message)
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
const onEdit = async (message: ChatMessageVO) => {
|
||||
emits('onEdit', message)
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
messageContainer.value.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -26,11 +26,9 @@ const promptList = [
|
|||
prompt: '写一首好听的诗歌?'
|
||||
}
|
||||
] // prompt 列表
|
||||
|
||||
const emits = defineEmits(['onPrompt'])
|
||||
|
||||
/** 选中 prompt 点击 */
|
||||
const handlerPromptClick = async ({ prompt }) => {
|
||||
emits('onPrompt', prompt)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -3,4 +3,4 @@
|
|||
<div class="p-30px">
|
||||
<el-skeleton animated />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
|
@ -11,9 +11,8 @@
|
|||
</template>
|
||||
<script setup lang="ts">
|
||||
const emits = defineEmits(['onNewConversation'])
|
||||
|
||||
/** 新建 conversation 聊天对话 */
|
||||
const handlerNewChat = () => {
|
||||
emits('onNewConversation')
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -19,7 +19,6 @@
|
|||
<ArrowDown />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 推理内容区域 -->
|
||||
<div
|
||||
v-show="isExpanded"
|
||||
|
|
@ -33,25 +32,20 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ArrowDown, ChatDotSquare } from '@element-plus/icons-vue'
|
||||
import MarkdownView from '@/components/MarkdownView/index.vue'
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps<{
|
||||
reasoningContent?: string
|
||||
content?: string
|
||||
}>()
|
||||
|
||||
const isExpanded = ref(true) // 默认展开
|
||||
|
||||
/** 计算属性:判断是否应该显示组件(有思考内容时,则展示) */
|
||||
const shouldShowComponent = computed(() => {
|
||||
return !(!props.reasoningContent || props.reasoningContent.trim() === '')
|
||||
})
|
||||
|
||||
/** 计算属性:标题文本 */
|
||||
const titleText = computed(() => {
|
||||
const hasReasoningContent = props.reasoningContent && props.reasoningContent.trim() !== ''
|
||||
|
|
@ -61,29 +55,24 @@ const titleText = computed(() => {
|
|||
}
|
||||
return '已深度思考'
|
||||
})
|
||||
|
||||
/** 切换展开/收缩状态 */
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条样式 */
|
||||
.max-h-300px::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.max-h-300px::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.max-h-300px::-webkit-scrollbar-thumb {
|
||||
background: rgba(156, 163, 175, 0.4);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.max-h-300px::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(156, 163, 175, 0.6);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -19,7 +19,6 @@
|
|||
class="text-12px transition-transform duration-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 可展开的搜索结果列表 -->
|
||||
<div v-show="isExpanded" class="flex flex-col gap-8px transition-all duration-200 ease-in-out">
|
||||
<div
|
||||
|
|
@ -40,24 +39,20 @@
|
|||
/>
|
||||
<Icon v-else icon="ep:link" class="w-full h-full text-[#666]" />
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- 标题和来源 -->
|
||||
<div class="flex items-center gap-4px mb-4px">
|
||||
<span class="text-12px text-[#999] truncate">{{ result.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 主标题 -->
|
||||
<div class="text-14px text-[#1a73e8] font-medium mb-4px line-clamp-2 leading-[1.4]">
|
||||
{{ result.title }}
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div class="text-13px text-[#666] line-clamp-2 leading-[1.4] mb-4px">
|
||||
{{ result.snippet }}
|
||||
</div>
|
||||
|
||||
<!-- URL -->
|
||||
<div class="text-12px text-[#006621] truncate">
|
||||
{{ result.url }}
|
||||
|
|
@ -67,7 +62,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 联网搜索详情弹窗 -->
|
||||
<el-popover
|
||||
v-model:visible="dialogVisible"
|
||||
|
|
@ -102,7 +96,6 @@
|
|||
<div class="text-12px text-[#006621] break-all">{{ selectedResult.url }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="max-h-[60vh] overflow-y-auto">
|
||||
<!-- 简短描述 -->
|
||||
|
|
@ -112,7 +105,6 @@
|
|||
{{ selectedResult.snippet }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容摘要 -->
|
||||
<div v-if="selectedResult.summary">
|
||||
<div class="text-14px font-medium text-[#333] mb-6px">内容摘要</div>
|
||||
|
|
@ -123,7 +115,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-end gap-8px mt-12px pt-12px border-t border-[#eee]">
|
||||
<el-button size="small" @click="dialogVisible = false">关闭</el-button>
|
||||
|
|
@ -135,7 +126,6 @@
|
|||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
webSearchPages: {
|
||||
|
|
@ -147,7 +137,6 @@ defineProps<{
|
|||
summary: string // 内容的文本摘要
|
||||
}[]
|
||||
}>()
|
||||
|
||||
const isExpanded = ref(false) // 是否展开搜索结果
|
||||
const selectedResult = ref<{
|
||||
name: string
|
||||
|
|
@ -159,32 +148,27 @@ const selectedResult = ref<{
|
|||
} | null>(null) // 选中的搜索结果
|
||||
const dialogVisible = ref(false) // 详情弹窗
|
||||
const resultRef = ref<HTMLElement>() // 详情弹窗 Ref
|
||||
|
||||
/** 切换展开/收起状态 */
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
/** 点击搜索结果处理 */
|
||||
const handleClick = (result: any) => {
|
||||
selectedResult.value = result
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 处理图片加载错误 */
|
||||
const handleImageError = (event: Event) => {
|
||||
const img = event.target as HTMLImageElement
|
||||
img.style.display = 'none'
|
||||
}
|
||||
|
||||
/** 打开URL */
|
||||
const openUrl = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.web-search-popover {
|
||||
max-width: 600px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
</template>
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
|
||||
// 定义属性
|
||||
defineProps({
|
||||
categoryList: {
|
||||
|
|
@ -28,12 +27,10 @@ defineProps({
|
|||
default: '全部'
|
||||
}
|
||||
})
|
||||
|
||||
// 定义回调
|
||||
const emits = defineEmits(['onCategoryClick'])
|
||||
|
||||
/** 处理分类点击事件 */
|
||||
const handleCategoryClick = async (category: string) => {
|
||||
emits('onCategoryClick', category)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -50,14 +50,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChatRoleVO } from '@/api/ai/model/chatRole'
|
||||
import { PropType, ref } from 'vue'
|
||||
import { More } from '@element-plus/icons-vue'
|
||||
|
||||
const tabsRef = ref<any>() // tabs ref
|
||||
|
||||
// 定义属性
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
|
|
@ -74,10 +71,8 @@ const props = defineProps({
|
|||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// 定义钩子
|
||||
const emits = defineEmits(['onDelete', 'onEdit', 'onUse', 'onPage'])
|
||||
|
||||
/** 操作:编辑、删除 */
|
||||
const handleMoreClick = async (data) => {
|
||||
const type = data[0]
|
||||
|
|
@ -88,12 +83,10 @@ const handleMoreClick = async (data) => {
|
|||
emits('onEdit', role)
|
||||
}
|
||||
}
|
||||
|
||||
/** 选中 */
|
||||
const handleUseClick = (role: any) => {
|
||||
emits('onUse', role)
|
||||
}
|
||||
|
||||
/** 滚动 */
|
||||
const handleTabsScroll = async () => {
|
||||
if (tabsRef.value) {
|
||||
|
|
@ -103,4 +96,4 @@ const handleTabsScroll = async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -60,7 +60,6 @@
|
|||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import RoleList from './RoleList.vue'
|
||||
|
|
@ -71,11 +70,9 @@ import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversat
|
|||
import { Search } from '@element-plus/icons-vue'
|
||||
import { TabsPaneContext } from 'element-plus'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
|
||||
const router = useRouter() // 路由对象
|
||||
const { currentRoute } = useRouter() // 路由
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
|
||||
// 属性定义
|
||||
const loading = ref<boolean>(false) // 加载中
|
||||
const activeTab = ref<string>('my-role') // 选中的角色 Tab
|
||||
|
|
@ -92,7 +89,6 @@ const publicRoleParams = reactive({
|
|||
const publicRoleList = ref<ChatRoleVO[]>([]) // public 分页大小
|
||||
const activeCategory = ref<string>('全部') // 选择中的分类
|
||||
const categoryList = ref<string[]>([]) // 角色分类类别
|
||||
|
||||
/** tabs 点击 */
|
||||
const handleTabsClick = async (tab: TabsPaneContext) => {
|
||||
// 设置切换状态
|
||||
|
|
@ -100,7 +96,6 @@ const handleTabsClick = async (tab: TabsPaneContext) => {
|
|||
// 切换的时候重新加载数据
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
/** 获取 my role 我的角色 */
|
||||
const getMyRole = async (append?: boolean) => {
|
||||
const params: ChatRolePageReqVO = {
|
||||
|
|
@ -115,7 +110,6 @@ const getMyRole = async (append?: boolean) => {
|
|||
myRoleList.value = list
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取 public role 公共角色 */
|
||||
const getPublicRole = async (append?: boolean) => {
|
||||
const params: ChatRolePageReqVO = {
|
||||
|
|
@ -131,7 +125,6 @@ const getPublicRole = async (append?: boolean) => {
|
|||
publicRoleList.value = list
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取选中的 tabs 角色 */
|
||||
const getActiveTabsRole = async () => {
|
||||
if (activeTab.value === 'my-role') {
|
||||
|
|
@ -142,12 +135,10 @@ const getActiveTabsRole = async () => {
|
|||
await getPublicRole()
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取角色分类列表 */
|
||||
const getRoleCategoryList = async () => {
|
||||
categoryList.value = ['全部', ...(await ChatRoleApi.getCategoryList())]
|
||||
}
|
||||
|
||||
/** 处理分类点击 */
|
||||
const handlerCategoryClick = async (category: string) => {
|
||||
// 切换选择的分类
|
||||
|
|
@ -155,7 +146,6 @@ const handlerCategoryClick = async (category: string) => {
|
|||
// 筛选
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const handlerAddRole = async () => {
|
||||
|
|
@ -165,20 +155,17 @@ const handlerAddRole = async () => {
|
|||
const handlerCardEdit = async (role) => {
|
||||
formRef.value.open('my-update', role.id, '编辑角色')
|
||||
}
|
||||
|
||||
/** 添加角色成功 */
|
||||
const handlerAddRoleSuccess = async (e) => {
|
||||
// 刷新数据
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
/** 删除角色 */
|
||||
const handlerCardDelete = async (role) => {
|
||||
await ChatRoleApi.deleteMy(role.id)
|
||||
// 刷新数据
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
/** 角色分页:获取下一页 */
|
||||
const handlerCardPage = async (type) => {
|
||||
try {
|
||||
|
|
@ -194,7 +181,6 @@ const handlerCardPage = async (type) => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择 card 角色:新建聊天对话 */
|
||||
const handlerCardUse = async (role) => {
|
||||
// 1. 创建对话
|
||||
|
|
@ -202,7 +188,6 @@ const handlerCardUse = async (role) => {
|
|||
roleId: role.id
|
||||
} as unknown as ChatConversationVO
|
||||
const conversationId = await ChatConversationApi.createChatConversationMy(data)
|
||||
|
||||
// 2. 跳转页面
|
||||
delView(unref(currentRoute))
|
||||
await router.replace({
|
||||
|
|
@ -212,7 +197,6 @@ const handlerCardUse = async (role) => {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
// 获取分类
|
||||
|
|
@ -226,21 +210,17 @@ onMounted(async () => {
|
|||
.el-tabs__nav-scroll {
|
||||
margin: 2px 8px !important;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.el-tab-pane {
|
||||
padding: 8px 0 0 0 !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -37,7 +37,6 @@
|
|||
</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- main:消息列表 -->
|
||||
<el-main class="m-0 p-0 relative h-full w-full">
|
||||
<div>
|
||||
|
|
@ -67,7 +66,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
|
||||
<!-- 底部 -->
|
||||
<el-footer class="flex flex-col !h-auto !p-0">
|
||||
<!-- TODO @芋艿:这块要想办法迁移下! -->
|
||||
|
|
@ -114,7 +112,6 @@
|
|||
</form>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
|
||||
<!-- 更新对话 Form -->
|
||||
<ConversationUpdateForm
|
||||
ref="conversationUpdateFormRef"
|
||||
|
|
@ -122,7 +119,6 @@
|
|||
/>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
|
||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
|
|
@ -133,19 +129,15 @@ import MessageListEmpty from './components/message/MessageListEmpty.vue'
|
|||
import MessageLoading from './components/message/MessageLoading.vue'
|
||||
import MessageNewConversation from './components/message/MessageNewConversation.vue'
|
||||
import MessageFileUpload from './components/message/MessageFileUpload.vue'
|
||||
|
||||
/** AI 聊天对话 列表 */
|
||||
defineOptions({ name: 'AiChat' })
|
||||
|
||||
const route = useRoute() // 路由
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 聊天对话
|
||||
const conversationListRef = ref()
|
||||
const activeConversationId = ref<number | null>(null) // 选中的对话编号
|
||||
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation
|
||||
const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
|
||||
|
||||
// 消息列表
|
||||
const messageRef = ref()
|
||||
const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列表
|
||||
|
|
@ -154,7 +146,6 @@ const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Tim
|
|||
// 消息滚动
|
||||
const textSpeed = ref<number>(50) // Typing speed in milliseconds
|
||||
const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds
|
||||
|
||||
// 发送消息输入框
|
||||
const isComposing = ref(false) // 判断用户是否在输入
|
||||
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
|
||||
|
|
@ -166,9 +157,7 @@ const uploadFiles = ref<string[]>([]) // 上传的文件 URL 列表
|
|||
// 接收 Stream 消息
|
||||
const receiveMessageFullText = ref('')
|
||||
const receiveMessageDisplayedText = ref('')
|
||||
|
||||
// =========== 【聊天对话】相关 ===========
|
||||
|
||||
/** 获取对话信息 */
|
||||
const getConversation = async (id: number | null) => {
|
||||
if (!id) {
|
||||
|
|
@ -181,7 +170,6 @@ const getConversation = async (id: number | null) => {
|
|||
activeConversation.value = conversation
|
||||
activeConversationId.value = conversation.id
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击某个对话
|
||||
*
|
||||
|
|
@ -194,7 +182,6 @@ const handleConversationClick = async (conversation: ChatConversationVO) => {
|
|||
message.alert('对话中,不允许切换!')
|
||||
return false
|
||||
}
|
||||
|
||||
// 更新选中的对话 id
|
||||
activeConversationId.value = conversation.id
|
||||
activeConversation.value = conversation
|
||||
|
|
@ -208,7 +195,6 @@ const handleConversationClick = async (conversation: ChatConversationVO) => {
|
|||
uploadFiles.value = []
|
||||
return true
|
||||
}
|
||||
|
||||
/** 删除某个对话*/
|
||||
const handlerConversationDelete = async (delConversation: ChatConversationVO) => {
|
||||
// 删除的对话如果是当前选中的,那么就重置
|
||||
|
|
@ -227,7 +213,6 @@ const handleConversationClear = async () => {
|
|||
activeConversation.value = null
|
||||
activeMessageList.value = []
|
||||
}
|
||||
|
||||
/** 修改聊天对话 */
|
||||
const conversationUpdateFormRef = ref()
|
||||
const openChatConversationUpdateForm = async () => {
|
||||
|
|
@ -237,7 +222,6 @@ const handleConversationUpdateSuccess = async () => {
|
|||
// 对话更新成功,刷新最新信息
|
||||
await getConversation(activeConversationId.value)
|
||||
}
|
||||
|
||||
/** 处理聊天对话的创建成功 */
|
||||
const handleConversationCreate = async () => {
|
||||
// 创建对话
|
||||
|
|
@ -250,9 +234,7 @@ const handleConversationCreateSuccess = async () => {
|
|||
// 清空文件列表
|
||||
uploadFiles.value = []
|
||||
}
|
||||
|
||||
// =========== 【消息列表】相关 ===========
|
||||
|
||||
/** 获取消息 message 列表 */
|
||||
const getMessageList = async () => {
|
||||
try {
|
||||
|
|
@ -263,12 +245,10 @@ const getMessageList = async () => {
|
|||
activeMessageListLoadingTimer.value = setTimeout(() => {
|
||||
activeMessageListLoading.value = true
|
||||
}, 60)
|
||||
|
||||
// 获取消息列表
|
||||
activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId(
|
||||
activeConversationId.value
|
||||
)
|
||||
|
||||
// 滚动到最下面
|
||||
await nextTick()
|
||||
await scrollToBottom()
|
||||
|
|
@ -281,7 +261,6 @@ const getMessageList = async () => {
|
|||
activeMessageListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息列表
|
||||
*
|
||||
|
|
@ -312,7 +291,6 @@ const messageList = computed(() => {
|
|||
}
|
||||
return []
|
||||
})
|
||||
|
||||
/** 处理删除 message 消息 */
|
||||
const handleMessageDelete = () => {
|
||||
if (conversationInProgress.value) {
|
||||
|
|
@ -322,7 +300,6 @@ const handleMessageDelete = () => {
|
|||
// 刷新 message 列表
|
||||
getMessageList()
|
||||
}
|
||||
|
||||
/** 处理 message 清空 */
|
||||
const handlerMessageClear = async () => {
|
||||
if (!activeConversationId.value) {
|
||||
|
|
@ -337,14 +314,11 @@ const handlerMessageClear = async () => {
|
|||
activeMessageList.value = []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 回到 message 列表的顶部 */
|
||||
const handleGoTopMessage = () => {
|
||||
messageRef.value.handlerGoTop()
|
||||
}
|
||||
|
||||
// =========== 【发送消息】相关 ===========
|
||||
|
||||
/** 处理来自 keydown 的发送消息 */
|
||||
const handleSendByKeydown = async (event) => {
|
||||
// 判断用户是否在输入
|
||||
|
|
@ -368,12 +342,10 @@ const handleSendByKeydown = async (event) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理来自【发送】按钮的发送消息 */
|
||||
const handleSendByButton = () => {
|
||||
doSendMessage(prompt.value?.trim() as string)
|
||||
}
|
||||
|
||||
/** 处理 prompt 输入变化 */
|
||||
const handlePromptInput = (event) => {
|
||||
// 非输入法 输入设置为 true
|
||||
|
|
@ -403,7 +375,6 @@ const onCompositionend = () => {
|
|||
isComposing.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
/** 真正执行【发送】消息操作 */
|
||||
const doSendMessage = async (content: string) => {
|
||||
// 校验
|
||||
|
|
@ -415,14 +386,11 @@ const doSendMessage = async (content: string) => {
|
|||
message.error('还没创建对话,不能发送!')
|
||||
return
|
||||
}
|
||||
|
||||
// 准备附件 URL 数组
|
||||
const attachmentUrls = [...uploadFiles.value]
|
||||
|
||||
// 清空输入框和文件列表
|
||||
prompt.value = ''
|
||||
uploadFiles.value = []
|
||||
|
||||
// 执行发送
|
||||
await doSendMessageStream({
|
||||
conversationId: activeConversationId.value,
|
||||
|
|
@ -430,7 +398,6 @@ const doSendMessage = async (content: string) => {
|
|||
attachmentUrls: attachmentUrls
|
||||
} as ChatMessageVO)
|
||||
}
|
||||
|
||||
/** 真正执行【发送】消息操作 */
|
||||
const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
||||
// 创建 AbortController 实例,以便中止请求
|
||||
|
|
@ -439,7 +406,6 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
conversationInProgress.value = true
|
||||
// 设置为空
|
||||
receiveMessageFullText.value = ''
|
||||
|
||||
try {
|
||||
// 1.1 先添加两个假数据,等 stream 返回再替换
|
||||
activeMessageList.value.push({
|
||||
|
|
@ -463,7 +429,6 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
await scrollToBottom() // 底部
|
||||
// 1.3 开始滚动
|
||||
textRoll()
|
||||
|
||||
// 2. 发送 event stream
|
||||
let isFirstChunk = true // 是否是第一个 chunk 消息段
|
||||
await ChatMessageApi.sendChatMessageStream(
|
||||
|
|
@ -482,12 +447,10 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果内容为空,就不处理。
|
||||
if (data.receive.content === '' && !data.receive.reasoningContent) {
|
||||
return
|
||||
}
|
||||
|
||||
// 首次返回需要添加一个 message 到页面,后面的都是更新
|
||||
if (isFirstChunk) {
|
||||
isFirstChunk = false
|
||||
|
|
@ -499,14 +462,12 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
data.send.attachmentUrls = userMessage.attachmentUrls
|
||||
activeMessageList.value.push(data.receive)
|
||||
}
|
||||
|
||||
// 处理 reasoningContent
|
||||
if (data.receive.reasoningContent) {
|
||||
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
|
||||
lastMessage.reasoningContent =
|
||||
lastMessage.reasoningContent + data.receive.reasoningContent
|
||||
}
|
||||
|
||||
// 处理正常内容
|
||||
if (data.receive.content !== '') {
|
||||
receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
|
||||
|
|
@ -528,7 +489,6 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 停止 stream 流式调用 */
|
||||
const stopStream = async () => {
|
||||
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
||||
|
|
@ -538,19 +498,15 @@ const stopStream = async () => {
|
|||
// 设置为 false
|
||||
conversationInProgress.value = false
|
||||
}
|
||||
|
||||
/** 编辑 message:设置为 prompt,可以再次编辑 */
|
||||
const handleMessageEdit = (message: ChatMessageVO) => {
|
||||
prompt.value = message.content
|
||||
}
|
||||
|
||||
/** 刷新 message:基于指定消息,再次发起对话 */
|
||||
const handleMessageRefresh = (message: ChatMessageVO) => {
|
||||
doSendMessage(message.content)
|
||||
}
|
||||
|
||||
// ============== 【消息滚动】相关 =============
|
||||
|
||||
/** 滚动到 message 底部 */
|
||||
const scrollToBottom = async (isIgnore?: boolean) => {
|
||||
await nextTick()
|
||||
|
|
@ -558,7 +514,6 @@ const scrollToBottom = async (isIgnore?: boolean) => {
|
|||
messageRef.value.scrollToBottom(isIgnore)
|
||||
}
|
||||
}
|
||||
|
||||
/** 自提滚动效果 */
|
||||
const textRoll = async () => {
|
||||
let index = 0
|
||||
|
|
@ -587,11 +542,9 @@ const textRoll = async () => {
|
|||
if (!conversationInProgress.value) {
|
||||
textSpeed.value = 10
|
||||
}
|
||||
|
||||
if (index < receiveMessageFullText.value.length) {
|
||||
receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
|
||||
index++
|
||||
|
||||
// 更新 message
|
||||
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
|
||||
lastMessage.content = receiveMessageDisplayedText.value
|
||||
|
|
@ -613,7 +566,6 @@ const textRoll = async () => {
|
|||
let timer = setTimeout(task, textSpeed.value)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
// 如果有 conversationId 参数,则默认选中
|
||||
|
|
@ -622,9 +574,8 @@ onMounted(async () => {
|
|||
activeConversationId.value = id
|
||||
await getConversation(id)
|
||||
}
|
||||
|
||||
// 获取列表数据
|
||||
activeMessageListLoading.value = true
|
||||
await getMessageList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -49,7 +49,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -95,15 +94,12 @@
|
|||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ChatConversationVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -116,7 +112,6 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -128,19 +123,16 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -153,11 +145,10 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -49,7 +49,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -105,16 +104,13 @@
|
|||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ChatMessageVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -128,7 +124,6 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -140,19 +135,16 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -165,11 +157,10 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
|
||||
|
||||
<ContentWrap>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="对话列表">
|
||||
|
|
@ -12,11 +10,9 @@
|
|||
</el-tabs>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ChatConversationList from './ChatConversationList.vue'
|
||||
import ChatMessageList from './ChatMessageList.vue'
|
||||
|
||||
/** AI 聊天对话 列表 */
|
||||
defineOptions({ name: 'AiChatManager' })
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -74,39 +74,31 @@ import { ImageVO, ImageMidjourneyButtonsVO } from '@/api/ai/image'
|
|||
import { PropType } from 'vue'
|
||||
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
|
||||
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
const message = useMessage() // 消息
|
||||
|
||||
const props = defineProps({
|
||||
detail: {
|
||||
type: Object as PropType<ImageVO>,
|
||||
require: true
|
||||
}
|
||||
})
|
||||
|
||||
const cardImageRef = ref<any>() // 卡片 image ref
|
||||
const cardImageLoadingInstance = ref<any>() // 卡片 image ref
|
||||
|
||||
/** 处理点击事件 */
|
||||
const handleButtonClick = async (type, detail: ImageVO) => {
|
||||
emits('onBtnClick', type, detail)
|
||||
}
|
||||
|
||||
/** 处理 Midjourney 按钮点击事件 */
|
||||
const handleMidjourneyBtnClick = async (button: ImageMidjourneyButtonsVO) => {
|
||||
// 确认窗体
|
||||
await message.confirm(`确认操作 "${button.label} ${button.emoji}" ?`)
|
||||
emits('onMjBtnClick', button, props.detail)
|
||||
}
|
||||
|
||||
const emits = defineEmits(['onBtnClick', 'onMjBtnClick']) // emits
|
||||
|
||||
/** 监听详情 */
|
||||
const { detail } = toRefs(props)
|
||||
watch(detail, async (newVal, oldVal) => {
|
||||
await handleLoading(newVal.status as string)
|
||||
})
|
||||
|
||||
/** 处理加载状态 */
|
||||
const handleLoading = async (status: number) => {
|
||||
// 情况一:如果是生成中,则设置加载中的 loading
|
||||
|
|
@ -123,9 +115,8 @@ const handleLoading = async (status: number) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await handleLoading(props.detail.status as string)
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
fit="contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 基础信息 -->
|
||||
<el-descriptions title="基础信息" :column="1" :label-width="100" border>
|
||||
<el-descriptions-item label="提交时间">
|
||||
|
|
@ -34,7 +33,6 @@
|
|||
<div class="break-all text-xs">{{ detail.picUrl }}</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- StableDiffusion 专属区域 -->
|
||||
<el-descriptions
|
||||
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && hasStableDiffusionOptions"
|
||||
|
|
@ -75,7 +73,6 @@
|
|||
{{ detail?.options?.seed }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- Dall3 专属区域 -->
|
||||
<el-descriptions
|
||||
v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style"
|
||||
|
|
@ -89,7 +86,6 @@
|
|||
{{ Dall3StyleList.find((item: ImageModelVO) => item.key === detail?.options?.style)?.name }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- Midjourney 专属区域 -->
|
||||
<el-descriptions
|
||||
v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && hasMidjourneyOptions"
|
||||
|
|
@ -112,7 +108,6 @@
|
|||
</el-descriptions>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||
import {
|
||||
|
|
@ -124,10 +119,8 @@ import {
|
|||
StableDiffusionStylePresets
|
||||
} from '@/views/ai/utils/constants'
|
||||
import { formatTime } from '@/utils'
|
||||
|
||||
const showDrawer = ref<boolean>(false) // 是否显示
|
||||
const detail = ref<ImageVO>({} as ImageVO) // 图片详细信息
|
||||
|
||||
// 计算属性:判断是否有 StableDiffusion 选项
|
||||
const hasStableDiffusionOptions = computed(() => {
|
||||
const options = detail.value?.options
|
||||
|
|
@ -140,13 +133,11 @@ const hasStableDiffusionOptions = computed(() => {
|
|||
options?.seed
|
||||
)
|
||||
})
|
||||
|
||||
// 计算属性:判断是否有 Midjourney 选项
|
||||
const hasMidjourneyOptions = computed(() => {
|
||||
const options = detail.value?.options
|
||||
return options?.version || options?.referImageUrl
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
|
|
@ -158,23 +149,19 @@ const props = defineProps({
|
|||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
/** 关闭抽屉 */
|
||||
const handleDrawerClose = async () => {
|
||||
emits('handleDrawerClose')
|
||||
}
|
||||
|
||||
/** 监听 drawer 是否打开 */
|
||||
const { show } = toRefs(props)
|
||||
watch(show, async (newValue, _oldValue) => {
|
||||
showDrawer.value = newValue as boolean
|
||||
})
|
||||
|
||||
/** 获取图片详情 */
|
||||
const getImageDetail = async (id: number) => {
|
||||
detail.value = await ImageApi.getImageMy(id)
|
||||
}
|
||||
|
||||
/** 监听 id 变化,加载最新图片详情 */
|
||||
const { id } = toRefs(props)
|
||||
watch(id, async (newVal, _oldVal) => {
|
||||
|
|
@ -182,6 +169,5 @@ watch(id, async (newVal, _oldVal) => {
|
|||
await getImageDetail(newVal)
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['handleDrawerClose'])
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -33,7 +33,6 @@
|
|||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 图片详情 -->
|
||||
<ImageDetail
|
||||
:show="isShowImageDetail"
|
||||
|
|
@ -53,10 +52,8 @@ import ImageCard from './ImageCard.vue'
|
|||
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
|
||||
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
|
||||
import download from '@/utils/download'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const router = useRouter() // 路由
|
||||
|
||||
// 图片分页相关的参数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
|
|
@ -72,24 +69,20 @@ const inProgressTimer = ref<any>() // 生成中的 image 定时器,轮询生
|
|||
// 图片详情相关的参数
|
||||
const isShowImageDetail = ref<boolean>(false) // 图片详情是否展示
|
||||
const showImageDetailId = ref<number>(0) // 图片详情的图片编号
|
||||
|
||||
/** 处理查看绘图作品 */
|
||||
const handleViewPublic = () => {
|
||||
router.push({
|
||||
name: 'AiImageSquare'
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看图片的详情 */
|
||||
const handleDetailOpen = async () => {
|
||||
isShowImageDetail.value = true
|
||||
}
|
||||
|
||||
/** 关闭图片的详情 */
|
||||
const handleDetailClose = async () => {
|
||||
isShowImageDetail.value = false
|
||||
}
|
||||
|
||||
/** 获得 image 图片列表 */
|
||||
const getImageList = async () => {
|
||||
try {
|
||||
|
|
@ -101,7 +94,6 @@ const getImageList = async () => {
|
|||
const { list, total } = await ImageApi.getImagePageMy(queryParams)
|
||||
imageList.value = list
|
||||
pageTotal.value = total
|
||||
|
||||
// 2. 计算需要轮询的图片
|
||||
const newWatImages = {}
|
||||
imageList.value.forEach((item) => {
|
||||
|
|
@ -118,7 +110,6 @@ const getImageList = async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 轮询生成中的 image 列表 */
|
||||
const refreshWatchImages = async () => {
|
||||
const imageIds = Object.keys(inProgressImageMap.value).map(Number)
|
||||
|
|
@ -140,7 +131,6 @@ const refreshWatchImages = async () => {
|
|||
})
|
||||
inProgressImageMap.value = newWatchImages
|
||||
}
|
||||
|
||||
/** 图片的点击事件 */
|
||||
const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
|
||||
// 详情
|
||||
|
|
@ -168,7 +158,6 @@ const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理 Midjourney 按钮点击事件 */
|
||||
const handleImageMidjourneyButtonClick = async (
|
||||
button: ImageMidjourneyButtonsVO,
|
||||
|
|
@ -184,11 +173,8 @@ const handleImageMidjourneyButtonClick = async (
|
|||
// 3. 刷新列表
|
||||
await getImageList()
|
||||
}
|
||||
|
||||
defineExpose({ getImageList }) // 暴露组件方法
|
||||
|
||||
const emits = defineEmits(['onRegeneration'])
|
||||
|
||||
/** 组件挂在的时候 */
|
||||
onMounted(async () => {
|
||||
// 获取 image 列表
|
||||
|
|
@ -198,11 +184,10 @@ onMounted(async () => {
|
|||
await refreshWatchImages()
|
||||
}, 1000 * 3)
|
||||
})
|
||||
|
||||
/** 组件取消挂在的时候 */
|
||||
onUnmounted(async () => {
|
||||
if (inProgressTimer.value) {
|
||||
clearInterval(inProgressTimer.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -93,9 +93,7 @@
|
|||
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
|
||||
import { AiPlatformEnum, ImageHotWords, OtherPlatformEnum } from '@/views/ai/utils/constants'
|
||||
import { ModelVO } from '@/api/ai/model/model'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 接收父组件传入的模型列表
|
||||
const props = defineProps({
|
||||
models: {
|
||||
|
|
@ -104,7 +102,6 @@ const props = defineProps({
|
|||
}
|
||||
})
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
||||
|
||||
// 定义属性
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
|
|
@ -115,7 +112,6 @@ const height = ref<number>(512) // 图片高度
|
|||
const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) // 平台
|
||||
const platformModels = ref<ModelVO[]>([]) // 模型列表
|
||||
const modelId = ref<number>() // 选中的模型
|
||||
|
||||
/** 选择热词 */
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 情况一:取消选中
|
||||
|
|
@ -123,12 +119,10 @@ const handleHotWordClick = async (hotWord: string) => {
|
|||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord // 选中
|
||||
prompt.value = hotWord // 替换提示词
|
||||
}
|
||||
|
||||
/** 图片生成 */
|
||||
const handleGenerateImage = async () => {
|
||||
// 二次确认
|
||||
|
|
@ -155,19 +149,16 @@ const handleGenerateImage = async () => {
|
|||
drawIn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
const settingValues = async (detail: ImageVO) => {
|
||||
prompt.value = detail.prompt
|
||||
width.value = detail.width
|
||||
height.value = detail.height
|
||||
}
|
||||
|
||||
/** 平台切换 */
|
||||
const handlerPlatformChange = async (platform: string) => {
|
||||
// 根据选择的平台筛选模型
|
||||
platformModels.value = props.models.filter((item: ModelVO) => item.platform === platform)
|
||||
|
||||
// 切换平台,默认选择一个模型
|
||||
if (platformModels.value.length > 0) {
|
||||
modelId.value = platformModels.value[0].id // 使用 model 属性作为值
|
||||
|
|
@ -175,7 +166,6 @@ const handlerPlatformChange = async (platform: string) => {
|
|||
modelId.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听 models 变化 */
|
||||
watch(
|
||||
() => props.models,
|
||||
|
|
@ -186,4 +176,4 @@ watch(
|
|||
)
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues })
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -106,9 +106,7 @@ import {
|
|||
ImageSizeVO
|
||||
} from '@/views/ai/utils/constants'
|
||||
import { ModelVO } from '@/api/ai/model/model'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 接收父组件传入的模型列表
|
||||
const props = defineProps({
|
||||
models: {
|
||||
|
|
@ -117,7 +115,6 @@ const props = defineProps({
|
|||
}
|
||||
})
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
||||
|
||||
// 定义属性
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
|
|
@ -125,7 +122,6 @@ const selectHotWord = ref<string>('') // 选中的热词
|
|||
const selectModel = ref<string>('dall-e-3') // 模型
|
||||
const selectSize = ref<string>('1024x1024') // 选中 size
|
||||
const style = ref<string>('vivid') // style 样式
|
||||
|
||||
/** 选择热词 */
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 情况一:取消选中
|
||||
|
|
@ -133,12 +129,10 @@ const handleHotWordClick = async (hotWord: string) => {
|
|||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord
|
||||
prompt.value = hotWord
|
||||
}
|
||||
|
||||
/** 选择 model 模型 */
|
||||
const handleModelClick = async (model: ImageModelVO) => {
|
||||
selectModel.value = model.key
|
||||
|
|
@ -151,7 +145,6 @@ const handleModelClick = async (model: ImageModelVO) => {
|
|||
// DALL-E-2 模型特定的处理
|
||||
style.value = 'natural' // 如果有其他DALL-E-2适合的默认风格
|
||||
}
|
||||
|
||||
// 更新其他相关参数
|
||||
// 例如可以默认选择最适合当前模型的尺寸
|
||||
const recommendedSize = Dall3SizeList.find(
|
||||
|
|
@ -159,22 +152,18 @@ const handleModelClick = async (model: ImageModelVO) => {
|
|||
(model.key === 'dall-e-3' && size.key === '1024x1024') ||
|
||||
(model.key === 'dall-e-2' && size.key === '512x512')
|
||||
)
|
||||
|
||||
if (recommendedSize) {
|
||||
selectSize.value = recommendedSize.key
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择 style 样式 */
|
||||
const handleStyleClick = async (imageStyle: ImageModelVO) => {
|
||||
style.value = imageStyle.key
|
||||
}
|
||||
|
||||
/** 选择 size 大小 */
|
||||
const handleSizeClick = async (imageSize: ImageSizeVO) => {
|
||||
selectSize.value = imageSize.key
|
||||
}
|
||||
|
||||
/** 图片生产 */
|
||||
const handleGenerateImage = async () => {
|
||||
// 从 models 中查找匹配的模型
|
||||
|
|
@ -185,7 +174,6 @@ const handleGenerateImage = async () => {
|
|||
message.error('该模型不可用,请选择其它模型')
|
||||
return
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
await message.confirm(`确认生成内容?`)
|
||||
try {
|
||||
|
|
@ -214,7 +202,6 @@ const handleGenerateImage = async () => {
|
|||
drawIn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
const settingValues = async (detail: ImageVO) => {
|
||||
prompt.value = detail.prompt
|
||||
|
|
@ -225,8 +212,6 @@ const settingValues = async (detail: ImageVO) => {
|
|||
) as ImageSizeVO
|
||||
await handleSizeClick(imageSize)
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues })
|
||||
</script>
|
||||
|
||||
</script>
|
||||
|
|
@ -119,9 +119,7 @@ import {
|
|||
NijiVersionList
|
||||
} from '@/views/ai/utils/constants'
|
||||
import { ModelVO } from '@/api/ai/model/model'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 接收父组件传入的模型列表
|
||||
const props = defineProps({
|
||||
models: {
|
||||
|
|
@ -130,7 +128,6 @@ const props = defineProps({
|
|||
}
|
||||
})
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
||||
|
||||
// 定义属性
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
|
|
@ -141,7 +138,6 @@ const selectModel = ref<string>('midjourney') // 选中的模型
|
|||
const selectSize = ref<string>('1:1') // 选中 size
|
||||
const selectVersion = ref<any>('6.0') // 选中的 version
|
||||
const versionList = ref<any>(MidjourneyVersions) // version 列表
|
||||
|
||||
/** 选择热词 */
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 情况一:取消选中
|
||||
|
|
@ -149,17 +145,14 @@ const handleHotWordClick = async (hotWord: string) => {
|
|||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord // 选中
|
||||
prompt.value = hotWord // 设置提示次
|
||||
}
|
||||
|
||||
/** 点击 size 尺寸 */
|
||||
const handleSizeClick = async (imageSize: ImageSizeVO) => {
|
||||
selectSize.value = imageSize.key
|
||||
}
|
||||
|
||||
/** 点击 model 模型 */
|
||||
const handleModelClick = async (model: ImageModelVO) => {
|
||||
selectModel.value = model.key
|
||||
|
|
@ -170,7 +163,6 @@ const handleModelClick = async (model: ImageModelVO) => {
|
|||
}
|
||||
selectVersion.value = versionList.value[0].value
|
||||
}
|
||||
|
||||
/** 图片生成 */
|
||||
const handleGenerateImage = async () => {
|
||||
// 从 models 中查找匹配的模型
|
||||
|
|
@ -181,7 +173,6 @@ const handleGenerateImage = async () => {
|
|||
message.error('该模型不可用,请选择其它模型')
|
||||
return
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
await message.confirm(`确认生成内容?`)
|
||||
try {
|
||||
|
|
@ -209,7 +200,6 @@ const handleGenerateImage = async () => {
|
|||
drawIn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
const settingValues = async (detail: ImageVO) => {
|
||||
// 提示词
|
||||
|
|
@ -229,8 +219,6 @@ const settingValues = async (detail: ImageVO) => {
|
|||
// image
|
||||
referImageUrl.value = detail.options.referImageUrl
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues })
|
||||
</script>
|
||||
|
||||
</script>
|
||||
|
|
@ -151,9 +151,7 @@ import {
|
|||
StableDiffusionStylePresets
|
||||
} from '@/views/ai/utils/constants'
|
||||
import { ModelVO } from '@/api/ai/model/model'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 接收父组件传入的模型列表
|
||||
const props = defineProps({
|
||||
models: {
|
||||
|
|
@ -162,7 +160,6 @@ const props = defineProps({
|
|||
}
|
||||
})
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
||||
|
||||
// 定义属性
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
|
|
@ -176,7 +173,6 @@ const seed = ref<number>(42) // 控制生成图像的随机性
|
|||
const scale = ref<number>(7.5) // 引导系数
|
||||
const clipGuidancePreset = ref<string>('NONE') // 文本提示相匹配的图像(clip_guidance_preset) 简称 CLIP
|
||||
const stylePreset = ref<string>('3d-model') // 风格
|
||||
|
||||
/** 选择热词 */
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 情况一:取消选中
|
||||
|
|
@ -184,12 +180,10 @@ const handleHotWordClick = async (hotWord: string) => {
|
|||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord // 选中
|
||||
prompt.value = hotWord // 替换提示词
|
||||
}
|
||||
|
||||
/** 图片生成 */
|
||||
const handleGenerateImage = async () => {
|
||||
// 从 models 中查找匹配的模型
|
||||
|
|
@ -201,14 +195,12 @@ const handleGenerateImage = async () => {
|
|||
message.error('该模型不可用,请选择其它模型')
|
||||
return
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
if (hasChinese(prompt.value)) {
|
||||
message.alert('暂不支持中文!')
|
||||
return
|
||||
}
|
||||
await message.confirm(`确认生成内容?`)
|
||||
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true
|
||||
|
|
@ -237,7 +229,6 @@ const handleGenerateImage = async () => {
|
|||
drawIn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
const settingValues = async (detail: ImageVO) => {
|
||||
prompt.value = detail.prompt
|
||||
|
|
@ -250,8 +241,6 @@ const settingValues = async (detail: ImageVO) => {
|
|||
clipGuidancePreset.value = detail.options?.clipGuidancePreset
|
||||
stylePreset.value = detail.options?.stylePreset
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues })
|
||||
</script>
|
||||
|
||||
</script>
|
||||
|
|
@ -41,7 +41,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ImageList from './components/ImageList.vue'
|
||||
import { AiPlatformEnum } from '@/views/ai/utils/constants'
|
||||
|
|
@ -52,13 +51,11 @@ import StableDiffusion from './components/stableDiffusion/index.vue'
|
|||
import Common from './components/common/index.vue'
|
||||
import { ModelApi, ModelVO } from '@/api/ai/model/model'
|
||||
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
const imageListRef = ref<any>() // image 列表 ref
|
||||
const dall3Ref = ref<any>() // dall3(openai) ref
|
||||
const midjourneyRef = ref<any>() // midjourney ref
|
||||
const stableDiffusionRef = ref<any>() // stable diffusion ref
|
||||
const commonRef = ref<any>() // stable diffusion ref
|
||||
|
||||
// 定义属性
|
||||
const selectPlatform = ref('common') // 选中的平台
|
||||
const platformOptions = [
|
||||
|
|
@ -79,17 +76,13 @@ const platformOptions = [
|
|||
value: AiPlatformEnum.STABLE_DIFFUSION
|
||||
}
|
||||
]
|
||||
|
||||
const models = ref<ModelVO[]>([]) // 模型列表
|
||||
|
||||
/** 绘画 start */
|
||||
const handleDrawStart = async (_platform: string) => {}
|
||||
|
||||
/** 绘画 complete */
|
||||
const handleDrawComplete = async (_platform: string) => {
|
||||
await imageListRef.value.getImageList()
|
||||
}
|
||||
|
||||
/** 重新生成:将画图详情填充到对应平台 */
|
||||
const handleRegeneration = async (image: ImageVO) => {
|
||||
// 切换平台
|
||||
|
|
@ -105,10 +98,9 @@ const handleRegeneration = async (image: ImageVO) => {
|
|||
}
|
||||
// TODO @fan:貌似 other 重新设置不行?
|
||||
}
|
||||
|
||||
/** 组件挂载的时候 */
|
||||
onMounted(async () => {
|
||||
// 获取模型列表
|
||||
models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE)
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 绘图创作" url="https://doc.iocoder.cn/ai/image/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -82,7 +80,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -161,20 +158,16 @@
|
|||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
/** AI 绘画 列表 */
|
||||
defineOptions({ name: 'AiImageManager' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ImageVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -189,7 +182,6 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -201,19 +193,16 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -226,7 +215,6 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 修改是否发布 */
|
||||
const handleUpdatePublicStatusChange = async (row: ImageVO) => {
|
||||
try {
|
||||
|
|
@ -243,11 +231,10 @@ const handleUpdatePublicStatusChange = async (row: ImageVO) => {
|
|||
row.publicStatus = !row.publicStatus
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -29,7 +29,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
// TODO @fan:加个 loading 加载中的状态
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ImageVO[]>([]) // 列表的数据
|
||||
|
|
@ -40,7 +39,6 @@ const queryParams = reactive({
|
|||
publicStatus: true,
|
||||
prompt: undefined
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -52,16 +50,13 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
</script>
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
<Icon icon="ep:document" class="mr-8px text-[#409eff]" />
|
||||
<span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 处理进度 -->
|
||||
<div class="flex-1">
|
||||
<el-progress
|
||||
|
|
@ -21,14 +20,12 @@
|
|||
:status="isProcessComplete(file) ? 'success' : ''"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分段数量 -->
|
||||
<div class="ml-10px text-[13px] text-[#606266]">
|
||||
分段数量:{{ file.count ? file.count : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部完成按钮 -->
|
||||
<div class="flex justify-end mt-20px">
|
||||
<el-button
|
||||
|
|
@ -41,38 +38,31 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const parent = inject('parent') as any
|
||||
const pollingTimer = ref<number | null>(null) // 轮询定时器 ID,用于跟踪和清除轮询进程
|
||||
|
||||
/** 判断文件处理是否完成 */
|
||||
const isProcessComplete = (file) => {
|
||||
return file.progress === 100
|
||||
}
|
||||
|
||||
/** 判断所有文件是否都处理完成 */
|
||||
const allProcessComplete = computed(() => {
|
||||
return props.modelValue.list.every((file) => isProcessComplete(file))
|
||||
})
|
||||
|
||||
/** 完成按钮点击事件处理 */
|
||||
const handleComplete = () => {
|
||||
if (parent?.exposed?.handleBack) {
|
||||
parent.exposed.handleBack()
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取文件处理进度 */
|
||||
const getProcessList = async () => {
|
||||
try {
|
||||
|
|
@ -82,7 +72,6 @@ const getProcessList = async () => {
|
|||
return
|
||||
}
|
||||
const result = await KnowledgeSegmentApi.getKnowledgeSegmentProcessList(documentIds)
|
||||
|
||||
// 2.1更新进度
|
||||
const updatedList = props.modelValue.list.map((file) => {
|
||||
const processInfo = result.find((item) => item.documentId === file.id)
|
||||
|
|
@ -100,13 +89,11 @@ const getProcessList = async () => {
|
|||
}
|
||||
return file
|
||||
})
|
||||
|
||||
// 2.2 更新数据
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
list: updatedList
|
||||
})
|
||||
|
||||
// 3. 如果未完成,继续轮询
|
||||
if (!updatedList.every((file) => isProcessComplete(file))) {
|
||||
pollingTimer.value = window.setTimeout(getProcessList, 3000)
|
||||
|
|
@ -117,7 +104,6 @@ const getProcessList = async () => {
|
|||
pollingTimer.value = window.setTimeout(getProcessList, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
/** 组件挂载时开始轮询 */
|
||||
onMounted(() => {
|
||||
// 1. 初始化进度为 0
|
||||
|
|
@ -125,16 +111,13 @@ onMounted(() => {
|
|||
...file,
|
||||
progress: 0
|
||||
}))
|
||||
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
list: initialList
|
||||
})
|
||||
|
||||
// 2. 开始轮询获取进度
|
||||
getProcessList()
|
||||
})
|
||||
|
||||
/** 组件卸载前清除轮询 */
|
||||
onBeforeUnmount(() => {
|
||||
// 1. 清除定时器
|
||||
|
|
@ -143,4 +126,4 @@ onBeforeUnmount(() => {
|
|||
pollingTimer.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="segment-settings mb-20px">
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="最大 Token 数">
|
||||
|
|
@ -27,11 +26,9 @@
|
|||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下部文件预览部分 -->
|
||||
<div class="mb-10px">
|
||||
<div class="text-16px font-bold mb-10px">分段预览</div>
|
||||
|
||||
<!-- 文件选择器 -->
|
||||
<div class="file-selector mb-10px">
|
||||
<el-dropdown v-if="modelData.list && modelData.list.length > 0" trigger="click">
|
||||
|
|
@ -60,7 +57,6 @@
|
|||
</el-dropdown>
|
||||
<div v-else class="text-gray-400">暂无上传文件</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件内容预览 -->
|
||||
<div class="file-preview bg-gray-50 p-15px rounded-md max-h-600px overflow-y-auto">
|
||||
<div v-if="splitLoading" class="flex justify-center items-center py-20px">
|
||||
|
|
@ -81,7 +77,6 @@
|
|||
<el-empty v-else description="暂无预览内容" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加底部按钮 -->
|
||||
<div class="mt-20px flex justify-between">
|
||||
<div>
|
||||
|
|
@ -95,47 +90,39 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, getCurrentInstance, inject, onMounted, PropType, ref } from 'vue'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<any>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const message = useMessage() // 消息提示
|
||||
const parent = inject('parent', null) // 获取父组件实例
|
||||
|
||||
const modelData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
}) // 表单数据
|
||||
|
||||
const splitLoading = ref(false) // 分段加载状态
|
||||
const currentFile = ref<any>(null) // 当前选中的文件
|
||||
const submitLoading = ref(false) // 提交按钮加载状态
|
||||
|
||||
/** 选择文件 */
|
||||
const selectFile = async (index: number) => {
|
||||
currentFile.value = modelData.value.list[index]
|
||||
await splitContent(currentFile.value)
|
||||
}
|
||||
|
||||
/** 获取文件分段内容 */
|
||||
const splitContent = async (file: any) => {
|
||||
if (!file || !file.url) {
|
||||
message.warning('文件 URL 不存在')
|
||||
return
|
||||
}
|
||||
|
||||
splitLoading.value = true
|
||||
try {
|
||||
// 调用后端分段接口,获取文档的分段内容、字符数和 Token 数
|
||||
|
|
@ -149,7 +136,6 @@ const splitContent = async (file: any) => {
|
|||
splitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理预览分段 */
|
||||
const handleAutoSegment = async () => {
|
||||
// 如果没有选中文件,默认选中第一个
|
||||
|
|
@ -161,11 +147,9 @@ const handleAutoSegment = async () => {
|
|||
message.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取分段内容
|
||||
await splitContent(currentFile.value)
|
||||
}
|
||||
|
||||
/** 上一步按钮处理 */
|
||||
const handlePrevStep = () => {
|
||||
const parentEl = parent || getCurrentInstance()?.parent
|
||||
|
|
@ -173,7 +157,6 @@ const handlePrevStep = () => {
|
|||
parentEl.exposed.goToPrevStep()
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存操作 */
|
||||
const handleSave = async () => {
|
||||
// 保存前验证
|
||||
|
|
@ -181,7 +164,6 @@ const handleSave = async () => {
|
|||
message.warning('请先预览分段内容')
|
||||
return
|
||||
}
|
||||
|
||||
// 设置按钮加载状态
|
||||
submitLoading.value = true
|
||||
try {
|
||||
|
|
@ -205,7 +187,6 @@ const handleSave = async () => {
|
|||
document.id = data[index]
|
||||
})
|
||||
}
|
||||
|
||||
// 进入下一步
|
||||
const parentEl = parent || getCurrentInstance()?.parent
|
||||
if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
|
||||
|
|
@ -218,7 +199,6 @@ const handleSave = async () => {
|
|||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 确保 segmentMaxTokens 存在
|
||||
|
|
@ -229,10 +209,9 @@ onMounted(async () => {
|
|||
if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
|
||||
currentFile.value = modelData.value.list[0]
|
||||
}
|
||||
|
||||
// 如果有选中的文件,获取分段内容
|
||||
if (currentFile.value) {
|
||||
await splitContent(currentFile.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -34,7 +34,6 @@
|
|||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="modelData.list && modelData.list.length > 0"
|
||||
class="mt-15px grid grid-cols-1 gap-2"
|
||||
|
|
@ -55,7 +54,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 添加下一步按钮 -->
|
||||
<el-form-item>
|
||||
<div class="flex justify-end w-full">
|
||||
|
|
@ -66,23 +64,19 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { useUpload } from '@/components/UploadFile/src/useUpload'
|
||||
import { generateAcceptedFileTypes } from '@/utils'
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<any>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const formRef = ref() // 表单引用
|
||||
const uploadRef = ref() // 上传组件引用
|
||||
const parent = inject('parent', null) // 获取父组件实例
|
||||
|
|
@ -90,7 +84,6 @@ const { uploadUrl, httpRequest } = useUpload() // 使用上传组件的钩子
|
|||
const message = useMessage() // 消息弹窗
|
||||
const fileList = ref([]) // 文件列表
|
||||
const uploadingCount = ref(0) // 上传中的文件数量
|
||||
|
||||
// 支持的文件类型和大小限制
|
||||
const supportedFileTypes = [
|
||||
'TXT',
|
||||
|
|
@ -114,10 +107,8 @@ const supportedFileTypes = [
|
|||
]
|
||||
const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase()) // 小写的扩展名列表
|
||||
const maxFileSize = 15 // 最大文件大小(MB)
|
||||
|
||||
// 构建 accept 属性值,用于限制文件选择对话框中可见的文件类型
|
||||
const acceptedFileTypes = computed(() => generateAcceptedFileTypes(supportedFileTypes))
|
||||
|
||||
/** 表单数据 */
|
||||
const modelData = computed({
|
||||
get: () => {
|
||||
|
|
@ -125,7 +116,6 @@ const modelData = computed({
|
|||
},
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
/** 确保 list 属性存在 */
|
||||
const ensureListExists = () => {
|
||||
if (!props.modelValue.list) {
|
||||
|
|
@ -135,12 +125,10 @@ const ensureListExists = () => {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 是否所有文件都已上传完成 */
|
||||
const isAllUploaded = computed(() => {
|
||||
return modelData.value.list && modelData.value.list.length > 0 && uploadingCount.value === 0
|
||||
})
|
||||
|
||||
/**
|
||||
* 上传前检查文件类型和大小
|
||||
*
|
||||
|
|
@ -160,12 +148,10 @@ const beforeUpload = (file) => {
|
|||
message.error(`文件大小不能超过 ${maxFileSize} MB!`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. 增加上传中的文件计数
|
||||
uploadingCount.value++
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传成功处理
|
||||
*
|
||||
|
|
@ -189,11 +175,9 @@ const handleUploadSuccess = (response, file) => {
|
|||
} else {
|
||||
message.error(`文件 ${file.name} 上传失败`)
|
||||
}
|
||||
|
||||
// 减少上传中的文件计数
|
||||
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传失败处理
|
||||
*
|
||||
|
|
@ -205,7 +189,6 @@ const handleUploadError = (error, file) => {
|
|||
// 减少上传中的文件计数
|
||||
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件变更处理
|
||||
*
|
||||
|
|
@ -216,7 +199,6 @@ const handleFileChange = (file) => {
|
|||
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件移除处理
|
||||
*
|
||||
|
|
@ -227,7 +209,6 @@ const handleFileRemove = (file) => {
|
|||
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从列表中移除文件
|
||||
*
|
||||
|
|
@ -243,7 +224,6 @@ const removeFile = (index: number) => {
|
|||
list: newList
|
||||
})
|
||||
}
|
||||
|
||||
/** 下一步按钮处理 */
|
||||
const handleNextStep = () => {
|
||||
// 1.1 检查是否有文件上传
|
||||
|
|
@ -256,18 +236,15 @@ const handleNextStep = () => {
|
|||
message.warning('请等待所有文件上传完成')
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 获取父组件的goToNextStep方法
|
||||
const parentEl = parent || getCurrentInstance()?.parent
|
||||
if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
|
||||
parentEl.exposed.goToNextStep()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
ensureListExists()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
{{ formData.id ? '编辑知识库文档' : '创建知识库文档' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 步骤条 -->
|
||||
<div class="flex-1 flex items-center justify-center h-full">
|
||||
<div class="w-400px flex items-center justify-between h-full">
|
||||
|
|
@ -40,23 +39,19 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧按钮 - 已移除 -->
|
||||
<div class="w-200px flex items-center justify-end gap-2"> </div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="mt-50px">
|
||||
<!-- 第一步:上传文档 -->
|
||||
<div v-if="currentStep === 0" class="mx-auto w-560px">
|
||||
<UploadStep v-model="formData" ref="uploadDocumentRef" />
|
||||
</div>
|
||||
|
||||
<!-- 第二步:文档分段 -->
|
||||
<div v-if="currentStep === 1" class="mx-auto w-560px">
|
||||
<SplitStep v-model="formData" ref="documentSegmentRef" />
|
||||
</div>
|
||||
|
||||
<!-- 第三步:处理并完成 -->
|
||||
<div v-if="currentStep === 2" class="mx-auto w-560px">
|
||||
<ProcessStep v-model="formData" ref="processCompleteRef" />
|
||||
|
|
@ -65,7 +60,6 @@
|
|||
</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
|
|
@ -73,11 +67,9 @@ import UploadStep from './UploadStep.vue'
|
|||
import SplitStep from './SplitStep.vue'
|
||||
import ProcessStep from './ProcessStep.vue'
|
||||
import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
|
||||
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
const route = useRoute() // 路由
|
||||
const router = useRouter() // 路由
|
||||
|
||||
// 组件引用
|
||||
const uploadDocumentRef = ref()
|
||||
const documentSegmentRef = ref()
|
||||
|
|
@ -101,16 +93,13 @@ const formData = ref({
|
|||
process?: number // 处理进度
|
||||
}> // 用于存储上传的文件列表
|
||||
}) // 表单数据
|
||||
|
||||
provide('parent', getCurrentInstance()) // 提供 parent 给子组件使用
|
||||
|
||||
/** 初始化数据 */
|
||||
const initData = async () => {
|
||||
// 【新增场景】从路由参数中获取知识库 ID
|
||||
if (route.query.knowledgeId) {
|
||||
formData.value.knowledgeId = route.query.knowledgeId as any
|
||||
}
|
||||
|
||||
// 【修改场景】从路由参数中获取文档 ID
|
||||
const documentId = route.query.id
|
||||
if (documentId) {
|
||||
|
|
@ -130,21 +119,18 @@ const initData = async () => {
|
|||
goToNextStep()
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换到下一步 */
|
||||
const goToNextStep = () => {
|
||||
if (currentStep.value < steps.length - 1) {
|
||||
currentStep.value++
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换到上一步 */
|
||||
const goToPrevStep = () => {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表页 */
|
||||
const handleBack = () => {
|
||||
// 先删除当前页签
|
||||
|
|
@ -152,12 +138,10 @@ const handleBack = () => {
|
|||
// 跳转到列表页
|
||||
router.push({ name: 'AiKnowledgeDocument', query: { knowledgeId: formData.value.knowledgeId } })
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await initData()
|
||||
})
|
||||
|
||||
/** 添加组件卸载前的清理代码 */
|
||||
onBeforeUnmount(() => {
|
||||
// 清理所有的引用
|
||||
|
|
@ -165,7 +149,6 @@ onBeforeUnmount(() => {
|
|||
documentSegmentRef.value = null
|
||||
processCompleteRef.value = null
|
||||
})
|
||||
|
||||
/** 暴露方法给子组件使用 */
|
||||
defineExpose({
|
||||
goToNextStep,
|
||||
|
|
@ -173,21 +156,17 @@ defineExpose({
|
|||
handleBack
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #3473ff;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: #3473ff;
|
||||
}
|
||||
|
||||
.border-primary {
|
||||
border-color: #3473ff;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -41,7 +41,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -106,11 +105,9 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<!-- <KnowledgeDocumentForm ref="formRef" @success="getList" /> -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
|
|
@ -119,15 +116,12 @@ import { useRoute, useRouter } from 'vue-router'
|
|||
import { checkPermi } from '@/utils/permission'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
// import KnowledgeDocumentForm from './KnowledgeDocumentForm.vue'
|
||||
|
||||
/** AI 知识库文档 列表 */
|
||||
defineOptions({ name: 'KnowledgeDocument' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
const route = useRoute() // 路由
|
||||
const router = useRouter() // 路由
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<KnowledgeDocumentVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -139,7 +133,6 @@ const queryParams = reactive({
|
|||
knowledgeId: undefined
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -151,19 +144,16 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 跳转到创建文档页面 */
|
||||
const handleCreate = () => {
|
||||
router.push({
|
||||
|
|
@ -171,7 +161,6 @@ const handleCreate = () => {
|
|||
query: { knowledgeId: queryParams.knowledgeId }
|
||||
})
|
||||
}
|
||||
|
||||
/** 跳转到更新文档页面 */
|
||||
const handleUpdate = (id: number) => {
|
||||
router.push({
|
||||
|
|
@ -179,7 +168,6 @@ const handleUpdate = (id: number) => {
|
|||
query: { id, knowledgeId: queryParams.knowledgeId }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -192,7 +180,6 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 修改状态操作 */
|
||||
const handleStatusChange = async (row: KnowledgeDocumentVO) => {
|
||||
try {
|
||||
|
|
@ -210,7 +197,6 @@ const handleStatusChange = async (row: KnowledgeDocumentVO) => {
|
|||
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到知识库分段页面 */
|
||||
const handleSegment = (id: number) => {
|
||||
router.push({
|
||||
|
|
@ -218,7 +204,6 @@ const handleSegment = (id: number) => {
|
|||
query: { documentId: id }
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
// 如果知识库 ID 不存在,显示错误提示并关闭页面
|
||||
|
|
@ -228,9 +213,8 @@ onMounted(() => {
|
|||
router.push({ name: 'AiKnowledge' })
|
||||
return
|
||||
}
|
||||
|
||||
// 从路由参数中获取知识库 ID
|
||||
queryParams.knowledgeId = route.query.knowledgeId as any
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -72,13 +72,10 @@ import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
|
|||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { ModelApi, ModelVO } from '@/api/ai/model/model'
|
||||
import { AiModelTypeEnum } from '../../utils/constants'
|
||||
|
||||
/** AI 知识库表单 */
|
||||
defineOptions({ name: 'KnowledgeForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
|
@ -101,7 +98,6 @@ const formRules = reactive({
|
|||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
const modelList = ref<ModelVO[]>([]) // 向量模型选项
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
|
|
@ -121,7 +117,6 @@ const open = async (type: string, id?: number) => {
|
|||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
|
|
@ -145,7 +140,6 @@ const submitForm = async () => {
|
|||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
|
|
@ -159,4 +153,4 @@ const resetForm = () => {
|
|||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 知识库" url="https://doc.iocoder.cn/ai/knowledge/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -59,7 +57,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -124,24 +121,19 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<KnowledgeForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
|
||||
import KnowledgeForm from './KnowledgeForm.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
/** AI 知识库列表 */
|
||||
defineOptions({ name: 'Knowledge' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<KnowledgeVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -153,7 +145,6 @@ const queryParams = reactive({
|
|||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -165,25 +156,21 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -196,7 +183,6 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 文档按钮操作 */
|
||||
const router = useRouter()
|
||||
const handleDocument = (id: number) => {
|
||||
|
|
@ -205,7 +191,6 @@ const handleDocument = (id: number) => {
|
|||
query: { knowledgeId: id }
|
||||
})
|
||||
}
|
||||
|
||||
/** 跳转到文档召回测试页面 */
|
||||
const handleRetrieval = (id: number) => {
|
||||
router.push({
|
||||
|
|
@ -213,9 +198,8 @@ const handleRetrieval = (id: number) => {
|
|||
query: { id }
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -37,7 +37,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 右侧召回结果区域 -->
|
||||
<ContentWrap class="flex-1 min-w-300px">
|
||||
<el-empty v-if="loading" description="正在检索中..." />
|
||||
|
|
@ -84,18 +83,15 @@
|
|||
</ContentWrap>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
|
||||
import { KnowledgeApi } from '@/api/ai/knowledge/knowledge'
|
||||
/** 文档召回测试 */
|
||||
defineOptions({ name: 'KnowledgeDocumentRetrieval' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const route = useRoute() // 路由
|
||||
const router = useRouter() // 路由
|
||||
|
||||
const loading = ref(false) // 加载状态
|
||||
const segments = ref<any[]>([]) // 召回结果
|
||||
const queryParams = reactive({
|
||||
|
|
@ -104,17 +100,14 @@ const queryParams = reactive({
|
|||
topK: 10,
|
||||
similarityThreshold: 0.5
|
||||
})
|
||||
|
||||
/** 调用文档召回测试接口 */
|
||||
const getRetrievalResult = async () => {
|
||||
if (!queryParams.content) {
|
||||
message.warning('请输入查询文本')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
segments.value = []
|
||||
|
||||
try {
|
||||
const data = await KnowledgeSegmentApi.searchKnowledgeSegment({
|
||||
knowledgeId: queryParams.id,
|
||||
|
|
@ -129,12 +122,10 @@ const getRetrievalResult = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 展开/收起段落内容 */
|
||||
const toggleExpand = (segment: any) => {
|
||||
segment.expanded = !segment.expanded
|
||||
}
|
||||
|
||||
/** 获取知识库信息 */
|
||||
const getKnowledgeInfo = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -146,7 +137,6 @@ const getKnowledgeInfo = async (id: number) => {
|
|||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
// 如果知识库 ID 不存在,显示错误提示并关闭页面
|
||||
|
|
@ -156,8 +146,7 @@ onMounted(() => {
|
|||
return
|
||||
}
|
||||
queryParams.id = route.query.id as any
|
||||
|
||||
// 获取知识库信息并设置默认值
|
||||
getKnowledgeInfo(queryParams.id as any)
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -24,13 +24,10 @@
|
|||
</template>
|
||||
<script setup lang="ts">
|
||||
import { KnowledgeSegmentApi, KnowledgeSegmentVO } from '@/api/ai/knowledge/segment'
|
||||
|
||||
/** AI 知识库分段表单 */
|
||||
defineOptions({ name: 'KnowledgeSegmentForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
|
@ -44,7 +41,6 @@ const formRules = reactive({
|
|||
content: [{ required: true, message: '切片内容不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number, documentId?: any) => {
|
||||
dialogVisible.value = true
|
||||
|
|
@ -52,7 +48,6 @@ const open = async (type: string, id?: number, documentId?: any) => {
|
|||
formType.value = type
|
||||
resetForm()
|
||||
formData.value.documentId = documentId as any
|
||||
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
|
|
@ -64,7 +59,6 @@ const open = async (type: string, id?: number, documentId?: any) => {
|
|||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
|
|
@ -88,7 +82,6 @@ const submitForm = async () => {
|
|||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
|
|
@ -98,4 +91,4 @@ const resetForm = () => {
|
|||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -46,7 +46,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -131,11 +130,9 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<KnowledgeSegmentForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
|
|
@ -143,15 +140,12 @@ import { KnowledgeSegmentApi, KnowledgeSegmentVO } from '@/api/ai/knowledge/segm
|
|||
import KnowledgeSegmentForm from './KnowledgeSegmentForm.vue'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { checkPermi } from '@/utils/permission'
|
||||
|
||||
/** AI 知识库分段 列表 */
|
||||
defineOptions({ name: 'KnowledgeSegment' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const router = useRouter() // 路由
|
||||
const route = useRoute() // 路由
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<KnowledgeSegmentVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -163,7 +157,6 @@ const queryParams = reactive({
|
|||
status: undefined
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -175,25 +168,21 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id, queryParams.documentId)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -206,7 +195,6 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 修改状态操作 */
|
||||
const handleStatusChange = async (row: KnowledgeSegmentVO) => {
|
||||
try {
|
||||
|
|
@ -224,7 +212,6 @@ const handleStatusChange = async (row: KnowledgeSegmentVO) => {
|
|||
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
// 如果文档 ID 不存在,显示错误提示并关闭页面
|
||||
|
|
@ -234,9 +221,8 @@ onMounted(() => {
|
|||
router.push({ name: 'AiKnowledgeDocument' })
|
||||
return
|
||||
}
|
||||
|
||||
// 从路由参数中获取文档 ID
|
||||
queryParams.documentId = route.query.documentId as any
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -48,10 +48,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MindMapContentExample } from '@/views/ai/utils/constants'
|
||||
|
||||
const emits = defineEmits(['submit', 'directGenerate'])
|
||||
defineProps<{
|
||||
isGenerating: boolean
|
||||
|
|
@ -60,9 +58,7 @@ defineProps<{
|
|||
const formData = reactive({
|
||||
prompt: ''
|
||||
})
|
||||
|
||||
const generatedContent = ref(MindMapContentExample) // 已有的内容
|
||||
|
||||
defineExpose({
|
||||
setGeneratedContent(newContent: string) {
|
||||
// 设置已有的内容,在生成结束的时候将结果赋值给该值
|
||||
|
|
@ -70,9 +66,8 @@ defineExpose({
|
|||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -12,13 +12,11 @@
|
|||
</el-button>
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<div ref="contentRef" class="hide-scroll-bar h-full box-border">
|
||||
<!--展示 markdown 的容器,最终生成的是 html 字符串,直接用 v-html 嵌入-->
|
||||
<div v-if="isGenerating" ref="mdContainerRef" class="wh-full overflow-y-auto">
|
||||
<div class="flex flex-col items-center justify-center" v-html="html"></div>
|
||||
</div>
|
||||
|
||||
<div ref="mindMapRef" class="wh-full">
|
||||
<svg ref="svgRef" :style="{ height: `${contentAreaHeight}px` }" class="w-full" />
|
||||
<div ref="toolBarRef" class="absolute bottom-[10px] right-5"></div>
|
||||
|
|
@ -26,17 +24,14 @@
|
|||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Markmap } from 'markmap-view'
|
||||
import { Transformer } from 'markmap-lib'
|
||||
import { Toolbar } from 'markmap-toolbar'
|
||||
import markdownit from 'markdown-it'
|
||||
import download from '@/utils/download'
|
||||
|
||||
const md = markdownit()
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const props = defineProps<{
|
||||
generatedContent: string // 生成结果
|
||||
isEnd: boolean // 是否结束
|
||||
|
|
@ -52,7 +47,6 @@ const html = ref('') // 生成过程中的文本
|
|||
const contentAreaHeight = ref(0) // 生成区域的高度,出去 header 部分
|
||||
let markMap: Markmap | null = null
|
||||
const transformer = new Transformer()
|
||||
|
||||
onMounted(() => {
|
||||
contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 获取区域高度
|
||||
/** 初始化思维导图 **/
|
||||
|
|
@ -65,7 +59,6 @@ onMounted(() => {
|
|||
message.error('思维导图初始化失败')
|
||||
}
|
||||
})
|
||||
|
||||
watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
|
||||
// 开始生成的时候清空一下 markdown 的内容
|
||||
if (isStart) {
|
||||
|
|
@ -80,7 +73,6 @@ watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
|
|||
update()
|
||||
}
|
||||
})
|
||||
|
||||
/** 更新思维导图的展示 */
|
||||
const update = () => {
|
||||
try {
|
||||
|
|
@ -91,7 +83,6 @@ const update = () => {
|
|||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理内容 */
|
||||
const processContent = (text: string) => {
|
||||
const arr: string[] = []
|
||||
|
|
@ -105,7 +96,6 @@ const processContent = (text: string) => {
|
|||
}
|
||||
return arr.join('\n')
|
||||
}
|
||||
|
||||
/** 下载图片:download SVG to png file */
|
||||
const downloadImage = () => {
|
||||
const svgElement = mindMapRef.value
|
||||
|
|
@ -120,7 +110,6 @@ const downloadImage = () => {
|
|||
drawWithImageSize: false
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
scrollBottom() {
|
||||
mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight)
|
||||
|
|
@ -131,17 +120,14 @@ defineExpose({
|
|||
.hide-scroll-bar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.my-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
box-sizing: border-box;
|
||||
flex-grow: 1;
|
||||
|
|
@ -150,18 +136,15 @@ defineExpose({
|
|||
@extend .hide-scroll-bar;
|
||||
}
|
||||
}
|
||||
|
||||
// markmap的tool样式覆盖
|
||||
:deep(.markmap) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.mm-toolbar-brand) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.mm-toolbar) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -17,13 +17,11 @@
|
|||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Left from './components/Left.vue'
|
||||
import Right from './components/Right.vue'
|
||||
import { AiMindMapApi, AiMindMapGenerateReqVO } from '@/api/ai/mindmap'
|
||||
import { MindMapContentExample } from '@/views/ai/utils/constants'
|
||||
|
||||
defineOptions({
|
||||
name: 'AiMindMap'
|
||||
})
|
||||
|
|
@ -32,26 +30,21 @@ const isGenerating = ref(false) // 是否正在生成思维导图
|
|||
const isStart = ref(false) // 开始生成,用来清空思维导图
|
||||
const isEnd = ref(true) // 用来判断结束的时候渲染思维导图
|
||||
const message = useMessage() // 消息提示
|
||||
|
||||
const generatedContent = ref('') // 生成思维导图结果
|
||||
|
||||
const leftRef = ref<InstanceType<typeof Left>>() // 左边组件
|
||||
const rightRef = ref<InstanceType<typeof Right>>() // 右边组件
|
||||
|
||||
/** 使用已有内容直接生成 **/
|
||||
const directGenerate = (existPrompt: string) => {
|
||||
isEnd.value = false // 先设置为 false 再设置为 true,让子组建的 watch 能够监听到
|
||||
generatedContent.value = existPrompt
|
||||
isEnd.value = true
|
||||
}
|
||||
|
||||
/** 停止 stream 生成 */
|
||||
const stopStream = () => {
|
||||
isGenerating.value = false
|
||||
isStart.value = false
|
||||
ctrl.value?.abort()
|
||||
}
|
||||
|
||||
/** 提交生成 */
|
||||
const submit = (data: AiMindMapGenerateReqVO) => {
|
||||
isGenerating.value = true
|
||||
|
|
@ -86,9 +79,8 @@ const submit = (data: AiMindMapGenerateReqVO) => {
|
|||
ctrl: ctrl.value
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
generatedContent.value = MindMapContentExample
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 思维导图" url="https://doc.iocoder.cn/ai/mindmap/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -51,7 +49,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -94,7 +91,6 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 思维导图的预览 -->
|
||||
<el-drawer v-model="previewVisible" :with-header="false" size="800px">
|
||||
<Right
|
||||
|
|
@ -106,19 +102,15 @@
|
|||
/>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { AiMindMapApi, MindMapVO } from '@/api/ai/mindmap'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import Right from '@/views/ai/mindmap/index/components/Right.vue'
|
||||
|
||||
/** AI 思维导图 列表 */
|
||||
defineOptions({ name: 'AiMindMapManager' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<MindMapVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -131,7 +123,6 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -143,19 +134,16 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -168,7 +156,6 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 预览操作按钮 */
|
||||
const previewVisible = ref(false) // drawer 的显示隐藏
|
||||
const previewVisible2 = ref(false) // right 的显示隐藏
|
||||
|
|
@ -181,11 +168,10 @@ const openPreview = async (row: MindMapVO) => {
|
|||
previewVisible2.value = true
|
||||
previewContent.value = row.generatedContent
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -48,13 +48,10 @@
|
|||
import { getIntDictOptions, DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
||||
import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
/** AI API 密钥 表单 */
|
||||
defineOptions({ name: 'ApiKeyForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
|
@ -74,7 +71,6 @@ const formRules = reactive({
|
|||
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
|
|
@ -92,7 +88,6 @@ const open = async (type: string, id?: number) => {
|
|||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
|
|
@ -116,7 +111,6 @@ const submitForm = async () => {
|
|||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
|
|
@ -129,4 +123,4 @@ const resetForm = () => {
|
|||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -58,7 +56,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -104,22 +101,17 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ApiKeyForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
||||
import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
||||
import ApiKeyForm from './ApiKeyForm.vue'
|
||||
|
||||
/** AI API 密钥 列表 */
|
||||
defineOptions({ name: 'AiApiKey' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ApiKeyVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -131,7 +123,6 @@ const queryParams = reactive({
|
|||
status: undefined
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -143,25 +134,21 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -174,9 +161,8 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -98,13 +98,10 @@ import { FormRules } from 'element-plus'
|
|||
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
|
||||
import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
|
||||
import { ToolApi, ToolVO } from '@/api/ai/model/tool'
|
||||
|
||||
/** AI 聊天角色 表单 */
|
||||
defineOptions({ name: 'ChatRoleForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
|
@ -128,12 +125,10 @@ const formRef = ref() // 表单 Ref
|
|||
const models = ref([] as ModelVO[]) // 聊天模型列表
|
||||
const knowledgeList = ref([] as KnowledgeVO[]) // 知识库列表
|
||||
const toolList = ref([] as ToolVO[]) // 工具列表
|
||||
|
||||
/** 是否【我】自己创建,私有角色 */
|
||||
const isUser = computed(() => {
|
||||
return formType.value === 'my-create' || formType.value === 'my-update'
|
||||
})
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '角色头像不能为空', trigger: 'blur' }],
|
||||
|
|
@ -143,7 +138,6 @@ const formRules = reactive<FormRules>({
|
|||
systemMessage: [{ required: true, message: '角色设定不能为空', trigger: 'blur' }],
|
||||
publicStatus: [{ required: true, message: '是否公开不能为空', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/** 打开弹窗 */
|
||||
// TODO @fan:title 是不是收敛到 type 判断生成 title,会更合理
|
||||
const open = async (type: string, id?: number, title?: string) => {
|
||||
|
|
@ -168,7 +162,6 @@ const open = async (type: string, id?: number, title?: string) => {
|
|||
toolList.value = await ToolApi.getToolSimpleList()
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
|
|
@ -200,7 +193,6 @@ const submitForm = async () => {
|
|||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
|
|
@ -220,4 +212,4 @@ const resetForm = () => {
|
|||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -57,7 +55,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -123,22 +120,17 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ChatRoleForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { ChatRoleApi, ChatRoleVO } from '@/api/ai/model/chatRole'
|
||||
import ChatRoleForm from './ChatRoleForm.vue'
|
||||
|
||||
/** AI 聊天角色 列表 */
|
||||
defineOptions({ name: 'AiChatRole' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ChatRoleVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -150,7 +142,6 @@ const queryParams = reactive({
|
|||
publicStatus: true
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -162,25 +153,21 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -193,9 +180,8 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -115,13 +115,10 @@ import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
|||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
/** API 模型的表单 */
|
||||
defineOptions({ name: 'ModelForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
|
@ -153,7 +150,6 @@ const formRules = reactive({
|
|||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
|
|
@ -173,7 +169,6 @@ const open = async (type: string, id?: number) => {
|
|||
apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
|
|
@ -202,7 +197,6 @@ const submitForm = async () => {
|
|||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
|
|
@ -220,4 +214,4 @@ const resetForm = () => {
|
|||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -51,7 +49,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -110,23 +107,18 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ModelForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ModelApi, ModelVO } from '@/api/ai/model/model'
|
||||
import ModelForm from './ModelForm.vue'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
||||
|
||||
/** API 模型列表 */
|
||||
defineOptions({ name: 'AiModel' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ModelVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -139,7 +131,6 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -151,25 +142,21 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -182,11 +169,10 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
await getList()
|
||||
// 获得下拉数据
|
||||
apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -35,13 +35,10 @@
|
|||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { ToolApi, ToolVO } from '@/api/ai/model/tool'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
/** AI 工具表单 */
|
||||
defineOptions({ name: 'ToolForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
|
@ -56,7 +53,6 @@ const formRules = reactive({
|
|||
name: [{ required: true, message: '工具名称不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
|
|
@ -74,7 +70,6 @@ const open = async (type: string, id?: number) => {
|
|||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
|
|
@ -98,7 +93,6 @@ const submitForm = async () => {
|
|||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
|
|
@ -109,4 +103,4 @@ const resetForm = () => {
|
|||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 工具调用(function calling)" url="https://doc.iocoder.cn/ai/tool/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -49,7 +47,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -97,23 +94,18 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ToolForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { ToolApi, ToolVO } from '@/api/ai/model/tool'
|
||||
import ToolForm from './ToolForm.vue'
|
||||
|
||||
/** AI 工具 列表 */
|
||||
defineOptions({ name: 'AiTool' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ToolVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -127,7 +119,6 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -139,25 +130,21 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -170,9 +157,8 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -6,15 +6,11 @@
|
|||
<List ref="listRef" class="flex-auto"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Mode from './mode/index.vue'
|
||||
import List from './list/index.vue'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const listRef = ref<Nullable<{generateMusic: (...args) => void}>>(null)
|
||||
|
||||
/*
|
||||
*@Description: 拿到左侧配置信息调用右侧音乐生成的方法
|
||||
*@MethodAuthor: xiaohong
|
||||
|
|
@ -23,4 +19,4 @@ const listRef = ref<Nullable<{generateMusic: (...args) => void}>>(null)
|
|||
function generateMusic (args: {formData: Recordable}) {
|
||||
unref(listRef)?.generateMusic(args.formData)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
<div class="text-[12px] text-gray-400">{{currentSong.singer}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 音频controls -->
|
||||
<div class="flex gap-[12px] items-center">
|
||||
<Icon icon="majesticons:back-circle" :size="20" class="text-gray-300 cursor-pointer"/>
|
||||
|
|
@ -24,7 +23,6 @@
|
|||
<source :src="audioUrl"/>
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<!-- 音量控制器 -->
|
||||
<div class="flex gap-[16px] items-center">
|
||||
<Icon :icon="audioProps.muted ? 'tabler:volume-off' : 'tabler:volume'" :size="20" class="cursor-pointer" @click="toggleStatus('muted')"/>
|
||||
|
|
@ -32,15 +30,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { formatPast } from '@/utils/formatTime'
|
||||
import audioUrl from '@/assets/audio/response.mp3'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const currentSong = inject('currentSong', {})
|
||||
|
||||
const audioRef = ref<Nullable<HTMLElement>>(null)
|
||||
// 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
||||
const audioProps = reactive({
|
||||
|
|
@ -51,7 +45,6 @@ const audioProps = reactive({
|
|||
muted: false,
|
||||
volume: 50,
|
||||
})
|
||||
|
||||
function toggleStatus (type: string) {
|
||||
audioProps[type] = !audioProps[type]
|
||||
if (type === 'paused' && audioRef.value) {
|
||||
|
|
@ -62,9 +55,8 @@ function toggleStatus (type: string) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新播放位置
|
||||
function audioTimeUpdate (args) {
|
||||
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss')
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -11,7 +11,6 @@
|
|||
</el-row>
|
||||
<el-empty v-else description="暂无音乐"/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 试听广场 -->
|
||||
<el-tab-pane v-loading="loading" label="试听广场" name="square">
|
||||
<el-row v-if="squareSongList.length" v-loading="loading" :gutter="12">
|
||||
|
|
@ -28,26 +27,19 @@
|
|||
<audioBar class="flex-none"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import songCard from './songCard/index.vue'
|
||||
import songInfo from './songInfo/index.vue'
|
||||
import audioBar from './audioBar/index.vue'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
|
||||
const currentType = ref('mine')
|
||||
// loading 状态
|
||||
const loading = ref(false)
|
||||
// 当前音乐
|
||||
const currentSong = ref({})
|
||||
|
||||
const mySongList = ref<Recordable[]>([])
|
||||
const squareSongList = ref<Recordable[]>([])
|
||||
|
||||
provide('currentSong', currentSong)
|
||||
|
||||
/*
|
||||
*@Description: 调接口生成音乐列表
|
||||
*@MethodAuthor: xiaohong
|
||||
|
|
@ -80,7 +72,6 @@ function generateMusic (formData: Recordable) {
|
|||
loading.value = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
/*
|
||||
*@Description: 设置当前播放的音乐
|
||||
*@MethodAuthor: xiaohong
|
||||
|
|
@ -89,13 +80,10 @@ function generateMusic (formData: Recordable) {
|
|||
function setCurrentSong (music: Recordable) {
|
||||
currentSong.value = music
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
generateMusic
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
display: flex;
|
||||
|
|
@ -105,4 +93,4 @@ defineExpose({
|
|||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -14,23 +14,17 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
defineProps({
|
||||
songInfo: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['play'])
|
||||
|
||||
const currentSong = inject('currentSong', {})
|
||||
|
||||
function playSong () {
|
||||
emits('play')
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -12,11 +12,7 @@
|
|||
<div class="text-[var(--el-text-color-secondary)] text-12px" v-html="currentSong.lyric"></div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const currentSong = inject('currentSong', {})
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -11,13 +11,11 @@
|
|||
placeholder="一首关于糟糕分手的欢快歌曲"
|
||||
/>
|
||||
</Title>
|
||||
|
||||
<Title title="纯音乐" desc="创建一首没有歌词的歌曲">
|
||||
<template #extra>
|
||||
<el-switch v-model="formData.pure" size="small"/>
|
||||
</template>
|
||||
</Title>
|
||||
|
||||
<Title title="版本" desc="描述您想要的音乐风格和主题,使用流派和氛围而不是特定的艺术家和歌曲">
|
||||
<el-select v-model="formData.version" placeholder="请选择">
|
||||
<el-option
|
||||
|
|
@ -36,20 +34,15 @@
|
|||
</Title>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Title from '../title/index.vue'
|
||||
|
||||
defineOptions({ name: 'Desc' })
|
||||
|
||||
const formData = reactive({
|
||||
desc: '',
|
||||
pure: false,
|
||||
version: '3'
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
formData
|
||||
})
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -4,26 +4,18 @@
|
|||
<el-radio-button value="desc"> 描述模式 </el-radio-button>
|
||||
<el-radio-button value="lyric"> 歌词模式 </el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<!-- 描述模式/歌词模式 切换 -->
|
||||
<component :is="generateMode === 'desc' ? desc : lyric" ref="modeRef" />
|
||||
|
||||
<el-button type="primary" round class="w-full" @click="generateMusic"> 创作音乐 </el-button>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import desc from './desc.vue'
|
||||
import lyric from './lyric.vue'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const emits = defineEmits(['generate-music'])
|
||||
|
||||
const generateMode = ref('lyric')
|
||||
|
||||
const modeRef = ref<Nullable<{ formData: Recordable }>>(null)
|
||||
|
||||
/*
|
||||
*@Description: 根据信息生成音乐
|
||||
*@MethodAuthor: xiaohong
|
||||
|
|
@ -32,4 +24,4 @@ const modeRef = ref<Nullable<{ formData: Recordable }>>(null)
|
|||
function generateMusic() {
|
||||
emits('generate-music', { formData: unref(modeRef)?.formData })
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -11,12 +11,10 @@
|
|||
placeholder="请输入您自己的歌词"
|
||||
/>
|
||||
</Title>
|
||||
|
||||
<Title title="音乐风格">
|
||||
<el-space class="flex-wrap">
|
||||
<el-tag v-for="tag in tags" :key="tag" round class="mb-8px">{{tag}}</el-tag>
|
||||
</el-space>
|
||||
|
||||
<el-button
|
||||
:type="showCustom ? 'primary': 'default'"
|
||||
round
|
||||
|
|
@ -26,7 +24,6 @@
|
|||
>自定义风格
|
||||
</el-button>
|
||||
</Title>
|
||||
|
||||
<Title v-show="showCustom" desc="描述您想要的音乐风格,Suno无法识别艺术家的名字,但可以理解流派和氛围" class="-mt-12px">
|
||||
<el-input
|
||||
v-model="formData.style"
|
||||
|
|
@ -38,11 +35,9 @@
|
|||
placeholder="输入音乐风格(英文)"
|
||||
/>
|
||||
</Title>
|
||||
|
||||
<Title title="音乐/歌曲名称">
|
||||
<el-input v-model="formData.name" placeholder="请输入音乐/歌曲名称"/>
|
||||
</Title>
|
||||
|
||||
<Title title="版本">
|
||||
<el-select v-model="formData.version" placeholder="请选择">
|
||||
<el-option
|
||||
|
|
@ -61,23 +56,18 @@
|
|||
</Title>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Title from '../title/index.vue'
|
||||
defineOptions({ name: 'Lyric' })
|
||||
|
||||
const tags = ['rock', 'punk', 'jazz', 'soul', 'country', 'kidsmusic', 'pop']
|
||||
|
||||
const showCustom = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
lyric: '',
|
||||
style: '',
|
||||
name: '',
|
||||
version: ''
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
formData
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -10,10 +10,8 @@
|
|||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String
|
||||
|
|
@ -22,4 +20,4 @@ defineProps({
|
|||
type: String
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 音乐创作" url="https://doc.iocoder.cn/ai/music/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -96,7 +94,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -201,20 +198,16 @@
|
|||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { MusicApi, MusicVO } from '@/api/ai/music'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { AiMusicStatusEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
/** AI 音乐 列表 */
|
||||
defineOptions({ name: 'AiMusicManager' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<MusicVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -230,7 +223,6 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -242,19 +234,16 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -267,7 +256,6 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 修改是否发布 */
|
||||
const handleUpdatePublicStatusChange = async (row: MusicVO) => {
|
||||
try {
|
||||
|
|
@ -284,11 +272,10 @@ const handleUpdatePublicStatusChange = async (row: MusicVO) => {
|
|||
row.publicStatus = !row.publicStatus
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -34,16 +34,13 @@
|
|||
<script lang="ts" setup>
|
||||
import { FormRules } from 'element-plus'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
|
||||
const modelData = defineModel<any>()
|
||||
|
||||
const formRef = ref() // 表单 Ref
|
||||
const formRules = reactive<FormRules>({
|
||||
code: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
|
||||
})
|
||||
|
||||
/** 表单校验 */
|
||||
const validate = async () => {
|
||||
await formRef.value?.validate()
|
||||
|
|
@ -51,4 +48,4 @@ const validate = async () => {
|
|||
defineExpose({
|
||||
validate
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -13,7 +13,6 @@
|
|||
测试
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 测试窗口 -->
|
||||
<el-drawer v-model="showTestDrawer" title="工作流测试" :modal="false">
|
||||
<fieldset>
|
||||
|
|
@ -59,17 +58,14 @@
|
|||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
|
||||
import * as WorkflowApi from '@/api/ai/workflow'
|
||||
// TODO @lesan:要不使用 ICon 哪个组件哈
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
|
||||
defineProps<{
|
||||
provider: any
|
||||
}>()
|
||||
|
||||
const tinyflowRef = ref()
|
||||
const workflowData = inject('workflowData') as Ref
|
||||
const showTestDrawer = ref(false)
|
||||
|
|
@ -78,12 +74,10 @@ const paramsOfStartNode = ref({})
|
|||
const testResult = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
/** 展示工作流测试抽屉 */
|
||||
const testWorkflowModel = () => {
|
||||
showTestDrawer.value = !showTestDrawer.value
|
||||
}
|
||||
|
||||
/** 运行流程 */
|
||||
const goRun = async () => {
|
||||
try {
|
||||
|
|
@ -93,37 +87,31 @@ const goRun = async () => {
|
|||
testResult.value = null
|
||||
/// 查找start节点
|
||||
const startNode = getStartNode()
|
||||
|
||||
// 获取参数定义
|
||||
const parameters = startNode.data?.parameters || []
|
||||
const paramDefinitions = {}
|
||||
parameters.forEach((param) => {
|
||||
paramDefinitions[param.name] = param.dataType
|
||||
})
|
||||
|
||||
// 参数类型转换
|
||||
const convertedParams = {}
|
||||
for (const { key, value } of params4Test.value) {
|
||||
const paramKey = key.trim()
|
||||
if (!paramKey) continue
|
||||
|
||||
let dataType = paramDefinitions[paramKey]
|
||||
if (!dataType) {
|
||||
dataType = 'String'
|
||||
}
|
||||
|
||||
try {
|
||||
convertedParams[paramKey] = convertParamValue(value, dataType)
|
||||
} catch (e) {
|
||||
throw new Error(`参数 ${paramKey} 转换失败: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
graph: JSON.stringify(val),
|
||||
params: convertedParams
|
||||
}
|
||||
|
||||
const response = await WorkflowApi.testWorkflow(data)
|
||||
testResult.value = response
|
||||
} catch (err) {
|
||||
|
|
@ -132,31 +120,24 @@ const goRun = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听测试抽屉的开启,获取开始节点参数列表 */
|
||||
watch(showTestDrawer, (value) => {
|
||||
if (!value) return
|
||||
|
||||
/// 查找start节点
|
||||
const startNode = getStartNode()
|
||||
|
||||
// 获取参数定义
|
||||
const parameters = startNode.data?.parameters || []
|
||||
const paramDefinitions = {}
|
||||
|
||||
// 加入参数选项方便用户添加非必须参数
|
||||
parameters.forEach((param) => {
|
||||
paramDefinitions[param.name] = param
|
||||
})
|
||||
|
||||
function mergeIfRequiredButNotSet(target) {
|
||||
let needPushList = []
|
||||
for (let key in paramDefinitions) {
|
||||
let param = paramDefinitions[key]
|
||||
|
||||
if (param.required) {
|
||||
let item = target.find((item) => item.key === key)
|
||||
|
||||
if (!item) {
|
||||
needPushList.push({ key: param.name, value: param.defaultValue || '' })
|
||||
}
|
||||
|
|
@ -166,10 +147,8 @@ watch(showTestDrawer, (value) => {
|
|||
}
|
||||
// 自动装载需必填的参数
|
||||
mergeIfRequiredButNotSet(params4Test.value)
|
||||
|
||||
paramsOfStartNode.value = paramDefinitions
|
||||
})
|
||||
|
||||
/** 获取开始节点 */
|
||||
const getStartNode = () => {
|
||||
const val = tinyflowRef.value.getData()
|
||||
|
|
@ -179,21 +158,17 @@ const getStartNode = () => {
|
|||
}
|
||||
return startNode
|
||||
}
|
||||
|
||||
/** 添加参数项 */
|
||||
const addParam = () => {
|
||||
params4Test.value.push({ key: '', value: '' })
|
||||
}
|
||||
|
||||
/** 删除参数项 */
|
||||
const removeParam = (index) => {
|
||||
params4Test.value.splice(index, 1)
|
||||
}
|
||||
|
||||
/** 类型转换函数 */
|
||||
const convertParamValue = (value, dataType) => {
|
||||
if (value === '') return null // 空值处理
|
||||
|
||||
switch (dataType) {
|
||||
case 'String':
|
||||
return String(value)
|
||||
|
|
@ -216,7 +191,6 @@ const convertParamValue = (value, dataType) => {
|
|||
throw new Error(`不支持的类型: ${dataType}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 表单校验 */
|
||||
const validate = async () => {
|
||||
try {
|
||||
|
|
@ -234,7 +208,6 @@ defineExpose({
|
|||
validate
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.result-content {
|
||||
background: white;
|
||||
|
|
@ -247,4 +220,4 @@ defineExpose({
|
|||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
{{ formData.name || '创建流程' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 步骤条 -->
|
||||
<div class="flex-1 flex items-center justify-center h-full">
|
||||
<div class="w-400px flex items-center justify-between h-full">
|
||||
|
|
@ -41,20 +40,17 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧按钮 -->
|
||||
<div class="w-200px flex items-center justify-end gap-2">
|
||||
<el-button type="primary" @click="handleSave"> 保 存 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="mt-50px">
|
||||
<!-- 第一步:基本信息 -->
|
||||
<div v-if="currentStep === 0" class="mx-auto w-560px">
|
||||
<BasicInfo v-model="formData" ref="basicInfoRef" />
|
||||
</div>
|
||||
|
||||
<!-- 第二步:工作流设计 -->
|
||||
<WorkflowDesign
|
||||
v-if="currentStep === 1"
|
||||
|
|
@ -66,7 +62,6 @@
|
|||
</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
|
@ -75,28 +70,23 @@ import BasicInfo from './BasicInfo.vue'
|
|||
import WorkflowDesign from './WorkflowDesign.vue'
|
||||
import { ModelApi } from '@/api/ai/model/model'
|
||||
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
const router = useRouter()
|
||||
const { delView } = useTagsViewStore()
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
|
||||
const basicInfoRef = ref()
|
||||
const workflowDesignRef = ref()
|
||||
|
||||
const validateBasic = async () => {
|
||||
await basicInfoRef.value?.validate()
|
||||
}
|
||||
const validateWorkflow = async () => {
|
||||
await workflowDesignRef.value?.validate()
|
||||
}
|
||||
|
||||
const currentStep = ref(-1)
|
||||
const steps = [
|
||||
{ title: '基本信息', validator: validateBasic },
|
||||
{ title: '工作流设计', validator: validateWorkflow }
|
||||
]
|
||||
|
||||
const formData: any = ref({
|
||||
id: undefined,
|
||||
name: '',
|
||||
|
|
@ -108,7 +98,6 @@ const formData: any = ref({
|
|||
const llmProvider = ref<any>([])
|
||||
const workflowData = ref<any>({})
|
||||
provide('workflowData', workflowData)
|
||||
|
||||
/** 初始化数据 */
|
||||
const actionType = route.params.type as string
|
||||
const initData = async () => {
|
||||
|
|
@ -118,7 +107,6 @@ const initData = async () => {
|
|||
formData.value = await WorkflowApi.getWorkflow(workflowId)
|
||||
workflowData.value = JSON.parse(formData.value.graph)
|
||||
}
|
||||
|
||||
// 加载模型列表
|
||||
const models = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
|
||||
llmProvider.value = {
|
||||
|
|
@ -132,11 +120,9 @@ const initData = async () => {
|
|||
}
|
||||
// TODO @lesan:知识库(可以看下 knowledge)
|
||||
// TODO @lesan:搜索引擎(这个之前有个 pr 搞了,,,可能来接下)
|
||||
|
||||
// 设置当前步骤
|
||||
currentStep.value = 0
|
||||
}
|
||||
|
||||
/** 校验所有步骤数据是否完整 */
|
||||
const validateAllSteps = async () => {
|
||||
try {
|
||||
|
|
@ -147,7 +133,6 @@ const validateAllSteps = async () => {
|
|||
currentStep.value = 0
|
||||
throw new Error('请完善基本信息')
|
||||
}
|
||||
|
||||
// 工作流设计校验
|
||||
try {
|
||||
await validateWorkflow()
|
||||
|
|
@ -160,13 +145,11 @@ const validateAllSteps = async () => {
|
|||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存操作 */
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 保存前校验所有步骤的数据
|
||||
await validateAllSteps()
|
||||
|
||||
// 更新表单数据
|
||||
const data = {
|
||||
...formData.value,
|
||||
|
|
@ -177,7 +160,6 @@ const handleSave = async () => {
|
|||
} else {
|
||||
await WorkflowApi.createWorkflow(data)
|
||||
}
|
||||
|
||||
// 保存成功,提示并跳转到列表页
|
||||
message.success('保存成功')
|
||||
delView(unref(router.currentRoute))
|
||||
|
|
@ -187,7 +169,6 @@ const handleSave = async () => {
|
|||
message.warning(error.message || '请完善所有步骤的必填信息')
|
||||
}
|
||||
}
|
||||
|
||||
/** 步骤切换处理 */
|
||||
const handleStepClick = async (index: number) => {
|
||||
try {
|
||||
|
|
@ -197,7 +178,6 @@ const handleStepClick = async (index: number) => {
|
|||
if (index !== 1) {
|
||||
await validateWorkflow()
|
||||
}
|
||||
|
||||
// 切换步骤
|
||||
currentStep.value = index
|
||||
} catch (error) {
|
||||
|
|
@ -205,7 +185,6 @@ const handleStepClick = async (index: number) => {
|
|||
message.warning('请先完善当前步骤必填信息')
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表页 */
|
||||
const handleBack = () => {
|
||||
// 先删除当前页签
|
||||
|
|
@ -213,28 +192,23 @@ const handleBack = () => {
|
|||
// 跳转到列表页
|
||||
router.push({ name: 'AiWorkflow' })
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await initData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- TODO @lesan:可以用 cursor 搞成 unocss 哈 -->
|
||||
<style lang="scss" scoped>
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #3473ff;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: #3473ff;
|
||||
}
|
||||
|
||||
.border-primary {
|
||||
border-color: #3473ff;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -61,7 +61,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -109,21 +108,16 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 添加或修改工作流对话框 -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import * as WorkflowApi from '@/api/ai/workflow'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
|
||||
defineOptions({ name: 'AiWorkflow' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
const { push } = useRouter() // 路由
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -136,7 +130,6 @@ const queryParams = reactive({
|
|||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -148,19 +141,16 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -173,7 +163,6 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const openForm = async (type: string, id?: number) => {
|
||||
if (type === 'create') {
|
||||
|
|
@ -185,9 +174,8 @@ const openForm = async (type: string, id?: number) => {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -23,7 +23,6 @@
|
|||
</span>
|
||||
</h3>
|
||||
</DefineLabel>
|
||||
|
||||
<div class="flex flex-col" v-bind="$attrs">
|
||||
<!-- tab -->
|
||||
<div class="w-full pt-2 bg-[#f5f7f9] flex justify-center">
|
||||
|
|
@ -59,7 +58,6 @@
|
|||
type="textarea"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<ReuseLabel :hint-click="() => example('reply')" hint="示例" label="原文" />
|
||||
<el-input
|
||||
|
|
@ -70,7 +68,6 @@
|
|||
showWordLimit
|
||||
type="textarea"
|
||||
/>
|
||||
|
||||
<ReuseLabel label="回复内容" />
|
||||
<el-input
|
||||
v-model="formData.prompt"
|
||||
|
|
@ -81,7 +78,6 @@
|
|||
type="textarea"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ReuseLabel label="长度" />
|
||||
<Tag v-model="formData.length" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LENGTH)" />
|
||||
<ReuseLabel label="格式" />
|
||||
|
|
@ -90,7 +86,6 @@
|
|||
<Tag v-model="formData.tone" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_TONE)" />
|
||||
<ReuseLabel label="语言" />
|
||||
<Tag v-model="formData.language" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LANGUAGE)" />
|
||||
|
||||
<div class="flex items-center justify-center mt-3">
|
||||
<el-button :disabled="isWriting" @click="reset">重置</el-button>
|
||||
<el-button :loading="isWriting" color="#846af7" @click="submit">生成</el-button>
|
||||
|
|
@ -99,7 +94,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createReusableTemplate } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
|
|
@ -108,21 +102,16 @@ import { WriteVO } from '@/api/ai/write'
|
|||
import { omit } from 'lodash-es'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
|
||||
|
||||
type TabType = WriteVO['type']
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
defineProps<{
|
||||
isWriting: boolean
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'submit', params: Partial<WriteVO>)
|
||||
(e: 'example', param: 'write' | 'reply')
|
||||
(e: 'reset')
|
||||
}>()
|
||||
|
||||
/** 点击示例的时候,将定义好的文章作为示例展示出来 **/
|
||||
const example = (type: 'write' | 'reply') => {
|
||||
formData.value = {
|
||||
|
|
@ -131,13 +120,11 @@ const example = (type: 'write' | 'reply') => {
|
|||
}
|
||||
emits('example', type)
|
||||
}
|
||||
|
||||
/** 重置,将表单值作为初选值 **/
|
||||
const reset = () => {
|
||||
formData.value = { ...initData }
|
||||
emits('reset')
|
||||
}
|
||||
|
||||
const selectedTab = ref<TabType>(AiWriteTypeEnum.WRITING)
|
||||
const tabs: {
|
||||
text: string
|
||||
|
|
@ -151,7 +138,6 @@ const [DefineTab, ReuseTab] = createReusableTemplate<{
|
|||
text: string
|
||||
itemClick: () => void
|
||||
}>()
|
||||
|
||||
/**
|
||||
* 可以在 template 里边定义可复用的组件,DefineLabel,ReuseLabel 是采用的解构赋值,都是 Vue 组件
|
||||
*
|
||||
|
|
@ -167,7 +153,6 @@ const [DefineLabel, ReuseLabel] = createReusableTemplate<{
|
|||
hint?: string
|
||||
hintClick?: () => void
|
||||
}>()
|
||||
|
||||
const initData: WriteVO = {
|
||||
type: 1,
|
||||
prompt: '',
|
||||
|
|
@ -178,10 +163,8 @@ const initData: WriteVO = {
|
|||
format: 1
|
||||
}
|
||||
const formData = ref<WriteVO>({ ...initData })
|
||||
|
||||
/** 用来记录切换之前所填写的数据,切换的时候给赋值回来 **/
|
||||
const recordFormData = {} as Record<AiWriteTypeEnum, WriteVO>
|
||||
|
||||
/** 切换tab **/
|
||||
const switchTab = (value: TabType) => {
|
||||
if (value !== selectedTab.value) {
|
||||
|
|
@ -192,7 +175,6 @@ const switchTab = (value: TabType) => {
|
|||
formData.value = { ...initData, ...recordFormData[value] }
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交写作 */
|
||||
const submit = () => {
|
||||
if (selectedTab.value === 2 && !formData.value.originalContent) {
|
||||
|
|
@ -210,4 +192,4 @@ const submit = () => {
|
|||
type: selectedTab.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
</el-button>
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<div ref="contentRef" class="hide-scroll-bar h-full box-border overflow-y-auto">
|
||||
<div class="w-full min-h-full relative flex-grow bg-white box-border p-3 sm:p-7">
|
||||
<!-- 终止生成内容的按钮 -->
|
||||
|
|
@ -40,13 +39,10 @@
|
|||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { copied, copy } = useClipboard({ legacy: true }) // 粘贴板
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
// 生成的结果
|
||||
|
|
@ -59,9 +55,7 @@ const props = defineProps({
|
|||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:content', 'stopStream'])
|
||||
|
||||
/** 通过计算属性,双向绑定,更改生成的内容,考虑到用户想要更改生成文章的情况 */
|
||||
const compContent = computed({
|
||||
get() {
|
||||
|
|
@ -71,7 +65,6 @@ const compContent = computed({
|
|||
emits('update:content', val)
|
||||
}
|
||||
})
|
||||
|
||||
/** 滚动 */
|
||||
const contentRef = ref<HTMLDivElement>()
|
||||
defineExpose({
|
||||
|
|
@ -79,13 +72,11 @@ defineExpose({
|
|||
contentRef.value?.scrollTo(0, contentRef.value?.scrollHeight)
|
||||
}
|
||||
})
|
||||
|
||||
/** 点击复制的时候复制内容 */
|
||||
const showCopy = computed(() => props.content && !props.isWriting) // 是否展示复制按钮,在生成内容完成的时候展示
|
||||
const copyContent = () => {
|
||||
copy(props.content)
|
||||
}
|
||||
|
||||
/** 复制成功的时候 copied.value 为 true */
|
||||
watch(copied, (val) => {
|
||||
if (val) {
|
||||
|
|
@ -93,22 +84,18 @@ watch(copied, (val) => {
|
|||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hide-scroll-bar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.my-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
box-sizing: border-box;
|
||||
flex-grow: 1;
|
||||
|
|
@ -117,4 +104,4 @@ watch(copied, (val) => {
|
|||
@extend .hide-scroll-bar;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
@ -24,8 +23,7 @@ const props = withDefaults(
|
|||
tags: () => []
|
||||
}
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -16,25 +16,20 @@
|
|||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Left from './components/Left.vue'
|
||||
import Right from './components/Right.vue'
|
||||
import { WriteApi, WriteVO } from '@/api/ai/write'
|
||||
import { WriteExample } from '@/views/ai/utils/constants'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const writeResult = ref('') // 写作结果
|
||||
const isWriting = ref(false) // 是否正在写作中
|
||||
const abortController = ref<AbortController>() // // 写作进行中 abort 控制器(控制 stream 写作)
|
||||
|
||||
/** 停止 stream 生成 */
|
||||
const stopStream = () => {
|
||||
abortController.value?.abort()
|
||||
isWriting.value = false
|
||||
}
|
||||
|
||||
/** 执行写作 */
|
||||
const rightRef = ref<InstanceType<typeof Right>>()
|
||||
const submit = (data: WriteVO) => {
|
||||
|
|
@ -65,14 +60,12 @@ const submit = (data: WriteVO) => {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 点击示例触发 */
|
||||
const handleExampleClick = (type: keyof typeof WriteExample) => {
|
||||
writeResult.value = WriteExample[type].data
|
||||
}
|
||||
|
||||
/** 点击重置的时候清空写作的结果**/
|
||||
const reset = () => {
|
||||
writeResult.value = ''
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="AI 写作助手" url="https://doc.iocoder.cn/ai/write/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -72,7 +70,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -152,21 +149,17 @@
|
|||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { WriteApi, AiWritePageReqVO, AiWriteRespVo } from '@/api/ai/write'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
/** AI 写作列表 */
|
||||
defineOptions({ name: 'AiWriteManager' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
const router = useRouter() // 路由
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<AiWriteRespVo[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -180,7 +173,6 @@ const queryParams = reactive<AiWritePageReqVO>({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -192,19 +184,16 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -217,11 +206,10 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -46,13 +46,10 @@
|
|||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
/** BPM 流程分类 表单 */
|
||||
defineOptions({ name: 'CategoryForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
|
@ -72,7 +69,6 @@ const formRules = reactive({
|
|||
sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
|
|
@ -90,7 +86,6 @@ const open = async (type: string, id?: number) => {
|
|||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
|
|
@ -114,7 +109,6 @@ const submitForm = async () => {
|
|||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
|
|
@ -127,4 +121,4 @@ const resetForm = () => {
|
|||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<template>
|
||||
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
@ -68,7 +66,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
|
|
@ -118,23 +115,18 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<CategoryForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
|
||||
import CategoryForm from './CategoryForm.vue'
|
||||
|
||||
/** BPM 流程分类 列表 */
|
||||
defineOptions({ name: 'BpmCategory' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<CategoryVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
|
|
@ -148,7 +140,6 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -160,25 +151,21 @@ const getList = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
|
@ -191,9 +178,8 @@ const handleDelete = async (id: number) => {
|
|||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue