修改Saas登录方式

main
lj7788 2026-02-04 18:16:22 +08:00
parent 132d024848
commit dc2017266d
741 changed files with 795 additions and 8834 deletions

View File

@ -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
*/

View File

@ -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);
}

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -35,3 +35,6 @@
border-left-color: var(--el-color-primary);
}
}
.hidden{
display: none;
}

View File

@ -38,7 +38,6 @@ export const formatToken = (token: string): string => {
// ========== 账号相关 ==========
export type LoginFormType = {
tenantName: string
username: string
password: string
rememberMe: boolean

View File

@ -3,6 +3,5 @@
</template>
<script lang="ts" setup>
defineOptions({ name: 'Error403' })
const { push } = useRouter()
</script>

View File

@ -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>

View File

@ -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,57 +247,44 @@ 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;
}

View File

@ -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,13 +100,11 @@ $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);
}

View File

@ -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.vueredirectUriencodedecode
function getUrlValue(key: string): string {
const url = new URL(decodeURIComponent(location.href))
return url.searchParams.get(key) ?? ''
}
// : socialLogintoken
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;

View File

@ -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,14 +253,12 @@ const resetPassword = async () => {
})
}
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.smsbtn {
margin-top: 33px;
}

View File

@ -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
// : typeredirect 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;

View File

@ -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'),

View File

@ -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,14 +188,12 @@ const signIn = async () => {
})
}
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.smsbtn {
margin-top: 33px;
}

View File

@ -18,12 +18,9 @@
</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)

View File

@ -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;

View File

@ -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,

View File

@ -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,23 +42,19 @@ 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%;
}

View File

@ -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,13 +102,11 @@ const submit = () => {
}
})
}
const init = async () => {
const res = await getUserProfile()
unref(formRef)?.setValues(res)
return res
}
onMounted(async () => {
await init()
})

View File

@ -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,7 +100,6 @@ onMounted(async () => {
border-top: 1px solid #e7eaec;
border-bottom: 1px solid #e7eaec;
}
.pull-right {
float: right !important;
}

View File

@ -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,7 +59,6 @@ const submit = (formEl: FormInstance | undefined) => {
}
})
}
const reset = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()

View File

@ -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 {

View File

@ -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,
() => {

View File

@ -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,

View File

@ -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 () => {
//

View File

@ -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) // 12
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 = {

View File

@ -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,16 +343,13 @@ 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);
}

View File

@ -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,7 +54,6 @@ const getFileTypeClass = (filename: string): string => {
return 'bg-gradient-to-br from-gray-5 to-gray-7'
}
}
/** 点击文件 */
const handleFileClick = (url: string) => {
window.open(url, '_blank')

View File

@ -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,7 +90,6 @@ const documentList = computed(() => {
})
return Array.from(docMap.values())
})
/** 点击 document 处理 */
const handleClick = (doc: any) => {
document.value = doc

View File

@ -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,17 +193,14 @@ 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)

View File

@ -26,9 +26,7 @@ const promptList = [
prompt: '写一首好听的诗歌?'
}
] // prompt
const emits = defineEmits(['onPrompt'])
/** 选中 prompt 点击 */
const handlerPromptClick = async ({ prompt }) => {
emits('onPrompt', prompt)

View File

@ -11,7 +11,6 @@
</template>
<script setup lang="ts">
const emits = defineEmits(['onNewConversation'])
/** 新建 conversation 聊天对话 */
const handlerNewChat = () => {
emits('onNewConversation')

View File

@ -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,28 +55,23 @@ 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);
}

View File

@ -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,30 +148,25 @@ 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;

View File

