初始化
This commit is contained in:
62
saiadmin-artd/src/views/auth/forget-password/index.vue
Normal file
62
saiadmin-artd/src/views/auth/forget-password/index.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<LoginLeftView />
|
||||
|
||||
<div class="relative flex-1">
|
||||
<AuthTopBar />
|
||||
|
||||
<div class="auth-right-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('forgetPassword.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('forgetPassword.subTitle') }}</p>
|
||||
<div class="mt-5">
|
||||
<span class="input-label" v-if="showInputLabel">账号</span>
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
:placeholder="$t('forgetPassword.placeholder')"
|
||||
v-model.trim="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="w-full custom-height"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('forgetPassword.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton class="w-full custom-height" plain @click="toLogin">
|
||||
{{ $t('forgetPassword.backBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ForgetPassword' })
|
||||
|
||||
const router = useRouter()
|
||||
const showInputLabel = ref(false)
|
||||
|
||||
const username = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const register = async () => {}
|
||||
|
||||
const toLogin = () => {
|
||||
router.push({ name: 'Login' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '../login/style.css';
|
||||
</style>
|
||||
216
saiadmin-artd/src/views/auth/login/index.vue
Normal file
216
saiadmin-artd/src/views/auth/login/index.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<!-- 登录页面 -->
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<LoginLeftView />
|
||||
|
||||
<div class="relative flex-1">
|
||||
<AuthTopBar />
|
||||
|
||||
<div class="auth-right-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('login.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('login.subTitle') }}</p>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:key="formKey"
|
||||
@keyup.enter="handleSubmit"
|
||||
style="margin-top: 25px"
|
||||
>
|
||||
<ElFormItem prop="username">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
:placeholder="$t('login.placeholder.username')"
|
||||
v-model.trim="formData.username"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
:placeholder="$t('login.placeholder.password')"
|
||||
v-model.trim="formData.password"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="code">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
:placeholder="$t('login.placeholder.code')"
|
||||
v-model.trim="formData.code"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #append>
|
||||
<img
|
||||
:src="captcha"
|
||||
style="height: 36px; cursor: pointer"
|
||||
@click="refreshCaptcha"
|
||||
/>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="flex-cb mt-2 text-sm">
|
||||
<ElCheckbox v-model="formData.rememberPassword">{{
|
||||
$t('login.rememberPwd')
|
||||
}}</ElCheckbox>
|
||||
<!-- <RouterLink class="text-theme" :to="{ name: 'ForgetPassword' }">{{
|
||||
$t('login.forgetPwd')
|
||||
}}</RouterLink> -->
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px">
|
||||
<ElButton
|
||||
class="w-full custom-height"
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('login.btnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- <div class="mt-5 text-sm text-gray-600">
|
||||
<span>{{ $t('login.noAccount') }}</span>
|
||||
<RouterLink class="text-theme" :to="{ name: 'Register' }">{{
|
||||
$t('login.register')
|
||||
}}</RouterLink>
|
||||
</div> -->
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { HttpError } from '@/utils/http/error'
|
||||
import { fetchCaptcha, fetchLogin, fetchGetUserInfo } from '@/api/auth'
|
||||
import { ElNotification, type FormInstance, type FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'Login' })
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const formKey = ref(0)
|
||||
|
||||
// 监听语言切换,重置表单
|
||||
watch(locale, () => {
|
||||
formKey.value++
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
const captcha = ref(
|
||||
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
||||
)
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
code: '',
|
||||
uuid: '',
|
||||
rememberPassword: true
|
||||
})
|
||||
|
||||
const rules = computed<FormRules>(() => ({
|
||||
username: [{ required: true, message: t('login.placeholder.username'), trigger: 'blur' }],
|
||||
password: [{ required: true, message: t('login.placeholder.password'), trigger: 'blur' }],
|
||||
code: [{ required: true, message: t('login.placeholder.code'), trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
refreshCaptcha()
|
||||
})
|
||||
|
||||
// 登录
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
// 表单验证
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
// 登录请求
|
||||
const { access_token, refresh_token } = await fetchLogin({
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
code: formData.code,
|
||||
uuid: formData.uuid
|
||||
})
|
||||
|
||||
// 验证token
|
||||
if (!access_token) {
|
||||
throw new Error('Login failed - no token received')
|
||||
}
|
||||
|
||||
// 存储token和用户信息
|
||||
userStore.setToken(access_token, refresh_token)
|
||||
const userInfo = await fetchGetUserInfo()
|
||||
userStore.setUserInfo(userInfo)
|
||||
userStore.setLoginStatus(true)
|
||||
|
||||
// 登录成功处理
|
||||
showLoginSuccessNotice()
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
// 处理 HttpError
|
||||
if (error instanceof HttpError) {
|
||||
// console.log(error.code)
|
||||
} else {
|
||||
// 处理非 HttpError
|
||||
// ElMessage.error('登录失败,请稍后重试')
|
||||
console.error('[Login] Unexpected error:', error)
|
||||
}
|
||||
} finally {
|
||||
refreshCaptcha()
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
const refreshCaptcha = async () => {
|
||||
fetchCaptcha().then((res) => {
|
||||
formData.uuid = res.uuid
|
||||
captcha.value = res.image
|
||||
})
|
||||
}
|
||||
|
||||
// 登录成功提示
|
||||
const showLoginSuccessNotice = () => {
|
||||
setTimeout(() => {
|
||||
ElNotification({
|
||||
title: t('login.success.title'),
|
||||
type: 'success',
|
||||
duration: 2500,
|
||||
zIndex: 10000,
|
||||
message: `${t('login.success.message')}, ${systemName}!`
|
||||
})
|
||||
}, 150)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './style.css';
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-select__wrapper) {
|
||||
height: 40px !important;
|
||||
}
|
||||
</style>
|
||||
38
saiadmin-artd/src/views/auth/login/style.css
Normal file
38
saiadmin-artd/src/views/auth/login/style.css
Normal file
@@ -0,0 +1,38 @@
|
||||
@reference '@styles/core/tailwind.css';
|
||||
|
||||
/* 授权页右侧区域 */
|
||||
.auth-right-wrap {
|
||||
@apply absolute inset-0 w-[440px] h-[650px] py-[5px] m-auto overflow-hidden
|
||||
max-sm:px-7 max-sm:w-full
|
||||
animate-[slideInRight_0.6s_cubic-bezier(0.25,0.46,0.45,0.94)_forwards]
|
||||
max-md:animate-none;
|
||||
|
||||
.form {
|
||||
@apply h-full py-[40px];
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply text-g-900 text-4xl font-semibold max-md:text-3xl max-sm:pt-10;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
@apply mt-[10px] text-g-600 text-sm;
|
||||
}
|
||||
|
||||
.custom-height {
|
||||
@apply !h-[40px];
|
||||
}
|
||||
}
|
||||
|
||||
/* 滑入动画 */
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
240
saiadmin-artd/src/views/auth/register/index.vue
Normal file
240
saiadmin-artd/src/views/auth/register/index.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<!-- 注册页面 -->
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<LoginLeftView />
|
||||
|
||||
<div class="relative flex-1">
|
||||
<AuthTopBar />
|
||||
|
||||
<div class="auth-right-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('register.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('register.subTitle') }}</p>
|
||||
<ElForm
|
||||
class="mt-7.5"
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
:key="formKey"
|
||||
>
|
||||
<ElFormItem prop="username">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
v-model.trim="formData.username"
|
||||
:placeholder="$t('register.placeholder.username')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
v-model.trim="formData.password"
|
||||
:placeholder="$t('register.placeholder.password')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="confirmPassword">
|
||||
<ElInput
|
||||
class="custom-height"
|
||||
v-model.trim="formData.confirmPassword"
|
||||
:placeholder="$t('register.placeholder.confirmPassword')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@keyup.enter="register"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="agreement">
|
||||
<ElCheckbox v-model="formData.agreement">
|
||||
{{ $t('register.agreeText') }}
|
||||
<RouterLink
|
||||
style="color: var(--theme-color); text-decoration: none"
|
||||
to="/privacy-policy"
|
||||
>{{ $t('register.privacyPolicy') }}</RouterLink
|
||||
>
|
||||
</ElCheckbox>
|
||||
</ElFormItem>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="w-full custom-height"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('register.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 text-sm text-g-600">
|
||||
<span>{{ $t('register.hasAccount') }}</span>
|
||||
<RouterLink class="text-theme" :to="{ name: 'Login' }">{{
|
||||
$t('register.toLogin')
|
||||
}}</RouterLink>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'Register' })
|
||||
|
||||
interface RegisterForm {
|
||||
username: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
agreement: boolean
|
||||
}
|
||||
|
||||
const USERNAME_MIN_LENGTH = 3
|
||||
const USERNAME_MAX_LENGTH = 20
|
||||
const PASSWORD_MIN_LENGTH = 6
|
||||
const REDIRECT_DELAY = 1000
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const router = useRouter()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const loading = ref(false)
|
||||
const formKey = ref(0)
|
||||
|
||||
// 监听语言切换,重置表单
|
||||
watch(locale, () => {
|
||||
formKey.value++
|
||||
})
|
||||
|
||||
const formData = reactive<RegisterForm>({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agreement: false
|
||||
})
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
* 当密码输入后,如果确认密码已填写,则触发确认密码的验证
|
||||
*/
|
||||
const validatePassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.placeholder.password')))
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.confirmPassword) {
|
||||
formRef.value?.validateField('confirmPassword')
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证确认密码
|
||||
* 检查确认密码是否与密码一致
|
||||
*/
|
||||
const validateConfirmPassword = (
|
||||
_rule: any,
|
||||
value: string,
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.rule.confirmPasswordRequired')))
|
||||
return
|
||||
}
|
||||
|
||||
if (value !== formData.password) {
|
||||
callback(new Error(t('register.rule.passwordMismatch')))
|
||||
return
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户协议
|
||||
* 确保用户已勾选同意协议
|
||||
*/
|
||||
const validateAgreement = (_rule: any, value: boolean, callback: (error?: Error) => void) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.rule.agreementRequired')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const rules = computed<FormRules<RegisterForm>>(() => ({
|
||||
username: [
|
||||
{ required: true, message: t('register.placeholder.username'), trigger: 'blur' },
|
||||
{
|
||||
min: USERNAME_MIN_LENGTH,
|
||||
max: USERNAME_MAX_LENGTH,
|
||||
message: t('register.rule.usernameLength'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{ required: true, validator: validatePassword, trigger: 'blur' },
|
||||
{ min: PASSWORD_MIN_LENGTH, message: t('register.rule.passwordLength'), trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
|
||||
agreement: [{ validator: validateAgreement, trigger: 'change' }]
|
||||
}))
|
||||
|
||||
/**
|
||||
* 注册用户
|
||||
* 验证表单后提交注册请求
|
||||
*/
|
||||
const register = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
// TODO: 替换为真实 API 调用
|
||||
// const params = {
|
||||
// username: formData.username,
|
||||
// password: formData.password
|
||||
// }
|
||||
// const res = await AuthService.register(params)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// ElMessage.success('注册成功')
|
||||
// toLogin()
|
||||
// }
|
||||
|
||||
// 模拟注册请求
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
ElMessage.success('注册成功')
|
||||
toLogin()
|
||||
}, REDIRECT_DELAY)
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到登录页面
|
||||
*/
|
||||
const toLogin = () => {
|
||||
setTimeout(() => {
|
||||
router.push({ name: 'Login' })
|
||||
}, REDIRECT_DELAY)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '../login/style.css';
|
||||
</style>
|
||||
53
saiadmin-artd/src/views/dashboard/console/index.vue
Normal file
53
saiadmin-artd/src/views/dashboard/console/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<!-- 工作台页面 -->
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="userInfo.dashboard === 'statistics'">
|
||||
<CardList></CardList>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :sm="24" :md="12" :lg="10">
|
||||
<ActiveUser />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="14">
|
||||
<SalesOverview />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<template v-if="userInfo.dashboard === 'work'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :sm="24" :md="24" :lg="12">
|
||||
<NewUser />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="6">
|
||||
<Dynamic />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="6">
|
||||
<TodoList />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<AboutProject />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardList from './modules/card-list.vue'
|
||||
import ActiveUser from './modules/active-user.vue'
|
||||
import SalesOverview from './modules/sales-overview.vue'
|
||||
import AboutProject from './modules/about-project.vue'
|
||||
import NewUser from './modules/new-user.vue'
|
||||
import Dynamic from './modules/dynamic-stats.vue'
|
||||
import TodoList from './modules/todo-list.vue'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userInfo = userStore.getUserInfo
|
||||
|
||||
const { scrollToTop } = useCommon()
|
||||
scrollToTop()
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="art-card p-5 flex-b mb-5 max-sm:mb-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-medium">关于项目</h2>
|
||||
<p class="text-g-700 mt-1">{{ systemName }} 是一款兼具设计美学与高效开发的后台系统</p>
|
||||
<p class="text-g-700 mt-1">使用了 webman + Vue3 + Element Plus 高性能、高颜值技术栈</p>
|
||||
|
||||
<div class="flex flex-wrap gap-3.5 max-w-150 mt-9">
|
||||
<div
|
||||
class="w-60 flex-cb h-12.5 px-3.5 border border-g-300 c-p rounded-lg text-sm bg-g-100 duration-300 hover:-translate-y-1 max-sm:w-full"
|
||||
v-for="link in linkList"
|
||||
:key="link.label"
|
||||
@click="goPage(link.url)"
|
||||
>
|
||||
<span class="text-g-700">{{ link.label }}</span>
|
||||
<ArtSvgIcon icon="ri:arrow-right-s-line" class="text-lg text-g-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img class="w-75 max-md:!hidden" src="@imgs/draw/draw1.png" alt="draw1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
|
||||
const linkList = [
|
||||
{ label: '项目官网', url: 'https://saithink.top/' },
|
||||
{ label: '文档', url: 'https://saithink.top/documents/' },
|
||||
{ label: 'Github', url: 'https://github.com/saithink/saiadmin' },
|
||||
{ label: '插件市场', url: 'https://saas.saithink.top/' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 在新标签页中打开指定 URL
|
||||
* @param url 要打开的网页地址
|
||||
*/
|
||||
const goPage = (url: string): void => {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="art-card h-105 p-4 box-border mb-5 max-sm:mb-4">
|
||||
<div class="art-card-header">
|
||||
<div class="title">
|
||||
<h4>月度登录汇总</h4>
|
||||
</div>
|
||||
</div>
|
||||
<ArtBarChart
|
||||
class="box-border p-2"
|
||||
barWidth="50%"
|
||||
height="calc(100% - 40px)"
|
||||
:showAxisLine="false"
|
||||
:data="yData"
|
||||
:xAxisData="xData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchLoginBarChart } from '@/api/dashboard'
|
||||
|
||||
/**
|
||||
* 登录数据
|
||||
*/
|
||||
const yData = ref<number[]>([])
|
||||
|
||||
/**
|
||||
* 时间数据
|
||||
*/
|
||||
const xData = ref<string[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
fetchLoginBarChart().then((data) => {
|
||||
yData.value = data.login_count
|
||||
xData.value = data.login_month
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<ElRow :gutter="20" class="flex">
|
||||
<ElCol :sm="12" :md="6" :lg="6">
|
||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
||||
<span class="text-g-700 text-sm">用户统计</span>
|
||||
<ArtCountTo class="text-[26px] font-medium mt-2" :target="statData.user" :duration="1300" />
|
||||
<div class="flex-c mt-1">
|
||||
<span class="text-xs text-g-600">较上周</span>
|
||||
<span class="ml-1 text-xs font-semibold text-success">+10%</span>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:group-line" class="text-xl text-theme" />
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :sm="12" :md="6" :lg="6">
|
||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
||||
<span class="text-g-700 text-sm">附件统计</span>
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
:target="statData.attach"
|
||||
:duration="1300"
|
||||
/>
|
||||
<div class="flex-c mt-1">
|
||||
<span class="text-xs text-g-600">较上周</span>
|
||||
<span class="ml-1 text-xs font-semibold text-success">+10%</span>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:attachment-line" class="text-xl text-theme" />
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :sm="12" :md="6" :lg="6">
|
||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
||||
<span class="text-g-700 text-sm">登录统计</span>
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
:target="statData.login"
|
||||
:duration="1300"
|
||||
/>
|
||||
<div class="flex-c mt-1">
|
||||
<span class="text-xs text-g-600">较上周</span>
|
||||
<span class="ml-1 text-xs font-semibold text-success">+12%</span>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:fire-line" class="text-xl text-theme" />
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :sm="12" :md="6" :lg="6">
|
||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
||||
<span class="text-g-700 text-sm">操作统计</span>
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
:target="statData.operate"
|
||||
:duration="1300"
|
||||
/>
|
||||
<div class="flex-c mt-1">
|
||||
<span class="text-xs text-g-600">较上周</span>
|
||||
<span class="ml-1 text-xs font-semibold text-danger">-5%</span>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:pie-chart-line" class="text-xl text-theme" />
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchStatistics } from '@/api/dashboard'
|
||||
|
||||
const statData = ref({
|
||||
user: 0,
|
||||
attach: 0,
|
||||
login: 0,
|
||||
operate: 0
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchStatistics().then((data) => {
|
||||
statData.value = data
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="art-card h-128 p-5 mb-5 max-sm:mb-4">
|
||||
<div class="art-card-header">
|
||||
<div class="title">
|
||||
<h4>动态</h4>
|
||||
<p>新增<span class="text-success">+6</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-9/10 mt-2 overflow-hidden">
|
||||
<ElScrollbar>
|
||||
<div
|
||||
class="h-17.5 leading-17.5 border-b border-g-300 text-sm overflow-hidden last:border-b-0"
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
>
|
||||
<span class="text-g-800 font-medium">{{ item.username }}</span>
|
||||
<span class="mx-2 text-g-600">{{ item.type }}</span>
|
||||
<span class="text-theme">{{ item.target }}</span>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface DynamicItem {
|
||||
username: string
|
||||
type: string
|
||||
target: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户动态列表
|
||||
* 记录用户的关注、发文、提问、兑换等各类活动
|
||||
*/
|
||||
const list = reactive<DynamicItem[]>([
|
||||
{
|
||||
username: '中小鱼',
|
||||
type: '关注了',
|
||||
target: '誶誶淰'
|
||||
},
|
||||
{
|
||||
username: '何小荷',
|
||||
type: '发表文章',
|
||||
target: 'Vue3 + Typescript + Vite 项目实战笔记'
|
||||
},
|
||||
{
|
||||
username: '中小鱼',
|
||||
type: '关注了',
|
||||
target: '誶誶淰'
|
||||
},
|
||||
{
|
||||
username: '何小荷',
|
||||
type: '发表文章',
|
||||
target: 'Vue3 + Typescript + Vite 项目实战笔记'
|
||||
},
|
||||
{
|
||||
username: '誶誶淰',
|
||||
type: '提出问题',
|
||||
target: '主题可以配置吗'
|
||||
},
|
||||
{
|
||||
username: '发呆草',
|
||||
type: '兑换了物品',
|
||||
target: '《奇特的一生》'
|
||||
},
|
||||
{
|
||||
username: '甜筒',
|
||||
type: '关闭了问题',
|
||||
target: '发呆草'
|
||||
},
|
||||
{
|
||||
username: '冷月呆呆',
|
||||
type: '兑换了物品',
|
||||
target: '《高效人士的七个习惯》'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
169
saiadmin-artd/src/views/dashboard/console/modules/new-user.vue
Normal file
169
saiadmin-artd/src/views/dashboard/console/modules/new-user.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="art-card p-5 h-128 overflow-hidden mb-5 max-sm:mb-4">
|
||||
<div class="art-card-header">
|
||||
<div class="title">
|
||||
<h4>新用户</h4>
|
||||
<p>这个月增长<span class="text-success">+20%</span></p>
|
||||
</div>
|
||||
<ElRadioGroup v-model="radio2">
|
||||
<ElRadioButton value="本月" label="本月"></ElRadioButton>
|
||||
<ElRadioButton value="上月" label="上月"></ElRadioButton>
|
||||
<ElRadioButton value="今年" label="今年"></ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
<ArtTable
|
||||
class="w-full"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
size="large"
|
||||
:border="false"
|
||||
:stripe="false"
|
||||
:header-cell-style="{ background: 'transparent' }"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="头像" prop="avatar" width="150px">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<img class="size-9 rounded-lg" :src="scope.row.avatar" alt="avatar" />
|
||||
<span class="ml-2">{{ scope.row.username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="地区" prop="province" />
|
||||
<ElTableColumn label="性别" prop="avatar">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span style="margin-left: 10px">{{ scope.row.sex === 1 ? '男' : '女' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="进度" width="240">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.pro"
|
||||
:color="scope.row.color"
|
||||
:stroke-width="4"
|
||||
:aria-label="`${scope.row.username}的完成进度: ${scope.row.pro}%`"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import avatar1 from '@/assets/images/avatar/avatar1.webp'
|
||||
import avatar2 from '@/assets/images/avatar/avatar2.webp'
|
||||
import avatar3 from '@/assets/images/avatar/avatar3.webp'
|
||||
import avatar4 from '@/assets/images/avatar/avatar4.webp'
|
||||
import avatar5 from '@/assets/images/avatar/avatar5.webp'
|
||||
import avatar6 from '@/assets/images/avatar/avatar6.webp'
|
||||
|
||||
interface UserTableItem {
|
||||
username: string
|
||||
province: string
|
||||
sex: 0 | 1
|
||||
age: number
|
||||
percentage: number
|
||||
pro: number
|
||||
color: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
const ANIMATION_DELAY = 100
|
||||
|
||||
const radio2 = ref('本月')
|
||||
|
||||
/**
|
||||
* 新用户表格数据
|
||||
* 包含用户基本信息和完成进度
|
||||
*/
|
||||
const tableData = reactive<UserTableItem[]>([
|
||||
{
|
||||
username: '中小鱼',
|
||||
province: '北京',
|
||||
sex: 0,
|
||||
age: 22,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'var(--art-primary)',
|
||||
avatar: avatar1
|
||||
},
|
||||
{
|
||||
username: '何小荷',
|
||||
province: '深圳',
|
||||
sex: 1,
|
||||
age: 21,
|
||||
percentage: 20,
|
||||
pro: 0,
|
||||
color: 'var(--art-secondary)',
|
||||
avatar: avatar2
|
||||
},
|
||||
{
|
||||
username: '誶誶淰',
|
||||
province: '上海',
|
||||
sex: 1,
|
||||
age: 23,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'var(--art-warning)',
|
||||
avatar: avatar3
|
||||
},
|
||||
{
|
||||
username: '发呆草',
|
||||
province: '长沙',
|
||||
sex: 0,
|
||||
age: 28,
|
||||
percentage: 50,
|
||||
pro: 0,
|
||||
color: 'var(--art-info)',
|
||||
avatar: avatar4
|
||||
},
|
||||
{
|
||||
username: '甜筒',
|
||||
province: '浙江',
|
||||
sex: 1,
|
||||
age: 26,
|
||||
percentage: 70,
|
||||
pro: 0,
|
||||
color: 'var(--art-error)',
|
||||
avatar: avatar5
|
||||
},
|
||||
{
|
||||
username: '冷月呆呆',
|
||||
province: '湖北',
|
||||
sex: 1,
|
||||
age: 25,
|
||||
percentage: 90,
|
||||
pro: 0,
|
||||
color: 'var(--art-success)',
|
||||
avatar: avatar6
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* 添加进度条动画效果
|
||||
* 延迟后将进度值从 0 更新到目标百分比,触发动画
|
||||
*/
|
||||
const addAnimation = (): void => {
|
||||
setTimeout(() => {
|
||||
tableData.forEach((item) => {
|
||||
item.pro = item.percentage
|
||||
})
|
||||
}, ANIMATION_DELAY)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addAnimation()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.art-card {
|
||||
:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
||||
color: var(--el-color-primary) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="art-card h-105 p-5 mb-5 max-sm:mb-4">
|
||||
<div class="art-card-header">
|
||||
<div class="title">
|
||||
<h4>近期登录统计</h4>
|
||||
</div>
|
||||
</div>
|
||||
<ArtLineChart
|
||||
height="calc(100% - 40px)"
|
||||
:data="yData"
|
||||
:xAxisData="xData"
|
||||
:showAreaColor="true"
|
||||
:showAxisLine="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchLoginChart } from '@/api/dashboard'
|
||||
|
||||
/**
|
||||
* 登录数据
|
||||
*/
|
||||
const yData = ref<number[]>([])
|
||||
|
||||
/**
|
||||
* 时间数据
|
||||
*/
|
||||
const xData = ref<string[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
fetchLoginChart().then((data) => {
|
||||
yData.value = data.login_count
|
||||
xData.value = data.login_date
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="art-card h-128 p-5 mb-5 max-sm:mb-4">
|
||||
<div class="art-card-header">
|
||||
<div class="title">
|
||||
<h4>代办事项</h4>
|
||||
<p>待处理<span class="text-danger">3</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-[calc(100%-40px)] overflow-auto">
|
||||
<ElScrollbar>
|
||||
<div
|
||||
class="flex-cb h-17.5 border-b border-g-300 text-sm last:border-b-0"
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm">{{ item.username }}</p>
|
||||
<p class="text-g-500 mt-1">{{ item.date }}</p>
|
||||
</div>
|
||||
<ElCheckbox v-model="item.complate" />
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface TodoItem {
|
||||
username: string
|
||||
date: string
|
||||
complate: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 待办事项列表
|
||||
* 记录每日工作任务及完成状态
|
||||
*/
|
||||
const list = reactive<TodoItem[]>([
|
||||
{
|
||||
username: '查看今天工作内容',
|
||||
date: '上午 09:30',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '回复邮件',
|
||||
date: '上午 10:30',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '工作汇报整理',
|
||||
date: '上午 11:00',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '产品需求会议',
|
||||
date: '下午 02:00',
|
||||
complate: false
|
||||
},
|
||||
{
|
||||
username: '整理会议内容',
|
||||
date: '下午 03:30',
|
||||
complate: false
|
||||
},
|
||||
{
|
||||
username: '明天工作计划',
|
||||
date: '下午 06:30',
|
||||
complate: false
|
||||
}
|
||||
])
|
||||
</script>
|
||||
395
saiadmin-artd/src/views/dashboard/user-center/index.vue
Normal file
395
saiadmin-artd/src/views/dashboard/user-center/index.vue
Normal file
@@ -0,0 +1,395 @@
|
||||
<!-- 个人中心页面 -->
|
||||
<template>
|
||||
<div class="w-full h-full p-0 bg-transparent border-none shadow-none">
|
||||
<div class="relative flex-b mt-2.5 max-md:block max-md:mt-1">
|
||||
<div class="w-112 mr-5 max-md:w-full max-md:mr-0">
|
||||
<div class="art-card-sm relative p-9 pb-6 overflow-hidden text-center">
|
||||
<img
|
||||
class="absolute top-0 left-0 w-full h-50 object-cover"
|
||||
src="@imgs/user/user-bg.jpg"
|
||||
/>
|
||||
<SaImageUpload
|
||||
class="w-20 h-20 mt-30 mx-auto"
|
||||
:width="80"
|
||||
:height="80"
|
||||
:showTips="false"
|
||||
v-model="avatar"
|
||||
@change="handleAvatarChange"
|
||||
round
|
||||
/>
|
||||
<h2 class="mt-5 text-xl font-normal">{{ userInfo.username }}</h2>
|
||||
<div class="w-75 mx-auto mt-2.5 text-left">
|
||||
<div class="mt-2.5">
|
||||
<ArtSvgIcon icon="ri:user-line" class="text-g-700" />
|
||||
<span class="ml-2 text-sm">{{ userInfo.realname }}</span>
|
||||
</div>
|
||||
<div class="mt-2.5">
|
||||
<ArtSvgIcon
|
||||
:icon="userInfo.gender === '1' ? 'ri:men-line' : 'ri:women-line'"
|
||||
class="text-g-700"
|
||||
/>
|
||||
<span class="ml-2 text-sm">{{ userInfo.gender === '1' ? '男' : '女' }}</span>
|
||||
</div>
|
||||
<div class="mt-2.5">
|
||||
<ArtSvgIcon icon="ri:mail-line" class="text-g-700" />
|
||||
<span class="ml-2 text-sm">{{ userInfo.email }}</span>
|
||||
</div>
|
||||
<div class="mt-2.5">
|
||||
<ArtSvgIcon icon="ri:phone-line" class="text-g-700" />
|
||||
<span class="ml-2 text-sm">{{ userInfo.phone }}</span>
|
||||
</div>
|
||||
<div class="mt-2.5">
|
||||
<ArtSvgIcon icon="ri:dribbble-fill" class="text-g-700" />
|
||||
<span class="ml-2 text-sm">{{ userInfo.department?.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="art-card-sm py-5 h-128 my-5">
|
||||
<div class="art-card-header border-b border-g-300">
|
||||
<h1 class="p-4 text-xl font-normal">日志信息</h1>
|
||||
<ElRadioGroup v-model="logType">
|
||||
<ElRadioButton :value="1" label="登录日志"></ElRadioButton>
|
||||
<ElRadioButton :value="2" label="操作日志"></ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
<div class="mt-7.5">
|
||||
<el-timeline class="pl-5 mt-3" v-if="logType === 1 && loginLogList.length > 0">
|
||||
<el-timeline-item
|
||||
v-for="(item, idx) in loginLogList"
|
||||
:key="idx"
|
||||
:timestamp="`您于 ${item.login_time} 登录系统`"
|
||||
placement="top"
|
||||
>
|
||||
<div class="py-2 text-xs">
|
||||
<span>地理位置:{{ item.ip_location || '未知' }}</span>
|
||||
<span class="ml-2">操作系统:{{ item.os }}</span>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<el-timeline class="pl-5 mt-3" v-if="logType === 2 && operationLogList.length > 0">
|
||||
<el-timeline-item
|
||||
v-for="(item, idx) in operationLogList"
|
||||
:key="idx"
|
||||
:timestamp="`您于 ${item.create_time} 执行了 ${item.service_name}`"
|
||||
placement="top"
|
||||
>
|
||||
<div class="py-2 text-xs">
|
||||
<span>地理位置:{{ item.ip_location || '未知' }}</span>
|
||||
<span class="ml-2">路由:{{ item.router }}</span>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden max-md:w-full max-md:mt-3.5">
|
||||
<div class="art-card-sm">
|
||||
<h1 class="p-4 text-xl font-normal border-b border-g-300">基本设置</h1>
|
||||
|
||||
<ElForm
|
||||
:model="form"
|
||||
class="box-border p-5 [&>.el-row_.el-form-item]:w-[calc(50%-10px)] [&>.el-row_.el-input]:w-full [&>.el-row_.el-select]:w-full"
|
||||
ref="ruleFormRef"
|
||||
:rules="rules"
|
||||
label-width="86px"
|
||||
label-position="top"
|
||||
>
|
||||
<ElRow>
|
||||
<ElFormItem label="姓名" prop="realname">
|
||||
<ElInput v-model="form.realname" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="性别" prop="gender" class="ml-5">
|
||||
<SaSelect
|
||||
v-model="form.gender"
|
||||
placeholder="请选择性别"
|
||||
dict="gender"
|
||||
valueType="string"
|
||||
:disabled="!isEdit"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElRow>
|
||||
<ElFormItem label="邮箱" prop="email">
|
||||
<ElInput v-model="form.email" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机" prop="phone" class="ml-5">
|
||||
<ElInput v-model="form.phone" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElFormItem label="个人介绍" prop="signed" class="h-32">
|
||||
<ElInput type="textarea" :rows="4" v-model="form.signed" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
|
||||
<div class="flex-c justify-end [&_.el-button]:!w-27.5">
|
||||
<ElButton type="primary" class="w-22.5" v-ripple @click="edit">
|
||||
{{ isEdit ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
|
||||
<div class="art-card-sm h-128 my-5">
|
||||
<h1 class="p-4 text-xl font-normal border-b border-g-300">更改密码</h1>
|
||||
|
||||
<ElForm
|
||||
:model="pwdForm"
|
||||
:rules="pwdRules"
|
||||
ref="pwdFormRef"
|
||||
class="box-border p-5"
|
||||
label-width="86px"
|
||||
label-position="top"
|
||||
>
|
||||
<ElFormItem label="当前密码" prop="oldPassword">
|
||||
<ElInput
|
||||
v-model="pwdForm.oldPassword"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="新密码" prop="newPassword">
|
||||
<ElInput
|
||||
v-model="pwdForm.newPassword"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
@input="checkSafe"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="密码安全度" prop="passwordSafePercent">
|
||||
<ElProgress
|
||||
:percentage="passwordSafePercent"
|
||||
:show-text="false"
|
||||
class="w-full"
|
||||
status="success"
|
||||
:stroke-width="12"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="确认新密码" prop="confirmPassword">
|
||||
<ElInput
|
||||
v-model="pwdForm.confirmPassword"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="flex-c justify-end [&_.el-button]:!w-27.5">
|
||||
<ElButton type="primary" class="w-22.5" v-ripple @click="editPwd">
|
||||
{{ isEditPwd ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { fetchGetLogin, fetchGetOperate, updateUserInfo, modifyPassword } from '@/api/auth'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'UserCenter' })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.getUserInfo)
|
||||
|
||||
const isEdit = ref(false)
|
||||
const isEditPwd = ref(false)
|
||||
const date = ref('')
|
||||
|
||||
/**
|
||||
* 用户信息表单
|
||||
*/
|
||||
const form = toReactive(userStore.info)
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
const pwdFormRef = ref<FormInstance>()
|
||||
const passwordSafePercent = ref(0)
|
||||
|
||||
const avatar = ref('')
|
||||
|
||||
/**
|
||||
* 密码修改表单
|
||||
*/
|
||||
const pwdForm = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
realname: [
|
||||
{ required: true, message: '请输入姓名', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }],
|
||||
phone: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
|
||||
gender: [{ required: true, message: '请选择性别', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const pwdRules = reactive<FormRules>({
|
||||
oldPassword: [{ required: true, message: '请输入当前密码', trigger: 'blur' }],
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [{ required: true, message: '请确认新密码', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const loginLogList = ref<Api.Common.ApiData[]>([])
|
||||
const operationLogList = ref<Api.Common.ApiData[]>([])
|
||||
const logType = ref(1) // 1: 登录日志, 2: 操作日志
|
||||
|
||||
// 监听radio切换
|
||||
watch(logType, (newVal) => {
|
||||
if (newVal === 1 && loginLogList.value.length === 0) {
|
||||
loadLogin()
|
||||
} else if (newVal === 2 && operationLogList.value.length === 0) {
|
||||
loadOperate()
|
||||
}
|
||||
})
|
||||
|
||||
const loadLogin = async () => {
|
||||
try {
|
||||
const data = await fetchGetLogin({
|
||||
page: 1,
|
||||
limit: 5,
|
||||
orderType: 'desc'
|
||||
})
|
||||
loginLogList.value = data.data || []
|
||||
} catch (error) {
|
||||
console.error('加载登录日志失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadOperate = async () => {
|
||||
try {
|
||||
const data = await fetchGetOperate({
|
||||
page: 1,
|
||||
limit: 5,
|
||||
orderType: 'desc'
|
||||
})
|
||||
operationLogList.value = data.data || []
|
||||
} catch (error) {
|
||||
console.error('加载操作日志失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
avatar.value = userInfo.value.avatar || ''
|
||||
getDate()
|
||||
loadLogin()
|
||||
loadOperate()
|
||||
})
|
||||
|
||||
/**
|
||||
* 根据当前时间获取问候语
|
||||
*/
|
||||
const getDate = () => {
|
||||
const h = new Date().getHours()
|
||||
|
||||
if (h >= 6 && h < 9) date.value = '早上好'
|
||||
else if (h >= 9 && h < 11) date.value = '上午好'
|
||||
else if (h >= 11 && h < 13) date.value = '中午好'
|
||||
else if (h >= 13 && h < 18) date.value = '下午好'
|
||||
else if (h >= 18 && h < 24) date.value = '晚上好'
|
||||
else date.value = '很晚了,早点睡'
|
||||
}
|
||||
|
||||
const handleAvatarChange = async (val: string | string[]) => {
|
||||
if (!val) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await updateUserInfo({
|
||||
id: form.id,
|
||||
avatar: val
|
||||
})
|
||||
userStore.setAvatar(val as string)
|
||||
ElMessage.success('修改头像成功')
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换用户信息编辑状态
|
||||
*/
|
||||
const edit = async () => {
|
||||
if (isEdit.value) {
|
||||
try {
|
||||
await ruleFormRef.value?.validate()
|
||||
await updateUserInfo(form)
|
||||
ElMessage.success('修改成功')
|
||||
isEdit.value = !isEdit.value
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
} else {
|
||||
isEdit.value = !isEdit.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换密码编辑状态
|
||||
*/
|
||||
const editPwd = async () => {
|
||||
if (isEditPwd.value) {
|
||||
try {
|
||||
await pwdFormRef.value?.validate()
|
||||
if (pwdForm.newPassword !== pwdForm.confirmPassword) {
|
||||
ElMessage.error('确认密码与新密码不一致')
|
||||
return
|
||||
}
|
||||
await modifyPassword(pwdForm)
|
||||
ElMessage.success('修改成功')
|
||||
Object.assign(pwdForm, { oldPassword: '', newPassword: '', confirmPassword: '' })
|
||||
isEditPwd.value = !isEditPwd.value
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
} else {
|
||||
isEditPwd.value = !isEditPwd.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查密码安全程度
|
||||
* @param password 密码
|
||||
*/
|
||||
const checkSafe = (password: string) => {
|
||||
if (password.length < 1) {
|
||||
passwordSafePercent.value = 0
|
||||
return
|
||||
}
|
||||
if (!(password.length >= 6)) {
|
||||
passwordSafePercent.value = 0
|
||||
return
|
||||
}
|
||||
passwordSafePercent.value = 10
|
||||
if (/\d/.test(password)) {
|
||||
passwordSafePercent.value += 10
|
||||
}
|
||||
if (/[a-z]/.test(password)) {
|
||||
passwordSafePercent.value += 10
|
||||
}
|
||||
if (/[A-Z]/.test(password)) {
|
||||
passwordSafePercent.value += 30
|
||||
}
|
||||
if (/[`~!@#$%^&*()_+<>?:"{},./;'[\]]/.test(password)) {
|
||||
passwordSafePercent.value += 40
|
||||
}
|
||||
}
|
||||
</script>
|
||||
16
saiadmin-artd/src/views/exception/403/index.vue
Normal file
16
saiadmin-artd/src/views/exception/403/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<!-- 403页面 -->
|
||||
<template>
|
||||
<ArtException
|
||||
:data="{
|
||||
title: '403',
|
||||
desc: $t('exceptionPage.403'),
|
||||
btnText: $t('exceptionPage.gohome'),
|
||||
imgUrl
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import imgUrl from '@imgs/svg/403.svg'
|
||||
defineOptions({ name: 'Exception403' })
|
||||
</script>
|
||||
16
saiadmin-artd/src/views/exception/404/index.vue
Normal file
16
saiadmin-artd/src/views/exception/404/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<!-- 404页面 -->
|
||||
<template>
|
||||
<ArtException
|
||||
:data="{
|
||||
title: '404',
|
||||
desc: $t('exceptionPage.404'),
|
||||
btnText: $t('exceptionPage.gohome'),
|
||||
imgUrl
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import imgUrl from '@imgs/svg/404.svg'
|
||||
defineOptions({ name: 'Exception404' })
|
||||
</script>
|
||||
16
saiadmin-artd/src/views/exception/500/index.vue
Normal file
16
saiadmin-artd/src/views/exception/500/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<!-- 500页面 -->
|
||||
<template>
|
||||
<ArtException
|
||||
:data="{
|
||||
title: '500',
|
||||
desc: $t('exceptionPage.500'),
|
||||
btnText: $t('exceptionPage.gohome'),
|
||||
imgUrl
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import imgUrl from '@imgs/svg/500.svg'
|
||||
defineOptions({ name: 'Exception500' })
|
||||
</script>
|
||||
29
saiadmin-artd/src/views/index/index.vue
Normal file
29
saiadmin-artd/src/views/index/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<!-- 布局容器 -->
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<aside id="app-sidebar">
|
||||
<ArtSidebarMenu />
|
||||
</aside>
|
||||
|
||||
<main id="app-main">
|
||||
<div id="app-header">
|
||||
<ArtHeaderBar />
|
||||
</div>
|
||||
<div id="app-content">
|
||||
<ArtPageContent />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="app-global">
|
||||
<ArtGlobalComponent />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'AppLayout' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
93
saiadmin-artd/src/views/index/style.scss
Normal file
93
saiadmin-artd/src/views/index/style.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
.app-layout {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: var(--default-bg-color);
|
||||
|
||||
#app-sidebar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#app-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
|
||||
#app-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#app-content {
|
||||
flex: 1;
|
||||
|
||||
:deep(.layout-content) {
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 40px);
|
||||
margin: auto;
|
||||
|
||||
// 子页面默认 style
|
||||
.page-content {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
background: var(--default-box-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 1180px) {
|
||||
.app-layout {
|
||||
#app-main {
|
||||
height: 100dvh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 800px) {
|
||||
.app-layout {
|
||||
position: relative;
|
||||
|
||||
#app-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 300;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#app-main {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
|
||||
#app-content {
|
||||
:deep(.layout-content) {
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 640px) {
|
||||
.app-layout {
|
||||
#app-main {
|
||||
#app-content {
|
||||
:deep(.layout-content) {
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
saiadmin-artd/src/views/outside/Iframe.vue
Normal file
42
saiadmin-artd/src/views/outside/Iframe.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="box-border w-full h-full" v-loading="isLoading">
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
:src="iframeUrl"
|
||||
frameborder="0"
|
||||
class="w-full h-full min-h-[calc(100vh-120px)] border-none"
|
||||
@load="handleIframeLoad"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IframeRouteManager } from '@/router/core'
|
||||
|
||||
defineOptions({ name: 'IframeView' })
|
||||
|
||||
const route = useRoute()
|
||||
const isLoading = ref(true)
|
||||
const iframeUrl = ref('')
|
||||
const iframeRef = ref<HTMLIFrameElement | null>(null)
|
||||
|
||||
/**
|
||||
* 初始化 iframe URL
|
||||
* 从路由配置中获取对应的外部链接地址
|
||||
*/
|
||||
onMounted(() => {
|
||||
const iframeRoute = IframeRouteManager.getInstance().findByPath(route.path)
|
||||
|
||||
if (iframeRoute?.meta) {
|
||||
iframeUrl.value = iframeRoute.meta.link || ''
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理 iframe 加载完成事件
|
||||
* 隐藏加载状态
|
||||
*/
|
||||
const handleIframeLoad = (): void => {
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
188
saiadmin-artd/src/views/plugin/saipackage/api/index.ts
Normal file
188
saiadmin-artd/src/views/plugin/saipackage/api/index.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 插件管理 API
|
||||
*
|
||||
* 提供插件安装、卸载、上传等功能接口
|
||||
*
|
||||
* @module api/tool/saipackage
|
||||
*/
|
||||
import request from '@/utils/http'
|
||||
|
||||
export interface AppInfo {
|
||||
app: string
|
||||
title: string
|
||||
about: string
|
||||
author: string
|
||||
version: string
|
||||
support?: string
|
||||
website?: string
|
||||
state: number
|
||||
npm_dependent_wait_install?: number
|
||||
composer_dependent_wait_install?: number
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
saiadmin_version?: {
|
||||
describe: string
|
||||
notes: string
|
||||
state: string
|
||||
}
|
||||
saipackage_version?: {
|
||||
describe: string
|
||||
notes: string
|
||||
state: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AppListResponse {
|
||||
data: AppInfo[]
|
||||
version: VersionInfo
|
||||
}
|
||||
|
||||
export interface StoreApp {
|
||||
id: number
|
||||
title: string
|
||||
about: string
|
||||
logo: string
|
||||
version: string
|
||||
price: string
|
||||
avatar?: string
|
||||
username: string
|
||||
sales_num: number
|
||||
content?: string
|
||||
screenshots?: string[]
|
||||
}
|
||||
|
||||
export interface StoreUser {
|
||||
nickname?: string
|
||||
username: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
export interface PurchasedApp {
|
||||
id: number
|
||||
app_id: number
|
||||
title: string
|
||||
logo: string
|
||||
version: string
|
||||
developer: string
|
||||
about: string
|
||||
}
|
||||
|
||||
export interface AppVersion {
|
||||
id: number
|
||||
version: string
|
||||
create_time: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 获取已安装的插件列表
|
||||
*/
|
||||
getAppList() {
|
||||
return request.get<AppListResponse>({ url: '/app/saipackage/install/index' })
|
||||
},
|
||||
|
||||
/**
|
||||
* 上传插件包
|
||||
*/
|
||||
uploadApp(data: FormData) {
|
||||
return request.post<AppInfo>({ url: '/app/saipackage/install/upload', data })
|
||||
},
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
*/
|
||||
installApp(data: { appName: string }) {
|
||||
return request.post<any>({ url: '/app/saipackage/install/install', data })
|
||||
},
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
*/
|
||||
uninstallApp(data: { appName: string }) {
|
||||
return request.post<any>({ url: '/app/saipackage/install/uninstall', data })
|
||||
},
|
||||
|
||||
/**
|
||||
* 重载后端
|
||||
*/
|
||||
reloadBackend() {
|
||||
return request.post<any>({ url: '/app/saipackage/install/reload' })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取在线商店应用列表
|
||||
*/
|
||||
getOnlineAppList(params: {
|
||||
page?: number
|
||||
limit?: number
|
||||
price?: string
|
||||
type?: string | number
|
||||
keywords?: string
|
||||
}) {
|
||||
return request.get<{ data: StoreApp[]; total: number }>({
|
||||
url: '/tool/install/online/appList',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
getStoreCaptcha() {
|
||||
return request.get<{ image: string; uuid: string }>({
|
||||
url: '/tool/install/online/storeCaptcha'
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 商店登录
|
||||
*/
|
||||
storeLogin(data: { username: string; password: string; code: string; uuid: string }) {
|
||||
return request.post<{ access_token: string }>({
|
||||
url: '/tool/install/online/storeLogin',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商店用户信息
|
||||
*/
|
||||
getStoreUserInfo(token: string) {
|
||||
return request.get<StoreUser>({
|
||||
url: '/tool/install/online/storeUserInfo',
|
||||
params: { token }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取已购应用列表
|
||||
*/
|
||||
getPurchasedApps(token: string) {
|
||||
return request.get<PurchasedApp[]>({
|
||||
url: '/tool/install/online/storePurchasedApps',
|
||||
params: { token }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取应用版本列表
|
||||
*/
|
||||
getAppVersions(token: string, app_id: number) {
|
||||
return request.get<AppVersion[]>({
|
||||
url: '/tool/install/online/storeAppVersions',
|
||||
params: { token, app_id }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载应用
|
||||
*/
|
||||
downloadApp(data: { token: string; id: number }) {
|
||||
return request.post<any>({
|
||||
url: '/tool/install/online/storeDownloadApp',
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
988
saiadmin-artd/src/views/plugin/saipackage/install/index.vue
Normal file
988
saiadmin-artd/src/views/plugin/saipackage/install/index.vue
Normal file
@@ -0,0 +1,988 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<ElCard class="flex flex-col flex-1 min-h-0 art-table-card" shadow="never">
|
||||
<!-- 提示警告 -->
|
||||
<ElAlert type="warning" :closable="false">
|
||||
仅支持上传由插件市场下载的zip压缩包进行安装,请您务必确认插件包文件来自官方渠道或经由官方认证的插件作者!
|
||||
</ElAlert>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="flex flex-wrap items-center my-2">
|
||||
<ElButton @click="getList" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:refresh-line" />
|
||||
</template>
|
||||
</ElButton>
|
||||
<ElButton @click="handleUpload" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:upload-line" />
|
||||
</template>
|
||||
上传插件包
|
||||
</ElButton>
|
||||
<ElButton type="danger" @click="handleTerminal" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:terminal-box-line" />
|
||||
</template>
|
||||
</ElButton>
|
||||
|
||||
<div class="flex items-center gap-1 ml-auto">
|
||||
<div class="version-title">saiadmin版本</div>
|
||||
<div class="version-value">{{ version?.saiadmin_version?.describe }}</div>
|
||||
<div class="version-title">状态</div>
|
||||
<div
|
||||
class="version-value"
|
||||
:class="[
|
||||
version?.saiadmin_version?.notes === '正常' ? 'text-green-500' : 'text-red-500'
|
||||
]"
|
||||
>
|
||||
{{ version?.saiadmin_version?.notes }}
|
||||
</div>
|
||||
<div class="version-title">saipackage安装器</div>
|
||||
<div class="version-value">{{ version?.saipackage_version?.describe }}</div>
|
||||
<div class="version-title">状态</div>
|
||||
<div
|
||||
class="version-value"
|
||||
:class="[
|
||||
version?.saipackage_version?.notes === '正常' ? 'text-green-500' : 'text-red-500'
|
||||
]"
|
||||
>
|
||||
{{ version?.saipackage_version?.notes }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab切换 -->
|
||||
<ElTabs v-model="activeTab" type="border-card">
|
||||
<!-- 本地安装 Tab -->
|
||||
<ElTabPane label="本地安装" name="local">
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="installList"
|
||||
:columns="columns"
|
||||
:show-table-header="false"
|
||||
>
|
||||
<!-- 插件标识列 -->
|
||||
<template #app="{ row }">
|
||||
<ElLink :href="row.website" target="_blank" type="primary">{{ row.app }}</ElLink>
|
||||
</template>
|
||||
|
||||
<!-- 状态列 -->
|
||||
<template #state="{ row }">
|
||||
<ElTag v-if="row.state === 0" type="danger">已卸载</ElTag>
|
||||
<ElTag v-else-if="row.state === 1" type="success">已安装</ElTag>
|
||||
<ElTag v-else-if="row.state === 2" type="primary">等待安装</ElTag>
|
||||
<ElTag v-else-if="row.state === 4" type="warning">等待安装依赖</ElTag>
|
||||
</template>
|
||||
|
||||
<!-- 前端依赖列 -->
|
||||
<template #npm="{ row }">
|
||||
<ElLink
|
||||
v-if="row.npm_dependent_wait_install === 1"
|
||||
type="primary"
|
||||
@click="handleExecFront(row)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:download-line" class="mr-1" />点击安装
|
||||
</ElLink>
|
||||
<ElTag v-else-if="row.state === 2" type="info">-</ElTag>
|
||||
<ElTag v-else type="success">已安装</ElTag>
|
||||
</template>
|
||||
|
||||
<!-- 后端依赖列 -->
|
||||
<template #composer="{ row }">
|
||||
<ElLink
|
||||
v-if="row.composer_dependent_wait_install === 1"
|
||||
type="primary"
|
||||
@click="handleExecBackend(row)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:download-line" class="mr-1" />点击安装
|
||||
</ElLink>
|
||||
<ElTag v-else-if="row.state === 2" type="info">-</ElTag>
|
||||
<ElTag v-else type="success">已安装</ElTag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<ElSpace>
|
||||
<ElPopconfirm
|
||||
title="确定要安装当前插件吗?"
|
||||
@confirm="handleInstall(row)"
|
||||
confirm-button-text="确定"
|
||||
cancel-button-text="取消"
|
||||
>
|
||||
<template #reference>
|
||||
<ElLink type="warning">
|
||||
<ArtSvgIcon icon="ri:apps-2-add-line" class="mr-1" />安装
|
||||
</ElLink>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
<ElPopconfirm
|
||||
title="确定要卸载当前插件吗?"
|
||||
@confirm="handleUninstall(row)"
|
||||
confirm-button-text="确定"
|
||||
cancel-button-text="取消"
|
||||
>
|
||||
<template #reference>
|
||||
<ElLink type="danger">
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" class="mr-1" />卸载
|
||||
</ElLink>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 在线商店 Tab -->
|
||||
<ElTabPane label="在线商店" name="online">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="flex flex-wrap items-center gap-4 mb-4">
|
||||
<ElInput
|
||||
v-model="searchForm.keywords"
|
||||
placeholder="请输入关键词"
|
||||
clearable
|
||||
class="!w-48"
|
||||
@keyup.enter="fetchOnlineApps"
|
||||
>
|
||||
<template #prefix>
|
||||
<ArtSvgIcon icon="ri:search-line" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElSelect v-model="searchForm.type" placeholder="类型" clearable class="!w-32">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="插件" :value="1" />
|
||||
<ElOption label="系统" :value="2" />
|
||||
<ElOption label="组件" :value="3" />
|
||||
<ElOption label="项目" :value="4" />
|
||||
</ElSelect>
|
||||
<ElSelect v-model="searchForm.price" placeholder="价格" class="!w-32">
|
||||
<ElOption label="全部" value="all" />
|
||||
<ElOption label="免费" value="free" />
|
||||
<ElOption label="付费" value="paid" />
|
||||
</ElSelect>
|
||||
<ElButton type="primary" @click="fetchOnlineApps">搜索</ElButton>
|
||||
|
||||
<!-- 商店账号 -->
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<template v-if="storeUser">
|
||||
<ElAvatar :size="24">
|
||||
<img v-if="storeUser.avatar" :src="storeUser.avatar" />
|
||||
<ArtSvgIcon v-else icon="ri:user-line" />
|
||||
</ElAvatar>
|
||||
<span class="font-medium">{{ storeUser.nickname || storeUser.username }}</span>
|
||||
<ElButton size="small" @click="showPurchasedApps">已购应用</ElButton>
|
||||
<ElButton size="small" @click="handleLogout">退出</ElButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElButton size="small" @click="handleLogin">登录</ElButton>
|
||||
<ElButton size="small" @click="handleRegister">注册</ElButton>
|
||||
<span class="text-sm text-gray-400">来管理已购插件</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用网格 -->
|
||||
<div class="app-grid">
|
||||
<div
|
||||
v-for="item in onlineApps"
|
||||
:key="item.id"
|
||||
class="app-card"
|
||||
@click="showDetail(item)"
|
||||
>
|
||||
<div class="app-card-header">
|
||||
<img :src="item.logo" :alt="item.title" class="app-logo" />
|
||||
<div class="app-info">
|
||||
<div class="app-title">{{ item.title }}</div>
|
||||
<div class="app-version">v{{ item.version }}</div>
|
||||
</div>
|
||||
<div class="app-price" :class="{ free: item.price === '0.00' }">
|
||||
{{ item.price === '0.00' ? '免费' : '¥' + item.price }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-about">{{ item.about }}</div>
|
||||
<div class="app-footer">
|
||||
<div class="app-author">
|
||||
<img
|
||||
:src="item.avatar || 'https://via.placeholder.com/24'"
|
||||
class="author-avatar"
|
||||
/>
|
||||
<span>{{ item.username }}</span>
|
||||
</div>
|
||||
<div class="app-sales">
|
||||
<ArtSvgIcon icon="ri:user-line" class="mr-1" />
|
||||
{{ item.sales_num }} 销量
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="flex justify-center mt-4">
|
||||
<ElPagination
|
||||
v-model:current-page="onlinePagination.current"
|
||||
v-model:page-size="onlinePagination.size"
|
||||
:total="onlinePagination.total"
|
||||
:page-sizes="[12, 24, 48]"
|
||||
layout="total, prev, pager, next, sizes"
|
||||
@size-change="fetchOnlineApps"
|
||||
@current-change="fetchOnlineApps"
|
||||
/>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
|
||||
<!-- 上传插件弹窗 -->
|
||||
<InstallForm ref="installFormRef" @success="getList" />
|
||||
|
||||
<!-- 终端弹窗 -->
|
||||
<TerminalBox ref="terminalRef" @success="getList" />
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<ElDrawer v-model="detailVisible" :size="600" :with-header="true">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-3">
|
||||
<img :src="currentApp?.logo" class="w-9 h-9 rounded-lg" />
|
||||
<div>
|
||||
<div class="text-lg font-semibold">{{ currentApp?.title }}</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
v{{ currentApp?.version }} · {{ currentApp?.username }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="detail-content">
|
||||
<div class="detail-price" :class="{ free: currentApp?.price === '0.00' }">
|
||||
{{ currentApp?.price === '0.00' ? '免费' : '¥' + currentApp?.price }}
|
||||
</div>
|
||||
<div class="detail-about">{{ currentApp?.about }}</div>
|
||||
|
||||
<!-- 截图预览 -->
|
||||
<div v-if="currentApp?.screenshots?.length" class="mb-6">
|
||||
<div class="text-base font-semibold mb-3">截图预览</div>
|
||||
<ElSpace wrap :size="12">
|
||||
<ElImage
|
||||
v-for="(img, idx) in currentApp?.screenshots"
|
||||
:key="idx"
|
||||
:src="img"
|
||||
:preview-src-list="currentApp?.screenshots"
|
||||
:preview-teleported="true"
|
||||
fit="cover"
|
||||
class="w-36 h-24 rounded-lg cursor-pointer"
|
||||
/>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 详情描述 -->
|
||||
<div class="detail-desc">
|
||||
<div class="text-base font-semibold mb-3">详细介绍</div>
|
||||
<div class="desc-content" v-html="renderMarkdown(currentApp?.content)"></div>
|
||||
</div>
|
||||
|
||||
<!-- 购买按钮 -->
|
||||
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<ElButton type="primary" size="large" class="w-full" @click="handleBuy">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:shopping-cart-line" />
|
||||
</template>
|
||||
前往购买
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
|
||||
<!-- 登录弹窗 -->
|
||||
<ElDialog v-model="loginVisible" title="登录应用商店" width="400" :close-on-click-modal="false">
|
||||
<ElForm :model="loginForm" @submit.prevent="submitLogin" label-position="top">
|
||||
<ElFormItem label="用户名/邮箱" required>
|
||||
<ElInput v-model="loginForm.username" placeholder="请输入用户名或邮箱" clearable>
|
||||
<template #prefix>
|
||||
<ArtSvgIcon icon="ri:user-line" />
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="密码" required>
|
||||
<ElInput
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<ArtSvgIcon icon="ri:lock-line" />
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="验证码" required>
|
||||
<div class="flex gap-2 w-full">
|
||||
<ElInput v-model="loginForm.code" placeholder="请输入验证码" clearable class="flex-1">
|
||||
<template #prefix>
|
||||
<ArtSvgIcon icon="ri:shield-check-line" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<img
|
||||
:src="captchaImage"
|
||||
@click="getCaptcha"
|
||||
class="h-8 w-24 cursor-pointer rounded"
|
||||
title="点击刷新"
|
||||
/>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" native-type="submit" class="w-full" :loading="loginLoading">
|
||||
登录
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
<div class="text-center text-sm text-gray-400">
|
||||
还没有账号?
|
||||
<ElLink type="primary" @click="handleRegister">立即注册</ElLink>
|
||||
</div>
|
||||
</ElForm>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 已购应用抽屉 -->
|
||||
<ElDrawer v-model="purchasedVisible" title="已购应用" :size="720">
|
||||
<div v-loading="purchasedLoading" class="purchased-list">
|
||||
<div v-for="app in purchasedApps" :key="app.id" class="purchased-card">
|
||||
<img :src="app.logo" class="purchased-logo" />
|
||||
<div class="purchased-info">
|
||||
<div class="purchased-title">{{ app.title }}</div>
|
||||
<div class="purchased-version">v{{ app.version }} · {{ app.developer }}</div>
|
||||
<div class="purchased-about">{{ app.about }}</div>
|
||||
</div>
|
||||
<div class="gap-2">
|
||||
<ElButton size="small" @click="viewDocs(app)">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:book-line" />
|
||||
</template>
|
||||
文档
|
||||
</ElButton>
|
||||
<ElButton type="primary" size="small" @click="showVersions(app)">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:download-line" />
|
||||
</template>
|
||||
下载
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty
|
||||
v-if="!purchasedLoading && purchasedApps.length === 0"
|
||||
description="暂无已购应用"
|
||||
/>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
|
||||
<!-- 版本选择对话框 -->
|
||||
<ElDialog
|
||||
v-model="versionVisible"
|
||||
:title="'选择版本 - ' + (currentPurchasedApp?.title || '')"
|
||||
width="500"
|
||||
>
|
||||
<div v-loading="versionLoading" class="version-list">
|
||||
<div v-for="ver in versionList" :key="ver.id" class="version-item">
|
||||
<div>
|
||||
<div class="version-info-row">
|
||||
<span class="version-name">v{{ ver.version }}</span>
|
||||
<span class="version-date">{{ ver.create_time }}</span>
|
||||
</div>
|
||||
<div class="version-remark">{{ ver.remark }}</div>
|
||||
</div>
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="downloadingId === ver.id"
|
||||
@click="downloadVersion(ver)"
|
||||
>
|
||||
下载安装
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElEmpty v-if="!versionLoading && versionList.length === 0" description="暂无可用版本" />
|
||||
</div>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { ColumnOption } from '@/types'
|
||||
import saipackageApi, {
|
||||
type AppInfo,
|
||||
type VersionInfo,
|
||||
type StoreApp,
|
||||
type StoreUser,
|
||||
type PurchasedApp,
|
||||
type AppVersion
|
||||
} from '../api/index'
|
||||
import InstallForm from './install-box.vue'
|
||||
import TerminalBox from './terminal.vue'
|
||||
|
||||
// ========== 基础状态 ==========
|
||||
const activeTab = ref('local')
|
||||
const version = ref<VersionInfo>({})
|
||||
const loading = ref(false)
|
||||
const installFormRef = ref()
|
||||
const terminalRef = ref()
|
||||
const installList = ref<AppInfo[]>([])
|
||||
|
||||
// ========== 本地安装相关 ==========
|
||||
const handleUpload = () => {
|
||||
installFormRef.value?.open()
|
||||
}
|
||||
|
||||
// 检查版本兼容性
|
||||
const checkVersionCompatibility = (support: string | undefined): boolean => {
|
||||
if (!support || !version.value?.saiadmin_version?.describe) {
|
||||
return false // 如果没有兼容性信息,默认不允许安装
|
||||
}
|
||||
|
||||
const currentVersion = version.value.saiadmin_version.describe
|
||||
const currentMatch = currentVersion.match(/^(\d+)\./)
|
||||
|
||||
if (!currentMatch) {
|
||||
return true
|
||||
}
|
||||
|
||||
const currentMajor = currentMatch[1]
|
||||
// support 格式为 "5.x" 或 "5.x|6.x",用 | 分隔多个支持的版本
|
||||
const supportVersions = support.split('|').map((v) => v.trim())
|
||||
|
||||
// 检查当前版本是否匹配任意一个支持的版本
|
||||
return supportVersions.some((ver) => {
|
||||
const supportMatch = ver.match(/^(\d+)\.x$/i)
|
||||
return supportMatch && supportMatch[1] === currentMajor
|
||||
})
|
||||
}
|
||||
|
||||
const handleInstall = async (record: AppInfo) => {
|
||||
// 检查
|
||||
if (version.value?.saipackage_version?.state === 'fail') {
|
||||
ElMessage.error('插件市场saipackage版本检测失败')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查版本兼容性
|
||||
if (!checkVersionCompatibility(record.support)) {
|
||||
ElMessage.error(
|
||||
`此插件仅支持 ${record.support} 版本框架,当前框架版本为 ${version.value?.saiadmin_version?.describe},不兼容无法安装`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await saipackageApi.installApp({ appName: record.app })
|
||||
ElMessage.success('安装成功')
|
||||
getList()
|
||||
saipackageApi.reloadBackend()
|
||||
} catch {
|
||||
// Error already handled by http utility
|
||||
}
|
||||
}
|
||||
|
||||
const handleUninstall = async (record: AppInfo) => {
|
||||
await saipackageApi.uninstallApp({ appName: record.app })
|
||||
ElMessage.success('卸载成功')
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleExecFront = (record: AppInfo) => {
|
||||
const extend = 'module-install:' + record.app
|
||||
terminalRef.value?.open()
|
||||
setTimeout(() => {
|
||||
terminalRef.value?.frontInstall(extend)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleExecBackend = (record: AppInfo) => {
|
||||
const extend = 'module-install:' + record.app
|
||||
terminalRef.value?.open()
|
||||
setTimeout(() => {
|
||||
terminalRef.value?.backendInstall(extend)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleTerminal = () => {
|
||||
terminalRef.value?.open()
|
||||
}
|
||||
|
||||
const columns: ColumnOption[] = [
|
||||
{ prop: 'app', label: '插件标识', width: 120, useSlot: true },
|
||||
{ prop: 'title', label: '插件名称', width: 150 },
|
||||
{ prop: 'about', label: '插件描述', showOverflowTooltip: true },
|
||||
{ prop: 'author', label: '作者', width: 120 },
|
||||
{ prop: 'version', label: '版本', width: 100 },
|
||||
{ prop: 'support', label: '框架兼容', width: 120, align: 'center' },
|
||||
{ prop: 'state', label: '插件状态', width: 100, useSlot: true },
|
||||
{ prop: 'npm', label: '前端依赖', width: 100, useSlot: true },
|
||||
{ prop: 'composer', label: '后端依赖', width: 100, useSlot: true },
|
||||
{ prop: 'operation', label: '操作', width: 140, fixed: 'right', useSlot: true }
|
||||
]
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const resp = await saipackageApi.getAppList()
|
||||
installList.value = resp?.data || []
|
||||
version.value = resp?.version || {}
|
||||
} catch {
|
||||
// Error already handled by http utility
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 在线商店相关 ==========
|
||||
const detailVisible = ref(false)
|
||||
const currentApp = ref<StoreApp | null>(null)
|
||||
const storeUser = ref<StoreUser | null>(null)
|
||||
const storeToken = ref(localStorage.getItem('storeToken') || '')
|
||||
const onlineApps = ref<StoreApp[]>([])
|
||||
const onlineLoading = ref(false)
|
||||
const onlinePagination = reactive({
|
||||
current: 1,
|
||||
size: 12,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 登录相关
|
||||
const loginVisible = ref(false)
|
||||
const loginLoading = ref(false)
|
||||
const captchaImage = ref('')
|
||||
const captchaUuid = ref('')
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keywords: '',
|
||||
type: '' as string | number,
|
||||
price: 'all'
|
||||
})
|
||||
|
||||
// 已购应用相关
|
||||
const purchasedVisible = ref(false)
|
||||
const purchasedLoading = ref(false)
|
||||
const purchasedApps = ref<PurchasedApp[]>([])
|
||||
const versionVisible = ref(false)
|
||||
const versionLoading = ref(false)
|
||||
const versionList = ref<AppVersion[]>([])
|
||||
const currentPurchasedApp = ref<PurchasedApp | null>(null)
|
||||
const downloadingId = ref<number | null>(null)
|
||||
|
||||
const handleLogin = () => {
|
||||
loginVisible.value = true
|
||||
getCaptcha()
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
window.open('https://saas.saithink.top/register', '_blank')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
storeUser.value = null
|
||||
storeToken.value = ''
|
||||
localStorage.removeItem('storeToken')
|
||||
}
|
||||
|
||||
const getCaptcha = async () => {
|
||||
try {
|
||||
const response = await saipackageApi.getStoreCaptcha()
|
||||
captchaImage.value = response?.image || ''
|
||||
captchaUuid.value = response?.uuid || ''
|
||||
} catch {
|
||||
// Error already handled by http utility
|
||||
}
|
||||
}
|
||||
|
||||
const submitLogin = async () => {
|
||||
if (!loginForm.username || !loginForm.password || !loginForm.code) {
|
||||
ElMessage.warning('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
loginLoading.value = true
|
||||
try {
|
||||
const response = await saipackageApi.storeLogin({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password,
|
||||
code: loginForm.code,
|
||||
uuid: captchaUuid.value
|
||||
})
|
||||
|
||||
storeToken.value = response?.access_token || ''
|
||||
localStorage.setItem('storeToken', response?.access_token || '')
|
||||
loginVisible.value = false
|
||||
loginForm.username = ''
|
||||
loginForm.password = ''
|
||||
loginForm.code = ''
|
||||
await fetchStoreUser()
|
||||
ElMessage.success('登录成功')
|
||||
} catch {
|
||||
getCaptcha()
|
||||
// Error already handled by http utility
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchStoreUser = async () => {
|
||||
if (!storeToken.value) return
|
||||
|
||||
try {
|
||||
const response = await saipackageApi.getStoreUserInfo(storeToken.value)
|
||||
storeUser.value = response || null
|
||||
} catch {
|
||||
handleLogout()
|
||||
}
|
||||
}
|
||||
|
||||
const fetchOnlineApps = async () => {
|
||||
onlineLoading.value = true
|
||||
try {
|
||||
const response = await saipackageApi.getOnlineAppList({
|
||||
page: onlinePagination.current,
|
||||
limit: onlinePagination.size,
|
||||
price: searchForm.price,
|
||||
type: searchForm.type,
|
||||
keywords: searchForm.keywords
|
||||
})
|
||||
|
||||
onlineApps.value = response?.data || []
|
||||
onlinePagination.total = response?.total || 0
|
||||
} catch {
|
||||
// Error already handled by http utility
|
||||
} finally {
|
||||
onlineLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showDetail = (item: StoreApp) => {
|
||||
currentApp.value = item
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const renderMarkdown = (content?: string) => {
|
||||
if (!content) return ''
|
||||
return content
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
||||
.replace(/\n/g, '<br/>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
|
||||
}
|
||||
|
||||
const handleBuy = () => {
|
||||
window.open('https://saas.saithink.top/store', '_blank')
|
||||
}
|
||||
|
||||
const showPurchasedApps = async () => {
|
||||
purchasedVisible.value = true
|
||||
purchasedLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await saipackageApi.getPurchasedApps(storeToken.value)
|
||||
purchasedApps.value = response || []
|
||||
} catch {
|
||||
// Error already handled by http utility
|
||||
}
|
||||
purchasedLoading.value = false
|
||||
}
|
||||
|
||||
const viewDocs = (app: PurchasedApp) => {
|
||||
window.open(`https://saas.saithink.top/store/docs-${app.app_id}`, '_blank')
|
||||
}
|
||||
|
||||
const showVersions = async (app: PurchasedApp) => {
|
||||
currentPurchasedApp.value = app
|
||||
versionVisible.value = true
|
||||
versionLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await saipackageApi.getAppVersions(storeToken.value, app.app_id)
|
||||
versionList.value = response || []
|
||||
} catch {
|
||||
// Error already handled by http utility
|
||||
}
|
||||
versionLoading.value = false
|
||||
}
|
||||
|
||||
const downloadVersion = async (ver: AppVersion) => {
|
||||
downloadingId.value = ver.id
|
||||
|
||||
try {
|
||||
await saipackageApi.downloadApp({
|
||||
token: storeToken.value,
|
||||
id: ver.id
|
||||
})
|
||||
|
||||
ElMessage.success('下载成功,即将刷新插件列表...')
|
||||
versionVisible.value = false
|
||||
purchasedVisible.value = false
|
||||
activeTab.value = 'local'
|
||||
getList()
|
||||
} catch {
|
||||
// Error already handled by http utility
|
||||
}
|
||||
downloadingId.value = null
|
||||
}
|
||||
|
||||
// 监听 tab 切换
|
||||
watch(activeTab, (val) => {
|
||||
if (val === 'online') {
|
||||
fetchOnlineApps()
|
||||
fetchStoreUser()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.version-title {
|
||||
padding: 5px 10px;
|
||||
background: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.version-value {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
max-height: calc(100vh - 380px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid var(--el-border-color);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.app-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.app-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.app-price {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-danger);
|
||||
|
||||
&.free {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
|
||||
.app-about {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.app-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.app-sales {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.detail-price {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-danger);
|
||||
margin-bottom: 16px;
|
||||
|
||||
&.free {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-about {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.desc-content {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.8;
|
||||
|
||||
:deep(code) {
|
||||
background: var(--el-fill-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.purchased-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.purchased-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.purchased-logo {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.purchased-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.purchased-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.purchased-version {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.purchased-about {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.version-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.version-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.version-name {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.version-date {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.version-remark {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="上传插件包-安装插件" width="800" :close-on-click-modal="false">
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
<div class="w-[400px]">
|
||||
<div class="text-lg text-red-500 font-bold mb-2">
|
||||
请您务必确认模块包文件来自官方渠道或经由官方认证的模块作者,否则系统可能被破坏,因为:
|
||||
</div>
|
||||
<div class="text-red-500">1. 模块可以修改和新增系统文件</div>
|
||||
<div class="text-red-500">2. 模块可以执行sql命令和代码</div>
|
||||
<div class="text-red-500">3. 模块可以安装新的前后端依赖</div>
|
||||
</div>
|
||||
|
||||
<!-- 已上传的应用信息 -->
|
||||
<div v-if="appInfo && appInfo.app" class="mt-10 w-[600px]">
|
||||
<ElDescriptions :column="1" border>
|
||||
<ElDescriptionsItem label="应用标识">{{ appInfo?.app }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="应用名称">{{ appInfo?.title }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="应用描述">{{ appInfo?.about }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="作者">{{ appInfo?.author }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="版本">{{ appInfo?.version }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</div>
|
||||
|
||||
<!-- 上传区域 -->
|
||||
<div v-else class="mt-10 w-[600px]">
|
||||
<ElUpload
|
||||
drag
|
||||
:http-request="uploadFileHandler"
|
||||
:show-file-list="false"
|
||||
accept=".zip,.rar"
|
||||
class="w-full"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
<ArtSvgIcon icon="ri:upload-cloud-line" class="text-4xl text-gray-400 mb-2" />
|
||||
<div class="text-gray-500">
|
||||
将插件包文件拖到此处,或
|
||||
<span class="text-primary ml-2">点击上传</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElUpload>
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
import saipackageApi, { type AppInfo } from '../api/index'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const uploadSize = 8 * 1024 * 1024
|
||||
|
||||
const initialApp: AppInfo = {
|
||||
app: '',
|
||||
title: '',
|
||||
about: '',
|
||||
author: '',
|
||||
version: '',
|
||||
state: 0
|
||||
}
|
||||
|
||||
const appInfo = reactive<AppInfo>({ ...initialApp })
|
||||
|
||||
const uploadFileHandler = async (options: UploadRequestOptions) => {
|
||||
const file = options.file
|
||||
if (!file) return
|
||||
|
||||
if (file.size > uploadSize) {
|
||||
ElMessage.warning(file.name + '超出文件大小限制(8MB)')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const dataForm = new FormData()
|
||||
dataForm.append('file', file)
|
||||
|
||||
const res = await saipackageApi.uploadApp(dataForm)
|
||||
if (res) {
|
||||
Object.assign(appInfo, res)
|
||||
ElMessage.success('上传成功')
|
||||
emit('success')
|
||||
}
|
||||
} catch {
|
||||
// Error already handled by http utility
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
Object.assign(appInfo, initialApp)
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
390
saiadmin-artd/src/views/plugin/saipackage/install/terminal.vue
Normal file
390
saiadmin-artd/src/views/plugin/saipackage/install/terminal.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 终端执行面板 -->
|
||||
<ElDialog v-model="visible" title="终端执行面板" width="960">
|
||||
<div>
|
||||
<ElEmpty v-if="terminal.taskList.length === 0" description="暂无任务" />
|
||||
<div v-else>
|
||||
<ElTimeline>
|
||||
<ElTimelineItem
|
||||
v-for="(item, idx) in terminal.taskList"
|
||||
:key="idx"
|
||||
:timestamp="item.createTime"
|
||||
placement="top"
|
||||
>
|
||||
<ElCollapse :model-value="terminal.taskList.map((_, i) => i)">
|
||||
<ElCollapseItem :name="idx">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-bold text-base">{{ item.command }}</span>
|
||||
<ElTag :type="getTagType(item.status)" size="small">
|
||||
{{ getTagText(item.status) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
<template #icon>
|
||||
<div class="flex gap-1">
|
||||
<ElButton
|
||||
type="warning"
|
||||
size="small"
|
||||
circle
|
||||
@click.stop="terminal.retryTask(idx)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:refresh-line" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="danger"
|
||||
size="small"
|
||||
circle
|
||||
@click.stop="terminal.delTask(idx)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:delete-bin-line" />
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="
|
||||
item.status === 2 ||
|
||||
item.status === 3 ||
|
||||
(item.status > 3 && item.showMessage)
|
||||
"
|
||||
class="exec-message"
|
||||
>
|
||||
<pre
|
||||
v-for="(msg, index) in item.message"
|
||||
:key="index"
|
||||
v-html="ansiToHtml(msg)"
|
||||
></pre>
|
||||
</div>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</ElTimelineItem>
|
||||
</ElTimeline>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
|
||||
<div class="flex justify-center flex-wrap gap-2">
|
||||
<ElButton type="success" @click="testTerminal">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:play-line" />
|
||||
</template>
|
||||
测试命令
|
||||
</ElButton>
|
||||
<ElButton @click="handleFronted">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:refresh-line" />
|
||||
</template>
|
||||
前端依赖更新
|
||||
</ElButton>
|
||||
<ElButton @click="handleBackend">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:refresh-line" />
|
||||
</template>
|
||||
后端依赖更新
|
||||
</ElButton>
|
||||
<ElButton type="warning" @click="webBuild">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:rocket-line" />
|
||||
</template>
|
||||
一键发布
|
||||
</ElButton>
|
||||
<ElButton @click="openConfig">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:settings-line" />
|
||||
</template>
|
||||
终端设置
|
||||
</ElButton>
|
||||
<ElButton type="danger" @click="terminal.cleanTaskList()">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-line" />
|
||||
</template>
|
||||
清理任务
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 终端设置弹窗 -->
|
||||
<ElDialog v-model="configVisible" title="终端设置" width="500">
|
||||
<ElForm label-width="120px">
|
||||
<ElFormItem label="NPM源">
|
||||
<ElSelect v-model="terminal.npmRegistry" class="w-80" @change="npmRegistryChange">
|
||||
<ElOption value="npm" label="npm官源" />
|
||||
<ElOption value="taobao" label="taobao" />
|
||||
<ElOption value="tencent" label="tencent" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="NPM包管理器">
|
||||
<ElSelect v-model="terminal.packageManager" class="w-80">
|
||||
<ElOption value="npm" label="npm" />
|
||||
<ElOption value="yarn" label="yarn" />
|
||||
<ElOption value="pnpm" label="pnpm" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Composer源">
|
||||
<ElSelect
|
||||
v-model="terminal.composerRegistry"
|
||||
class="w-80"
|
||||
@change="composerRegistryChange"
|
||||
>
|
||||
<ElOption value="composer" label="composer官源" />
|
||||
<ElOption value="tencent" label="tencent" />
|
||||
<ElOption value="huawei" label="huawei" />
|
||||
<ElOption value="kkame" label="kkame" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useTerminalStore, TaskStatus } from '../store/terminal'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const terminal = useTerminalStore()
|
||||
const visible = ref(false)
|
||||
const configVisible = ref(false)
|
||||
|
||||
const testTerminal = () => {
|
||||
terminal.addNodeTask('test', '', () => {})
|
||||
}
|
||||
|
||||
const webBuild = () => {
|
||||
ElMessageBox.confirm('确认重新打包前端并发布项目吗?', '前端打包发布', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
terminal.addNodeTask('web-build', '', () => {
|
||||
ElMessage.success('前端打包发布成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleFronted = () => {
|
||||
ElMessageBox.confirm('确认更新前端Node依赖吗?', '前端依赖更新', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
terminal.addNodeTask('web-install', '', () => {
|
||||
ElMessage.success('前端依赖更新成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleBackend = () => {
|
||||
ElMessageBox.confirm('确认更新后端composer包吗?', 'composer包更新', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
terminal.addTask('composer.update', '', () => {
|
||||
ElMessage.success('composer包更新成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const frontInstall = (extend = '') => {
|
||||
terminal.addNodeTask('web-install', extend, () => {
|
||||
ElMessage.success('前端依赖更新成功')
|
||||
emit('success')
|
||||
})
|
||||
}
|
||||
|
||||
const backendInstall = (extend = '') => {
|
||||
terminal.addTask('composer.update', extend, () => {
|
||||
ElMessage.success('composer包更新成功')
|
||||
setTimeout(() => {
|
||||
emit('success')
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
const npmRegistryChange = (val: string) => {
|
||||
const command = 'set-npm-registry.' + val
|
||||
configVisible.value = false
|
||||
terminal.addTask(command, '', () => {
|
||||
ElMessage.success('NPM源设置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const composerRegistryChange = (val: string) => {
|
||||
const command = 'set-composer-registry.' + val
|
||||
configVisible.value = false
|
||||
terminal.addTask(command, '', () => {
|
||||
ElMessage.success('Composer源设置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const getTagType = (
|
||||
status: TaskStatus
|
||||
): 'success' | 'warning' | 'info' | 'danger' | 'primary' => {
|
||||
switch (status) {
|
||||
case TaskStatus.WAITING:
|
||||
return 'info'
|
||||
case TaskStatus.CONNECTING:
|
||||
return 'primary'
|
||||
case TaskStatus.RUNNING:
|
||||
return 'warning'
|
||||
case TaskStatus.SUCCESS:
|
||||
return 'success'
|
||||
case TaskStatus.FAILED:
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
const getTagText = (status: TaskStatus) => {
|
||||
switch (status) {
|
||||
case TaskStatus.WAITING:
|
||||
return '等待执行'
|
||||
case TaskStatus.CONNECTING:
|
||||
return '连接中'
|
||||
case TaskStatus.RUNNING:
|
||||
return '执行中'
|
||||
case TaskStatus.SUCCESS:
|
||||
return '执行成功'
|
||||
case TaskStatus.FAILED:
|
||||
return '执行失败'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// ESC 字符,用于 ANSI 转义序列
|
||||
const ESC = String.fromCharCode(0x1b)
|
||||
|
||||
const ansiToHtml = (text: string) => {
|
||||
// 先处理 ANSI 颜色代码
|
||||
const colorPattern = new RegExp(`${ESC}\\[([0-9;]+)m`, 'g')
|
||||
let result = text.replace(colorPattern, function (match, codes) {
|
||||
const codeList = codes.split(';').map((c: string) => parseInt(c, 10))
|
||||
|
||||
// 如果是重置代码 (0 或空),返回闭标签
|
||||
if (codeList.length === 1 && (codeList[0] === 0 || isNaN(codeList[0]))) {
|
||||
return '</span>'
|
||||
}
|
||||
|
||||
const styles: string[] = []
|
||||
codeList.forEach((c: number) => {
|
||||
switch (c) {
|
||||
case 0:
|
||||
// 重置 - 不添加样式,在上面已处理
|
||||
break
|
||||
case 1:
|
||||
styles.push('font-weight:bold')
|
||||
break
|
||||
case 3:
|
||||
styles.push('font-style:italic')
|
||||
break
|
||||
case 4:
|
||||
styles.push('text-decoration:underline')
|
||||
break
|
||||
case 30:
|
||||
styles.push('color:black')
|
||||
break
|
||||
case 31:
|
||||
styles.push('color:red')
|
||||
break
|
||||
case 32:
|
||||
styles.push('color:green')
|
||||
break
|
||||
case 33:
|
||||
styles.push('color:yellow')
|
||||
break
|
||||
case 34:
|
||||
styles.push('color:blue')
|
||||
break
|
||||
case 35:
|
||||
styles.push('color:magenta')
|
||||
break
|
||||
case 36:
|
||||
styles.push('color:cyan')
|
||||
break
|
||||
case 37:
|
||||
styles.push('color:white')
|
||||
break
|
||||
// 亮色/高亮色
|
||||
case 90:
|
||||
styles.push('color:#888')
|
||||
break
|
||||
case 91:
|
||||
styles.push('color:#f55')
|
||||
break
|
||||
case 92:
|
||||
styles.push('color:#5f5')
|
||||
break
|
||||
case 93:
|
||||
styles.push('color:#ff5')
|
||||
break
|
||||
case 94:
|
||||
styles.push('color:#55f')
|
||||
break
|
||||
case 95:
|
||||
styles.push('color:#f5f')
|
||||
break
|
||||
case 96:
|
||||
styles.push('color:#5ff')
|
||||
break
|
||||
case 97:
|
||||
styles.push('color:#fff')
|
||||
break
|
||||
}
|
||||
})
|
||||
return styles.length ? `<span style="${styles.join(';')}">` : ''
|
||||
})
|
||||
|
||||
// 清理可能残留的其他 ANSI 转义序列 (如光标移动等)
|
||||
const cleanupPattern = new RegExp(`${ESC}\\[[0-9;]*[A-Za-z]`, 'g')
|
||||
result = result.replace(cleanupPattern, '')
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const openConfig = () => {
|
||||
configVisible.value = true
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
defineExpose({ open, close, frontInstall, backendInstall })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.exec-message {
|
||||
font-size: 12px;
|
||||
line-height: 1.5em;
|
||||
min-height: 30px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
background-color: #000;
|
||||
color: #c0c0c0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c8c9cc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
345
saiadmin-artd/src/views/plugin/saipackage/store/terminal.ts
Normal file
345
saiadmin-artd/src/views/plugin/saipackage/store/terminal.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* 终端状态管理模块 - saipackage插件
|
||||
*
|
||||
* 提供终端命令执行任务队列的状态管理
|
||||
*
|
||||
* @module store/terminal
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
/** 任务状态枚举 */
|
||||
export enum TaskStatus {
|
||||
/** 等待执行 */
|
||||
WAITING = 1,
|
||||
/** 连接中 */
|
||||
CONNECTING = 2,
|
||||
/** 执行中 */
|
||||
RUNNING = 3,
|
||||
/** 执行成功 */
|
||||
SUCCESS = 4,
|
||||
/** 执行失败 */
|
||||
FAILED = 5,
|
||||
/** 未知 */
|
||||
UNKNOWN = 6
|
||||
}
|
||||
|
||||
/** 终端任务接口 */
|
||||
export interface TerminalTask {
|
||||
/** 任务唯一标识 */
|
||||
uuid: string
|
||||
/** 命令名称 */
|
||||
command: string
|
||||
/** 任务状态 */
|
||||
status: TaskStatus
|
||||
/** 执行消息 */
|
||||
message: string[]
|
||||
/** 创建时间 */
|
||||
createTime: string
|
||||
/** 是否显示消息 */
|
||||
showMessage: boolean
|
||||
/** 回调函数 */
|
||||
callback?: (status: number) => void
|
||||
/** 扩展参数 */
|
||||
extend: string
|
||||
}
|
||||
|
||||
// 扩展 window 类型
|
||||
declare global {
|
||||
interface Window {
|
||||
eventSource?: EventSource
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成UUID
|
||||
*/
|
||||
const generateUUID = (): string => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
const formatDateTime = (): string => {
|
||||
const now = new Date()
|
||||
return now.toLocaleString('zh-CN', {
|
||||
hour12: false,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建终端 WebSocket URL
|
||||
*/
|
||||
const buildTerminalUrl = (commandKey: string, uuid: string, extend: string): string => {
|
||||
const env = import.meta.env
|
||||
const baseURL = env.VITE_API_URL || ''
|
||||
const userStore = useUserStore()
|
||||
const token = userStore.accessToken
|
||||
const terminalUrl = '/app/saipackage/index/terminal'
|
||||
return `${baseURL}${terminalUrl}?command=${commandKey}&uuid=${uuid}&extend=${extend}&token=${token}`
|
||||
}
|
||||
|
||||
export const useTerminalStore = defineStore(
|
||||
'saipackageTerminal',
|
||||
() => {
|
||||
// 状态
|
||||
const show = ref(false)
|
||||
const taskList = ref<TerminalTask[]>([])
|
||||
const npmRegistry = ref('npm')
|
||||
const packageManager = ref('pnpm')
|
||||
const composerRegistry = ref('composer')
|
||||
|
||||
/**
|
||||
* 设置任务状态
|
||||
*/
|
||||
const setTaskStatus = (idx: number, status: TaskStatus) => {
|
||||
if (taskList.value[idx]) {
|
||||
taskList.value[idx].status = status
|
||||
taskList.value[idx].showMessage = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加任务消息
|
||||
*/
|
||||
const addTaskMessage = (idx: number, message: string) => {
|
||||
if (taskList.value[idx]) {
|
||||
taskList.value[idx].message = taskList.value[idx].message.concat(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换任务消息显示
|
||||
*/
|
||||
const setTaskShowMessage = (idx: number, val?: boolean) => {
|
||||
if (taskList.value[idx]) {
|
||||
taskList.value[idx].showMessage = val ?? !taskList.value[idx].showMessage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空任务列表
|
||||
*/
|
||||
const cleanTaskList = () => {
|
||||
taskList.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务完成回调
|
||||
*/
|
||||
const taskCompleted = (idx: number) => {
|
||||
const task = taskList.value[idx]
|
||||
if (!task || typeof task.callback !== 'function') return
|
||||
|
||||
const status = task.status
|
||||
if (status === TaskStatus.FAILED || status === TaskStatus.UNKNOWN) {
|
||||
task.callback(TaskStatus.FAILED)
|
||||
} else if (status === TaskStatus.SUCCESS) {
|
||||
task.callback(TaskStatus.SUCCESS)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据UUID查找任务索引
|
||||
*/
|
||||
const findTaskIdxFromUuid = (uuid: string): number | false => {
|
||||
for (let i = 0; i < taskList.value.length; i++) {
|
||||
if (taskList.value[i].uuid === uuid) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据猜测查找任务索引
|
||||
*/
|
||||
const findTaskIdxFromGuess = (idx: number): number | false => {
|
||||
if (!taskList.value[idx]) {
|
||||
let taskKey = -1
|
||||
for (let i = 0; i < taskList.value.length; i++) {
|
||||
if (
|
||||
taskList.value[i].status === TaskStatus.CONNECTING ||
|
||||
taskList.value[i].status === TaskStatus.RUNNING
|
||||
) {
|
||||
taskKey = i
|
||||
}
|
||||
}
|
||||
return taskKey === -1 ? false : taskKey
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动EventSource连接
|
||||
*/
|
||||
const startEventSource = (taskKey: number) => {
|
||||
const task = taskList.value[taskKey]
|
||||
if (!task) return
|
||||
|
||||
window.eventSource = new EventSource(buildTerminalUrl(task.command, task.uuid, task.extend))
|
||||
|
||||
window.eventSource.onmessage = (e: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data)
|
||||
if (!data || !data.data) return
|
||||
|
||||
const taskIdx = findTaskIdxFromUuid(data.uuid)
|
||||
if (taskIdx === false) return
|
||||
|
||||
if (data.data === 'exec-error') {
|
||||
setTaskStatus(taskIdx, TaskStatus.FAILED)
|
||||
window.eventSource?.close()
|
||||
taskCompleted(taskIdx)
|
||||
startTask()
|
||||
} else if (data.data === 'exec-completed') {
|
||||
window.eventSource?.close()
|
||||
if (taskList.value[taskIdx].status !== TaskStatus.SUCCESS) {
|
||||
setTaskStatus(taskIdx, TaskStatus.FAILED)
|
||||
}
|
||||
taskCompleted(taskIdx)
|
||||
startTask()
|
||||
} else if (data.data === 'connection-success') {
|
||||
setTaskStatus(taskIdx, TaskStatus.RUNNING)
|
||||
} else if (data.data === 'exec-success') {
|
||||
setTaskStatus(taskIdx, TaskStatus.SUCCESS)
|
||||
} else {
|
||||
addTaskMessage(taskIdx, data.data)
|
||||
}
|
||||
} catch {
|
||||
// JSON parse error
|
||||
}
|
||||
}
|
||||
|
||||
window.eventSource.onerror = () => {
|
||||
window.eventSource?.close()
|
||||
const taskIdx = findTaskIdxFromGuess(taskKey)
|
||||
if (taskIdx === false) return
|
||||
setTaskStatus(taskIdx, TaskStatus.FAILED)
|
||||
taskCompleted(taskIdx)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 Node 相关任务
|
||||
*/
|
||||
const addNodeTask = (command: string, extend: string = '', callback?: () => void) => {
|
||||
const manager = packageManager.value === 'unknown' ? 'npm' : packageManager.value
|
||||
const fullCommand = `${command}.${manager}`
|
||||
addTask(fullCommand, extend, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加任务
|
||||
*/
|
||||
const addTask = (command: string, extend: string = '', callback?: () => void) => {
|
||||
const task: TerminalTask = {
|
||||
uuid: generateUUID(),
|
||||
createTime: formatDateTime(),
|
||||
status: TaskStatus.WAITING,
|
||||
command,
|
||||
message: [],
|
||||
showMessage: false,
|
||||
extend,
|
||||
callback: callback ? () => callback() : undefined
|
||||
}
|
||||
taskList.value.push(task)
|
||||
|
||||
// 检查是否有已经失败的任务
|
||||
if (show.value === false) {
|
||||
for (const t of taskList.value) {
|
||||
if (t.status === TaskStatus.FAILED || t.status === TaskStatus.UNKNOWN) {
|
||||
ElMessage.warning('任务列表中存在失败的任务')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startTask()
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始执行任务
|
||||
*/
|
||||
const startTask = () => {
|
||||
let taskKey: number | null = null
|
||||
|
||||
// 寻找可以开始执行的命令
|
||||
for (let i = 0; i < taskList.value.length; i++) {
|
||||
const task = taskList.value[i]
|
||||
if (task.status === TaskStatus.WAITING) {
|
||||
taskKey = i
|
||||
break
|
||||
}
|
||||
if (task.status === TaskStatus.CONNECTING || task.status === TaskStatus.RUNNING) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (taskKey !== null) {
|
||||
setTaskStatus(taskKey, TaskStatus.CONNECTING)
|
||||
startEventSource(taskKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试任务
|
||||
*/
|
||||
const retryTask = (idx: number) => {
|
||||
if (taskList.value[idx]) {
|
||||
taskList.value[idx].message = []
|
||||
setTaskStatus(idx, TaskStatus.WAITING)
|
||||
startTask()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
*/
|
||||
const delTask = (idx: number) => {
|
||||
const task = taskList.value[idx]
|
||||
if (task && task.status !== TaskStatus.CONNECTING && task.status !== TaskStatus.RUNNING) {
|
||||
taskList.value.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
taskList,
|
||||
npmRegistry,
|
||||
packageManager,
|
||||
composerRegistry,
|
||||
setTaskStatus,
|
||||
addTaskMessage,
|
||||
setTaskShowMessage,
|
||||
cleanTaskList,
|
||||
addNodeTask,
|
||||
addTask,
|
||||
retryTask,
|
||||
delTask,
|
||||
startTask
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: 'saipackageTerminal',
|
||||
storage: localStorage,
|
||||
pick: ['npmRegistry', 'composerRegistry', 'packageManager']
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default useTerminalStore
|
||||
28
saiadmin-artd/src/views/result/fail/index.vue
Normal file
28
saiadmin-artd/src/views/result/fail/index.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<ArtResultPage
|
||||
type="fail"
|
||||
title="提交失败"
|
||||
message="请核对并修改以下信息后,再重新提交。"
|
||||
iconCode="ri:close-fill"
|
||||
>
|
||||
<template #content>
|
||||
<p>您提交的内容有如下错误:</p>
|
||||
<p>
|
||||
<ArtSvgIcon icon="ri:close-circle-line" class="text-red-500 mr-1" />
|
||||
<span>您的账户已被冻结</span>
|
||||
</p>
|
||||
<p>
|
||||
<ArtSvgIcon icon="ri:close-circle-line" class="text-red-500 mr-1" />
|
||||
<span>您的账户还不具备申请资格</span>
|
||||
</p>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<ElButton type="primary" v-ripple>返回修改</ElButton>
|
||||
<ElButton v-ripple>查看</ElButton>
|
||||
</template>
|
||||
</ArtResultPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ResultFail' })
|
||||
</script>
|
||||
21
saiadmin-artd/src/views/result/success/index.vue
Normal file
21
saiadmin-artd/src/views/result/success/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<ArtResultPage
|
||||
type="success"
|
||||
title="提交成功"
|
||||
message="提交结果页用于反馈一系列操作任务的处理结果,如果仅是简单操作,使用 Message 全局提示反馈即可。灰色区域可以显示一些补充的信息。"
|
||||
iconCode="ri:check-fill"
|
||||
>
|
||||
<template #content>
|
||||
<p>已提交申请,等待部门审核。</p>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<ElButton type="primary" v-ripple>返回修改</ElButton>
|
||||
<ElButton v-ripple>查看</ElButton>
|
||||
<ElButton v-ripple>打印</ElButton>
|
||||
</template>
|
||||
</ArtResultPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ResultSuccess' })
|
||||
</script>
|
||||
321
saiadmin-artd/src/views/safeguard/attachment/index.vue
Normal file
321
saiadmin-artd/src/views/safeguard/attachment/index.vue
Normal file
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<div class="box-border flex gap-4 h-full max-md:block max-md:gap-0 max-md:h-auto">
|
||||
<div class="flex-shrink-0 w-64 h-full max-md:w-full max-md:h-auto max-md:mb-5">
|
||||
<ElCard class="tree-card art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<b>附件分类</b>
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="primary"
|
||||
@click="categoryShowDialog('add')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<ElScrollbar>
|
||||
<ElTree
|
||||
:data="treeData"
|
||||
:props="{ children: 'children', label: 'label' }"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:expand-on-click-node="false"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="flex items-center justify-between w-full" v-if="data.id > 1">
|
||||
<span>{{ node.label }}</span>
|
||||
<div class="tree-node-actions">
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="secondary"
|
||||
@click="categoryShowDialog('edit', data)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="error"
|
||||
@click="categoryDeleteRow(data, categoryApi.delete, getCategoryList)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-grow min-w-0">
|
||||
<ElCard class="art-table-card !mt-0" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<ElSpace wrap>
|
||||
<ElUpload
|
||||
v-permission="'core:system:uploadImage'"
|
||||
class="upload-btn"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:before-upload="beforeUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<ElButton :icon="UploadFilled">上传图片</ElButton>
|
||||
</ElUpload>
|
||||
<ElButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="moveDialogVisible = true"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:swap-box-line" />
|
||||
</template>
|
||||
移动
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
<ElSpace wrap>
|
||||
<SaSelect
|
||||
v-model="searchForm.storage_mode"
|
||||
placeholder="请选择存储模式"
|
||||
dict="upload_mode"
|
||||
@change="handleSearch"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
/>
|
||||
<ElInput
|
||||
v-model="searchForm.origin_name"
|
||||
placeholder="请输入文件名称"
|
||||
:suffix-icon="Search"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</ElSpace>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:attachment:edit'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类弹窗 -->
|
||||
<CategoryDialog
|
||||
v-model="categoryDialogVisible"
|
||||
:dialog-type="categoryDialogType"
|
||||
:data="categoryDialogData"
|
||||
@success="getCategoryList"
|
||||
/>
|
||||
|
||||
<!-- 移动弹窗 -->
|
||||
<MoveDialog v-model="moveDialogVisible" :selected-rows="selectedRows" @success="refreshData" />
|
||||
|
||||
<!-- 表单弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/attachment'
|
||||
import categoryApi from '@/api/safeguard/category'
|
||||
import { uploadImage } from '@/api/auth'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { Search, UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { UploadRequestOptions, UploadProps } from 'element-plus'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import CategoryDialog from './modules/category-dialog.vue'
|
||||
import MoveDialog from './modules/move-dialog.vue'
|
||||
|
||||
/** 附件分类数据 */
|
||||
const treeData = ref([])
|
||||
|
||||
/** 获取附件分类数据 */
|
||||
const getCategoryList = () => {
|
||||
categoryApi.list({ tree: true }).then((data: any) => {
|
||||
treeData.value = data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换附件分类
|
||||
* @param data
|
||||
*/
|
||||
const handleNodeClick = (data: any) => {
|
||||
if (data.id === 1) {
|
||||
searchParams.category_id = undefined
|
||||
} else {
|
||||
searchParams.category_id = data.id
|
||||
}
|
||||
getData()
|
||||
}
|
||||
|
||||
/** 附件分类弹窗相关 */
|
||||
const {
|
||||
dialogType: categoryDialogType,
|
||||
dialogVisible: categoryDialogVisible,
|
||||
dialogData: categoryDialogData,
|
||||
showDialog: categoryShowDialog,
|
||||
deleteRow: categoryDeleteRow
|
||||
} = useSaiAdmin()
|
||||
|
||||
/** 移动弹窗相关 */
|
||||
const moveDialogVisible = ref(false)
|
||||
|
||||
/** 附件弹窗相关 */
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
selectedRows,
|
||||
handleSelectionChange,
|
||||
deleteRow,
|
||||
deleteSelectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
/** 附件搜索表单 */
|
||||
const searchForm = ref({
|
||||
origin_name: undefined,
|
||||
storage_mode: undefined,
|
||||
category_id: undefined,
|
||||
orderField: 'create_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
/** 附件表格相关 */
|
||||
const {
|
||||
columns,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
getData,
|
||||
searchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'url', label: '预览', saiType: 'image', width: 80 },
|
||||
{ prop: 'origin_name', label: '文件名称', minWidth: 160, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'storage_mode',
|
||||
label: '存储模式',
|
||||
width: 100,
|
||||
saiType: 'dict',
|
||||
saiDict: 'upload_mode'
|
||||
},
|
||||
{ prop: 'mime_type', label: '文件类型', width: 160, showOverflowTooltip: true },
|
||||
{ prop: 'size_info', label: '文件大小', width: 100 },
|
||||
{ prop: 'create_time', label: '上传时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
/** 附件搜索 */
|
||||
const handleSearch = () => {
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getData()
|
||||
}
|
||||
|
||||
/** 附件上传前验证 */
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 附件处理上传 */
|
||||
const handleUpload = async (options: UploadRequestOptions) => {
|
||||
const { file } = options
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await uploadImage(formData)
|
||||
ElMessage.success('上传成功')
|
||||
refreshData()
|
||||
} catch (error: any) {
|
||||
console.error('上传失败:', error)
|
||||
ElMessage.error(error.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化附件分类数据 */
|
||||
onMounted(() => {
|
||||
getCategoryList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tree-node-actions {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.el-tree-node__content:hover .tree-node-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增分类' : '编辑分类'"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="上级分类" prop="parent_id">
|
||||
<el-tree-select
|
||||
v-model="formData.parent_id"
|
||||
:data="optionData.treeData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="category_name">
|
||||
<el-input v-model="formData.category_name" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/category'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
treeData: <any[]>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
parent_id: [{ required: true, message: '请选择上级分类', trigger: 'change' }],
|
||||
category_name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
parent_id: null,
|
||||
level: '',
|
||||
category_name: '',
|
||||
sort: 100
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
const data = await api.list({ tree: true })
|
||||
optionData.treeData = data
|
||||
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增文件' : '编辑文件'"
|
||||
width="800px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="文件名称" prop="origin_name">
|
||||
<el-input v-model="formData.origin_name" placeholder="请输入文件名称" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import api from '@/api/safeguard/attachment'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
origin_name: [{ required: true, message: '请输入文件名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 初始表单数据
|
||||
const initialFormData = {
|
||||
id: '',
|
||||
origin_name: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
// 初始化页面数据
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm(props.data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = (data: any) => {
|
||||
if (data) {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
;(formData as any)[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'edit') {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="移动到分类"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item>
|
||||
<div class="text-gray-600 mb-2">
|
||||
已选择 <span class="text-primary font-medium">{{ selectedCount }}</span> 个文件
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标分类" prop="category_id">
|
||||
<el-tree-select
|
||||
v-model="formData.category_id"
|
||||
:data="optionData.treeData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="请选择目标分类"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定移动</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/attachment'
|
||||
import categoryApi from '@/api/safeguard/category'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
selectedRows: any[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
selectedRows: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
treeData: <any[]>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 选中数量
|
||||
*/
|
||||
const selectedCount = computed(() => props.selectedRows.length)
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
category_id: [{ required: true, message: '请选择目标分类', trigger: 'change' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
category_id: null
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
const data = await categoryApi.list({ tree: true })
|
||||
optionData.treeData = data
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
const ids = props.selectedRows.map((row) => row.id)
|
||||
await api.move({
|
||||
ids: ids,
|
||||
category_id: formData.category_id
|
||||
})
|
||||
|
||||
ElMessage.success(`成功移动 ${ids.length} 个文件`)
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
:label-width="'70px'"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="formData.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="formData.phone" placeholder="请输入手机号" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
249
saiadmin-artd/src/views/safeguard/cache/index.vue
vendored
Normal file
249
saiadmin-artd/src/views/safeguard/cache/index.vue
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div class="page-content mb-5">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 字典缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">数据字典-缓存信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.dict_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.dict_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理缓存
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.dict_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 配置缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">系统配置-缓存信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.config_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.config_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理缓存
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.config_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="缓存前缀">
|
||||
{{ cacheInfo.config_cache?.prefix }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 菜单缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">菜单数据-缓存信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.menu_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.menu_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.menu_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="缓存前缀">
|
||||
{{ cacheInfo.menu_cache?.prefix }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 权限缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">权限按钮-缓存信息</span>
|
||||
<span class="text-sm text-gray-500"> 缓存权限按钮的数据 </span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.button_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.button_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.button_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="缓存前缀">
|
||||
{{ cacheInfo.button_cache?.prefix }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色前缀">
|
||||
{{ cacheInfo.button_cache?.role }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24" class="mb-4">
|
||||
<!-- 反射文件缓存 信息 -->
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">反射文件-缓存信息</span>
|
||||
<span class="text-sm text-gray-500"> 缓存反射文件的反射属性的方法名称和权限参数 </span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="缓存TAG">
|
||||
<div class="flex-c">
|
||||
<span>{{ cacheInfo.reflection_cache?.tag }}</span>
|
||||
<ElButton
|
||||
v-permission="'core:server:clear'"
|
||||
class="ml-2"
|
||||
v-ripple
|
||||
@click="handleClearCache(cacheInfo.reflection_cache?.tag)"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
</template>
|
||||
清理缓存
|
||||
</ElButton>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ cacheInfo.reflection_cache?.expire }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="非验证方法缓存前缀">
|
||||
{{ cacheInfo.reflection_cache?.no_need }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="方法名称和权限缓存参数">
|
||||
{{ cacheInfo.reflection_cache?.attr }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/server'
|
||||
import { onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const cacheInfo = reactive({
|
||||
menu_cache: {} as any,
|
||||
button_cache: {} as any,
|
||||
config_cache: {} as any,
|
||||
dict_cache: {} as any,
|
||||
reflection_cache: {} as any
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新缓存信息
|
||||
*/
|
||||
const updateCacheInfo = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.cache({})
|
||||
cacheInfo.menu_cache = data.menu_cache
|
||||
cacheInfo.button_cache = data.button_cache
|
||||
cacheInfo.config_cache = data.config_cache
|
||||
cacheInfo.dict_cache = data.dict_cache
|
||||
cacheInfo.reflection_cache = data.reflection_cache
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
*/
|
||||
const handleClearCache = (tag: string): void => {
|
||||
if (!tag) {
|
||||
ElMessage.warning('请选择要清理的缓存')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要清理标签:【${tag}】的缓存吗?`, '清理选中缓存', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
api.clear({ tag }).then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
updateCacheInfo()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateCacheInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 200px;
|
||||
}
|
||||
:deep(.el-descriptions__content) {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
214
saiadmin-artd/src/views/safeguard/database/index.vue
Normal file
214
saiadmin-artd/src/views/safeguard/database/index.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:database:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleOptimizeRows()"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:tools-fill" />
|
||||
</template>
|
||||
优化表
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:database:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleFragmentRows()"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:wrench-line" />
|
||||
</template>
|
||||
清理碎片
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="name"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:database:index'"
|
||||
type="primary"
|
||||
icon="ri:node-tree"
|
||||
tool-tip="表结构"
|
||||
@click="handleTableDialog(row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:recycle:index'"
|
||||
type="success"
|
||||
icon="ri:recycle-line"
|
||||
tool-tip="回收站"
|
||||
@click="handleRecycleDialog(row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 表结构信息 -->
|
||||
<TableDialog v-model="dialogVisible" :data="dialogData" />
|
||||
|
||||
<!-- 回收站 -->
|
||||
<RecycleList v-model="recycleVisible" :data="recycleData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import api from '@/api/safeguard/database'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import TableDialog from './modules/table-dialog.vue'
|
||||
import RecycleList from './modules/recycle-list.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
orderField: 'create_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'name', label: '表名称', minWidth: 200 },
|
||||
{ prop: 'comment', label: '表注释', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'engine', label: '表引擎', width: 120 },
|
||||
{ prop: 'update_time', label: '更新时间', width: 180, sortable: true },
|
||||
{ prop: 'rows', label: '总行数', width: 120 },
|
||||
{ prop: 'data_free', label: '碎片大小', width: 120 },
|
||||
{ prop: 'data_length', label: '数据大小', width: 120 },
|
||||
{ prop: 'collation', label: '字符集', width: 180 },
|
||||
{ prop: 'create_time', label: '创建时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogVisible, dialogData, selectedRows, handleSelectionChange } = useSaiAdmin()
|
||||
const recycleVisible = ref(false)
|
||||
const recycleData = ref({})
|
||||
|
||||
/**
|
||||
* 表结构
|
||||
* @param row
|
||||
*/
|
||||
const handleTableDialog = (row: Record<string, any>): void => {
|
||||
dialogVisible.value = true
|
||||
dialogData.value = row
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收站
|
||||
* @param row
|
||||
*/
|
||||
const handleRecycleDialog = (row: Record<string, any>): void => {
|
||||
recycleVisible.value = true
|
||||
recycleData.value = row
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化表
|
||||
*/
|
||||
const handleOptimizeRows = (): void => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要优化的行')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要优化选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'优化选中数据',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
).then(() => {
|
||||
api.optimize({ tables: selectedRows.value.map((row) => row.name) }).then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
refreshData()
|
||||
selectedRows.value = []
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理表碎片
|
||||
*/
|
||||
const handleFragmentRows = (): void => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要清理碎片的行')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要清理选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'清理碎片操作',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
).then(() => {
|
||||
api.fragment({ tables: selectedRows.value.map((row) => row.name) }).then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
refreshData()
|
||||
selectedRows.value = []
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
:title="`回收站 - ${props.data?.name}`"
|
||||
size="70%"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="art-full-height">
|
||||
<!-- 表格头部 -->
|
||||
<div>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:recycle:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleDestroyRows()"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
销毁
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:recycle:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleRestoreRows()"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:restart-line" />
|
||||
</template>
|
||||
恢复
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 数据详情插槽 -->
|
||||
<template #json_data="{ row }">
|
||||
{{ JSON.stringify(row) }}
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/database'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
searchForm.value.table = props.data?.name
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getData()
|
||||
}
|
||||
|
||||
const searchForm = ref({
|
||||
table: null
|
||||
})
|
||||
|
||||
const {
|
||||
loading,
|
||||
data: tableData,
|
||||
columns,
|
||||
getData,
|
||||
pagination,
|
||||
searchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.getRecycle,
|
||||
immediate: false,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'delete_time', label: '删除时间', width: 180 },
|
||||
{ prop: 'json_data', label: '数据详情', useSlot: true, showOverflowTooltip: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { handleSelectionChange, selectedRows } = useSaiAdmin()
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁选中数据
|
||||
*/
|
||||
const handleDestroyRows = (): void => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要销毁的行')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要销毁选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'销毁选中数据',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
).then(() => {
|
||||
api
|
||||
.delete({ table: searchForm.value.table, ids: selectedRows.value.map((row) => row.id) })
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
refreshData()
|
||||
selectedRows.value = []
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复选中数据
|
||||
*/
|
||||
const handleRestoreRows = (): void => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要恢复的行')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要恢复选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'恢复选中数据',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
).then(() => {
|
||||
api
|
||||
.recovery({ table: searchForm.value.table, ids: selectedRows.value.map((row) => row.id) })
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
refreshData()
|
||||
selectedRows.value = []
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="表结构信息" width="800px" align-center @close="handleClose">
|
||||
<div>
|
||||
<el-table :data="tableData" style="width: 100%">
|
||||
<el-table-column prop="column_name" label="字段名称" width="180"> </el-table-column>
|
||||
<el-table-column prop="column_type" label="字段类型" width="120"> </el-table-column>
|
||||
<el-table-column prop="column_key" label="字段索引" width="100"> </el-table-column>
|
||||
<el-table-column prop="column_default" label="默认值" width="100"> </el-table-column>
|
||||
<el-table-column prop="column_comment" label="字段注释" min-width="200" showOverflowTooltip>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/database'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const tableData = ref<Api.Common.ApiData[]>([])
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
if (props.data.name) {
|
||||
const data = await api.getDetailed({ table: props.data.name })
|
||||
tableData.value = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="表名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入表名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
402
saiadmin-artd/src/views/safeguard/dict/index.vue
Normal file
402
saiadmin-artd/src/views/safeguard/dict/index.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<!-- 左右页面 -->
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<div class="box-border flex gap-4 h-full max-md:block max-md:gap-0 max-md:h-auto">
|
||||
<div class="flex-shrink-0 h-full max-md:w-full max-md:h-auto max-md:mb-5">
|
||||
<ElCard class="left-card art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<b>数据字典</b>
|
||||
</template>
|
||||
<ElSpace wrap>
|
||||
<SaButton type="primary" icon="ri:refresh-line" @click="refreshTypeData" />
|
||||
<SaButton
|
||||
v-permission="'core:dict:edit'"
|
||||
type="primary"
|
||||
@click="typeShowDialog('add')"
|
||||
/>
|
||||
<SaButton v-permission="'core:dict:edit'" type="secondary" @click="updateTypeDialog" />
|
||||
<SaButton v-permission="'core:dict:edit'" type="error" @click="deleteTypeDialog" />
|
||||
</ElSpace>
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="typeData"
|
||||
:columns="typeColumns"
|
||||
:pagination="typePagination"
|
||||
highlight-current-row
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 基础列 -->
|
||||
<template #name-header="{ column }">
|
||||
<ElPopover placement="bottom" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<div class="flex items-center gap-2 text-theme c-p custom-header">
|
||||
<span>{{ column.label }}</span>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
<ElInput
|
||||
v-model="typeSearch.name"
|
||||
placeholder="搜索字典名称"
|
||||
size="small"
|
||||
clearable
|
||||
@input="handleTypeSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElPopover>
|
||||
</template>
|
||||
<template #code-header="{ column }">
|
||||
<ElPopover placement="bottom" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<div class="flex items-center gap-2 text-theme c-p custom-header">
|
||||
<span>{{ column.label }}</span>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
<ElInput
|
||||
v-model="typeSearch.code"
|
||||
placeholder="搜索字典标识"
|
||||
size="small"
|
||||
clearable
|
||||
@input="handleTypeSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElPopover>
|
||||
</template>
|
||||
<template #id="{ row }">
|
||||
<ElRadio
|
||||
v-model="selectedId"
|
||||
:value="row.id"
|
||||
@update:modelValue="handleTypeChange(row.id, row)"
|
||||
/>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 min-w-0" v-if="selectedId === 0">
|
||||
<ElCard class="flex flex-col flex-5 min-h-0 !mt-0" shadow="never">
|
||||
<el-empty description="请先选择左侧字典类型配置" />
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 min-w-0" v-if="selectedId > 0">
|
||||
<DictSearch v-model="searchForm" @search="handleSearch" @reset="handleReset" />
|
||||
|
||||
<ElCard class="flex flex-col flex-5 min-h-0 art-table-card" shadow="never">
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:dict:edit'"
|
||||
@click="showDataDialog('add', { type_id: selectedId })"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:dict:edit'"
|
||||
@click="deleteSelectedRows(api.dataDelete, getDictData)"
|
||||
:disabled="selectedRows.length === 0"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="dictData"
|
||||
:columns="dictColumns"
|
||||
:pagination="dictPagination"
|
||||
highlight-current-row
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 基础列 -->
|
||||
<template #label="{ row }">
|
||||
<ElTag
|
||||
:style="{
|
||||
backgroundColor: getColor(row.color, 'bg'),
|
||||
borderColor: getColor(row.color, 'border'),
|
||||
color: getColor(row.color, 'text')
|
||||
}"
|
||||
>
|
||||
{{ row.label }}
|
||||
</ElTag>
|
||||
</template>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:dict:edit'"
|
||||
type="secondary"
|
||||
@click="showDataDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:dict:edit'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.dataDelete, getDictData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字典编辑弹窗 -->
|
||||
<TypeEditDialog
|
||||
v-model="typeVisible"
|
||||
:dialog-type="typeDialogType"
|
||||
:data="currentTypeData"
|
||||
@success="getTypeData()"
|
||||
/>
|
||||
|
||||
<!-- 字典项编辑弹窗 -->
|
||||
<DictEditDialog
|
||||
v-model="dictVisible"
|
||||
:dialog-type="dictDialogType"
|
||||
:data="currentDictData"
|
||||
@success="getDictData()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api/safeguard/dict'
|
||||
import DictSearch from '@/views/safeguard/dict/modules/dict-search.vue'
|
||||
import DictEditDialog from './modules/dict-edit-dialog.vue'
|
||||
import TypeEditDialog from './modules/type-edit-dialog.vue'
|
||||
|
||||
// 字典类型数据
|
||||
const {
|
||||
dialogType: typeDialogType,
|
||||
dialogVisible: typeVisible,
|
||||
dialogData: currentTypeData,
|
||||
showDialog: typeShowDialog,
|
||||
deleteRow: typeDeleteRow
|
||||
} = useSaiAdmin()
|
||||
|
||||
// 字典类型
|
||||
const selectedId = ref(0)
|
||||
const selectedRow = ref({})
|
||||
const typeSearch = ref({
|
||||
name: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
/** 修改字典类型 */
|
||||
const updateTypeDialog = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要修改的数据')
|
||||
return
|
||||
}
|
||||
typeShowDialog('edit', { ...selectedRow.value })
|
||||
}
|
||||
|
||||
/** 删除字典类型 */
|
||||
const deleteTypeDialog = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
typeDeleteRow({ ...selectedRow.value }, api.delete, refreshTypeData)
|
||||
}
|
||||
|
||||
/** 字典类型搜索 */
|
||||
const handleTypeSearch = () => {
|
||||
Object.assign(searchTypeParams, typeSearch.value)
|
||||
getTypeData()
|
||||
}
|
||||
|
||||
/** 字典类型切换 */
|
||||
const handleTypeChange = (val: any, row?: any) => {
|
||||
selectedId.value = val
|
||||
selectedRow.value = row
|
||||
searchForm.value.type_id = val
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getDictData()
|
||||
}
|
||||
|
||||
/** 刷新数据 */
|
||||
const refreshTypeData = () => {
|
||||
selectedId.value = 0
|
||||
selectedRow.value = {}
|
||||
getTypeData()
|
||||
getDictData()
|
||||
}
|
||||
|
||||
// 字典类型数据
|
||||
const {
|
||||
data: typeData,
|
||||
columns: typeColumns,
|
||||
getData: getTypeData,
|
||||
searchParams: searchTypeParams,
|
||||
loading,
|
||||
pagination: typePagination,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.typeList,
|
||||
apiParams: {
|
||||
...typeSearch.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ prop: 'id', label: '选中', width: 80, align: 'center', useSlot: true },
|
||||
{ prop: 'name', label: '字典名称', useHeaderSlot: true, width: 150 },
|
||||
{ prop: 'code', label: '字典标识', useHeaderSlot: true, width: 150 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status', width: 100 }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 字典项数据
|
||||
const {
|
||||
dialogType: dictDialogType,
|
||||
dialogVisible: dictVisible,
|
||||
dialogData: currentDictData,
|
||||
showDialog: showDataDialog,
|
||||
deleteRow,
|
||||
handleSelectionChange,
|
||||
selectedRows,
|
||||
deleteSelectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
/** 字典项搜索 */
|
||||
const searchForm = ref({
|
||||
label: '',
|
||||
value: '',
|
||||
status: '',
|
||||
type_id: null
|
||||
})
|
||||
|
||||
// 字典项数据
|
||||
const {
|
||||
data: dictData,
|
||||
columns: dictColumns,
|
||||
getData: getDictData,
|
||||
pagination: dictPagination,
|
||||
searchParams
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.dataList,
|
||||
immediate: false,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'label', label: '字典标签', useSlot: true },
|
||||
{ prop: 'value', label: '字典键值' },
|
||||
{ prop: 'color', label: '颜色' },
|
||||
{ prop: 'sort', label: '排序' },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status' },
|
||||
{ prop: 'operation', label: '操作', useSlot: true, width: 120 }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 字典项搜索
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
if (selectedId.value) {
|
||||
Object.assign(searchParams, params)
|
||||
getDictData()
|
||||
}
|
||||
}
|
||||
|
||||
// 字典项重置搜索
|
||||
const handleReset = () => {
|
||||
if (!selectedId.value) {
|
||||
ElMessage.warning('请选择字典类型')
|
||||
return
|
||||
}
|
||||
Object.assign(searchParams, {
|
||||
label: '',
|
||||
value: '',
|
||||
status: '',
|
||||
type_id: selectedId.value
|
||||
})
|
||||
getDictData()
|
||||
}
|
||||
|
||||
const getColor = (color: string | undefined, type: 'bg' | 'border' | 'text') => {
|
||||
// 如果没有指定颜色,使用默认主色调
|
||||
if (!color) {
|
||||
const colors = {
|
||||
bg: 'var(--el-color-primary-light-9)',
|
||||
border: 'var(--el-color-primary-light-8)',
|
||||
text: 'var(--el-color-primary)'
|
||||
}
|
||||
return colors[type]
|
||||
}
|
||||
|
||||
// 如果是 hex 颜色,转换为 RGB
|
||||
let r, g, b
|
||||
if (color.startsWith('#')) {
|
||||
const hex = color.slice(1)
|
||||
r = parseInt(hex.slice(0, 2), 16)
|
||||
g = parseInt(hex.slice(2, 4), 16)
|
||||
b = parseInt(hex.slice(4, 6), 16)
|
||||
} else if (color.startsWith('rgb')) {
|
||||
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)
|
||||
if (match) {
|
||||
r = parseInt(match[1])
|
||||
g = parseInt(match[2])
|
||||
b = parseInt(match[3])
|
||||
} else {
|
||||
return color
|
||||
}
|
||||
} else {
|
||||
return color
|
||||
}
|
||||
|
||||
// 根据类型返回不同的颜色变体
|
||||
switch (type) {
|
||||
case 'bg':
|
||||
// 背景色 - 更浅的版本
|
||||
return `rgba(${r}, ${g}, ${b}, 0.1)`
|
||||
case 'border':
|
||||
// 边框色 - 中等亮度
|
||||
return `rgba(${r}, ${g}, ${b}, 0.3)`
|
||||
case 'text':
|
||||
// 文字色 - 原始颜色
|
||||
return `rgb(${r}, ${g}, ${b})`
|
||||
default:
|
||||
return color
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.left-card :deep(.el-card__body) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 10px 2px 10px 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增字典项数据' : '编辑字典项数据'"
|
||||
width="600px"
|
||||
align-center
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="字典标签" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入字典标签" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典键值" prop="value">
|
||||
<el-input v-model="formData.value" placeholder="请输入字典键值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色选择" prop="color">
|
||||
<el-color-picker v-model="formData.color" color-format="hex" :predefine="predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<SaiRadio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/dict'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import SaiRadio from '@/components/sai/sa-radio/index.vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const predefineColors = ref([
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#ffd700',
|
||||
'#90ee90',
|
||||
'#00ced1',
|
||||
'#1e90ff',
|
||||
'#c71585',
|
||||
'#5d87ff',
|
||||
'#b48df3',
|
||||
'#1d84ff',
|
||||
'#60c041',
|
||||
'#38c0fc',
|
||||
'#f9901f',
|
||||
'#ff80c8',
|
||||
'#909399'
|
||||
])
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
label: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||
value: [{ required: true, message: '请输入字典键值', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
type_id: '',
|
||||
code: '',
|
||||
label: '',
|
||||
color: '#5d87ff',
|
||||
value: '',
|
||||
remark: '',
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
// 初始化页面数据
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm(props.data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = (data: any) => {
|
||||
if (data) {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
;(formData as any)[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.dataSave(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.dataUpdate(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dictStore.refresh()
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="80px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="字典标签" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入字典标签" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="字典键值" prop="value">
|
||||
<el-input v-model="formData.value" placeholder="请输入字典键值" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增字典数据' : '编辑字典数据'"
|
||||
width="600px"
|
||||
align-center
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="字典名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入字典名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标识" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入字典标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<SaiRadio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/dict'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import SaiRadio from '@/components/sai/sa-radio/index.vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入字典名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入字典标识', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
// 初始化页面数据
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm(props.data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = (data: any) => {
|
||||
if (data) {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
;(formData as any)[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
121
saiadmin-artd/src/views/safeguard/email-log/index.vue
Normal file
121
saiadmin-artd/src/views/safeguard/email-log/index.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:email:destroy'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #status="{ row }">
|
||||
<ElTag v-if="row.status == 'success'" type="success">成功</ElTag>
|
||||
<ElTag v-else type="danger">失败</ElTag>
|
||||
</template>
|
||||
<template #operation="{ row }">
|
||||
<div class="flex">
|
||||
<SaButton
|
||||
v-permission="'core:email:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/safeguard/emailLog'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
from: undefined,
|
||||
email: undefined,
|
||||
status: undefined,
|
||||
create_time: undefined,
|
||||
orderField: 'create_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: '编号', width: 100, align: 'center' },
|
||||
{ prop: 'gateway', label: '服务Host' },
|
||||
{ prop: 'from', label: '发件人', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'email', label: '收件人', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'code', label: '验证码' },
|
||||
{ prop: 'status', label: '发送状态', useSlot: true },
|
||||
{ prop: 'response', label: '发送结果', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'create_time', label: '发送时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 80, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { deleteRow, deleteSelectedRows, selectedRows, handleSelectionChange } = useSaiAdmin()
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="true"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="发件人" prop="from">
|
||||
<el-input v-model="formData.from" placeholder="请输入发件人" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="收件人" prop="email">
|
||||
<el-input v-model="formData.email" placeholder="请输入收件人" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="发送状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择发送状态" clearable>
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="failure" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(12)" v-show="isExpanded">
|
||||
<el-form-item label="发送时间" prop="create_time">
|
||||
<el-date-picker
|
||||
v-model="formData.create_time"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
122
saiadmin-artd/src/views/safeguard/login-log/index.vue
Normal file
122
saiadmin-artd/src/views/safeguard/login-log/index.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:logs:deleteLogin'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #status="{ row }">
|
||||
<ElTag v-if="row.status == 1" type="success">成功</ElTag>
|
||||
<ElTag v-else type="danger">失败</ElTag>
|
||||
</template>
|
||||
<template #operation="{ row }">
|
||||
<div class="flex">
|
||||
<SaButton
|
||||
v-permission="'core:logs:deleteLogin'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/safeguard/loginLog'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
username: undefined,
|
||||
ip: undefined,
|
||||
status: undefined,
|
||||
login_time: undefined,
|
||||
orderField: 'login_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: '编号', width: 100, align: 'center' },
|
||||
{ prop: 'username', label: '登录用户' },
|
||||
{ prop: 'status', label: '登录状态', useSlot: true },
|
||||
{ prop: 'ip', label: '登录IP' },
|
||||
{ prop: 'ip_location', label: '登录地点' },
|
||||
{ prop: 'os', label: '操作系统' },
|
||||
{ prop: 'browser', label: '浏览器' },
|
||||
{ prop: 'message', label: '登录信息', showOverflowTooltip: true },
|
||||
{ prop: 'login_time', label: '登录时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 80, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { deleteRow, deleteSelectedRows, selectedRows, handleSelectionChange } = useSaiAdmin()
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="true"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="登录用户" prop="username">
|
||||
<el-input v-model="formData.username" placeholder="请输入登录用户" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="登录IP" prop="ip">
|
||||
<el-input v-model="formData.ip" placeholder="请输入登录IP" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="登录状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择登录状态" clearable>
|
||||
<el-option label="成功" value="1" />
|
||||
<el-option label="失败" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(12)" v-show="isExpanded">
|
||||
<el-form-item label="登录时间" prop="login_time">
|
||||
<el-date-picker
|
||||
v-model="formData.login_time"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
170
saiadmin-artd/src/views/safeguard/oper-log/index.vue
Normal file
170
saiadmin-artd/src/views/safeguard/oper-log/index.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'core:logs:deleteOper'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton type="success" @click="handleParams(row)" />
|
||||
<SaButton
|
||||
v-permission="'core:logs:deleteOper'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import api from '@/api/safeguard/operLog'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
username: undefined,
|
||||
ip: undefined,
|
||||
service_name: undefined,
|
||||
router: undefined,
|
||||
create_time: undefined,
|
||||
orderField: 'create_time',
|
||||
orderType: 'desc'
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: '编号', width: 100, align: 'center' },
|
||||
{ prop: 'username', label: '操作用户' },
|
||||
{ prop: 'service_name', label: '业务名称' },
|
||||
{ prop: 'router', label: '路由', minWidth: 180, showOverflowTooltip: true },
|
||||
{ prop: 'ip', label: '操作IP' },
|
||||
{ prop: 'ip_location', label: '操作地点' },
|
||||
{ prop: 'create_time', label: '操作时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { deleteRow, deleteSelectedRows, selectedRows, handleSelectionChange } = useSaiAdmin()
|
||||
|
||||
// 预览参数
|
||||
const handleParams = (row: any) => {
|
||||
let formattedData = row.request_data
|
||||
// 尝试格式化JSON数据
|
||||
if (row.request_data) {
|
||||
try {
|
||||
// 如果已经是对象,直接格式化;如果是字符串,先解析再格式化
|
||||
const parsedData =
|
||||
typeof row.request_data === 'string' ? JSON.parse(row.request_data) : row.request_data
|
||||
formattedData = JSON.stringify(parsedData, null, 2)
|
||||
} catch (error) {
|
||||
// 如果解析失败,保持原样显示
|
||||
formattedData = row.request_data
|
||||
console.log('Error parsing JSON:', error)
|
||||
}
|
||||
}
|
||||
|
||||
ElMessageBox({
|
||||
title: '请求参数',
|
||||
message: h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
maxHeight: '400px',
|
||||
minWidth: '380px',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '16px',
|
||||
borderRadius: '4px'
|
||||
}
|
||||
},
|
||||
[
|
||||
h(
|
||||
'pre',
|
||||
{
|
||||
style: {
|
||||
margin: 0,
|
||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
formattedData
|
||||
)
|
||||
]
|
||||
),
|
||||
callback: () => {}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="true"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="操作用户" prop="username">
|
||||
<el-input v-model="formData.username" placeholder="请输入操作用户" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="操作路由" prop="router">
|
||||
<el-input v-model="formData.router" placeholder="请输入操作路由" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="操作IP" prop="ip">
|
||||
<el-input v-model="formData.ip" placeholder="请输入操作IP" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(12)" v-show="isExpanded">
|
||||
<el-form-item label="操作时间" prop="create_time">
|
||||
<el-date-picker
|
||||
v-model="formData.create_time"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
213
saiadmin-artd/src/views/safeguard/server/index.vue
Normal file
213
saiadmin-artd/src/views/safeguard/server/index.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="page-content mb-5">
|
||||
<el-row :gutter="20">
|
||||
<!-- 内存 信息 -->
|
||||
<el-col :span="24" class="mb-4">
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">内存信息</span>
|
||||
</template>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex-1">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="总内存">
|
||||
{{ serverInfo.memory.total }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已使用内存">
|
||||
{{ serverInfo.memory.used }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="PHP使用内存">
|
||||
{{ serverInfo.memory.php }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="空闲内存">
|
||||
{{ serverInfo.memory.free }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="使用率">
|
||||
{{ serverInfo.memory.rate }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div class="w-80 p-4 text-center">
|
||||
<div class="pb-3.5">
|
||||
<span class="text-base font-medium">内存使用率</span>
|
||||
</div>
|
||||
<el-progress
|
||||
type="dashboard"
|
||||
:percentage="Number.parseFloat(serverInfo.memory.rate)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- PHP 信息 -->
|
||||
<el-col :span="24" class="mb-4">
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="text-lg font-medium">PHP及环境信息</span>
|
||||
</template>
|
||||
<div class="py-2">
|
||||
<el-descriptions :column="2" border class="php-config" v-if="serverInfo.phpEnv">
|
||||
<el-descriptions-item
|
||||
label="PHP版本"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.php_version }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="操作系统"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.os }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="项目路径"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
<div class="project-path">{{ serverInfo.phpEnv?.project_path }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="内存限制"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.memory_limit }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="最大执行时间"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{
|
||||
serverInfo.phpEnv?.max_execution_time === '0'
|
||||
? '无限制'
|
||||
: `${serverInfo.phpEnv?.max_execution_time}秒`
|
||||
}}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="错误报告"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.error_reporting }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="显示错误"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.display_errors }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="上传限制"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.upload_max_filesize }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="POST大小"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.post_max_size }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="扩展目录"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.extension_dir }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
label="扩展目录"
|
||||
label-class-name="php-label"
|
||||
content-class-name="php-content"
|
||||
>
|
||||
{{ serverInfo.phpEnv?.loaded_extensions }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 磁盘 信息 -->
|
||||
<el-col :span="24" class="mb-4">
|
||||
<el-card class="art-table-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span><i class="el-icon-disk"></i> 磁盘监控</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="serverInfo.disk" style="width: 100%">
|
||||
<el-table-column prop="filesystem" label="文件系统" />
|
||||
<el-table-column prop="size" label="总大小" />
|
||||
<el-table-column prop="used" label="已用空间" />
|
||||
<el-table-column prop="available" label="可用空间" />
|
||||
<el-table-column prop="use_percentage" label="使用率">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="parseInt(row.use_percentage.replace('%', ''))"
|
||||
:stroke-width="12"
|
||||
:show-text="true"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="mounted_on" label="挂载点" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/safeguard/server'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const serverInfo = reactive({
|
||||
memory: {
|
||||
total: '',
|
||||
used: '',
|
||||
rate: '',
|
||||
php: '',
|
||||
free: ''
|
||||
},
|
||||
disk: [] as any[],
|
||||
phpEnv: {} as any
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新服务器信息
|
||||
*/
|
||||
const updateServer = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.monitor({})
|
||||
serverInfo.memory = data.memory
|
||||
serverInfo.phpEnv = data.phpEnv
|
||||
serverInfo.disk = data.disk
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateServer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 200px;
|
||||
}
|
||||
:deep(.el-descriptions__content) {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
429
saiadmin-artd/src/views/system/config/index.vue
Normal file
429
saiadmin-artd/src/views/system/config/index.vue
Normal file
@@ -0,0 +1,429 @@
|
||||
<!-- 左右页面 -->
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<div class="box-border flex gap-4 h-full max-md:block max-md:gap-0 max-md:h-auto">
|
||||
<div class="flex-shrink-0 h-full max-md:w-full max-md:h-auto max-md:mb-5">
|
||||
<ElCard class="left-card art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<b>系统设置</b>
|
||||
</template>
|
||||
<ElSpace wrap>
|
||||
<SaButton type="primary" icon="ri:refresh-line" @click="reloadConfigData" />
|
||||
<SaButton v-permission="'core:config:edit'" type="primary" @click="showDialog('add')" />
|
||||
<SaButton
|
||||
v-permission="'core:config:edit'"
|
||||
type="secondary"
|
||||
@click="updateConfigDialog"
|
||||
/>
|
||||
<SaButton v-permission="'core:config:edit'" type="error" @click="deleteConfigData" />
|
||||
</ElSpace>
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="groupData"
|
||||
:columns="groupColumns"
|
||||
:pagination="groupPagination"
|
||||
highlight-current-row
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 基础列 -->
|
||||
<template #name-header="{ column }">
|
||||
<ElPopover placement="bottom" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<div class="flex items-center gap-2 text-theme c-p custom-header">
|
||||
<span>{{ column.label }}</span>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
<ElInput
|
||||
v-model="configSearch.name"
|
||||
placeholder="搜索配置名称"
|
||||
size="small"
|
||||
clearable
|
||||
@input="handleConfigSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElPopover>
|
||||
</template>
|
||||
<template #code-header="{ column }">
|
||||
<ElPopover placement="bottom" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<div class="flex items-center gap-2 text-theme c-p custom-header">
|
||||
<span>{{ column.label }}</span>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
<ElInput
|
||||
v-model="configSearch.code"
|
||||
placeholder="搜索配置标识"
|
||||
size="small"
|
||||
clearable
|
||||
@input="handleConfigSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElPopover>
|
||||
</template>
|
||||
<template #id="{ row }">
|
||||
<ElRadio
|
||||
v-model="selectedId"
|
||||
:value="row.id"
|
||||
@update:modelValue="handleGroupChange(row.id, row)"
|
||||
/>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<ElCard class="art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex justify-between">
|
||||
<b>{{ selectedRow.name || '未选择配置' }}</b>
|
||||
<SaButton
|
||||
v-permission="'core:config:edit'"
|
||||
type="primary"
|
||||
icon="ri:settings-4-line"
|
||||
@click="handleConfigManage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||
<ElForm ref="formRef" :model="formData" label-width="140px">
|
||||
<template v-for="(item, index) in formArray" :key="index">
|
||||
<ElFormItem :label="item.name" :prop="item.key" v-show="item.display">
|
||||
<template v-if="item.input_type === 'select'">
|
||||
<el-select
|
||||
v-model="item.value"
|
||||
:options="item.config_select_data"
|
||||
@change="handleSelect($event, item)"
|
||||
:placeholder="'请选择' + item.name"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="item.input_type === 'input'">
|
||||
<el-input v-model="item.value" :placeholder="'请输入' + item.name" />
|
||||
</template>
|
||||
<template v-if="item.input_type === 'radio'">
|
||||
<el-radio-group v-model="item.value" :options="item.config_select_data" />
|
||||
</template>
|
||||
<template v-if="item.input_type === 'textarea'">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="item.value"
|
||||
:placeholder="'请输入' + item.name"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="item.input_type === 'uploadImage'">
|
||||
<sa-image-picker v-model="item.value" />
|
||||
</template>
|
||||
<template v-if="item.input_type === 'uploadFile'">
|
||||
<sa-file-upload v-model="item.value" />
|
||||
</template>
|
||||
<template v-if="item.input_type === 'wangEditor'">
|
||||
<sa-editor v-model="item.value" />
|
||||
</template>
|
||||
<div class="text-gray-400 text-xs py-2">{{ item.remark }}</div>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem v-permission="'core:config:update'" v-if="formArray.length > 0">
|
||||
<ElButton type="primary" @click="submit(formArray)">保存修改</ElButton>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-permission="'core:config:update'"
|
||||
label="测试邮件"
|
||||
v-if="selectedRow.code === 'email_config'"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ElInput
|
||||
v-model="email"
|
||||
style="width: 300px"
|
||||
placeholder="请输入正确的邮箱接收地址"
|
||||
/>
|
||||
<ElButton @click="sendMail()">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:mail-line" />
|
||||
</template>
|
||||
发送
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<el-empty v-if="selectedId === 0" description="请先选择左侧配置" />
|
||||
</ElForm>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置编辑弹窗 -->
|
||||
<GroupEditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="reloadConfigData()"
|
||||
/>
|
||||
|
||||
<!-- 配置项管理 -->
|
||||
<ConfigList v-model="configVisible" :data="selectedRow" @success="getConfigData()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api/system/config'
|
||||
import GroupEditDialog from './modules/group-edit-dialog.vue'
|
||||
import ConfigList from './modules/config-list.vue'
|
||||
|
||||
defineOptions({ name: 'TreeTable' })
|
||||
|
||||
// 刷新配置数据
|
||||
const reloadConfigData = () => {
|
||||
selectedId.value = 0
|
||||
selectedRow.value = {}
|
||||
formArray.value = []
|
||||
getGroupData()
|
||||
}
|
||||
|
||||
// 修改配置
|
||||
const updateConfigDialog = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要修改的数据')
|
||||
return
|
||||
}
|
||||
showDialog('edit', selectedRow.value)
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const deleteConfigData = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要修改的数据')
|
||||
return
|
||||
}
|
||||
deleteRow({ ...selectedRow.value }, api.delete, reloadConfigData)
|
||||
}
|
||||
|
||||
// 配置数据
|
||||
const formData = ref({})
|
||||
const formArray = ref<any[]>([])
|
||||
const email = ref('')
|
||||
|
||||
const configVisible = ref(false)
|
||||
|
||||
// 配置选中行
|
||||
const selectedId = ref(0)
|
||||
const selectedRow = ref<any>({})
|
||||
const configSearch = ref({
|
||||
name: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
// 配置搜索
|
||||
const handleConfigSearch = () => {
|
||||
Object.assign(searchConfigParams, configSearch.value)
|
||||
getGroupData()
|
||||
}
|
||||
|
||||
const searchForm = ref({
|
||||
label: '',
|
||||
value: '',
|
||||
status: '',
|
||||
group_id: null
|
||||
})
|
||||
|
||||
/**
|
||||
* 配置分组改变时,获取配置数据
|
||||
*/
|
||||
const handleGroupChange = (val: any, row?: any) => {
|
||||
selectedId.value = val
|
||||
selectedRow.value = row
|
||||
searchForm.value.group_id = val
|
||||
getConfigData()
|
||||
}
|
||||
|
||||
const getConfigData = () => {
|
||||
api.configList({ group_id: selectedId.value, saiType: 'all' }).then((data) => {
|
||||
formArray.value = data.map((item: any) => {
|
||||
if (
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
} else {
|
||||
item.display = true
|
||||
}
|
||||
return item
|
||||
})
|
||||
if (selectedId.value === 2) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key === 'upload_mode') {
|
||||
handleSelect(item.value, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 配置名称
|
||||
const {
|
||||
data: groupData,
|
||||
columns: groupColumns,
|
||||
getData: getGroupData,
|
||||
searchParams: searchConfigParams,
|
||||
loading,
|
||||
pagination: groupPagination,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.groupList,
|
||||
apiParams: {
|
||||
...configSearch.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ prop: 'id', label: '选中', width: 80, align: 'center', useSlot: true },
|
||||
{ prop: 'name', label: '配置名称', useHeaderSlot: true, width: 150 },
|
||||
{ prop: 'code', label: '配置标识', useHeaderSlot: true, width: 150 }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, deleteRow } = useSaiAdmin()
|
||||
|
||||
const handleConfigManage = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要管理的配置')
|
||||
return
|
||||
}
|
||||
configVisible.value = true
|
||||
}
|
||||
|
||||
// 发送测试邮件
|
||||
const sendMail = async () => {
|
||||
const reg = /^[a-z0-9]+([._\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/
|
||||
if (!reg.test(email.value)) {
|
||||
ElMessage.warning('请输入正确的邮箱地址')
|
||||
return
|
||||
}
|
||||
await api.emailTest({ email: email.value })
|
||||
ElMessage.success('发送成功')
|
||||
}
|
||||
|
||||
// 自定义处理切换显示
|
||||
const handleSelect = async (val: any, ele: any) => {
|
||||
if (ele.key === 'upload_mode') {
|
||||
if (val == 1) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('local_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (val == 2) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('oss_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (val == 3) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('qiniu_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (val == 4) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('cos_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (val == 5) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('s3_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async (params: any) => {
|
||||
const data = {
|
||||
group_id: selectedId.value,
|
||||
config: params
|
||||
}
|
||||
await api.batchUpdate(data)
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.left-card :deep(.el-card__body) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 10px 2px 10px 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增配置' : '编辑配置'"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="配置标识" prop="key">
|
||||
<el-input v-model="formData.key" placeholder="请输入配置标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置标题" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入配置标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="组件类型" prop="input_type">
|
||||
<el-select v-model="formData.input_type" :options="inputComponent" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="组件数据"
|
||||
prop="config_select_data"
|
||||
v-if="['select', 'radio'].includes(formData.input_type)"
|
||||
>
|
||||
<el-row
|
||||
:gutter="10"
|
||||
class="mb-2"
|
||||
v-for="(item, index) in formData.config_select_data"
|
||||
:key="index"
|
||||
>
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.label" placeholder="请输入label"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.value" placeholder="请输入value"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button type="danger" @click="removeConfigSelectData(index)">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-button type="primary" @click="addConfigSelectData">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/config'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
interface ConfigSelectData {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const inputComponent = [
|
||||
{ label: '文本框', value: 'input' },
|
||||
{ label: '文本域', value: 'textarea' },
|
||||
{ label: '下拉选择框', value: 'select' },
|
||||
{ label: '单选框', value: 'radio' },
|
||||
{ label: '图片上传', value: 'uploadImage' },
|
||||
{ label: '文件上传', value: 'uploadFile' },
|
||||
{ label: '富文本编辑器', value: 'wangEditor' }
|
||||
]
|
||||
|
||||
const addConfigSelectData = () => {
|
||||
formData.config_select_data.push({ label: '', value: '' })
|
||||
}
|
||||
|
||||
const removeConfigSelectData = (index: number) => {
|
||||
formData.config_select_data.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
key: [{ required: true, message: '请输入配置标识', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入配置标题', trigger: 'blur' }],
|
||||
input_type: [{ required: true, message: '请输入组件类型', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
group_id: null,
|
||||
key: '',
|
||||
value: '',
|
||||
name: '',
|
||||
input_type: 'input',
|
||||
config_select_data: [] as ConfigSelectData[],
|
||||
sort: 100,
|
||||
remark: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
formData.config_select_data = []
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.configSave(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.configUpdate(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
192
saiadmin-artd/src/views/system/config/modules/config-list.vue
Normal file
192
saiadmin-artd/src/views/system/config/modules/config-list.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="配置管理"
|
||||
size="70%"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="art-full-height">
|
||||
<!-- 表格头部 -->
|
||||
<div>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:config:edit'" @click="handleAddClick" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:config:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.configDelete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:config:edit'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:config:edit'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.configDelete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ConfigEditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/config'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import ConfigEditDialog from './config-edit-dialog.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
searchForm.value.group_id = props.data?.id
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getData()
|
||||
}
|
||||
|
||||
const handleAddClick = () => {
|
||||
showDialog('add', { group_id: searchForm.value.group_id })
|
||||
}
|
||||
|
||||
const searchForm = ref({
|
||||
label: '',
|
||||
value: '',
|
||||
status: '',
|
||||
group_id: null
|
||||
})
|
||||
|
||||
const {
|
||||
loading,
|
||||
data: tableData,
|
||||
columns,
|
||||
getData,
|
||||
pagination,
|
||||
searchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.configList,
|
||||
immediate: false,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'key', label: '配置标识' },
|
||||
{ prop: 'name', label: '配置标题' },
|
||||
{ prop: 'input_type', label: '组件类型', width: 100 },
|
||||
{ prop: 'sort', label: '排序', width: 100, sortable: true },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'operation', label: '操作', useSlot: true, width: 100 }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
deleteRow,
|
||||
deleteSelectedRows,
|
||||
handleSelectionChange,
|
||||
selectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
emit('success')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="80px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="字典标签" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入字典标签" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="字典键值" prop="value">
|
||||
<el-input v-model="formData.value" placeholder="请输入字典键值" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增配置分组' : '编辑配置分组'"
|
||||
width="600px"
|
||||
align-center
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="配置名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入配置名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置标识" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入配置标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/config'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入配置标识', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
143
saiadmin-artd/src/views/system/dept/index.vue
Normal file
143
saiadmin-artd/src/views/system/dept/index.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:dept:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon v-if="isExpanded" icon="ri:collapse-diagonal-line" />
|
||||
<ArtSvgIcon v-else icon="ri:expand-diagonal-line" />
|
||||
</template>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:default-expand-all="true"
|
||||
@sort-change="handleSortChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:dept:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:dept:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/dept'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const isExpanded = ref(true)
|
||||
const tableRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ prop: 'name', label: '部门名称', minWidth: 200 },
|
||||
{ prop: 'code', label: '部门编码', minWidth: 120 },
|
||||
{ prop: 'leader.username', label: '部门领导', minWidth: 120 },
|
||||
{ prop: 'remark', label: '描述', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'sort', label: '排序', width: 100 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status', width: 100 },
|
||||
{ prop: 'create_time', label: '创建日期', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, deleteRow } = useSaiAdmin()
|
||||
|
||||
/**
|
||||
* 切换展开/收起所有菜单
|
||||
*/
|
||||
const toggleExpand = (): void => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value?.elTableRef && data.value) {
|
||||
const processRows = (rows: any[]) => {
|
||||
rows.forEach((row) => {
|
||||
if (row.children?.length) {
|
||||
tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
|
||||
processRows(row.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
processRows(data.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
194
saiadmin-artd/src/views/system/dept/modules/edit-dialog.vue
Normal file
194
saiadmin-artd/src/views/system/dept/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增部门' : '编辑部门'"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="上级部门" prop="parent_id">
|
||||
<el-tree-select
|
||||
v-model="formData.parent_id"
|
||||
:data="optionData.treeData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入部门名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入部门编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门领导">
|
||||
<sa-user v-model="formData.leader_id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入部门描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/dept'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
treeData: <any[]>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
parent_id: [{ required: true, message: '请选择上级部门', trigger: 'change' }],
|
||||
name: [{ required: true, message: '请输入部门名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入部门编码', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
parent_id: null,
|
||||
level: '',
|
||||
name: '',
|
||||
code: '',
|
||||
leader_id: null,
|
||||
remark: '',
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
const data = await api.list({ tree: true })
|
||||
optionData.treeData = [
|
||||
{
|
||||
id: 0,
|
||||
value: 0,
|
||||
label: '无上级部门',
|
||||
children: data
|
||||
}
|
||||
]
|
||||
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
77
saiadmin-artd/src/views/system/dept/modules/table-search.vue
Normal file
77
saiadmin-artd/src/views/system/dept/modules/table-search.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="部门名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入部门名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="部门编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入部门编码" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
223
saiadmin-artd/src/views/system/menu/index.vue
Normal file
223
saiadmin-artd/src/views/system/menu/index.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:menu:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:menu:destroy'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon v-if="isExpanded" icon="ri:collapse-diagonal-line" />
|
||||
<ArtSvgIcon v-else icon="ri:expand-diagonal-line" />
|
||||
</template>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:default-expand-all="false"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex justify-end gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:menu:save'"
|
||||
v-if="row.type < 3"
|
||||
type="primary"
|
||||
@click="handleAdd(row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:menu:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:menu:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/menu'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import { h } from 'vue'
|
||||
import ArtSvgIcon from '@/components/core/base/art-svg-icon/index.vue'
|
||||
|
||||
// 状态管理
|
||||
const isExpanded = ref(false)
|
||||
const tableRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
path: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'name', label: '菜单名称', minWidth: 150 },
|
||||
{
|
||||
prop: 'type',
|
||||
label: '菜单类型',
|
||||
align: 'center',
|
||||
saiType: 'dict',
|
||||
saiDict: 'menu_type',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'icon',
|
||||
label: '图标',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
formatter: (row: any) => {
|
||||
return h(ArtSvgIcon, { icon: row.icon })
|
||||
}
|
||||
},
|
||||
{ prop: 'code', label: '组件名称' },
|
||||
{
|
||||
prop: 'path',
|
||||
label: '路由',
|
||||
formatter: (row: any) => {
|
||||
if (row.type === 3) return ''
|
||||
return row.path || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'slug',
|
||||
label: '权限标识',
|
||||
minWidth: 160,
|
||||
formatter: (row: any) => {
|
||||
if (row.type === 2) {
|
||||
return row.children?.length ? row.children.length + '个权限标识' : ''
|
||||
}
|
||||
if (row.type === 3) {
|
||||
return row.slug || ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
{ prop: 'sort', label: '排序', width: 100 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status', width: 100 },
|
||||
{ prop: 'operation', label: '操作', width: 140, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
selectedRows,
|
||||
deleteRow,
|
||||
deleteSelectedRows,
|
||||
handleSelectionChange
|
||||
} = useSaiAdmin()
|
||||
|
||||
/**
|
||||
* 切换展开/收起所有菜单
|
||||
*/
|
||||
const toggleExpand = (): void => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value?.elTableRef && data.value) {
|
||||
const processRows = (rows: any[]) => {
|
||||
rows.forEach((row) => {
|
||||
if (row.children?.length) {
|
||||
tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
|
||||
processRows(row.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
processRows(data.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子项
|
||||
* @param row
|
||||
*/
|
||||
const handleAdd = (row: any) => {
|
||||
let data = { parent_id: row.id, type: 1 }
|
||||
if (row.type === 2) {
|
||||
data.type = 3
|
||||
}
|
||||
showDialog('add', data)
|
||||
}
|
||||
</script>
|
||||
322
saiadmin-artd/src/views/system/menu/modules/edit-dialog.vue
Normal file
322
saiadmin-artd/src/views/system/menu/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增菜单' : '编辑菜单'"
|
||||
width="820px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="菜单类型" prop="type">
|
||||
<sa-radio v-model="formData.type" type="button" dict="menu_type"></sa-radio>
|
||||
</el-form-item>
|
||||
<el-form-item label="上级菜单" prop="parent_id">
|
||||
<el-tree-select
|
||||
v-model="formData.parent_id"
|
||||
:data="optionData.treeData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type < 3">
|
||||
<el-form-item prop="path">
|
||||
<template #label>
|
||||
<sa-label
|
||||
label="路由地址"
|
||||
tooltip="一级菜单:以 / 开头的绝对路径(如 /dashboard) 二级及以下:相对路径(如 console、user)"
|
||||
/>
|
||||
</template>
|
||||
<el-input v-model="formData.path" placeholder="如:/dashboard 或 console" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type != 3">
|
||||
<el-form-item label="组件名称" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="如: User" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type === 2">
|
||||
<el-form-item prop="component">
|
||||
<template #label>
|
||||
<sa-label label="组件路径" tooltip="填写组件路径(views目录下) 目录菜单:留空" />
|
||||
</template>
|
||||
<el-autocomplete
|
||||
class="w-full"
|
||||
v-model="formData.component"
|
||||
:fetch-suggestions="querySearch"
|
||||
clearable
|
||||
placeholder="如:/system/user 或留空"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type != 3">
|
||||
<el-form-item label="菜单图标" prop="icon">
|
||||
<sa-icon-picker v-model="formData.icon" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type === 3">
|
||||
<el-form-item label="权限标识" prop="slug">
|
||||
<el-input v-model="formData.slug" placeholder="请输入权限标识" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" v-if="formData.type === 4">
|
||||
<el-form-item label="外链地址" prop="link_url">
|
||||
<el-input v-model="formData.link_url" placeholder="如:https://saithink.top" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="sort">
|
||||
<template #label>
|
||||
<sa-label label="排序" tooltip="数字越大越靠前" />
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="formData.sort"
|
||||
placeholder="请输入排序"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="status">
|
||||
<template #label>
|
||||
<sa-label label="状态" tooltip="禁用后,该菜单项将不可用" />
|
||||
</template>
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_iframe">
|
||||
<template #label>
|
||||
<sa-label label="是否内嵌" tooltip="外链模式下有效" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_iframe" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_keep_alive">
|
||||
<template #label>
|
||||
<sa-label label="是否缓存" tooltip="切换tabs不刷新" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_keep_alive" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_hidden">
|
||||
<template #label>
|
||||
<sa-label label="是否隐藏" tooltip="不在菜单栏显示,但是可以通过路由访问" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_hidden" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_fixed_tab">
|
||||
<template #label>
|
||||
<sa-label label="是否固定" tooltip="固定在tabs导航栏" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_fixed_tab" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_full_page">
|
||||
<template #label>
|
||||
<sa-label label="是否全屏" tooltip="不继承左侧菜单和顶部导航栏" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_full_page" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/menu'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
treeData: <any[]>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理自动查找视图文件
|
||||
*/
|
||||
const modules = import.meta.glob('/src/views/**/*.vue')
|
||||
const getModulesKey = () => {
|
||||
return Object.keys(modules).map((item) => item.replace('/src/views/', '/').replace('.vue', ''))
|
||||
}
|
||||
const componentsOptions = ref(getModulesKey())
|
||||
const querySearch = (queryString: string, cb: any) => {
|
||||
const results = queryString
|
||||
? componentsOptions.value.filter((item) =>
|
||||
item.toLowerCase().includes(queryString.toLowerCase())
|
||||
)
|
||||
: componentsOptions.value
|
||||
cb(results.map((item) => ({ value: item })))
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
parent_id: [{ required: true, message: '请选择上级菜单', trigger: 'change' }],
|
||||
name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
|
||||
path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入组件名称', trigger: 'blur' }],
|
||||
slug: [{ required: true, message: '请输入权限标识', trigger: 'blur' }],
|
||||
link_url: [{ required: true, message: '请输入外链地址', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
parent_id: null,
|
||||
type: 1,
|
||||
component: '',
|
||||
name: '',
|
||||
slug: '',
|
||||
path: '',
|
||||
icon: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
link_url: '',
|
||||
is_iframe: 2,
|
||||
is_keep_alive: 2,
|
||||
is_hidden: 2,
|
||||
is_fixed_tab: 2,
|
||||
is_full_page: 2,
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
const data = await api.list({ tree: true })
|
||||
optionData.treeData = [
|
||||
{
|
||||
id: 0,
|
||||
value: 0,
|
||||
label: '无上级菜单',
|
||||
children: data
|
||||
}
|
||||
]
|
||||
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
77
saiadmin-artd/src/views/system/menu/modules/table-search.vue
Normal file
77
saiadmin-artd/src/views/system/menu/modules/table-search.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="菜单名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入菜单名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="菜单路由" prop="path">
|
||||
<el-input v-model="formData.path" placeholder="请输入菜单路由" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
142
saiadmin-artd/src/views/system/post/index.vue
Normal file
142
saiadmin-artd/src/views/system/post/index.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:post:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:post:destroy'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
<SaImport
|
||||
v-permission="'core:post:import'"
|
||||
download-url="/core/post/downloadTemplate"
|
||||
upload-url="/core/post/import"
|
||||
@success="refreshData"
|
||||
/>
|
||||
<SaExport v-permission="'core:post:export'" url="/core/post/export" />
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:post:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:post:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/post'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: '编号', width: 100, align: 'center' },
|
||||
{ prop: 'name', label: '岗位名称', minWidth: 120 },
|
||||
{ prop: 'code', label: '岗位编码', minWidth: 120 },
|
||||
{ prop: 'remark', label: '描述', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'sort', label: '排序', width: 100 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status', width: 100 },
|
||||
{ prop: 'create_time', label: '创建日期', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
deleteRow,
|
||||
deleteSelectedRows,
|
||||
handleSelectionChange,
|
||||
selectedRows
|
||||
} = useSaiAdmin()
|
||||
</script>
|
||||
164
saiadmin-artd/src/views/system/post/modules/edit-dialog.vue
Normal file
164
saiadmin-artd/src/views/system/post/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增岗位' : '编辑岗位'"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="岗位名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入岗位名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入岗位编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入岗位描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/post'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入岗位编码', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
77
saiadmin-artd/src/views/system/post/modules/table-search.vue
Normal file
77
saiadmin-artd/src/views/system/post/modules/table-search.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="岗位名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="岗位编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入岗位编码" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
152
saiadmin-artd/src/views/system/role/index.vue
Normal file
152
saiadmin-artd/src/views/system/role/index.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:role:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2" v-if="row.id !== 1">
|
||||
<SaButton
|
||||
v-permission="'core:role:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:role:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
<ElDropdown>
|
||||
<ArtIconButton
|
||||
icon="ri:more-2-fill"
|
||||
class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-permission="'core:role:menu'"
|
||||
@click="showPermissionDialog('edit', row)"
|
||||
>
|
||||
<div class="flex-c gap-2">
|
||||
<ArtSvgIcon icon="ri:user-3-line" />
|
||||
<span>菜单权限</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
|
||||
<!-- 菜单权限弹窗 -->
|
||||
<PermissionDialog
|
||||
v-model="permissionDialogVisible"
|
||||
:dialog-type="permissionDialogType"
|
||||
:data="permissionDialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/role'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import PermissionDialog from './modules/permission-dialog.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
pagination,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ prop: 'id', label: '编号', minWidth: 60, align: 'center' },
|
||||
{ prop: 'name', label: '角色名称', minWidth: 120 },
|
||||
{ prop: 'code', label: '角色编码', minWidth: 120 },
|
||||
{ prop: 'level', label: '角色级别', minWidth: 100, sortable: true },
|
||||
{ prop: 'remark', label: '角色描述', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'sort', label: '排序', minWidth: 100 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status' },
|
||||
{ prop: 'create_time', label: '创建日期', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 140, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, deleteRow } = useSaiAdmin()
|
||||
|
||||
// 权限配置
|
||||
const {
|
||||
dialogType: permissionDialogType,
|
||||
dialogVisible: permissionDialogVisible,
|
||||
dialogData: permissionDialogData,
|
||||
showDialog: showPermissionDialog
|
||||
} = useSaiAdmin()
|
||||
</script>
|
||||
171
saiadmin-artd/src/views/system/role/modules/edit-dialog.vue
Normal file
171
saiadmin-artd/src/views/system/role/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||||
width="600px"
|
||||
align-center
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色标识" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入角色编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色级别" prop="level">
|
||||
<el-input-number v-model="formData.level" placeholder="角色级别" :max="99" :min="1" />
|
||||
</el-form-item>
|
||||
<div class="text-xs text-gray-400 pl-32 pb-4"
|
||||
>控制角色的权限层级, 不能操作职级高于自己的角色</div
|
||||
>
|
||||
<el-form-item label="描述" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入角色描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/role'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
level: [{ required: true, message: '请输入角色级别', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
level: 1,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
title="菜单权限"
|
||||
width="600px"
|
||||
align-center
|
||||
class="el-dialog-border"
|
||||
@close="handleClose"
|
||||
>
|
||||
<ElScrollbar height="70vh">
|
||||
<ElTree
|
||||
ref="treeRef"
|
||||
:data="menuList"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-expand-all="false"
|
||||
:check-strictly="checkStrictly"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex-c gap-2">
|
||||
<span>{{ data.label }}</span>
|
||||
<ElTag :type="getMenuTypeTag(data)" size="small">{{ getMenuTypeText(data) }}</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
<template #footer>
|
||||
<ElButton @click="toggleExpandAll" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon v-if="isExpandAll" icon="ri:collapse-diagonal-line" />
|
||||
<ArtSvgIcon v-else icon="ri:expand-diagonal-line" />
|
||||
</template>
|
||||
{{ isExpandAll ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
<ElButton @click="toggleCheck" style="margin-left: 8px">{{
|
||||
checkStrictly ? '非关联选择模式' : '关联选择模式'
|
||||
}}</ElButton>
|
||||
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/role'
|
||||
import menuApi from '@/api/system/menu'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const menuList = ref<Api.Common.ApiData[]>([])
|
||||
const treeRef = ref()
|
||||
|
||||
const isExpandAll = ref(true)
|
||||
const checkStrictly = ref(true)
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 菜单列表
|
||||
menuList.value = await menuApi.accessMenu({ tree: true })
|
||||
// 角色数据
|
||||
const data = await api.menuByRole({ id: props.data?.id })
|
||||
treeRef.value.setCheckedKeys(data.menus?.map((item: any) => item.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单类型标签颜色
|
||||
* @param row 菜单行数据
|
||||
* @returns 标签颜色类型
|
||||
*/
|
||||
const getMenuTypeTag = (row: any): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
|
||||
if (row.type == 1) return 'info'
|
||||
if (row.type == 2) return 'primary'
|
||||
if (row.type == 3) return 'danger'
|
||||
if (row.type == 4) return 'success'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单类型文本
|
||||
* @param row 菜单行数据
|
||||
* @returns 菜单类型文本
|
||||
*/
|
||||
const getMenuTypeText = (row: any): string => {
|
||||
if (row.type == 1) return '目录'
|
||||
if (row.type == 2) return '菜单'
|
||||
if (row.type == 3) return '按钮'
|
||||
if (row.type == 4) return '外链'
|
||||
return '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并清空选中状态
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
treeRef.value?.setCheckedKeys([])
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存权限配置
|
||||
*/
|
||||
const savePermission = async () => {
|
||||
// TODO: 调用保存权限接口
|
||||
const checkedKeys = treeRef.value.getCheckedKeys()
|
||||
try {
|
||||
await api.menuPermission({
|
||||
id: props.data?.id,
|
||||
menu_ids: checkedKeys
|
||||
})
|
||||
ElMessage.success('保存成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换全部展开/收起状态
|
||||
*/
|
||||
const toggleExpandAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
const nodes = tree.store.nodesMap
|
||||
// 这里保留 any,因为 Element Plus 的内部节点类型较复杂
|
||||
Object.values(nodes).forEach((node: any) => {
|
||||
node.expanded = !isExpandAll.value
|
||||
})
|
||||
|
||||
isExpandAll.value = !isExpandAll.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换关联选择/非关联选择状态
|
||||
*/
|
||||
const toggleCheck = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
checkStrictly.value = !checkStrictly.value
|
||||
}
|
||||
</script>
|
||||
77
saiadmin-artd/src/views/system/role/modules/table-search.vue
Normal file
77
saiadmin-artd/src/views/system/role/modules/table-search.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入角色名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="角色编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入角色编码" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
280
saiadmin-artd/src/views/system/user/index.vue
Normal file
280
saiadmin-artd/src/views/system/user/index.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<div class="box-border flex gap-4 h-full max-md:block max-md:gap-0 max-md:h-auto">
|
||||
<div class="flex-shrink-0 w-64 h-full max-md:w-full max-md:h-auto max-md:mb-5">
|
||||
<ElCard class="tree-card art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<b>部门列表</b>
|
||||
</template>
|
||||
<ElScrollbar>
|
||||
<ElTree
|
||||
:data="treeData"
|
||||
:props="{ children: 'children', label: 'label' }"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</ElScrollbar>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-grow min-w-0">
|
||||
<!-- 搜索栏 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="handleReset" />
|
||||
|
||||
<ElCard class="flex flex-col flex-1 min-h-0 art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:user:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2" v-if="row.id !== 1 && userStore.info.id !== row.id">
|
||||
<SaButton
|
||||
v-permission="'core:user:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:user:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
<ElDropdown>
|
||||
<ArtIconButton
|
||||
icon="ri:more-2-fill"
|
||||
class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem>
|
||||
<div
|
||||
class="flex-c gap-2"
|
||||
v-permission="'core:user:home'"
|
||||
@click="showWorkDialog('edit', row)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:home-office-line" />
|
||||
<span>设置首页</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem>
|
||||
<div
|
||||
class="flex-c gap-2"
|
||||
v-permission="'core:user:password'"
|
||||
@click="handlePassword(row)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:key-line" />
|
||||
<span>修改密码</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem>
|
||||
<div
|
||||
class="flex-c gap-2"
|
||||
v-permission="'core:user:cache'"
|
||||
@click="handleCache(row)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
<span>清理缓存</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表单弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
<!-- 工作台弹窗 -->
|
||||
<WorkDialog
|
||||
v-model="workDialogVisible"
|
||||
:dialog-type="workDialogType"
|
||||
:data="workDialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import WorkDialog from './modules/work-dialog.vue'
|
||||
import api from '@/api/system/user'
|
||||
import deptApi from '@/api/system/dept'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const treeData = ref([])
|
||||
|
||||
// 编辑框
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, handleSelectionChange, deleteRow } =
|
||||
useSaiAdmin()
|
||||
|
||||
const {
|
||||
dialogType: workDialogType,
|
||||
dialogVisible: workDialogVisible,
|
||||
dialogData: workDialogData,
|
||||
showDialog: showWorkDialog
|
||||
} = useSaiAdmin()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
username: undefined,
|
||||
phone: undefined,
|
||||
email: undefined,
|
||||
dept_id: undefined,
|
||||
status: ''
|
||||
})
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
getData,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'index', width: 60, label: '序号' },
|
||||
{
|
||||
prop: 'avatar',
|
||||
label: '用户名',
|
||||
minWidth: 200,
|
||||
saiType: 'imageAndText',
|
||||
saiFirst: 'username',
|
||||
saiSecond: 'email'
|
||||
},
|
||||
{ prop: 'phone', label: '手机号', width: 120 },
|
||||
{ prop: 'depts.name', label: '部门', minWidth: 150 },
|
||||
{ prop: 'status', label: '状态', width: 80, saiType: 'dict', saiDict: 'data_status' },
|
||||
{ prop: 'dashboard', label: '首页', width: 100, saiType: 'dict', saiDict: 'dashboard' },
|
||||
{ prop: 'login_time', label: '上次登录', width: 170, sortable: true },
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
useSlot: true
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
const handleReset = () => {
|
||||
searchForm.value.dept_id = undefined
|
||||
resetSearchParams()
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换部门
|
||||
* @param data
|
||||
*/
|
||||
const handleNodeClick = (data: any) => {
|
||||
searchParams.dept_id = data.id
|
||||
getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门数据
|
||||
*/
|
||||
const getDeptList = () => {
|
||||
deptApi.accessDept().then((data: any) => {
|
||||
treeData.value = data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param row
|
||||
*/
|
||||
const handlePassword = (row: any) => {
|
||||
ElMessageBox.prompt('请输入新密码', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^.{6,16}$/,
|
||||
inputErrorMessage: '密码长度在6到16之间',
|
||||
type: 'warning'
|
||||
}).then(({ value }) => {
|
||||
api.changePassword({ id: row.id, password: value }).then(() => {
|
||||
ElMessage.success('修改密码成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
* @param row
|
||||
*/
|
||||
const handleCache = (row: any) => {
|
||||
ElMessageBox.confirm('确定要清理缓存吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
api.clearCache({ id: row.id }).then(() => {
|
||||
ElMessage.success('清理缓存成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDeptList()
|
||||
})
|
||||
</script>
|
||||
301
saiadmin-artd/src/views/system/user/modules/edit-dialog.vue
Normal file
301
saiadmin-artd/src/views/system/user/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增用户' : '编辑用户'"
|
||||
width="800px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="头像" prop="avatar">
|
||||
<sa-image-picker v-model="formData.avatar" round />
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="formData.username" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="真实姓名" prop="realname">
|
||||
<el-input v-model="formData.realname" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="dialogType === 'add'">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input type="password" v-model="formData.password" show-password />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="确认密码" prop="password_confirm">
|
||||
<el-input type="password" v-model="formData.password_confirm" show-password />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="formData.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门" prop="dept_id">
|
||||
<el-tree-select
|
||||
v-model="formData.dept_id"
|
||||
:data="optionData.deptData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="角色" prop="role_ids">
|
||||
<el-select v-model="formData.role_ids" multiple clearable>
|
||||
<el-option
|
||||
v-for="role in optionData.roleList"
|
||||
:key="(role as any)?.id"
|
||||
:value="(role as any)?.id"
|
||||
:label="(role as any)?.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="岗位" prop="post_ids">
|
||||
<el-select v-model="formData.post_ids" multiple clearable>
|
||||
<el-option
|
||||
v-for="post in optionData.postList"
|
||||
:key="(post as any)?.id"
|
||||
:value="(post as any)?.id"
|
||||
:label="(post as any)?.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<sa-radio v-model="formData.gender" dict="gender" valueType="string" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import api from '@/api/system/user'
|
||||
import deptApi from '@/api/system/dept'
|
||||
import roleApi from '@/api/system/role'
|
||||
import postApi from '@/api/system/post'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
deptData: <any>[],
|
||||
roleList: <any>[],
|
||||
postList: <any>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const validatePasswordConfirm = (rule: any, value: any, callback: any) => {
|
||||
if (value !== formData.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password_confirm: [
|
||||
{ required: true, message: '请输入确认密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
|
||||
{ validator: validatePasswordConfirm, trigger: 'blur' }
|
||||
],
|
||||
dept_id: [{ required: true, message: '请选择部门', trigger: 'change' }],
|
||||
role_ids: [{ required: true, message: '请选择角色', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 初始表单数据
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
avatar: '',
|
||||
username: '',
|
||||
password: '',
|
||||
password_confirm: '',
|
||||
realname: '',
|
||||
dept_id: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
role_ids: [],
|
||||
post_ids: [],
|
||||
status: 1,
|
||||
gender: '',
|
||||
remark: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
// 初始化页面数据
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 部门数据
|
||||
const deptData = await deptApi.accessDept()
|
||||
optionData.deptData = deptData
|
||||
// 角色数据
|
||||
const roleData = await roleApi.accessRole()
|
||||
optionData.roleList = roleData
|
||||
// 岗位数据
|
||||
const postData = await postApi.accessPost()
|
||||
optionData.postList = postData
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
if (props.data.id) {
|
||||
let data = await api.read(props.data.id)
|
||||
if (data.postList) {
|
||||
const post = (data.postList as any[])?.map((item: any) => item.id)
|
||||
data.post_ids = post
|
||||
}
|
||||
const role = (data.roleList as any[])?.map((item: any) => item.id)
|
||||
data.role_ids = role
|
||||
data.password = ''
|
||||
initForm(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = (data: any) => {
|
||||
if (data) {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
;(formData as any)[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
76
saiadmin-artd/src/views/system/user/modules/table-search.vue
Normal file
76
saiadmin-artd/src/views/system/user/modules/table-search.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
:label-width="'70px'"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="formData.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="formData.phone" placeholder="请输入手机号" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
132
saiadmin-artd/src/views/system/user/modules/work-dialog.vue
Normal file
132
saiadmin-artd/src/views/system/user/modules/work-dialog.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="设置工作台" width="600px" align-center @close="handleClose">
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="工作台" prop="dashboard">
|
||||
<sa-select v-model="formData.dashboard" dict="dashboard" placeholder="请选择工作台" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
dashboard: [{ required: true, message: '请选择工作台', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
dashboard: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'edit') {
|
||||
await api.setHomePage(formData)
|
||||
ElMessage.success('操作成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
805
saiadmin-artd/src/views/tool/code/components/editInfo.vue
Normal file
805
saiadmin-artd/src/views/tool/code/components/editInfo.vue
Normal file
@@ -0,0 +1,805 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
:title="`编辑生成信息 - ${record?.table_comment}`"
|
||||
size="100%"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-loading="loading" element-loading-text="加载数据中...">
|
||||
<el-form ref="formRef" :model="form">
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- 配置信息 Tab -->
|
||||
<el-tab-pane label="配置信息" name="base_config">
|
||||
<el-divider content-position="left">基础信息</el-divider>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="表名称" prop="table_name" label-width="100px">
|
||||
<el-input v-model="form.table_name" disabled />
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
数据库表的名称,自动读取数据库表名称
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item
|
||||
label="表描述"
|
||||
prop="table_comment"
|
||||
label-width="100px"
|
||||
:rules="[{ required: true, message: '表描述必填' }]"
|
||||
>
|
||||
<el-input v-model="form.table_comment" />
|
||||
<div class="text-xs text-gray-400 mt-1"> 表的描述,自动读取数据库表注释 </div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item
|
||||
label="实体类"
|
||||
prop="class_name"
|
||||
label-width="100px"
|
||||
:rules="[{ required: true, message: '实体类必填' }]"
|
||||
>
|
||||
<el-input v-model="form.class_name" />
|
||||
<div class="text-xs text-gray-400 mt-1"> 生成的实体类名称,可以修改去掉前缀 </div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item
|
||||
label="业务名称"
|
||||
prop="business_name"
|
||||
label-width="100px"
|
||||
:rules="[{ required: true, message: '业务名称必填' }]"
|
||||
>
|
||||
<el-input v-model="form.business_name" />
|
||||
<div class="text-xs text-gray-400 mt-1"> 英文、业务名称、同一个分组包下唯一 </div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="数据源" prop="source" label-width="100px">
|
||||
<el-select v-model="form.source" placeholder="请选择数据源" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in dataSourceList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-400 mt-1"> 数据库配置文件中配置的数据源 </div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="备注信息" prop="remark" label-width="100px">
|
||||
<el-input v-model="form.remark" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">生成信息</el-divider>
|
||||
<el-row :gutter="24">
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item
|
||||
label="应用类型"
|
||||
prop="template"
|
||||
label-width="100px"
|
||||
:rules="[{ required: true, message: '应用类型必选' }]"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.template"
|
||||
placeholder="请选择生成模板"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option label="webman应用[app]" value="app" />
|
||||
<el-option label="webman插件[plugin]" value="plugin" />
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-400 mt-1"
|
||||
>默认app模板,生成文件放app目录下,plugin应用需要先手动初始化</div
|
||||
>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item
|
||||
label="应用名称"
|
||||
prop="namespace"
|
||||
label-width="100px"
|
||||
:rules="[{ required: true, message: '应用名称必填' }]"
|
||||
>
|
||||
<el-input v-model="form.namespace" />
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
plugin插件名称, 或者app下应用名称, 禁止使用saiadmin
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item
|
||||
label="分组包名"
|
||||
prop="package_name"
|
||||
label-width="100px"
|
||||
:rules="[{ required: true, message: '分组包名必填' }]"
|
||||
>
|
||||
<el-input v-model="form.package_name" placeholder="请输入分组包名" clearable />
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
生成的文件放在分组包名目录下,功能模块分组
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item
|
||||
label="生成类型"
|
||||
prop="tpl_category"
|
||||
label-width="100px"
|
||||
:rules="[{ required: true, message: '生成类型必填' }]"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.tpl_category"
|
||||
placeholder="请选择所属模块"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option label="单表CRUD" value="single" />
|
||||
<el-option label="树表CRUD" value="tree" />
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
单表须有主键,树表须指定id、parent_id、name等字段
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item
|
||||
label="生成路径"
|
||||
prop="generate_path"
|
||||
label-width="100px"
|
||||
:rules="[{ required: true, message: '生成路径必填' }]"
|
||||
>
|
||||
<el-input v-model="form.generate_path" />
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
前端根目录文件夹名称,必须与后端根目录同级
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item label="模型类型" prop="stub" label-width="100px">
|
||||
<div class="flex-col">
|
||||
<el-radio-group v-model="form.stub">
|
||||
<el-radio value="think">ThinkOrm</el-radio>
|
||||
<el-radio value="eloquent">EloquentORM</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-xs text-gray-400 mt-1">生成不同驱动模型的代码</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item label="所属菜单" prop="belong_menu_id" label-width="100px">
|
||||
<el-cascader
|
||||
v-model="form.belong_menu_id"
|
||||
:options="menus"
|
||||
:props="{
|
||||
expandTrigger: 'hover',
|
||||
checkStrictly: true,
|
||||
value: 'id',
|
||||
label: 'label'
|
||||
}"
|
||||
style="width: 100%"
|
||||
placeholder="生成功能所属菜单"
|
||||
clearable
|
||||
/>
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
默认为工具菜单栏目下的子菜单。不选择则为顶级菜单栏目
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item
|
||||
label="菜单名称"
|
||||
prop="menu_name"
|
||||
label-width="100px"
|
||||
:rules="[{ required: true, message: '菜单名称必选' }]"
|
||||
>
|
||||
<el-input v-model="form.menu_name" placeholder="请输入菜单名称" clearable />
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
显示在菜单栏目上的菜单名称、以及代码中的业务功能名称
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="表单效果" prop="component_type" label-width="100px">
|
||||
<div class="flex-col">
|
||||
<el-radio-group v-model="form.component_type">
|
||||
<el-radio-button :value="1">弹出框</el-radio-button>
|
||||
<el-radio-button :value="2">抽屉</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="text-xs text-gray-400 mt-1">表单显示方式</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="表单宽度" prop="form_width" label-width="100px">
|
||||
<div class="flex-col">
|
||||
<el-input-number v-model="form.form_width" :min="200" :max="10000" />
|
||||
<div class="text-xs text-gray-400 mt-1">表单组件的宽度,单位为px</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="表单全屏" prop="is_full" label-width="100px">
|
||||
<div class="flex-col">
|
||||
<el-radio-group v-model="form.is_full">
|
||||
<el-radio :value="1">否</el-radio>
|
||||
<el-radio :value="2">是</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-xs text-gray-400 mt-1">编辑表单是否全屏</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 树表配置 -->
|
||||
<template v-if="form.tpl_category === 'tree'">
|
||||
<el-divider content-position="left">树表配置</el-divider>
|
||||
<el-row :gutter="24">
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item label="树主ID" prop="tree_id" label-width="100px">
|
||||
<el-select
|
||||
v-model="formOptions.tree_id"
|
||||
placeholder="请选择树表的主ID"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in form.columns"
|
||||
:key="index"
|
||||
:label="`${item.column_name} - ${item.column_comment}`"
|
||||
:value="item.column_name"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-400 mt-1">指定树表的主要ID,一般为主键</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item label="树父ID" prop="tree_parent_id" label-width="100px">
|
||||
<el-select
|
||||
v-model="formOptions.tree_parent_id"
|
||||
placeholder="请选择树表的父ID"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in form.columns"
|
||||
:key="index"
|
||||
:label="`${item.column_name} - ${item.column_comment}`"
|
||||
:value="item.column_name"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-400 mt-1">指定树表的父ID,比如:parent_id</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="8" :xl="8">
|
||||
<el-form-item label="树名称" prop="tree_name" label-width="100px">
|
||||
<el-select
|
||||
v-model="formOptions.tree_name"
|
||||
placeholder="请选择树表的名称字段"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in form.columns"
|
||||
:key="index"
|
||||
:label="`${item.column_name} - ${item.column_comment}`"
|
||||
:value="item.column_name"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-400 mt-1">指定树显示的名称字段,比如:name</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 字段配置 Tab -->
|
||||
<el-tab-pane label="字段配置" name="field_config">
|
||||
<el-table :data="form.columns" max-height="750">
|
||||
<el-table-column prop="sort" label="排序" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.sort"
|
||||
style="width: 100px"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="column_name"
|
||||
label="字段名称"
|
||||
width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="column_comment" label="字段描述" width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.column_comment" clearable />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="column_type" label="物理类型" width="100" />
|
||||
<el-table-column prop="is_required" label="必填" width="80" align="center">
|
||||
<template #header>
|
||||
<div class="flex-c justify-center items-center gap-1">
|
||||
<span>必填</span>
|
||||
<el-checkbox @change="(val) => handlerAll(val, 'required')" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox v-model="row.is_required" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_insert" label="表单" width="80" align="center">
|
||||
<template #header>
|
||||
<div class="flex-c justify-center items-center gap-1">
|
||||
<span>表单</span>
|
||||
<el-checkbox @change="(val) => handlerAll(val, 'insert')" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox v-model="row.is_insert" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_list" label="列表" width="80" align="center">
|
||||
<template #header>
|
||||
<div class="flex-c justify-center items-center gap-1">
|
||||
<span>列表</span>
|
||||
<el-checkbox @change="(val) => handlerAll(val, 'list')" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox v-model="row.is_list" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_query" label="查询" width="80" align="center">
|
||||
<template #header>
|
||||
<div class="flex-c justify-center items-center gap-1">
|
||||
<span>查询</span>
|
||||
<el-checkbox @change="(val) => handlerAll(val, 'query')" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox v-model="row.is_query" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_sort" label="排序" width="80" align="center">
|
||||
<template #header>
|
||||
<div class="flex-c justify-center items-center gap-1">
|
||||
<span>排序</span>
|
||||
<el-checkbox @change="(val) => handlerAll(val, 'sort')" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox v-model="row.is_sort" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="query_type" label="查询方式" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.query_type" clearable>
|
||||
<el-option
|
||||
v-for="item in queryType"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="view_type" label="页面控件">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-select
|
||||
v-model="row.view_type"
|
||||
style="width: 140px"
|
||||
@change="changeViewType(row)"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in viewComponent"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-link
|
||||
v-if="notNeedSettingComponents.includes(row.view_type)"
|
||||
@click="settingComponentRef.open(row)"
|
||||
>
|
||||
设置
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dict_type" label="数据字典">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.dict_type"
|
||||
clearable
|
||||
placeholder="选择数据字典"
|
||||
:disabled="!['saSelect', 'radio', 'checkbox'].includes(row.view_type)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, key) in dictStore.dictList"
|
||||
:key="key"
|
||||
:label="key"
|
||||
:value="key"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 关联配置 Tab -->
|
||||
<el-tab-pane label="关联配置" name="relation_config">
|
||||
<el-alert type="info" :closable="false">
|
||||
模型关联支持:一对一、一对多、一对一(反向)、多对多。
|
||||
</el-alert>
|
||||
|
||||
<el-button type="primary" class="mt-4 mb-4" @click="addRelation">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-line" />
|
||||
</template>
|
||||
新增关联
|
||||
</el-button>
|
||||
|
||||
<div v-for="(item, index) in formOptions.relations" :key="index">
|
||||
<el-divider content-position="left">
|
||||
{{ item.name ? item.name : '定义新关联' }}
|
||||
<el-link type="danger" class="ml-5" @click="delRelation(index)">
|
||||
<ArtSvgIcon icon="ri:delete-bin-line" class="mr-1" />
|
||||
删除定义
|
||||
</el-link>
|
||||
</el-divider>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="关联类型" label-width="100px">
|
||||
<el-select
|
||||
v-model="item.type"
|
||||
placeholder="请选择关联类型"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="types in relationsType"
|
||||
:key="types.value"
|
||||
:label="types.name"
|
||||
:value="types.value"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-400 mt-1">指定关联类型</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="关联名称" label-width="100px">
|
||||
<el-input v-model="item.name" placeholder="设置关联名称" clearable />
|
||||
<div class="text-xs text-gray-400 mt-1">属性名称,代码中with调用的名称</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="关联模型" label-width="100px">
|
||||
<el-input v-model="item.model" placeholder="设置关联模型" clearable />
|
||||
<div class="text-xs text-gray-400 mt-1">选择要关联的实体模型</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item
|
||||
:label="
|
||||
item.type === 'belongsTo'
|
||||
? '外键'
|
||||
: item.type === 'belongsToMany'
|
||||
? '外键'
|
||||
: '当前模型主键'
|
||||
"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-input v-model="item.localKey" placeholder="设置键名" clearable />
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
{{
|
||||
item.type === 'belongsTo'
|
||||
? '关联模型_id'
|
||||
: item.type === 'belongsToMany'
|
||||
? '关联模型_id'
|
||||
: '当前模型主键'
|
||||
}}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col v-show="item.type === 'belongsToMany'" :span="8">
|
||||
<el-form-item label="中间模型" label-width="100px">
|
||||
<el-input v-model="item.table" placeholder="请输入中间模型" clearable />
|
||||
<div class="text-xs text-gray-400 mt-1">多对多关联的中间模型</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item
|
||||
:label="item.type === 'belongsTo' ? '关联主键' : '外键'"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-input v-model="item.foreignKey" placeholder="设置键名" clearable />
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
{{ item.type === 'belongsTo' ? '关联模型主键' : '当前模型_id' }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 设置组件弹窗 -->
|
||||
<SettingComponent ref="settingComponentRef" @confirm="confirmSetting" />
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { CheckboxValueType, FormInstance } from 'element-plus'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
|
||||
// 接口导入
|
||||
import generate from '@/api/tool/generate'
|
||||
import database from '@/api/safeguard/database'
|
||||
import menuApi from '@/api/system/menu'
|
||||
|
||||
import SettingComponent from './settingComponent.vue'
|
||||
|
||||
// 导入变量
|
||||
import { relationsType, queryType, viewComponent } from '../js/vars'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const record = ref<any>({})
|
||||
const loading = ref(true)
|
||||
const submitLoading = ref(false)
|
||||
const activeTab = ref('base_config')
|
||||
const formRef = ref<FormInstance>()
|
||||
const settingComponentRef = ref()
|
||||
|
||||
const notNeedSettingComponents = ref([
|
||||
'uploadFile',
|
||||
'uploadImage',
|
||||
'imagePicker',
|
||||
'chunkUpload',
|
||||
'editor',
|
||||
'date',
|
||||
'userSelect'
|
||||
])
|
||||
|
||||
const form = ref<any>({
|
||||
generate_menus: ['index', 'save', 'update', 'read', 'destroy'],
|
||||
columns: []
|
||||
})
|
||||
|
||||
// form扩展组
|
||||
const formOptions = ref<any>({
|
||||
relations: []
|
||||
})
|
||||
// 菜单列表
|
||||
const menus = ref<any[]>([])
|
||||
// 数据源
|
||||
const dataSourceList = ref<{ label: string; value: string }[]>([])
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
loading.value = true
|
||||
// 获取数据源
|
||||
const data = await database.getDataSource()
|
||||
dataSourceList.value = data.map((item: any) => ({
|
||||
label: item,
|
||||
value: item
|
||||
}))
|
||||
const response = await generate.read({ id: props.data?.id })
|
||||
record.value = response
|
||||
initForm()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置组件确认
|
||||
*/
|
||||
const confirmSetting = (name: string, value: any) => {
|
||||
form.value.columns.find((item: any, idx: number) => {
|
||||
if (item.column_name === name) {
|
||||
form.value.columns[idx].options = value
|
||||
}
|
||||
})
|
||||
ElMessage.success('组件设置成功')
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换页面控件类型
|
||||
*/
|
||||
const changeViewType = (record: any) => {
|
||||
if (
|
||||
record.view_type === 'uploadImage' ||
|
||||
record.view_type === 'imagePicker' ||
|
||||
record.view_type === 'uploadFile' ||
|
||||
record.view_type === 'chunkUpload'
|
||||
) {
|
||||
record.options = { multiple: false, limit: 1 }
|
||||
} else if (record.view_type === 'editor') {
|
||||
record.options = { height: 400 }
|
||||
} else if (record.view_type === 'date') {
|
||||
record.options = { mode: 'date' }
|
||||
} else if (record.view_type === 'userSelect') {
|
||||
record.options = { multiple: false }
|
||||
} else {
|
||||
record.options = {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async () => {
|
||||
if (form.value.namespace === 'saiadmin') {
|
||||
ElMessage.error('应用名称不能为saiadmin')
|
||||
return
|
||||
}
|
||||
|
||||
const validResult = await formRef.value?.validate().catch((err) => err)
|
||||
if (validResult !== true) {
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
form.value.options = formOptions.value
|
||||
await generate.update({ ...form.value })
|
||||
ElMessage.success('更新成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全选 / 全不选
|
||||
*/
|
||||
const handlerAll = (value: CheckboxValueType, type: string) => {
|
||||
form.value.columns.forEach((item: any) => {
|
||||
item['is_' + type] = value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增关联定义
|
||||
*/
|
||||
const addRelation = () => {
|
||||
formOptions.value.relations.push({
|
||||
name: '',
|
||||
type: 'hasOne',
|
||||
model: '',
|
||||
foreignKey: '',
|
||||
localKey: '',
|
||||
table: ''
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除关联定义
|
||||
*/
|
||||
const delRelation = (idx: number | string) => {
|
||||
formOptions.value.relations.splice(idx, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
// 设置form数据
|
||||
for (const name in record.value) {
|
||||
if (name === 'generate_menus') {
|
||||
form.value[name] = record.value[name] ? record.value[name].split(',') : []
|
||||
} else {
|
||||
form.value[name] = record.value[name]
|
||||
}
|
||||
}
|
||||
|
||||
if (record.value.options && record.value.options.relations) {
|
||||
formOptions.value.relations = record.value.options.relations
|
||||
} else {
|
||||
formOptions.value.relations = []
|
||||
}
|
||||
|
||||
if (record.value.tpl_category === 'tree') {
|
||||
formOptions.value.tree_id = record.value.options.tree_id
|
||||
formOptions.value.tree_name = record.value.options.tree_name
|
||||
formOptions.value.tree_parent_id = record.value.options.tree_parent_id
|
||||
}
|
||||
|
||||
// 请求表字段
|
||||
generate.getTableColumns({ table_id: record.value.id }).then((data: any) => {
|
||||
form.value.columns = []
|
||||
data.forEach((item: any) => {
|
||||
item.is_required = item.is_required === 2 ? true : false
|
||||
item.is_insert = item.is_insert === 2 ? true : false
|
||||
item.is_edit = item.is_edit === 2 ? true : false
|
||||
item.is_list = item.is_list === 2 ? true : false
|
||||
item.is_query = item.is_query === 2 ? true : false
|
||||
item.is_sort = item.is_sort === 2 ? true : false
|
||||
form.value.columns.push(item)
|
||||
})
|
||||
})
|
||||
|
||||
// 请求菜单列表
|
||||
menuApi.list({ tree: true, menu: true }).then((data: any) => {
|
||||
menus.value = data
|
||||
menus.value.unshift({ id: 0, value: 0, label: '顶级菜单' })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
228
saiadmin-artd/src/views/tool/code/components/loadTable.vue
Normal file
228
saiadmin-artd/src/views/tool/code/components/loadTable.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="装载数据表"
|
||||
size="70%"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="art-full-height">
|
||||
<el-alert type="info" :closable="false">
|
||||
<template #title>
|
||||
<div>1、支持配置多数据源;</div>
|
||||
<div>
|
||||
2、载入表[sa_shop_category]会自动处理为[SaShopCategory]类,可以编辑对类名进行修改[ShopCategory]
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<ElSpace wrap>
|
||||
<el-select v-model="searchForm.source" placeholder="切换数据源" style="width: 200px">
|
||||
<el-option
|
||||
v-for="item in dataSourceList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="searchForm.name"
|
||||
placeholder="请输入数据表名称"
|
||||
style="width: 300px"
|
||||
clearable
|
||||
/>
|
||||
</ElSpace>
|
||||
<ElSpace wrap>
|
||||
<ElButton class="reset-button" @click="handleReset" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:reset-right-line" />
|
||||
</template>
|
||||
重置
|
||||
</ElButton>
|
||||
<ElButton type="primary" class="search-button" @click="handleSearch" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:search-line" />
|
||||
</template>
|
||||
查询
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<div>
|
||||
<ElSpace wrap>
|
||||
<ElButton :disabled="selectedRows.length === 0" @click="handleLoadTable" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:check-fill" />
|
||||
</template>
|
||||
确认选择
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="name"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api/safeguard/database'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import generate from '@/api/tool/generate'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const selectedRows = ref<Record<string, any>[]>([])
|
||||
const dataSourceList = ref<{ label: string; value: string }[]>([])
|
||||
const searchForm = ref({
|
||||
name: '',
|
||||
source: ''
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
const response = await api.getDataSource()
|
||||
dataSourceList.value = response.map((item: any) => ({
|
||||
label: item,
|
||||
value: item
|
||||
}))
|
||||
searchForm.value.source = dataSourceList.value[0]?.value || ''
|
||||
refreshData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格数据
|
||||
*/
|
||||
const refreshData = () => {
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
const handleSearch = () => {
|
||||
refreshData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
const handleReset = () => {
|
||||
searchForm.value.name = ''
|
||||
refreshData()
|
||||
}
|
||||
|
||||
// 表格行选择变化
|
||||
const handleSelectionChange = (selection: Record<string, any>[]): void => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 确认选择装载数据表
|
||||
const handleLoadTable = async () => {
|
||||
if (selectedRows.value.length < 1) {
|
||||
ElMessage.info('至少要选择一条数据')
|
||||
return
|
||||
}
|
||||
const names = selectedRows.value.map((item) => ({
|
||||
name: item.name,
|
||||
comment: item.comment,
|
||||
sourceName: item.name
|
||||
}))
|
||||
|
||||
await generate.loadTable({
|
||||
source: searchForm.value.source,
|
||||
names
|
||||
})
|
||||
ElMessage.success('装载成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedRows.value = []
|
||||
}
|
||||
|
||||
const {
|
||||
loading,
|
||||
data: tableData,
|
||||
columns,
|
||||
getData,
|
||||
pagination,
|
||||
searchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
immediate: false,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'name', label: '表名称' },
|
||||
{ prop: 'comment', label: '表注释' },
|
||||
{ prop: 'engine', label: '引擎' },
|
||||
{ prop: 'collation', label: '编码' },
|
||||
{ prop: 'create_time', label: '创建时间' }
|
||||
]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
111
saiadmin-artd/src/views/tool/code/components/preview.vue
Normal file
111
saiadmin-artd/src/views/tool/code/components/preview.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="预览代码" size="100%" destroy-on-close @close="handleClose">
|
||||
<el-tabs v-model="activeTab" type="card">
|
||||
<el-tab-pane
|
||||
v-for="item in previewCode"
|
||||
:key="item.name"
|
||||
:label="item.tab_name"
|
||||
:name="item.name"
|
||||
>
|
||||
<div class="relative">
|
||||
<SaCode :code="item.code" :language="item.lang" />
|
||||
<el-button class="copy-button" type="primary" @click="handleCopy(item.code)">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:file-copy-line" />
|
||||
</template>
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import generate from '@/api/tool/generate'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const activeTab = ref('controller')
|
||||
const previewCode = ref<any[]>([])
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
*/
|
||||
const initPage = async () => {
|
||||
try {
|
||||
const response = await generate.preview({ id: props.data?.id })
|
||||
previewCode.value = response
|
||||
activeTab.value = previewCode.value[0]?.name || 'controller'
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制代码到剪贴板
|
||||
*/
|
||||
const { copy } = useClipboard()
|
||||
const handleCopy = async (code: string) => {
|
||||
try {
|
||||
await copy(code)
|
||||
ElMessage.success('代码已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 0px;
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="`设置组件 - ${row?.column_comment}`"
|
||||
width="600px"
|
||||
draggable
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<!-- 编辑器相关 -->
|
||||
<template v-if="row.view_type === 'editor'">
|
||||
<el-form-item label="编辑器高度" prop="height">
|
||||
<el-input-number v-model="form.height" :max="1000" :min="100" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 上传、资源选择器相关 -->
|
||||
<template
|
||||
v-if="['uploadImage', 'imagePicker', 'uploadFile', 'chunkUpload'].includes(row.view_type)"
|
||||
>
|
||||
<el-form-item label="是否多选" prop="multiple">
|
||||
<el-radio-group v-model="form.multiple">
|
||||
<el-radio :value="true">是</el-radio>
|
||||
<el-radio :value="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-xs text-gray-400 ml-2">多个文件必须选是,字段自动处理为数组</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="数量限制" prop="limit">
|
||||
<el-input-number v-model="form.limit" :max="10" :min="1" />
|
||||
<div class="text-xs text-gray-400 ml-2">限制上传数量</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 用户选择器 -->
|
||||
<template v-if="row.view_type === 'userSelect'">
|
||||
<el-form-item label="是否多选" prop="multiple">
|
||||
<el-radio-group v-model="form.multiple">
|
||||
<el-radio :value="true">是</el-radio>
|
||||
<el-radio :value="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-xs text-gray-400 ml-2">多个用户,字段自动处理为数组</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 日期、时间选择器 -->
|
||||
<template v-if="['date'].includes(row.view_type)">
|
||||
<el-form-item label="选择器类型" prop="mode">
|
||||
<el-select v-model="form.mode" clearable>
|
||||
<el-option label="日期选择器" value="date" />
|
||||
<el-option label="日期时间择器" value="datetime" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="save">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
(e: 'confirm', name: string, value: any): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const row = ref<any>({})
|
||||
const form = ref<any>({})
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
*/
|
||||
const open = (record: any) => {
|
||||
row.value = record
|
||||
if (
|
||||
record.view_type === 'uploadImage' ||
|
||||
record.view_type === 'imagePicker' ||
|
||||
record.view_type === 'uploadFile' ||
|
||||
record.view_type === 'chunkUpload'
|
||||
) {
|
||||
form.value = record.options ? { ...record.options } : { multiple: false }
|
||||
} else if (record.view_type === 'editor') {
|
||||
form.value = record.options ? { ...record.options } : { height: 400 }
|
||||
} else if (record.view_type === 'date' || record.view_type === 'datetime') {
|
||||
form.value = record.options ? { ...record.options } : { mode: record.view_type }
|
||||
} else if (record.view_type === 'userSelect') {
|
||||
form.value = record.options ? { ...record.options } : { multiple: false }
|
||||
} else {
|
||||
form.value = record.options ? { ...record.options } : {}
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = () => {
|
||||
emit('confirm', row.value.column_name, form.value)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
272
saiadmin-artd/src/views/tool/code/index.vue
Normal file
272
saiadmin-artd/src/views/tool/code/index.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:database:index'" @click="showTableDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:upload-2-line" />
|
||||
</template>
|
||||
装载
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'tool:code:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="batchGenerate"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:download-2-line" />
|
||||
</template>
|
||||
生成
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'tool:code:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 生成类型列 -->
|
||||
<template #tpl_category="{ row }">
|
||||
<el-tag v-if="row.tpl_category === 'single'" type="success">单表CRUD</el-tag>
|
||||
<el-tag v-else type="danger">树表CRUD</el-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'tool:code:edit'"
|
||||
type="secondary"
|
||||
icon="ri:eye-line"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'tool:code:edit'"
|
||||
type="primary"
|
||||
icon="ri:refresh-line"
|
||||
@click="syncTable(row.id)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'tool:code:edit'"
|
||||
type="secondary"
|
||||
@click="showEditDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'tool:code:edit'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
<ElDropdown>
|
||||
<ArtIconButton
|
||||
icon="ri:more-2-fill"
|
||||
class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem>
|
||||
<div
|
||||
v-permission="'tool:code:edit'"
|
||||
class="flex-c gap-2"
|
||||
@click="generateFile(row.id)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:folder-add-line" />
|
||||
<span>生成到项目</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem>
|
||||
<div
|
||||
v-permission="'tool:code:edit'"
|
||||
class="flex-c gap-2"
|
||||
@click="generateCode(row.id)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:download-line" />
|
||||
<span>代码下载</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 装载数据表 -->
|
||||
<LoadTable v-model="tableVisible" :dialog-type="dialogType" @success="refreshData" />
|
||||
|
||||
<!-- 预览代码 -->
|
||||
<Preview v-model="dialogVisible" :data="dialogData" />
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditInfo v-model="editVisible" :data="editDialogData" @success="refreshData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '@/api/tool/generate'
|
||||
import { downloadFile } from '@/utils/tool'
|
||||
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import LoadTable from './components/loadTable.vue'
|
||||
import Preview from './components/preview.vue'
|
||||
import EditInfo from './components/editInfo.vue'
|
||||
|
||||
// 编辑弹窗
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
handleSelectionChange,
|
||||
deleteRow,
|
||||
deleteSelectedRows,
|
||||
selectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
const { dialogVisible: tableVisible, showDialog: showTableDialog } = useSaiAdmin()
|
||||
|
||||
const {
|
||||
dialogVisible: editVisible,
|
||||
dialogData: editDialogData,
|
||||
showDialog: showEditDialog
|
||||
} = useSaiAdmin()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
table_name: undefined,
|
||||
source: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
pagination,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection', width: 50 },
|
||||
{ prop: 'table_name', label: '表名称', minWidth: 180, align: 'left' },
|
||||
{ prop: 'table_comment', label: '表描述', minWidth: 150, align: 'left' },
|
||||
{ prop: 'template', label: '应用类型', minWidth: 120 },
|
||||
{ prop: 'namespace', label: '应用名称', minWidth: 120 },
|
||||
{ prop: 'stub', label: '模板类型', minWidth: 120 },
|
||||
{ prop: 'tpl_category', label: '生成类型', minWidth: 120, useSlot: true },
|
||||
{ prop: 'update_time', label: '更新时间', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 220, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 生成代码下载
|
||||
*/
|
||||
const generateCode = async (ids: number | string) => {
|
||||
ElMessage.info('代码生成下载中,请稍后')
|
||||
const response = await api.generateCode({
|
||||
ids: ids.toString().split(',')
|
||||
})
|
||||
if (response) {
|
||||
downloadFile(response, 'code.zip')
|
||||
ElMessage.success('代码生成成功,开始下载')
|
||||
} else {
|
||||
ElMessage.error('文件下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步表结构
|
||||
*/
|
||||
const syncTable = async (id: number) => {
|
||||
ElMessageBox.confirm('执行同步操作将会覆盖已经设置的表结构,确定要同步吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
api.async({ id }).then(() => {
|
||||
ElMessage.success('同步成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成到项目
|
||||
*/
|
||||
const generateFile = async (id: number) => {
|
||||
ElMessageBox.confirm('生成到项目将会覆盖原有文件,确定要生成吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
api.generateFile({ id }).then(() => {
|
||||
ElMessage.success('生成到项目成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成代码
|
||||
*/
|
||||
const batchGenerate = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.error('至少要选择一条数据')
|
||||
return
|
||||
}
|
||||
generateCode(selectedRows.value.map((item: any) => item.id).join(','))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
</style>
|
||||
45
saiadmin-artd/src/views/tool/code/js/vars.ts
Normal file
45
saiadmin-artd/src/views/tool/code/js/vars.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const relationsType: { name: string; value: string }[] = [
|
||||
{ name: '一对一[hasOne]', value: 'hasOne' },
|
||||
{ name: '一对多[hasMany]', value: 'hasMany' },
|
||||
{ name: '一对一(反向)[belongsTo]', value: 'belongsTo' },
|
||||
{ name: '多对多[belongsToMany]', value: 'belongsToMany' }
|
||||
]
|
||||
|
||||
export const queryType: { label: string; value: string }[] = [
|
||||
{ label: '=', value: 'eq' },
|
||||
{ label: '!=', value: 'neq' },
|
||||
{ label: '>', value: 'gt' },
|
||||
{ label: '>=', value: 'gte' },
|
||||
{ label: '<', value: 'lt' },
|
||||
{ label: '<=', value: 'lte' },
|
||||
{ label: 'LIKE', value: 'like' },
|
||||
{ label: 'IN', value: 'in' },
|
||||
{ label: 'NOT IN', value: 'notin' },
|
||||
{ label: 'BETWEEN', value: 'between' }
|
||||
]
|
||||
|
||||
// 页面控件
|
||||
export const viewComponent: { label: string; value: string }[] = [
|
||||
{ label: '输入框', value: 'input' },
|
||||
{ label: '密码框', value: 'password' },
|
||||
{ label: '文本域', value: 'textarea' },
|
||||
{ label: '数字输入框', value: 'inputNumber' },
|
||||
{ label: '标签输入框', value: 'inputTag' },
|
||||
{ label: '开关', value: 'switch' },
|
||||
{ label: '滑块', value: 'slider' },
|
||||
{ label: '数据下拉框', value: 'select' },
|
||||
{ label: '字典下拉框', value: 'saSelect' },
|
||||
{ label: '树形下拉框', value: 'treeSelect' },
|
||||
{ label: '字典单选框', value: 'radio' },
|
||||
{ label: '字典复选框', value: 'checkbox' },
|
||||
{ label: '日期选择器', value: 'date' },
|
||||
{ label: '时间选择器', value: 'time' },
|
||||
{ label: '评分器', value: 'rate' },
|
||||
{ label: '级联选择器', value: 'cascader' },
|
||||
{ label: '用户选择器', value: 'userSelect' },
|
||||
{ label: '图片上传', value: 'uploadImage' },
|
||||
{ label: '图片选择', value: 'imagePicker' },
|
||||
{ label: '文件上传', value: 'uploadFile' },
|
||||
{ label: '大文件切片', value: 'chunkUpload' },
|
||||
{ label: '富文本编辑器', value: 'editor' }
|
||||
]
|
||||
66
saiadmin-artd/src/views/tool/code/modules/table-search.vue
Normal file
66
saiadmin-artd/src/views/tool/code/modules/table-search.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="表名称" prop="table_name">
|
||||
<el-input v-model="formData.table_name" placeholder="请输入数据表名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="数据源" prop="source">
|
||||
<el-input v-model="formData.source" placeholder="请输入数据源名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
160
saiadmin-artd/src/views/tool/crontab/index.vue
Normal file
160
saiadmin-artd/src/views/tool/crontab/index.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'tool:crontab:edit'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'tool:crontab:run'"
|
||||
type="primary"
|
||||
icon="ri:play-fill"
|
||||
toolTip="运行任务"
|
||||
@click="handleRun(row)"
|
||||
/>
|
||||
<SaButton
|
||||
type="primary"
|
||||
icon="ri:history-line"
|
||||
toolTip="运行日志"
|
||||
@click="showTableDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'tool:crontab:edit'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'tool:crontab:edit'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
|
||||
<!-- 日志弹窗 -->
|
||||
<LogListDialog v-model="tableVisible" :dialog-type="tableDialogType" :data="tableData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '@/api/tool/crontab'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import LogListDialog from './modules/log-list.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
type: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ prop: 'id', label: '编号', width: 100, align: 'center' },
|
||||
{ prop: 'name', label: '任务名称', minWidth: 120 },
|
||||
{
|
||||
prop: 'type',
|
||||
label: '任务类型',
|
||||
saiType: 'dict',
|
||||
saiDict: 'crontab_task_type',
|
||||
minWidth: 120
|
||||
},
|
||||
{ prop: 'rule', label: '定时规则', minWidth: 140 },
|
||||
{ prop: 'target', label: '调用目标', minWidth: 200, showOverflowTooltip: true },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status', width: 100 },
|
||||
{ prop: 'update_time', label: '更新日期', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 180, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, deleteRow, handleSelectionChange } =
|
||||
useSaiAdmin()
|
||||
|
||||
const {
|
||||
dialogVisible: tableVisible,
|
||||
dialogType: tableDialogType,
|
||||
dialogData: tableData,
|
||||
showDialog: showTableDialog
|
||||
} = useSaiAdmin()
|
||||
|
||||
// 运行任务
|
||||
const handleRun = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要运行任务【${row.name}】吗?`, '运行任务', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
api.run({ id: row.id }).then(() => {
|
||||
ElMessage.success('任务运行成功')
|
||||
refreshData()
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
278
saiadmin-artd/src/views/tool/crontab/modules/edit-dialog.vue
Normal file
278
saiadmin-artd/src/views/tool/crontab/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增定时任务' : '编辑定时任务'"
|
||||
width="800px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="任务名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入任务名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型" prop="type">
|
||||
<sa-select v-model="formData.type" dict="crontab_task_type" />
|
||||
</el-form-item>
|
||||
<el-form-item label="定时规则" prop="task_style">
|
||||
<el-space>
|
||||
<el-select v-model="formData.task_style" :style="{ width: '100px' }">
|
||||
<el-option :value="1" label="每天" />
|
||||
<el-option :value="2" label="每小时" />
|
||||
<el-option :value="3" label="N小时" />
|
||||
<el-option :value="4" label="N分钟" />
|
||||
<el-option :value="5" label="N秒" />
|
||||
<el-option :value="6" label="每周" />
|
||||
<el-option :value="7" label="每月" />
|
||||
<el-option :value="8" label="每年" />
|
||||
</el-select>
|
||||
<template v-if="formData.task_style == 8">
|
||||
<el-input-number
|
||||
v-model="formData.month"
|
||||
:precision="0"
|
||||
:min="1"
|
||||
:max="12"
|
||||
controls-position="right"
|
||||
:style="{ width: '100px' }"
|
||||
/>
|
||||
<span>月</span>
|
||||
</template>
|
||||
<template v-if="formData.task_style > 6">
|
||||
<el-input-number
|
||||
v-model="formData.day"
|
||||
:precision="0"
|
||||
:min="1"
|
||||
:max="31"
|
||||
controls-position="right"
|
||||
:style="{ width: '100px' }"
|
||||
/>
|
||||
<span>日</span>
|
||||
</template>
|
||||
<el-select
|
||||
v-if="formData.task_style == 6"
|
||||
v-model="formData.week"
|
||||
:style="{ width: '100px' }"
|
||||
>
|
||||
<el-option :value="1" label="周一" />
|
||||
<el-option :value="2" label="周二" />
|
||||
<el-option :value="3" label="周三" />
|
||||
<el-option :value="4" label="周四" />
|
||||
<el-option :value="5" label="周五" />
|
||||
<el-option :value="6" label="周六" />
|
||||
<el-option :value="0" label="周日" />
|
||||
</el-select>
|
||||
<template v-if="[1, 3, 6, 7, 8].includes(formData.task_style)">
|
||||
<el-input-number
|
||||
v-model="formData.hour"
|
||||
:precision="0"
|
||||
:min="0"
|
||||
:max="23"
|
||||
controls-position="right"
|
||||
:style="{ width: '100px' }"
|
||||
/>
|
||||
<span>时</span>
|
||||
</template>
|
||||
<template v-if="formData.task_style != 5">
|
||||
<el-input-number
|
||||
v-model="formData.minute"
|
||||
:precision="0"
|
||||
:min="0"
|
||||
:max="59"
|
||||
controls-position="right"
|
||||
:style="{ width: '100px' }"
|
||||
/>
|
||||
<span>分</span>
|
||||
</template>
|
||||
<template v-if="formData.task_style == 5">
|
||||
<el-input-number
|
||||
v-model="formData.second"
|
||||
:precision="0"
|
||||
:min="0"
|
||||
:max="59"
|
||||
controls-position="right"
|
||||
:style="{ width: '100px' }"
|
||||
/>
|
||||
<span>秒</span>
|
||||
</template>
|
||||
</el-space>
|
||||
</el-form-item>
|
||||
<el-form-item label="调用目标" prop="target">
|
||||
<el-input
|
||||
v-model="formData.target"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入调用目标"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务参数" prop="params">
|
||||
<el-input
|
||||
v-model="formData.parameter"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入任务参数"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/tool/crontab'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '任务类型不能为空', trigger: 'blur' }],
|
||||
task_style: [{ required: true, message: '定时规则不能为空', trigger: 'blur' }],
|
||||
target: [{ required: true, message: '调用目标不能为空', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
name: '',
|
||||
type: '',
|
||||
rule: '',
|
||||
task_style: 1,
|
||||
month: 1,
|
||||
day: 1,
|
||||
week: 1,
|
||||
hour: 1,
|
||||
minute: 1,
|
||||
second: 1,
|
||||
target: '',
|
||||
parameter: '',
|
||||
status: 1,
|
||||
remark: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
// 提取数字
|
||||
const extractNumber = (str: string) => {
|
||||
const match = str.match(/\d+/)
|
||||
return match ? Number.parseInt(match[0]) : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
const words = formData['rule'].split(' ')
|
||||
formData['second'] = extractNumber(words[0])
|
||||
formData['minute'] = extractNumber(words[1])
|
||||
formData['hour'] = extractNumber(words[2])
|
||||
formData['day'] = extractNumber(words[3])
|
||||
formData['month'] = extractNumber(words[4])
|
||||
formData['week'] = extractNumber(words[5])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
222
saiadmin-artd/src/views/tool/crontab/modules/log-list.vue
Normal file
222
saiadmin-artd/src/views/tool/crontab/modules/log-list.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="任务执行日志"
|
||||
size="70%"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="art-full-height">
|
||||
<div class="flex justify-between items-center">
|
||||
<ElSpace wrap>
|
||||
<el-date-picker
|
||||
v-model="searchForm.create_time"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
/>
|
||||
</ElSpace>
|
||||
<ElSpace wrap>
|
||||
<ElButton class="reset-button" @click="handleReset" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:reset-right-line" />
|
||||
</template>
|
||||
重置
|
||||
</ElButton>
|
||||
<ElButton type="primary" class="search-button" @click="handleSearch" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:search-line" />
|
||||
</template>
|
||||
查询
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<div>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'tool:crontab:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleLoadTable"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<template #status="{ row }">
|
||||
<ElTag v-if="row.status == 1" type="success">成功</ElTag>
|
||||
<ElTag v-else type="danger">失败</ElTag>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '@/api/tool/crontab'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const selectedRows = ref<Record<string, any>[]>([])
|
||||
const searchForm = ref({
|
||||
crontab_id: '',
|
||||
orderType: 'desc',
|
||||
create_time: []
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
if (!props.data?.id) {
|
||||
ElMessage.error('请先选择一个任务')
|
||||
return
|
||||
}
|
||||
searchForm.value.crontab_id = props.data.id
|
||||
refreshData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格数据
|
||||
*/
|
||||
const refreshData = () => {
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
const handleSearch = () => {
|
||||
refreshData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
const handleReset = () => {
|
||||
searchForm.value.create_time = []
|
||||
refreshData()
|
||||
}
|
||||
|
||||
// 表格行选择变化
|
||||
const handleSelectionChange = (selection: Record<string, any>[]): void => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 确认选择装载数据表
|
||||
const handleLoadTable = async () => {
|
||||
if (selectedRows.value.length < 1) {
|
||||
ElMessage.info('至少要选择一条数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'删除选中数据',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
).then(() => {
|
||||
api.deleteCrontabLog({ ids: selectedRows.value.map((row) => row.id) }).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
refreshData()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedRows.value = []
|
||||
}
|
||||
|
||||
const {
|
||||
loading,
|
||||
data: tableData,
|
||||
columns,
|
||||
getData,
|
||||
pagination,
|
||||
searchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.logPageList,
|
||||
immediate: false,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'create_time', label: '执行时间', sortable: true },
|
||||
{ prop: 'target', label: '调用目标' },
|
||||
{ prop: 'parameter', label: '任务参数' },
|
||||
{ prop: 'status', label: '执行状态', useSlot: true, width: 100 }
|
||||
]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="任务名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入任务名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="任务类型" prop="type">
|
||||
<sa-select v-model="formData.type" dict="crontab_task_type" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user