@ -15,7 +15,6 @@
</template>
<script setup lang="ts">
import { PropType } from 'vue'
//
defineProps({
categoryList: {
@ -28,10 +27,8 @@ defineProps({
default: '全部'
}
})
//
const emits = defineEmits(['onCategoryClick'])
/** 处理分类点击事件 */
const handleCategoryClick = async (category: string) => {
emits('onCategoryClick', category)

View File

@ -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) {

View File

@ -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,20 +210,16 @@ 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;
}

View File

@ -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,7 +574,6 @@ onMounted(async () => {
activeConversationId.value = id
await getConversation(id)
}
//
activeMessageListLoading.value = true
await getMessageList()

View File

@ -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,7 +145,6 @@ const handleDelete = async (id: number) => {
await getList()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
getList()

View File

@ -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,7 +157,6 @@ const handleDelete = async (id: number) => {
await getList()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
getList()

View File

@ -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>

View File

@ -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,7 +115,6 @@ const handleLoading = async (status: number) => {
}
}
}
/** 初始化 */
onMounted(async () => {
await handleLoading(props.detail.status as string)

View File

@ -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>

View File

@ -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,7 +184,6 @@ onMounted(async () => {
await refreshWatchImages()
}, 1000 * 3)
})
/** 组件取消挂在的时候 */
onUnmounted(async () => {
if (inProgressTimer.value) {

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,7 +98,6 @@ const handleRegeneration = async (image: ImageVO) => {
}
// TODO @fan other
}
/** 组件挂载的时候 */
onMounted(async () => {
//

View File

@ -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,7 +231,6 @@ const handleUpdatePublicStatusChange = async (row: ImageVO) => {
row.publicStatus = !row.publicStatus
}
}
/** 初始化 **/
onMounted(async () => {
getList()

View File

@ -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>

View File

@ -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.

View File

@ -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,7 +209,6 @@ 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)

View File

@ -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>

View File

@ -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,20 +156,16 @@ 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;
}

View File

@ -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,7 +213,6 @@ onMounted(() => {
router.push({ name: 'AiKnowledge' })
return
}
// ID
queryParams.knowledgeId = route.query.knowledgeId as any
getList()

View File

@ -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) // 12
@ -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 = {

View File

@ -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,7 +198,6 @@ const handleRetrieval = (id: number) => {
query: { id }
})
}
/** 初始化 **/
onMounted(() => {
getList()

View File

@ -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,7 +146,6 @@ onMounted(() => {
return
}
queryParams.id = route.query.id as any
//
getKnowledgeInfo(queryParams.id as any)
})

View File

@ -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) // 12
@ -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 = {

View File

@ -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,7 +221,6 @@ onMounted(() => {
router.push({ name: 'AiKnowledgeDocument' })
return
}
// ID
queryParams.documentId = route.query.documentId as any
getList()

View File

@ -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,7 +66,6 @@ defineExpose({
}
})
</script>
<style lang="scss" scoped>
.title {
color: var(--el-color-primary);

View File

@ -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,16 +136,13 @@ defineExpose({
@extend .hide-scroll-bar;
}
}
// markmaptool
:deep(.markmap) {
width: 100%;
}
:deep(.mm-toolbar-brand) {
display: none;
}
:deep(.mm-toolbar) {
display: flex;
flex-direction: row;

View File

@ -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,7 +79,6 @@ const submit = (data: AiMindMapGenerateReqVO) => {
ctrl: ctrl.value
})
}
/** 初始化 */
onMounted(() => {
generatedContent.value = MindMapContentExample

View File

@ -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,7 +168,6 @@ const openPreview = async (row: MindMapVO) => {
previewVisible2.value = true
previewContent.value = row.generatedContent
}
/** 初始化 **/
onMounted(async () => {
getList()

View File

@ -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) // 12
@ -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 = {

View File

@ -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,7 +161,6 @@ const handleDelete = async (id: number) => {
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()

View File

@ -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) // 12
@ -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 @fantitle 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 = {

View File

@ -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,7 +180,6 @@ const handleDelete = async (id: number) => {
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()

View File

@ -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) // 12
@ -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 = {

View File

@ -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,7 +169,6 @@ const handleDelete = async (id: number) => {
await getList()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getList()

View File

@ -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) // 12
@ -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 = {

View File

@ -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,7 +157,6 @@ const handleDelete = async (id: number) => {
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()

View File

@ -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

View File

@ -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,7 +55,6 @@ function toggleStatus (type: string) {
}
}
}
//
function audioTimeUpdate (args) {
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss')

View File

@ -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;

View File

@ -14,22 +14,16 @@
</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')
}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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,22 +56,17 @@
</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
})

View File

@ -10,10 +10,8 @@
<slot></slot>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'Index' })
defineProps({
title: {
type: String

View File

@ -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,7 +272,6 @@ const handleUpdatePublicStatusChange = async (row: MusicVO) => {
row.publicStatus = !row.publicStatus
}
}
/** 初始化 **/
onMounted(async () => {
getList()

View File

@ -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()

View File

@ -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;

View File

@ -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,27 +192,22 @@ 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;
}

View File

@ -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,7 +174,6 @@ const openForm = async (type: string, id?: number) => {
})
}
}
/** 初始化 **/
onMounted(() => {
getList()

View File

@ -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 里边定义可复用的组件DefineLabelReuseLabel 是采用的解构赋值都是 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) {

View File

@ -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;

View File

@ -12,7 +12,6 @@
</span>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
@ -24,7 +23,6 @@ const props = withDefaults(
tags: () => []
}
)
const emits = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()

View File

@ -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,12 +60,10 @@ const submit = (data: WriteVO) => {
}
})
}
/** 点击示例触发 */
const handleExampleClick = (type: keyof typeof WriteExample) => {
writeResult.value = WriteExample[type].data
}
/** 点击重置的时候清空写作的结果**/
const reset = () => {
writeResult.value = ''

View File

@ -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,7 +206,6 @@ const handleDelete = async (id: number) => {
await getList()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
getList()

View File

@ -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) // 12
@ -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 = {

View File

@ -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,7 +178,6 @@ const handleDelete = async (id: number) => {
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()

View File

@ -14,7 +14,6 @@
</fc-designer>
</div>
</ContentWrap>
<!-- 表单保存的弹窗 -->
<Dialog v-model="dialogVisible" title="保存表单" width="600">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
@ -51,16 +50,13 @@ import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useFormCreateDesigner } from '@/components/FormCreate'
import { useRoute } from 'vue-router'
defineOptions({ name: 'BpmFormEditor' })
const { t } = useI18n() //
const message = useMessage() //
const route = useRoute() //
const { push, currentRoute } = useRouter() //
const { query } = useRoute() //
const { delView } = useTagsViewStore() //
//
const designerConfig = ref({
switchType: [], // ,
@ -104,12 +100,10 @@ const formRules = reactive({
status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 处理保存按钮 */
const handleSave = () => {
dialogVisible.value = true
}
/** 提交表单 */
const submitForm = async () => {
//
@ -140,7 +134,6 @@ const close = () => {
delView(unref(currentRoute))
push('/bpm/manager/form')
}
/** 初始化 **/
onMounted(async () => {
//
@ -152,7 +145,6 @@ onMounted(async () => {
const data = await FormApi.getForm(id)
formData.value = data
setConfAndFields(designer, data.conf, data.fields)
if (route.query.type !== 'copy') {
return
}
@ -162,7 +154,6 @@ onMounted(async () => {
formData.value.name += '_copy'
})
</script>
<style>
.my-designer {
._fc-l,

View File

@ -1,6 +1,4 @@
<template>
<doc-alert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
@ -35,7 +33,6 @@
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
@ -93,25 +90,20 @@
@pagination="getList"
/>
</ContentWrap>
<!-- 表单详情的弹窗 -->
<Dialog v-model="detailVisible" title="表单详情" width="800">
<form-create :option="detailData.option" :rule="detailData.rule" />
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as FormApi from '@/api/bpm/form'
import { setConfAndFields2 } from '@/utils/formCreate'
defineOptions({ name: 'BpmForm' })
const message = useMessage() //
const { t } = useI18n() //
const { currentRoute, push } = useRouter() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
@ -121,7 +113,6 @@ const queryParams = reactive({
name: null
})
const queryFormRef = ref() //
/** 查询列表 */
const getList = async () => {
loading.value = true
@ -133,19 +124,16 @@ const getList = async () => {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const openForm = (type: string, id?: number) => {
const toRouter: { name: string; query: { type: string; id?: number } } = {
@ -161,7 +149,6 @@ const openForm = (type: string, id?: number) => {
}
push(toRouter)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
@ -174,7 +161,6 @@ const handleDelete = async (id: number) => {
await getList()
} catch {}
}
/** 详情操作 */
const detailVisible = ref(false)
const detailData = ref({

View File

@ -46,12 +46,9 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import * as UserGroupApi from '@/api/bpm/userGroup'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'UserGroupForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
@ -71,7 +68,6 @@ const formRules = reactive({
})
const formRef = ref() // Ref
const userList = ref<any[]>([]) //
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
@ -91,7 +87,6 @@ const open = async (type: string, id?: number) => {
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
@ -117,7 +112,6 @@ const submitForm = async () => {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {

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