Comparer les révisions

46 Révisions

Auteur SHA1 Message Date
lejianwen 3998c2a921 fix: webclient share
feat: peer list
2025-08-31 13:38:27 +08:00
lejianwen ffe0961b4a feat: Oauth 2025-08-10 15:46:29 +08:00
lejianwen c5d16aed80 feat: Oauth 2025-08-10 15:39:25 +08:00
lejianwen fe2626f6cb feat: Update package 2025-08-10 11:19:09 +08:00
Tao Chen 4be05adb0c feat: 优化oauth设置页面 (#22)
* hide ClientSecret when re-edit

* add rediret_url at oauth form(readonly, for user to know what's the redirect url)

* format defaultRedirect
2025-08-10 11:15:48 +08:00
Tao Chen 09a40d3502 feat: Login (#21)
* if auto_oidc=true, auto login by oidc and hiden login-form if diable_pwd

* also hide divider
2025-07-31 10:46:35 +08:00
lejianwen 35bac6b91c fix: 2025-07-20 17:50:47 +08:00
ludong 15443701c6 fix: i18n (#18)
* zh_TW添加缺少的DeviceGroupManager以及Import

* 使用繁體
2025-07-14 20:00:39 +08:00
lejianwen 5bbf0ca7d8 feat: user remark
fix: remove oauth redirect url
2025-07-08 12:18:25 +08:00
Plynksiy Nikita 9b2af8794a feat: Update HTML language, improve SCSS font settings, and enhance Russian translations (#16) 2025-06-19 15:09:02 +08:00
lejianwen fc7e0595a1 feat: Add Export in some menu
fix: Left menu height
fix: Export csv when double quotation marks in data
2025-06-15 17:16:27 +08:00
lejianwen 42cc3974d4 feat: Add Oauth via LinuxDo 2025-06-15 15:43:32 +08:00
lejianwen 3084e40039 fix: 2025-05-28 17:06:12 +08:00
lejianwen 4b818f73e5 fix: after register page empty 2025-05-25 17:11:57 +08:00
lejianwen 269bd0fd87 feat: user check 2025-05-25 16:33:12 +08:00
lejianwen 48a38156f7 fix: i18n (lejianwen/rustdesk-api#222) 2025-05-08 11:11:13 +08:00
lejianwen 357f34e0e4 fix: user edit 2025-05-08 11:08:29 +08:00
lejianwen 0dfab9edfb fix: 2025-04-24 11:51:53 +08:00
lejianwen 0967e78ab7 style: 2025-04-24 08:51:02 +08:00
lejianwen 83f25444ba style: 2025-04-14 21:13:43 +08:00
lejianwen 7129dccbbc fix: el-radio 2025-04-14 20:33:48 +08:00
lejianwen 68bbcf17b0 feat: share to group 2025-04-14 19:09:25 +08:00
lejianwen 24c480b2b7 feat: trans 2025-04-14 12:41:38 +08:00
lejianwen 00b09aae76 fix 2025-03-15 21:07:56 +08:00
lejianwen f822e5bebf feat: Add import 2025-03-15 21:00:07 +08:00
lejianwen cb7837e6e7 feat(admin): Add filter by ip and username 2025-03-15 19:50:01 +08:00
lejianwen b1ebedefc6 feat: Add device group 2025-02-23 21:34:56 +08:00
lejianwen c20592620d fix: PKCE plain value 2025-02-20 19:41:28 +08:00
lejianwen d22aae110f fix: PKCE plain value 2025-02-20 19:26:42 +08:00
Tao Chen a631c7f8f0 feat: Feat/OIDC pcke (#14)
* add pkce for oidc

* fix

* test

* test
2025-02-19 09:56:22 +08:00
lejianwen aeafcdde60 fix: Document title 2025-02-12 21:08:33 +08:00
lejianwen 8617959d15 feat(i18n): add zh-tw 2025-01-19 21:19:57 +08:00
Jia-Bin 68ebc75e59 Add Traditional Chinese 2025-01-19 21:06:50 +08:00
lejianwen 8d89478913 docs: 2025-01-16 20:56:33 +08:00
lejianwen cc8fa46299 fix: must_login 2025-01-16 20:49:25 +08:00
lejianwen e4342ae3af style: usage 2025-01-15 21:23:39 +08:00
lejianwen ce7ee9cce3 feat: usage 2025-01-15 20:26:56 +08:00
lejianwen 96cf2b29d5 fix: Rustdesk Commands Must Login 2025-01-12 21:56:00 +08:00
lejianwen 0e9391a4c5 refactor: Rustdesk Commands 2025-01-12 21:22:29 +08:00
lejianwen 127419e89b feat: Add Relay server cmd 2025-01-04 20:48:46 +08:00
lejianwen 7cf1a14950 feat: Upgrade Server command 2025-01-02 21:48:02 +08:00
lejianwen aab4ea4785 feat: Add rustdesk command 2024-12-31 23:17:08 +08:00
lejianwen bf00eb3fed feat: Add My Login log 2024-12-27 19:25:22 +08:00
lejianwen a1e5e9a9da fix: Support markdown to welcome msg 2024-12-25 19:59:41 +08:00
lejianwen 21254e2b2c feat: Support markdown to welcome msg 2024-12-25 19:21:14 +08:00
f589d923e9 feat(i18n): Merge pull request #9 from jimmyGALLAND/trans-fr
add locale french
2024-12-22 13:58:39 +08:00
51 fichiers modifiés avec 4913 ajouts et 5169 suppressions
+2 -2
Voir le fichier
@@ -1,10 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rustdesk-Api-Admin</title> <title>Rustdesk API Admin</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+2083 -543
Voir le fichier
Fichier diff supprimé car celui-ci est trop grand Voir la Diff
+11 -10
Voir le fichier
@@ -7,25 +7,26 @@
"serve": "vite preview" "serve": "vite preview"
}, },
"dependencies": { "dependencies": {
"axios": "1.7.4", "axios": "1.8.2",
"clipboard": "2.0.4",
"element-plus": "^2.8.2", "element-plus": "^2.8.2",
"fast-sha256": "^1.3.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"marked": "^15.0.4",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "2.0.3", "pinia": "2.2.8",
"vue": "3.2.37", "vue": "3.5.13",
"vue-router": "^4.0.12", "vue-router": "^4.0.12"
"fast-sha256": "^1.3.0",
"clipboard": "2.0.4"
}, },
"devDependencies": { "devDependencies": {
"@element-plus/icons": "0.0.11", "@element-plus/icons": "0.0.11",
"@vitejs/plugin-vue": "^1.9.3", "@vitejs/plugin-vue": "5.2.1",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"qs": "^6.10.2", "qs": "^6.10.2",
"sass-loader": "^12.3.0",
"sass": "^1.43.4", "sass": "^1.43.4",
"vite": "^2.9.18", "sass-loader": "^12.3.0",
"ts-proto": "^1.141.1" "ts-proto": "^1.141.1",
"vite": "6.3.4"
} }
} }
+38
Voir le fichier
@@ -0,0 +1,38 @@
import request from '@/utils/request'
export function list (params) {
return request({
url: '/device_group/list',
params,
})
}
export function detail (id) {
return request({
url: `/device_group/detail/${id}`,
})
}
export function create (data) {
return request({
url: '/device_group/create',
method: 'post',
data,
})
}
export function update (data) {
return request({
url: '/device_group/update',
method: 'post',
data,
})
}
export function remove (data) {
return request({
url: '/device_group/delete',
method: 'post',
data,
})
}
+24
Voir le fichier
@@ -0,0 +1,24 @@
import request from '@/utils/request'
export function list (params) {
return request({
url: '/my/login_log/list',
params,
})
}
export function remove (data) {
return request({
url: '/my/login_log/delete',
method: 'post',
data,
})
}
export function batchDelete (data) {
return request({
url: '/my/login_log/batchDelete',
method: 'post',
data,
})
}
+40
Voir le fichier
@@ -0,0 +1,40 @@
import request from '@/utils/request'
export function list (params) {
return request({
url: '/rustdesk/cmdList',
params,
})
}
export function create (data) {
return request({
url: '/rustdesk/cmdCreate',
method: 'post',
data,
})
}
export function update (data) {
return request({
url: '/rustdesk/cmdUpdate',
method: 'post',
data,
})
}
export function remove (data) {
return request({
url: '/rustdesk/cmdDelete',
method: 'post',
data,
})
}
export function sendCmd (data) {
return request({
url: '/rustdesk/sendCmd',
method: 'post',
data,
})
}
+11 -11
Voir le fichier
@@ -52,14 +52,14 @@
new_password: '', new_password: '',
confirmPwd: '', confirmPwd: '',
}) })
const chagePwdRules = reactive({ const chagePwdRules = computed(_ => ({
old_password: [{ required: true, message: '请输入旧密码', trigger: 'blur' }], old_password: [{ required: true, message: T('ParamRequired', { param: T('OldPassword') }), trigger: 'blur' }],
new_password: [ new_password: [
{ required: true, message: '请输入新密码', trigger: 'blur' }, { required: true, message: T('ParamRequired', { param: T('NewPassword') }), trigger: 'blur' },
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (value === changePwdForm.old_password) { if (value === changePwdForm.old_password) {
callback(new Error('新密码不能与旧密码相同')) callback(new Error(T('NewPasswordEqualOldPassword'))) //'新密码不能与旧密码相同'
} else { } else {
callback() callback()
} }
@@ -67,11 +67,11 @@
trigger: 'blur', trigger: 'blur',
}], }],
confirmPwd: [ confirmPwd: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' }, { required: true, message: T('ParamRequired', { param: T('ConfirmPassword') }), trigger: 'blur' },
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (value !== changePwdForm.new_password) { if (value !== changePwdForm.new_password) {
callback(new Error('两次输入密码不一致')) callback(new Error(T('PasswordNotMatchConfirmPassword')))
} else { } else {
callback() callback()
} }
@@ -79,7 +79,7 @@
trigger: 'blur', trigger: 'blur',
}, },
], ],
}) }))
const cpwd = ref(null) const cpwd = ref(null)
const cancelChangePwd = () => { const cancelChangePwd = () => {
emit('update:visible', false) emit('update:visible', false)
@@ -94,9 +94,9 @@
return return
} }
console.log('changePassword') console.log('changePassword')
const confirm = await ElMessageBox.confirm('确定修改密码么?', { const confirm = await ElMessageBox.confirm(T('Confirm?', { param: T('ChangePassword') }), {
confirmButtonText: '确定', confirmButtonText: T('Confirm'),
cancelButtonText: '取消', cancelButtonText: T('Cancel'),
}).catch(_ => false) }).catch(_ => false)
if (!confirm) { if (!confirm) {
return return
@@ -105,7 +105,7 @@
if (!res) { if (!res) {
return return
} }
ElMessageBox.alert('修改成功', '修改密码', { ElMessageBox.alert(T('OperationSuccess'), T('ChangePassword'), {
autofocus: true, autofocus: true,
confirmButtonText: 'OK', confirmButtonText: 'OK',
callback: (action) => { callback: (action) => {
+4 -4
Voir le fichier
@@ -13,8 +13,8 @@
}) })
</script> </script>
<style scoped> <style scoped lang="scss">
.scroll-sidebar{ .scroll-sidebar {
position: fixed; background-color: #2d3a4b;
} }
</style> </style>
+1 -2
Voir le fichier
@@ -15,7 +15,7 @@
<el-main class="app-main"> <el-main class="app-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear"> <transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="[...cachedTags]"> <keep-alive :include="cachedTags">
<component :is="Component"/> <component :is="Component"/>
</keep-alive> </keep-alive>
</transition> </transition>
@@ -60,7 +60,6 @@
} }
.app-left { .app-left {
height: 100%;
transition: width 0.5s; transition: width 0.5s;
} }
+4 -1
Voir le fichier
@@ -13,6 +13,7 @@ NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/register'] const whiteList = ['/login', '/register']
const routeStore = useRouteStore(pinia) const routeStore = useRouteStore(pinia)
const appStore = useAppStore(pinia) const appStore = useAppStore(pinia)
appStore.getAdminConfig()
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
document.title = T(to.meta?.title) + ' - ' + appStore.setting.title document.title = T(to.meta?.title) + ' - ' + appStore.setting.title
@@ -40,7 +41,9 @@ router.beforeEach(async (to, from, next) => {
} else { } else {
next({ ...to, replace: true }) next({ ...to, replace: true })
} }
} else { }/* else if (to.path === '/404') {
next({path: to.redirectedFrom?.fullPath, replace: true})
}*/ else {
next() next()
} }
} }
+19 -1
Voir le fichier
@@ -51,7 +51,7 @@ export const asyncRoutes = [
{ {
path: '/my', path: '/my',
name: 'My', name: 'My',
redirect: '/my/info', redirect: '/',
meta: { title: 'My', icon: 'UserFilled' }, meta: { title: 'My', icon: 'UserFilled' },
component: () => import('@/layout/index.vue'), component: () => import('@/layout/index.vue'),
children: [ children: [
@@ -91,6 +91,12 @@ export const asyncRoutes = [
meta: { title: 'ShareRecord', icon: 'Share' /*keepAlive: true*/ }, meta: { title: 'ShareRecord', icon: 'Share' /*keepAlive: true*/ },
component: () => import('@/views/my/share_record/index.vue'), component: () => import('@/views/my/share_record/index.vue'),
}, },
{
path: 'loginLog',
name: 'MyLoginLog',
meta: { title: 'LoginLog', icon: 'List' /*keepAlive: true*/ },
component: () => import('@/views/my/login_log/index.vue'),
},
], ],
}, },
{ {
@@ -112,6 +118,12 @@ export const asyncRoutes = [
meta: { title: 'GroupManage', icon: 'ChatRound' /*keepAlive: true*/ }, meta: { title: 'GroupManage', icon: 'ChatRound' /*keepAlive: true*/ },
component: () => import('@/views/group/index.vue'), component: () => import('@/views/group/index.vue'),
}, },
{
path: 'deviceGroup',
name: 'DeviceGroup',
meta: { title: 'DeviceGroupManage', icon: 'ChatRound' /*keepAlive: true*/ },
component: () => import('@/views/group/deviceGroupList.vue'),
},
{ {
path: 'index', path: 'index',
name: 'UserList', name: 'UserList',
@@ -184,6 +196,12 @@ export const asyncRoutes = [
meta: { title: 'ShareRecord', icon: 'Share' /*keepAlive: true*/ }, meta: { title: 'ShareRecord', icon: 'Share' /*keepAlive: true*/ },
component: () => import('@/views/share_record/index.vue'), component: () => import('@/views/share_record/index.vue'),
}, },
{
path: '/serverCmd',
name: 'ServerCmd',
meta: { title: 'ServerCmd', icon: 'Tools' /*keepAlive: true*/ },
component: () => import('@/views/rustdesk/control.vue'),
},
], ],
}, },
] ]
+32 -7
Voir le fichier
@@ -6,22 +6,24 @@ import ko from 'element-plus/es/locale/lang/ko'
import ru from 'element-plus/es/locale/lang/ru' import ru from 'element-plus/es/locale/lang/ru'
import fr from 'element-plus/es/locale/lang/fr' import fr from 'element-plus/es/locale/lang/fr'
import es from 'element-plus/es/locale/lang/es' import es from 'element-plus/es/locale/lang/es'
import { admin, app } from '@/api/config' import zhTw from 'element-plus/es/locale/lang/zh-tw'
import { admin, app, server } from '@/api/config'
const langs = { const langs = {
'zh-CN': { name: '中文', value: zhCn, sideBarWidth: '210px' }, 'zh-CN': { name: '中文', value: zhCn, sideBarWidth: '210px' },
'en': { name: 'English', value: en, sideBarWidth: '230px' }, 'en': { name: 'English', value: en, sideBarWidth: '230px' },
'fr': { name: 'Français', value: fr, sideBarWidth: '280px' }, 'fr': { name: 'Français', value: fr, sideBarWidth: '280px' },
'ko': { name: '한국어', value: ko, sideBarWidth: '230px' }, 'ko': { name: '한국어', value: ko, sideBarWidth: '230px' },
'ru': { name: 'русский', value: ru, sideBarWidth: '250px' }, 'ru': { name: 'Русский', value: ru, sideBarWidth: '250px' },
'es': { name: 'español', value: es, sideBarWidth: '280px' }, 'es': { name: 'Español', value: es, sideBarWidth: '280px' },
'zh-TW': { name: '中文繁体', value: zhTw, sideBarWidth: '210px' },
} }
const defaultLang = localStorage.getItem('lang') || navigator.language || 'zh-CN' const defaultLang = localStorage.getItem('lang') || navigator.language || 'zh-CN'
export const useAppStore = defineStore({ export const useAppStore = defineStore({
id: 'App', id: 'App',
state: () => ({ state: () => ({
setting: { setting: {
title: 'Rustdesk-Api-Admin', title: 'Rustdesk API Admin',
hello: '', hello: '',
sideIsCollapse: false, sideIsCollapse: false,
logo, logo,
@@ -31,6 +33,12 @@ export const useAppStore = defineStore({
appConfig: { appConfig: {
web_client: 1, web_client: 1,
}, },
rustdeskConfig: {
'id_server': '',
'key': '',
'relay_server': '',
'api_server': '',
},
}, },
}), }),
@@ -50,20 +58,37 @@ export const useAppStore = defineStore({
loadConfig () { loadConfig () {
this.getAppConfig() this.getAppConfig()
this.getAdminConfig() this.getAdminConfig()
this.loadRustdeskConfig()
}, },
getAppConfig () { getAppConfig () {
console.log('getAppConfig') console.log('getAppConfig')
app().then(res => { return app().then(res => {
this.setting.appConfig = res.data this.setting.appConfig = res.data
}) })
}, },
getAdminConfig () { getAdminConfig () {
console.log('getAdminConfig') console.log('getAdminConfig')
admin().then(res => { return admin().then(res => {
this.setting.title = res.data.title this.replaceAdminTitle(res.data.title)
this.setting.hello = res.data.hello this.setting.hello = res.data.hello
}) })
}, },
replaceAdminTitle (newTitle) {
document.title = document.title.replace(`- ${this.setting.title}`, `- ${newTitle}`)
this.setting.title = newTitle
},
async loadRustdeskConfig () {
console.log('loadRustdeskConfig')
const res = await server().catch(_ => false)
if (res) {
this.setting.rustdeskConfig = res.data
const prefix = 'wc-'
localStorage.setItem(`${prefix}custom-rendezvous-server`, res.data.id_server)
localStorage.setItem(`${prefix}key`, res.data.key)
localStorage.setItem(`${prefix}api-server`, res.data.api_server)
}
},
}, },
}) })
+5
Voir le fichier
@@ -10,6 +10,11 @@ $primaryColor: #409eff;
--tag-bg-color: #efefef; --tag-bg-color: #efefef;
} }
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
.list-body { .list-body {
margin: 10px 0; margin: 10px 0;
} }
+1 -1
Voir le fichier
@@ -38,7 +38,7 @@ export function jsonToCsv (data) {
let keys = Object.keys(data[0]) let keys = Object.keys(data[0])
csv += keys.join(',') + '\n' csv += keys.join(',') + '\n'
data.forEach(row => { data.forEach(row => {
csv += keys.map(key => `"${row[key]}"`).join(',') + '\n' csv += keys.map(key => `"${row[key].toString().replaceAll('"', '""')}"`).join(',') + '\n'
}) })
return new Blob([csv], { type: 'text/csv' }) return new Blob([csv], { type: 'text/csv' })
} }
+10 -8
Voir le fichier
@@ -4,19 +4,21 @@ import zhCN from '@/utils/i18n/zh_CN.json'
import ko from '@/utils/i18n/ko.json' import ko from '@/utils/i18n/ko.json'
import ru from '@/utils/i18n/ru.json' import ru from '@/utils/i18n/ru.json'
import es from '@/utils/i18n/es.json' import es from '@/utils/i18n/es.json'
import zhTW from '@/utils/i18n/zh_TW.json'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
const trans = {
'en': en,
'fr': fr,
'zh-CN': zhCN,
'ko': ko,
'ru': ru,
'es': es,
'zh-TW': zhTW,
}
export function T (key, params, num = 0) { export function T (key, params, num = 0) {
const appStore = useAppStore() const appStore = useAppStore()
const lang = appStore.setting.lang const lang = appStore.setting.lang
const trans = {
'en': en,
'fr': fr,
'zh-CN': zhCN,
'ko': ko,
'ru': ru,
'es': es,
}
const tran = trans[lang]?.[key] const tran = trans[lang]?.[key]
if (!tran) { if (!tran) {
return key return key
+63
Voir le fichier
@@ -439,6 +439,15 @@
"LastOnlineIp": { "LastOnlineIp": {
"One": "Last Online Ip" "One": "Last Online Ip"
}, },
"OldPassword": {
"One": "Old Password"
},
"NewPasswordEqualOldPassword": {
"One": "New Password cannot be the same as Old Password"
},
"NewPassword": {
"One": "New Password"
},
"ConfirmPassword": { "ConfirmPassword": {
"One": "Confirm Password" "One": "Confirm Password"
}, },
@@ -468,5 +477,59 @@
}, },
"Captcha": { "Captcha": {
"One": "Captcha" "One": "Captcha"
},
"ServerCmd": {
"One": "Server Cmd"
},
"Simple": {
"One": "Simple"
},
"Advanced": {
"One": "Advanced"
},
"Available": {
"One": "Available"
},
"NotAvailable": {
"One": "Not Available"
},
"ServerCmdTips": {
"One": "This feature is experimental. Please ensure that the API and hbbs and hbbr are on the same loopback address. For more information, please refer to the {wiki}"
},
"Refresh": {
"One": "Refresh"
},
"Save": {
"One": "Save"
},
"SendCustom": {
"One": "Send Custom"
},
"Send": {
"One": "Send"
},
"SendCmd": {
"One": "Send Cmd"
},
"Result": {
"One": "Result"
},
"DeviceGroupManage": {
"One": "Device Group Manage"
},
"Drop file here or click to upload": {
"One": "Drop file here or click to upload"
},
"Please upload csv file": {
"One": "Please upload csv file"
},
"Columns": {
"One": "Columns"
},
"You can reference export file": {
"One": "You can reference export file"
},
"Remark": {
"One": "Remark"
} }
} }
+60
Voir le fichier
@@ -439,6 +439,15 @@
"LastOnlineIp": { "LastOnlineIp": {
"One": "Última IP en línea" "One": "Última IP en línea"
}, },
"OldPassword": {
"One": "Contraseña antigua"
},
"NewPasswordEqualOldPassword": {
"One": "La nueva contraseña no puede ser igual a la antigua"
},
"NewPassword": {
"One": "Nueva contraseña"
},
"ConfirmPassword": { "ConfirmPassword": {
"One": "Confirmar contraseña" "One": "Confirmar contraseña"
}, },
@@ -471,5 +480,56 @@
}, },
"Captcha": { "Captcha": {
"One": "Captcha" "One": "Captcha"
},
"ServerCmd": {
"One": "Comando del servidor"
},
"Simple": {
"One": "Simple"
},
"Advanced": {
"One": "Avanzado"
},
"Available": {
"One": "Disponible"
},
"NotAvailable": {
"One": "No disponible"
},
"ServerCmdTips": {
"One": "Esta función es experimental. Asegúrese de que la API y hbbs y hbbr estén en la misma dirección de bucle de retorno. Para obtener más información, consulte el {wiki}"
},
"Refresh": {
"One": "Actualizar"
},
"Save": {
"One": "Guardar"
},
"SendCustom": {
"One": "Enviar personalizado"
},
"Send": {
"One": "Enviar"
},
"SendCmd": {
"One": "Enviar comando"
},
"Result": {
"One": "Resultado"
},
"Drop file here or click to upload": {
"One": "Suelte el archivo aquí o haga clic para cargar"
},
"Please upload csv file": {
"One": "Por favor, suba un archivo csv"
},
"Columns": {
"One": "Columnas"
},
"You can reference export file": {
"One": "Puede hacer referencia al archivo de exportación"
},
"Remark": {
"One": "Remark"
} }
} }
+238 -178
Voir le fichier
@@ -1,192 +1,192 @@
{ {
"Login": { "Login": {
"One": "Connexion" "One": "Connexion"
}, },
"Logout": { "Logout": {
"One": "Déconnexion" "One": "Déconnexion"
}, },
"Register": { "Register": {
"One": "S'inscrire" "One": "S'inscrire"
}, },
"Confirm": { "Confirm": {
"One": "Confirmer" "One": "Confirmer"
}, },
"Username": { "Username": {
"One": "Nom d'utilisateur" "One": "Nom d'utilisateur"
}, },
"Password": { "Password": {
"One": "Mot de passe" "One": "Mot de passe"
}, },
"LoginSuccess": { "LoginSuccess": {
"One": "Connexion réussie" "One": "Connexion réussie"
}, },
"ForgotPassword": { "ForgotPassword": {
"One": "Mot de passe oublié" "One": "Mot de passe oublié"
}, },
"ResetPassword": { "ResetPassword": {
"One": "Réinitialiser le mot de passe" "One": "Réinitialiser le mot de passe"
}, },
"ChangePassword": { "ChangePassword": {
"One": "Modifier le mot de passe" "One": "Modifier le mot de passe"
}, },
"Userinfo": { "Userinfo": {
"One": "Informations utilisateur" "One": "Informations utilisateur"
}, },
"ParamRequired": { "ParamRequired": {
"One": "{param} est obligatoire" "One": "{param} est obligatoire"
}, },
"HasBind": { "HasBind": {
"One": "A une liaison" "One": "A une liaison"
}, },
"NoBind": { "NoBind": {
"One": "Aucune liaison" "One": "Aucune liaison"
}, },
"UnBind": { "UnBind": {
"One": "Délier" "One": "Délier"
}, },
"ToBind": { "ToBind": {
"One": "Lier" "One": "Lier"
}, },
"Confirm?": { "Confirm?": {
"One": "{param} Confirmer?" "One": "{param} Confirmer?"
}, },
"Cancel": { "Cancel": {
"One": "Annuler" "One": "Annuler"
}, },
"Platform": { "Platform": {
"One": "Plateforme" "One": "Plateforme"
}, },
"Status": { "Status": {
"One": "État" "One": "État"
}, },
"Actions": { "Actions": {
"One": "Actions" "One": "Actions"
}, },
"Filter": { "Filter": {
"One": "Filtre" "One": "Filtre"
}, },
"Add": { "Add": {
"One": "Ajouter" "One": "Ajouter"
}, },
"Hostname": { "Hostname": {
"One": "Nom d'hôte" "One": "Nom d'hôte"
}, },
"Alias": { "Alias": {
"One": "Alias" "One": "Alias"
}, },
"Hash": { "Hash": {
"One": "Hachage" "One": "Hachage"
}, },
"Tags": { "Tags": {
"One": "Etiquettes" "One": "Etiquettes"
}, },
"Edit": { "Edit": {
"One": "Modifier" "One": "Modifier"
}, },
"Delete": { "Delete": {
"One": "Supprimer" "One": "Supprimer"
}, },
"Create": { "Create": {
"One": "Créer" "One": "Créer"
}, },
"Update" : { "Update": {
"One" : "Mise à jour" "One": "Mise à jour"
}, },
"LoginName" : { "LoginName": {
"One" : "Nom de connexion" "One": "Nom de connexion"
}, },
"Submit" : { "Submit": {
"One" : "Envoyer" "One": "Envoyer"
}, },
"OperationSuccess" : { "OperationSuccess": {
"One" : "Opération réussie" "One": "Opération réussie"
}, },
"Owner" : { "Owner": {
"One" : "Propriétaire" "One": "Propriétaire"
}, },
"Name" : { "Name": {
"One" : "Nom" "One": "Nom"
}, },
"Color" : { "Color": {
"One" : "Couleur" "One": "Couleur"
}, },
"CreatedAt" : { "CreatedAt": {
"One" : "Créé le" "One": "Créé le"
}, },
"UpdatedAt" : { "UpdatedAt": {
"One" : "Mis à jour le" "One": "Mis à jour le"
}, },
"Memory" : { "Memory": {
"One" : "Mémoire" "One": "Mémoire"
}, },
"Os" : { "Os": {
"One" : "Os" "One": "Os"
}, },
"Uuid" : { "Uuid": {
"One" : "Uuid" "One": "Uuid"
}, },
"Version" : { "Version": {
"One": "Version" "One": "Version"
}, },
"Type": { "Type": {
"One": "Type" "One": "Type"
}, },
"Group": { "Group": {
"One": "Groupe" "One": "Groupe"
}, },
"CommonGroup": { "CommonGroup": {
"One": "Groupe commun" "One": "Groupe commun"
}, },
"CommonGroupNote": { "CommonGroupNote": {
"One": "Seuls les administrateurs peuvent voir les membres du groupe et leurs appareils" "One": "Seuls les administrateurs peuvent voir les membres du groupe et leurs appareils"
}, },
"SharedGroup": { "SharedGroup": {
"One": "Groupe partagé" "One": "Groupe partagé"
}, },
"SharedGroupNote": { "SharedGroupNote": {
"One": "Tous les utilisateurs peuvent voir les membres du groupe et leurs appareils" "One": "Tous les utilisateurs peuvent voir les membres du groupe et leurs appareils"
}, },
"Nickname": { "Nickname": {
"One": "Pseudonyme" "One": "Pseudonyme"
}, },
"UserTags": { "UserTags": {
"One": "Etiquettes utilisateur" "One": "Etiquettes utilisateur"
}, },
"UserAddressBook": { "UserAddressBook": {
"One": "Carnet d'adresses utilisateur" "One": "Carnet d'adresses utilisateur"
}, },
"IsAdmin": { "IsAdmin": {
"One": "Est administrateur" "One": "Est administrateur"
}, },
"PleaseInputNewPassword": { "PleaseInputNewPassword": {
"One": "Veuillez saisir nouveau mot de passe" "One": "Veuillez saisir nouveau mot de passe"
}, },
"AutoRegister": { "AutoRegister": {
"One": "Enregistrement automatique" "One": "Enregistrement automatique"
}, },
"AutoRegisterNote": { "AutoRegisterNote": {
"One": "Si activé, un compte sera automatiquement enregistré lorsqu'un utilisateur se connectera avec OAuth sans lier un compte existant" "One": "Si activé, un compte sera automatiquement enregistré lorsqu'un utilisateur se connectera avec OAuth sans lier un compte existant"
}, },
"ThirdName": { "ThirdName": {
"One": "Troisième nom" "One": "Troisième nom"
}, },
"Close": { "Close": {
"One": "Fermer" "One": "Fermer"
}, },
"OauthBinding": { "OauthBinding": {
"One": "Vous autorisez la liaison" "One": "Vous autorisez la liaison"
}, },
"OauthLogining": { "OauthLogining": {
"One": "Vous autorisez la connexion" "One": "Vous autorisez la connexion"
}, },
"OauthCloseNote": { "OauthCloseNote": {
"One": "Si vous ne l'autorisez pas, veuillez fermer la page directement" "One": "Si vous ne l'autorisez pas, veuillez fermer la page directement"
}, },
"OperationSuccessAndCloseAfter3Seconds": { "OperationSuccessAndCloseAfter3Seconds": {
"One": "Opération réussie, fermer après 3 secondes" "One": "Opération réussie, fermer après 3 secondes"
}, },
"ConfirmOauth": { "ConfirmOauth": {
"One": "Confirmer Oauth" "One": "Confirmer Oauth"
}, },
"View": { "View": {
"One": "Informations" "One": "Informations"
@@ -195,261 +195,270 @@
"One": "Appareil" "One": "Appareil"
}, },
"ChangeLang": { "ChangeLang": {
"One": "Changer la langue" "One": "Changer la langue"
}, },
"My": { "My": {
"One": "Moi" "One": "Moi"
}, },
"Info": { "Info": {
"One": "Info" "One": "Info"
}, },
"AddressBooks": { "AddressBooks": {
"One": "Carnets d'adresses" "One": "Carnets d'adresses"
}, },
"System": { "System": {
"One": "Système" "One": "Système"
}, },
"PeerManage": { "PeerManage": {
"One": "Appareils" "One": "Appareils"
}, },
"AddressBookManage": { "AddressBookManage": {
"One": "Carnets d'adresses" "One": "Carnets d'adresses"
}, },
"GroupManage": { "GroupManage": {
"One": "Groupes" "One": "Groupes"
}, },
"UserManage": { "UserManage": {
"One": "Utilisateurs" "One": "Utilisateurs"
}, },
"UserAdd": { "UserAdd": {
"One": "Ajout d'utilisateur" "One": "Ajout d'utilisateur"
}, },
"UserEdit": { "UserEdit": {
"One": "Utilisateur Modifier" "One": "Utilisateur Modifier"
}, },
"TagsManage": { "TagsManage": {
"One": "Etiquettes" "One": "Etiquettes"
}, },
"OauthManage": { "OauthManage": {
"One": "Oauth" "One": "Oauth"
}, },
"LoginLog": { "LoginLog": {
"One": "Journal de connexion" "One": "Journal de connexion"
}, },
"LastOnlineTime": { "LastOnlineTime": {
"One": "Dernière heure de connexion" "One": "Dernière heure de connexion"
}, },
"JustNow": { "JustNow": {
"One": "Juste maintenant" "One": "Juste maintenant"
}, },
"MinutesAgo": { "MinutesAgo": {
"One": "Il y a {param} minute", "One": "Il y a {param} minute",
"Other": "Il y a {param} minutes" "Other": "Il y a {param} minutes"
}, },
"HoursAgo": { "HoursAgo": {
"One": "Il y a {param} heure", "One": "Il y a {param} heure",
"Other": "Il y a {param} heures" "Other": "Il y a {param} heures"
}, },
"DaysAgo": { "DaysAgo": {
"One": "Il y a {param} jour", "One": "Il y a {param} jour",
"Other": "Il y a {param} jours" "Other": "Il y a {param} jours"
}, },
"MonthsAgo": { "MonthsAgo": {
"One": " Il y a {param} mois", "One": " Il y a {param} mois",
"Autre": "Il y a {param} mois" "Autre": "Il y a {param} mois"
}, },
"YearsAgo": { "YearsAgo": {
"One": "Il y a {param} an", "One": "Il y a {param} an",
"Autre": "Il y a {param} ans" "Autre": "Il y a {param} ans"
}, },
"MinutesLess": { "MinutesLess": {
"One": "Moins de {param} minute", "One": "Moins de {param} minute",
"Autre": "Moins de {param} minutes" "Autre": "Moins de {param} minutes"
}, },
"HoursLess": { "HoursLess": {
"One": "Moins de {param} heure", "One": "Moins de {param} heure",
"Autre": "Moins de {param} heures" "Autre": "Moins de {param} heures"
}, },
"DaysLess": { "DaysLess": {
"One": "Moins de {param} jour", "One": "Moins de {param} jour",
"Autre": "Moins de {param} jours" "Autre": "Moins de {param} jours"
}, },
"Export": { "Export": {
"One": "Exporter" "One": "Exporter"
}, },
"AddToAddressBook": { "AddToAddressBook": {
"One": "Ajouter au carnet d'adresses" "One": "Ajouter au carnet d'adresses"
}, },
"BatchDelete": { "BatchDelete": {
"One": "Suppression par lot" "One": "Suppression par lot"
}, },
"PleaseSelectData": { "PleaseSelectData": {
"One": "Veuillez sélectionner des données" "One": "Veuillez sélectionner des données"
}, },
"PasswordType": { "PasswordType": {
"One": "Type de mot de passe" "One": "Type de mot de passe"
}, },
"OncePassword": { "OncePassword": {
"One": "Mot de passe à usage unique" "One": "Mot de passe à usage unique"
}, },
"FixedPassword": { "FixedPassword": {
"One": "Mot de passe fixe" "One": "Mot de passe fixe"
}, },
"FixedPasswordWarning": { "FixedPasswordWarning": {
"One": "Les mots de passe fixes peuvent être divulgués, veuillez donc les utiliser avec prudence et l'utilisation de mots de passe à usage unique est recommandée" "One": "Les mots de passe fixes peuvent être divulgués, veuillez donc les utiliser avec prudence et l'utilisation de mots de passe à usage unique est recommandée"
}, },
"ExpireTime": { "ExpireTime": {
"One": "Heure d'expiration" "One": "Heure d'expiration"
}, },
"Minutes": { "Minutes": {
"One": "{param} Minute", "One": "{param} Minute",
"Other": "{param} Minutes" "Other": "{param} Minutes"
}, },
"Hours": { "Hours": {
"One": "{param} Heure", "One": "{param} Heure",
"Other": "{param} Heures" "Other": "{param} Heures"
}, },
"Days": { "Days": {
"One": "{param} Jour", "One": "{param} Jour",
"Other": "{param} Jours" "Other": "{param} Jours"
}, },
"Weeks": { "Weeks": {
"One": "{param} Semaine", "One": "{param} Semaine",
"Other": "{param} Semaines" "Other": "{param} Semaines"
}, },
"Months": { "Months": {
"One": "{param} Mois", "One": "{param} Mois",
"Other": "{param} Mois" "Other": "{param} Mois"
}, },
"Forever": { "Forever": {
"One": "Forever" "One": "Forever"
}, },
"Error": { "Error": {
"One": "Erreur" "One": "Erreur"
}, },
"IDNotExist": { "IDNotExist": {
"One": "L'ID n'existe pas" "One": "L'ID n'existe pas"
}, },
"RemoteDesktopOffline": { "RemoteDesktopOffline": {
"One": "Le bureau à distance est hors ligne" "One": "Le bureau à distance est hors ligne"
}, },
"KeyMismatch": { "KeyMismatch": {
"One": "Non-correspondance de clé" "One": "Non-correspondance de clé"
}, },
"KeyOveruse": { "KeyOveruse": {
"One": "Surutilisation de clé" "One": "Surutilisation de clé"
}, },
"Link": { "Link": {
"One": "Lien" "One": "Lien"
}, },
"CopySuccess": { "CopySuccess": {
"One": "Copie réussie" "One": "Copie réussie"
}, },
"CopyFailed": { "CopyFailed": {
"One": "Copie échouée" "One": "Copie échouée"
}, },
"Timeout": { "Timeout": {
"One": "Expiration du délai" "One": "Expiration du délai"
}, },
"AuditConnLog": { "AuditConnLog": {
"One": "Journal de connexion" "One": "Journal de connexion"
}, },
"Peer": { "Peer": {
"One": "Appareil", "One": "Appareil",
"Other": "Appareils distants" "Other": "Appareils distants"
}, },
"FromPeer": { "FromPeer": {
"One": "Par Appareil distant" "One": "Par Appareil distant"
}, },
"FromName": { "FromName": {
"One": "Par nom" "One": "Par nom"
}, },
"CloseTime": { "CloseTime": {
"One": "Heure de clôture" "One": "Heure de clôture"
}, },
"AuditFileLog": { "AuditFileLog": {
"One": "Fichier Journal" "One": "Fichier Journal"
}, },
"Common": { "Common": {
"One": "Commun" "One": "Commun"
}, },
"File": { "File": {
"One": "Fichier" "One": "Fichier"
}, },
"Num": { "Num": {
"One": "Num" "One": "Num"
}, },
"Ip": { "Ip": {
"One": "Ip" "One": "Ip"
}, },
"FileName": { "FileName": {
"One": "Nom du fichier" "One": "Nom du fichier"
}, },
"FileInfo": { "FileInfo": {
"One": "Informations sur le fichier" "One": "Informations sur le fichier"
}, },
"Path": { "Path": {
"One": "Chemin" "One": "Chemin"
}, },
"IndexNum": { "IndexNum": {
"One": "Num. index" "One": "Num. index"
}, },
"ToRemote": { "ToRemote": {
"One": "Vers distant" "One": "Vers distant"
}, },
"ToLocal": { "ToLocal": {
"One": "Vers local" "One": "Vers local"
}, },
"AddressBookName": { "AddressBookName": {
"One": "Nom du carnet d'adresses" "One": "Nom du carnet d'adresses"
}, },
"AddRule": { "AddRule": {
"One": "Ajouter une règle" "One": "Ajouter une règle"
}, },
"ShareRules": { "ShareRules": {
"One": "Partager des règles" "One": "Partager des règles"
}, },
"Rule": { "Rule": {
"One": "Règle" "One": "Règle"
}, },
"Read": { "Read": {
"One": "Lecture" "One": "Lecture"
}, },
"ReadWrite": { "ReadWrite": {
"One": "Lecture et écriture" "One": "Lecture et écriture"
}, },
"FullControl": { "FullControl": {
"One": "Contrôle total" "One": "Contrôle total"
}, },
"ShareTo": { "ShareTo": {
"One": "Partager avec" "One": "Partager avec"
}, },
"MyAddressBook": { "MyAddressBook": {
"One": "Mon carnet d'adresses" "One": "Mon carnet d'adresses"
}, },
"AddressBook": { "AddressBook": {
"One": "Carnet d'adresses" "One": "Carnet d'adresses"
}, },
"AddressBookNameManage": { "AddressBookNameManage": {
"One": "Noms du carnet d'adresses" "One": "Noms du carnet d'adresses"
}, },
"MyAddressBookTips": { "MyAddressBookTips": {
"One": "\"Mon carnet d'adresses\" est la valeur par défaut du système, ne peut pas être modifié ou supprimé" "One": "\"Mon carnet d'adresses\" est la valeur par défaut du système, ne peut pas être modifié ou supprimé"
}, },
"LastOnlineIp": { "LastOnlineIp": {
"One": "Dernière adresse IP en ligne" "One": "Dernière adresse IP en ligne"
},
"OldPassword": {
"One": "Ancien mot de passe"
},
"NewPasswordEqualOldPassword": {
"One": "Le nouveau mot de passe est identique à l'ancien mot de passe"
},
"NewPassword": {
"One": "Nouveau mot de passe"
}, },
"ConfirmPassword": { "ConfirmPassword": {
"One": "Confirmer le mot de passe" "One": "Confirmer le mot de passe"
}, },
"PasswordNotMatchConfirmPassword": { "PasswordNotMatchConfirmPassword": {
"One": "Les mots de passe ne correspondent pas" "One": "Les mots de passe ne correspondent pas"
}, },
"ToLogin": { "ToLogin": {
"One": "Pour se connecter" "One": "Pour se connecter"
}, },
"UserToken": { "UserToken": {
"One": "Jeton Utilisateur" "One": "Jeton Utilisateur"
}, },
"Token": { "Token": {
"One": "Jeton" "One": "Jeton"
@@ -458,21 +467,72 @@
"One": "Mon appareil" "One": "Mon appareil"
}, },
"BatchAddToAB": { "BatchAddToAB": {
"One": "Ajout par lots au carnet d'adresses" "One": "Ajout par lots au carnet d'adresses"
}, },
"BatchEditTags": { "BatchEditTags": {
"One": "Modification par lots des étiquettes" "One": "Modification par lots des étiquettes"
}, },
"User": { "User": {
"One": "Utilisateur" "One": "Utilisateur"
}, },
"ShareRecord": { "ShareRecord": {
"One": "Enregistrement des partages" "One": "Enregistrement des partages"
}, },
"Second": { "Second": {
"One": "Seconde" "One": "Seconde"
}, },
"Captcha": { "Captcha": {
"One": "Captcha" "One": "Captcha"
},
"ServerCmd": {
"One": "Commande serveur"
},
"Simple": {
"One": "Simple"
},
"Advanced": {
"One": "Avancé"
},
"Available": {
"One": "Disponible"
},
"NotAvailable": {
"One": "Non disponible"
},
"ServerCmdTips": {
"One": "Cette fonctionnalité est expérimentale. Veuillez vous assurer que l'API et hbbs et hbbr sont sur la même adresse de boucle locale. Pour plus d'informations, veuillez consulter le {wiki}"
},
"Refresh": {
"One": "Actualiser"
},
"Save": {
"One": "Enregistrer"
},
"SendCustom": {
"One": "Envoyer personnalisé"
},
"Send": {
"One": "Envoyer"
},
"SendCmd": {
"One": "Envoyer Cmd"
},
"Result": {
"One": "Résultat"
},
"Drop file here or click to upload": {
"One": "Déposez le fichier ici ou cliquez pour télécharger"
},
"Please upload csv file": {
"One": "Veuillez télécharger le fichier csv"
},
"Columns": {
"One": "Colonnes"
},
"You can reference export file": {
"One": "Vous pouvez vous référer au fichier d'exportation"
},
"Remark": {
"One": "Remarque"
} }
} }
+60
Voir le fichier
@@ -425,6 +425,15 @@
"LastOnlineIp": { "LastOnlineIp": {
"One": "마지막 온라인 IP" "One": "마지막 온라인 IP"
}, },
"OldPassword": {
"One": "이전 비밀번호"
},
"NewPasswordEqualOldPassword": {
"One": "새 비밀번호는 이전 비밀번호와 같을 수 없습니다"
},
"NewPassword": {
"One": "새 비밀번호"
},
"ConfirmPassword": { "ConfirmPassword": {
"One": "비밀번호 확인" "One": "비밀번호 확인"
}, },
@@ -457,5 +466,56 @@
}, },
"Captcha": { "Captcha": {
"One": "Captcha" "One": "Captcha"
},
"ServerCmd": {
"One": "서버 명령"
},
"Simple": {
"One": "간단"
},
"Advanced": {
"One": "고급"
},
"Available": {
"One": "사용 가능"
},
"NotAvailable": {
"One": "사용 불가"
},
"ServerCmdTips": {
"One": "이 기능은 실험적입니다. API 및 hbbs 및 hbbr이 동일한 루프백 주소에 있는지 확인하십시오. 자세한 내용은 {wiki}를 참조하십시오."
},
"Refresh": {
"One": "새로 고침"
},
"Save": {
"One": "저장"
},
"SendCustom": {
"One": "사용자 정의 전송"
},
"Send": {
"One": "보내기"
},
"SendCmd": {
"One": "명령 보내기"
},
"Result": {
"One": "결과"
},
"Drop file here or click to upload": {
"One": "여기에 파일을 드롭하거나 클릭하여 업로드하십시오"
},
"Please upload csv file": {
"One": "csv 파일을 업로드하십시오"
},
"Columns": {
"One": "열"
},
"You can reference export file": {
"One": "내보내기 파일을 참조할 수 있습니다"
},
"Remark": {
"One": "비고"
} }
} }
+68 -5
Voir le fichier
@@ -33,7 +33,7 @@
"One": "Данные пользователя" "One": "Данные пользователя"
}, },
"ParamRequired": { "ParamRequired": {
"One": "{param} обязательный" "One": "Поле {param} обязательно"
}, },
"HasBind": { "HasBind": {
"One": "Связано" "One": "Связано"
@@ -42,7 +42,7 @@
"One": "Не связано" "One": "Не связано"
}, },
"UnBind": { "UnBind": {
"One": "Разъединить" "One": "Отвязать"
}, },
"ToBind": { "ToBind": {
"One": "Привязать" "One": "Привязать"
@@ -168,7 +168,7 @@
"One": "Если включено, аккаунт будет автоматически зарегистрирован, когда пользователь входит через OAuth без привязки к существующему аккаунту" "One": "Если включено, аккаунт будет автоматически зарегистрирован, когда пользователь входит через OAuth без привязки к существующему аккаунту"
}, },
"ThirdName": { "ThirdName": {
"One": "Третье имя" "One": "Название стороннего сервиса"
}, },
"Close": { "Close": {
"One": "Закрыть" "One": "Закрыть"
@@ -192,7 +192,7 @@
"One": "Устройство" "One": "Устройство"
}, },
"ChangeLang": { "ChangeLang": {
"One": "Сменить на китайский" "One": "Переключить на китайский"
}, },
"My": { "My": {
"One": "Мой" "One": "Мой"
@@ -431,7 +431,7 @@
"One": "Адресная книга" "One": "Адресная книга"
}, },
"AddressBookNameManage": { "AddressBookNameManage": {
"One": "именами адресных книг" "One": "Имена адресных книг"
}, },
"MyAddressBookTips": { "MyAddressBookTips": {
"One": "«Моя адресная книга» является стандартным для системы, не может быть изменена или удалена" "One": "«Моя адресная книга» является стандартным для системы, не может быть изменена или удалена"
@@ -439,6 +439,15 @@
"LastOnlineIp": { "LastOnlineIp": {
"One": "Последний IP онлайн" "One": "Последний IP онлайн"
}, },
"OldPassword": {
"One": "Старый пароль"
},
"NewPasswordEqualOldPassword": {
"One": "Новый пароль совпадает со старым паролем"
},
"NewPassword": {
"One": "Новый пароль"
},
"ConfirmPassword": { "ConfirmPassword": {
"One": "Подтвердите пароль" "One": "Подтвердите пароль"
}, },
@@ -471,6 +480,60 @@
}, },
"Captcha": { "Captcha": {
"One": "Captcha" "One": "Captcha"
},
"ServerCmd": {
"One": "Команда сервера"
},
"Simple": {
"One": "Простой"
},
"Advanced": {
"One": "Расширенный"
},
"Available": {
"One": "Доступно"
},
"NotAvailable": {
"One": "Недоступно"
},
"ServerCmdTips": {
"One": "Эта функция экспериментальная. Пожалуйста, убедитесь, что API и hbbs и hbbr находятся на одном адресе обратной петли. Для получения дополнительной информации обратитесь к {wiki}"
},
"Refresh": {
"One": "Обновить"
},
"Save": {
"One": "Сохранить"
},
"SendCustom": {
"One": "Отправить настраиваемое"
},
"Send": {
"One": "Отправить"
},
"SendCmd": {
"One": "Отправить команду"
},
"Result": {
"One": "Результат"
},
"DeviceGroupManage": {
"One": "Группы устройств"
},
"Drop file here or click to upload": {
"One": "Перетащите файл сюда или нажмите, чтобы загрузить"
},
"Please upload csv file": {
"One": "Пожалуйста, загрузите файл csv"
},
"Columns": {
"One": "Столбцы"
},
"You can reference export file": {
"One": "Вы можете ссылаться на экспортируемый файл"
},
"Remark": {
"One": "Примечание"
} }
} }
+60
Voir le fichier
@@ -446,6 +446,9 @@
"NewPassword": { "NewPassword": {
"One": "新密码" "One": "新密码"
}, },
"NewPasswordEqualOldPassword": {
"One": "新密码不能与旧密码相同"
},
"ConfirmPassword": { "ConfirmPassword": {
"One": "确认密码" "One": "确认密码"
}, },
@@ -487,5 +490,62 @@
}, },
"Captcha": { "Captcha": {
"One": "验证码" "One": "验证码"
},
"ServerCmd": {
"One": "服务端命令"
},
"Simple": {
"One": "简单"
},
"Advanced": {
"One": "高级"
},
"Available": {
"One": "可用"
},
"NotAvailable": {
"One": "不可用"
},
"ServerCmdTips": {
"One": "此处功能为实验性质,请确保api和 hbbs/hbbr 处于同一回环地址下,更多说明请参考 {wiki}"
},
"Refresh": {
"One": "刷新"
},
"Save": {
"One": "保存"
},
"SendCustom": {
"One": "发送自定义"
},
"Send": {
"One": "发送"
},
"SendCmd": {
"One": "发送命令"
},
"Result": {
"One": "结果"
},
"DeviceGroupManage": {
"One": "设备组管理"
},
"Import": {
"One": "导入"
},
"Drop file here or click to upload": {
"One": "将文件拖到此处或单击上传"
},
"Please upload csv file": {
"One": "请上传csv文件"
},
"Columns": {
"One": "列"
},
"You can reference export file": {
"One": "您可以参考导出文件"
},
"Remark": {
"One": "备注"
} }
} }
+551
Voir le fichier
@@ -0,0 +1,551 @@
{
"Login": {
"One": "登入"
},
"Logout": {
"One": "退出登入"
},
"Register": {
"One": "註冊"
},
"Confirm": {
"One": "確認"
},
"Username": {
"One": "使用者名稱"
},
"Password": {
"One": "密碼"
},
"LoginSuccess": {
"One": "登入成功"
},
"ForgotPassword": {
"One": "忘記密碼"
},
"ResetPassword": {
"One": "重置密碼"
},
"ChangePassword": {
"One": "修改密碼"
},
"Userinfo": {
"One": "使用者資訊"
},
"ParamRequired": {
"One": "{param} 是必須的"
},
"HasBind": {
"One": "已繫結"
},
"NoBind": {
"One": "未繫結"
},
"UnBind": {
"One": "解綁"
},
"ToBind": {
"One": "繫結"
},
"Confirm?": {
"One": "確定{param}?"
},
"Cancel": {
"One": "取消"
},
"Platform": {
"One": "平臺"
},
"Status": {
"One": "狀態"
},
"Actions": {
"One": "操作"
},
"Filter": {
"One": "篩選"
},
"Add": {
"One": "新增"
},
"Hostname": {
"One": "主機名"
},
"Alias": {
"One": "別名"
},
"Hash": {
"One": "雜湊"
},
"Tags": {
"One": "標籤"
},
"Edit": {
"One": "編輯"
},
"Delete": {
"One": "刪除"
},
"Create": {
"One": "建立"
},
"Update": {
"One": "更新"
},
"LoginName": {
"One": "登入名"
},
"Submit": {
"One": "提交"
},
"OperationSuccess": {
"One": "操作成功"
},
"Owner": {
"One": "所有者"
},
"Name": {
"One": "名稱"
},
"Color": {
"One": "顏色"
},
"CreatedAt": {
"One": "建立時間"
},
"UpdatedAt": {
"One": "更新時間"
},
"Memory": {
"One": "記憶體"
},
"Os": {
"One": "作業系統"
},
"Uuid": {
"One": "UUID"
},
"Version": {
"One": "版本"
},
"Type": {
"One": "型別"
},
"Group": {
"One": "組"
},
"CommonGroup": {
"One": "普通組"
},
"CommonGroupNote": {
"One": "只有管理員才能檢視群組成員及其裝置"
},
"SharedGroup": {
"One": "共享組"
},
"SharedGroupNote": {
"One": "所有使用者都可以檢視群組成員及其裝置"
},
"Nickname": {
"One": "暱稱"
},
"UserTags": {
"One": "使用者標籤"
},
"UserAddressBook": {
"One": "使用者地址簿"
},
"IsAdmin": {
"One": "是否管理員"
},
"PleaseInputNewPassword": {
"One": "請輸入新密碼"
},
"AutoRegister": {
"One": "自動註冊"
},
"AutoRegisterNote": {
"One": "如果啟用,則當用戶使用 OAuth 登入時,將自動註冊一個帳戶,而無需繫結現有帳戶"
},
"ThirdName": {
"One": "第三方名稱"
},
"Close": {
"One": "關閉"
},
"OauthBinding": {
"One": "您正在授權繫結"
},
"OauthLogining": {
"One": "您正在授權登入"
},
"OauthCloseNote": {
"One": "如不是您發起的授權,請直接關閉頁面"
},
"OperationSuccessAndCloseAfter3Seconds": {
"One": "操作成功,3秒後將自動關閉頁面"
},
"ConfirmOauth": {
"One": "確認授權"
},
"Device": {
"One": "裝置"
},
"ChangeLang": {
"One": "ToEnglish"
},
"My": {
"One": "我的"
},
"Info": {
"One": "資訊"
},
"AddressBooks": {
"One": "地址簿"
},
"System": {
"One": "系統"
},
"PeerManage": {
"One": "裝置管理"
},
"AddressBookManage": {
"One": "地址簿管理"
},
"GroupManage": {
"One": "群組管理"
},
"UserManage": {
"One": "使用者管理"
},
"UserAdd": {
"One": "使用者新增"
},
"UserEdit": {
"One": "使用者編輯"
},
"TagsManage": {
"One": "標籤管理"
},
"OauthManage": {
"One": "Oauth 管理"
},
"LoginLog": {
"One": "登入日誌"
},
"LastOnlineTime": {
"One": "最後線上時間"
},
"JustNow": {
"One": "剛剛"
},
"MinutesAgo": {
"One": "{param} 分鐘前"
},
"HoursAgo": {
"One": "{param} 小時前"
},
"DaysAgo": {
"One": "{param} 天前"
},
"MonthsAgo": {
"One": "{param} 月前"
},
"YearsAgo": {
"One": "{param} 年前"
},
"MinutesLess": {
"One": "{param} 分鐘內"
},
"HoursLess": {
"One": "{param} 小時內"
},
"DaysLess": {
"One": "{param} 天內"
},
"Export": {
"One": "匯出"
},
"AddToAddressBook": {
"One": "新增到地址簿"
},
"BatchDelete": {
"One": "批量刪除"
},
"PleaseSelectData": {
"One": "請選擇資料"
},
"PasswordType": {
"One": "密碼型別"
},
"OncePassword": {
"One": "一次性密碼"
},
"FixedPassword": {
"One": "固定密碼"
},
"FixedPasswordWarning": {
"One": "固定密碼可能存在洩露風險,請謹慎使用,建議使用一次性密碼"
},
"ExpireTime": {
"One": "過期時間"
},
"ShareByWebClient": {
"One": "通過 Web Client 分享"
},
"Minutes": {
"One": "{param} 分鐘"
},
"Hours": {
"One": "{param} 小時"
},
"Days": {
"One": "{param} 天"
},
"Weeks": {
"One": "{param} 周"
},
"Months": {
"One": "{param} 月"
},
"Forever": {
"One": "永久"
},
"Error": {
"One": "錯誤"
},
"IDNotExist": {
"One": "ID 不存在"
},
"RemoteDesktopOffline": {
"One": "遠端電腦不線上"
},
"KeyMismatch": {
"One": "KEY 不匹配"
},
"KeyOveruse": {
"One": "KEY 使用過度"
},
"Link": {
"One": "連結"
},
"CopySuccess": {
"One": "複製成功"
},
"CopyFailed": {
"One": "複製失敗"
},
"Timeout": {
"One": "超時"
},
"AuditConnLog": {
"One": "連線日誌"
},
"Peer": {
"One": "裝置"
},
"FromPeer": {
"One": "來源裝置"
},
"FromName": {
"One": "來源名稱"
},
"CloseTime": {
"One": "關閉時間"
},
"AuditFileLog": {
"One": "檔案日誌"
},
"Common": {
"One": "普通"
},
"File": {
"One": "檔案"
},
"Num": {
"One": "數量"
},
"Ip": {
"One": "IP"
},
"FileName": {
"One": "檔名"
},
"FileInfo": {
"One": "檔案資訊"
},
"Path": {
"One": "路徑"
},
"IndexNum": {
"One": "序號"
},
"ToRemote": {
"One": "到遠端"
},
"ToLocal": {
"One": "到本地"
},
"AddressBookName": {
"One": "地址簿名稱"
},
"AddressBookCollection": {
"One": "地址簿集"
},
"AddRule": {
"One": "新增規則"
},
"ShareRules": {
"One": "分享規則"
},
"Rule": {
"One": "規則"
},
"Read": {
"One": "讀取"
},
"ReadWrite": {
"One": "讀寫"
},
"FullControl": {
"One": "完全控制"
},
"ShareTo": {
"One": "分享給"
},
"MyAddressBook": {
"One": "我的地址簿"
},
"AddressBook": {
"One": "地址簿"
},
"AddressBookNameManage": {
"One": "地址簿名稱"
},
"MyAddressBookTips": {
"One": "\"我的地址簿\"是系統預設,不可修改、刪除"
},
"LastOnlineIp": {
"One": "最後線上 IP"
},
"or login in with": {
"One": "或使用以下登陸"
},
"Optional, default is": {
"One": "可選, 預設值是"
},
"Check your IdP docs, without": {
"One": "檢查 IdP 的文件, 不包含"
},
"For OIDC login without a password, enter any 4-20 letters": {
"One": "如果通過 OIDC 登入且未設定密碼,請輸入任意 4-20 個字元"
},
"OldPassword": {
"One": "舊密碼"
},
"NewPassword": {
"One": "新密碼"
},
"NewPasswordEqualOldPassword": {
"One": "新密碼不能與舊密碼相同"
},
"ConfirmPassword": {
"One": "確認密碼"
},
"PasswordNotMatchConfirmPassword": {
"One": "密碼與確認密碼不匹配"
},
"ToLogin": {
"One": "去登入"
},
"UserToken": {
"One": "使用者 Token"
},
"Email": {
"One": "電子信箱"
},
"MyPeer": {
"One": "我的裝置"
},
"View": {
"One": "檢視"
},
"Information": {
"One": "資訊"
},
"BatchAddToAB": {
"One": "批量新增到地址簿"
},
"BatchEditTags": {
"One": "批量編輯標籤"
},
"User": {
"One": "使用者"
},
"ShareRecord": {
"One": "分享記錄"
},
"Second": {
"One": "秒"
},
"Captcha": {
"One": "驗證碼"
},
"ServerCmd": {
"One": "服務端命令"
},
"Simple": {
"One": "簡單"
},
"Advanced": {
"One": "高階"
},
"Available": {
"One": "可用"
},
"NotAvailable": {
"One": "不可用"
},
"ServerCmdTips": {
"One": "此處功能為實驗性質,請確保 API 和 hbbs/hbbr 處於同一迴環地址下,更多說明請參考 {wiki}"
},
"Refresh": {
"One": "重新整理"
},
"Save": {
"One": "儲存"
},
"SendCustom": {
"One": "傳送自定義"
},
"Send": {
"One": "傳送"
},
"SendCmd": {
"One": "傳送命令"
},
"Result": {
"One": "結果"
},
"DeviceGroupManage": {
"One": "設備組管理"
},
"Import": {
"One": "導入"
},
"Drop file here or click to upload": {
"One": "將檔案拖到這裡,或點擊上傳"
},
"Please upload csv file": {
"One": "請上傳 csv 檔案"
},
"Columns": {
"One": "欄位"
},
"You can reference export file": {
"One": "您可以參考匯出檔案"
},
"Remark": {
"One": "備註"
}
}
-4272
Voir le fichier
Fichier diff supprimé car celui-ci est trop grand Voir la Diff
+10 -28
Voir le fichier
@@ -1,40 +1,22 @@
import { ref } from 'vue'
import { server } from '@/api/config'
import Websock from '@/utils/webclient/websock' import Websock from '@/utils/webclient/websock'
import * as rendezvous from '@/utils/webclient/rendezvous' import * as rendezvous from '@/utils/webclient/rendezvous'
import * as message from '@/utils/webclient/message' import * as message from '@/utils/webclient/message'
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import { T } from '@/utils/i18n' import { T } from '@/utils/i18n'
import { useAppStore } from '@/store/app'
const prefix = 'wc-'
const app = useAppStore()
export const toWebClientLink = (row) => { export const toWebClientLink = (row) => {
//v2 //v2
window.open(`${rustdeskConfig.value.api_server}/webclient2/#/${row.id}`) console.log(app.setting.rustdeskConfig)
window.open(`${app.setting.rustdeskConfig.api_server}/webclient2/#/${row.id}`)
} }
export const rustdeskConfig = ref({})
export async function loadRustdeskConfig () {
console.log('loadRustdeskConfig')
if (rustdeskConfig.value.id_server === undefined || rustdeskConfig.value.key === undefined) {
const res = await server().catch(_ => false)
if (res) {
rustdeskConfig.value = res.data
localStorage.setItem(`${prefix}custom-rendezvous-server`, res.data.id_server)
localStorage.setItem(`${prefix}key`, res.data.key)
localStorage.setItem(`${prefix}api-server`, res.data.api_server)
}
}
return {
rustdeskConfig,
}
}
loadRustdeskConfig()
export async function getPeerSlat (id) { export async function getPeerSlat (id) {
const [addr, port] = rustdeskConfig.value.id_server.split(':') const [addr, port] = app.setting.rustdeskConfig.id_server.split(':')
if (!addr) { if (!addr) {
return return
} }
@@ -45,7 +27,7 @@ export async function getPeerSlat (id) {
const nat_type = rendezvous.NatType.SYMMETRIC const nat_type = rendezvous.NatType.SYMMETRIC
const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({ const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({
id, id,
licence_key: rustdeskConfig.value.key || undefined, licence_key: app.setting.rustdeskConfig.value.key || undefined,
conn_type, conn_type,
nat_type, nat_type,
token: undefined, token: undefined,
@@ -87,7 +69,7 @@ export async function getPeerSlat (id) {
await _ws.open() await _ws.open()
console.log(new Date() + ': Connected to relay server') console.log(new Date() + ': Connected to relay server')
const request_relay = rendezvous.RequestRelay.fromPartial({ const request_relay = rendezvous.RequestRelay.fromPartial({
licence_key: rustdeskConfig.value.key || undefined, licence_key: app.setting.rustdeskConfig.key || undefined,
uuid, uuid,
}) })
_ws.sendRendezvous({ request_relay }) _ws.sendRendezvous({ request_relay })
@@ -112,5 +94,5 @@ export async function getPeerSlat (id) {
} }
export function getV2ShareUrl (token) { export function getV2ShareUrl (token) {
return `${rustdeskConfig.value.api_server}/webclient2/#/?share_token=${token}` return `${app.setting.rustdeskConfig.api_server}/webclient2/#/?share_token=${token}`
} }
+52 -8
Voir le fichier
@@ -59,13 +59,20 @@ export function useRepositories (api_type = 'my') {
{ label: T('ReadWrite'), value: 2 }, { label: T('ReadWrite'), value: 2 },
{ label: T('FullControl'), value: 3 }, { label: T('FullControl'), value: 3 },
]) ])
const TYPE_U = 1
const TYPE_G = 2
const types = computed(_ => [
{ label: T('Group'), value: TYPE_G },
{ label: T('User'), value: TYPE_U },
])
const formVisible = ref(false) const formVisible = ref(false)
const formData = reactive({ const formData = reactive({
id: 0, id: 0,
collection_id: null, collection_id: null,
type: 1, type: TYPE_U,
rule: 1, rule: 1,
g_id: null,
u_id: null,
to_id: null, to_id: null,
user_id: null, user_id: null,
}) })
@@ -76,28 +83,60 @@ export function useRepositories (api_type = 'my') {
Object.keys(formData).forEach(key => { Object.keys(formData).forEach(key => {
formData[key] = row[key] formData[key] = row[key]
}) })
if (row.type === TYPE_U) {
formData.u_id = row.to_id
formData.g_id = users.value.find(u => u.id === row.to_id)?.group_id
} else {
formData.g_id = row.to_id
formData.u_id = null
}
} }
const toAdd = () => { const toAdd = () => {
//初始化formData
formData.id = 0
formData.type = TYPE_U
formData.rule = 1
formData.g_id = null
formData.u_id = null
formVisible.value = true formVisible.value = true
} }
const submit = async () => { const submit = async () => {
const api = formData.id ? apis[api_type].update : apis[api_type].create const api = formData.id ? apis[api_type].update : apis[api_type].create
const res = await api(formData).catch(_ => false) const form = {
...formData,
}
form.to_id = form.type === TYPE_G ? form.g_id : form.u_id
const res = await api(form).catch(_ => false)
if (res) { if (res) {
ElMessage.success(T('OperationSuccess')) ElMessage.success(T('OperationSuccess'))
formVisible.value = false formVisible.value = false
getList() getList()
} }
} }
const groupUsersList = ref([]) const groups = ref([])
const users = ref([])
const getGroupUsers = async () => { const getGroupUsers = async () => {
const res = await groupUsers({ user_id: formData.user_id }).catch(_ => false) const res = await groupUsers().catch(_ => false)
if (res) { if (res) {
groupUsersList.value = res.data groups.value = res.data.groups.map(item => {
if (!item.children) {
item.children = []
}
res.data.users.map(u => {
if (item.id === u.group_id) {
item.children.push(u)
}
})
return item
})
users.value = res.data.users
} }
} }
const changeGId = () => {
formData.u_id = null
}
return { return {
listRes, listRes,
listQuery, listQuery,
@@ -110,7 +149,12 @@ export function useRepositories (api_type = 'my') {
toAdd, toAdd,
submit, submit,
rules, rules,
groupUsersList, types,
groups,
users,
getGroupUsers, getGroupUsers,
TYPE_G,
TYPE_U,
changeGId,
} }
} }
+47 -14
Voir le fichier
@@ -17,10 +17,20 @@
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="type" :label="T('Type')" align="center">
<template #default="{row}">
<div>
{{ types.find(t => t.value === row.type)?.label }}
</div>
</template>
</el-table-column>
<el-table-column prop="to_id" :label="T('ShareTo')" align="center"> <el-table-column prop="to_id" :label="T('ShareTo')" align="center">
<template #default="{row}"> <template #default="{row}">
<div v-if="row.type===1"> <div v-if="row.type===TYPE_U">
{{ groupUsersList.find(u => u.id === row.to_id)?.username }} {{ users.find(u => u.id === row.to_id)?.username }}
</div>
<div v-else-if="row.type===TYPE_G">
{{ groups.find(g => g.id === row.to_id)?.name }}
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -50,22 +60,40 @@
</el-form-item> </el-form-item>
<el-form-item :label="T('Rule')" prop="rule" required> <el-form-item :label="T('Rule')" prop="rule" required>
<el-radio-group v-model="formData.rule"> <el-radio-group v-model="formData.rule">
<el-radio v-for="item in rules" :key="item.value" :label="item.value"> <el-radio v-for="item in rules" :key="item.value" :value="item.value">
{{ item.label }} {{ item.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item :label="T('ShareTo')" prop="to_id" required> <el-form-item :label="T('Type')" prop="type" required>
<el-radio-group v-model="formData.type">
<el-radio v-for="item in types" :key="item.value" :value="parseInt(item.value)">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="T('ShareTo')" prop="g_id" required>
<!-- <el-input-number v-model="formData.to_id"></el-input-number>--> <!-- <el-input-number v-model="formData.to_id"></el-input-number>-->
<el-select v-model="formData.to_id"> <div style="width: 30%">
<el-option <el-select v-model="formData.g_id" @change="changeGId">
v-for="item in groupUsersList" <el-option
:key="item.id" v-for="item in groups"
:label="item.username" :key="item.id"
:value="item.id" :label="item.name"
:disabled="!item.status" :value="item.id"
></el-option> ></el-option>
</el-select> </el-select>
</div>
<div style="width: 30%;margin-left: 20px">
<el-select v-model="formData.u_id" v-if="formData.type===TYPE_U">
<el-option
v-for="item in users.filter(u => u.group_id === formData.g_id)"
:key="item.id"
:label="item.username"
:value="item.id"
></el-option>
</el-select>
</div>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="formVisible = false">{{ T('Cancel') }}</el-button> <el-button @click="formVisible = false">{{ T('Cancel') }}</el-button>
@@ -104,8 +132,13 @@
toAdd, toAdd,
submit, submit,
rules, rules,
groupUsersList, types,
groups,
users,
getGroupUsers, getGroupUsers,
TYPE_G,
TYPE_U,
changeGId,
} = useRepositories(props.is_my ? 'my' : 'admin') } = useRepositories(props.is_my ? 'my' : 'admin')
formData.collection_id = props.collection.id formData.collection_id = props.collection.id
+2
Voir le fichier
@@ -11,6 +11,7 @@
<el-form-item> <el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button> <el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
<el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button> <el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button>
<el-button type="success" @click="toExport">{{ T('Export') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@@ -62,6 +63,7 @@
handlerQuery, handlerQuery,
del, del,
batchdel, batchdel,
toExport,
} = useRepositories() } = useRepositories()
onMounted(getList) onMounted(getList)
+2
Voir le fichier
@@ -11,6 +11,7 @@
<el-form-item> <el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button> <el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
<el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button> <el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button>
<el-button type="success" @click="toExport">{{ T('Export') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@@ -109,6 +110,7 @@
handlerQuery, handlerQuery,
del, del,
batchdel, batchdel,
toExport,
} = useFileRepositories() } = useFileRepositories()
onMounted(getList) onMounted(getList)
+26 -1
Voir le fichier
@@ -1,9 +1,9 @@
import { reactive } from 'vue' import { reactive } from 'vue'
import { list, remove, fileList, fileRemove, batchDelete, fileBatchDelete } from '@/api/audit' import { list, remove, fileList, fileRemove, batchDelete, fileBatchDelete } from '@/api/audit'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute } from 'vue-router'
import { formatTime } from '@/utils/time' import { formatTime } from '@/utils/time'
import { T } from '@/utils/i18n' import { T } from '@/utils/i18n'
import { downBlob, jsonToCsv } from '@/utils/file'
export function useRepositories () { export function useRepositories () {
const listRes = reactive({ const listRes = reactive({
@@ -73,6 +73,17 @@ export function useRepositories () {
getList() getList()
} }
} }
const toExport = async () => {
const q = { ...listQuery }
q.page_size = 1000000
q.page = 1
const res = await list(q).catch(_ => false)
if (res) {
const csv = jsonToCsv(res.data.list)
downBlob(csv, 'connectLog.csv')
}
}
return { return {
listRes, listRes,
listQuery, listQuery,
@@ -80,6 +91,7 @@ export function useRepositories () {
handlerQuery, handlerQuery,
del, del,
batchdel, batchdel,
toExport,
} }
} }
@@ -151,6 +163,18 @@ export function useFileRepositories () {
getList() getList()
} }
} }
const toExport = async () => {
const q = { ...listQuery }
q.page_size = 1000000
q.page = 1
const res = await fileList(q).catch(_ => false)
if (res) {
const csv = jsonToCsv(res.data.list)
downBlob(csv, 'fileTransformLog.csv')
}
}
return { return {
listRes, listRes,
listQuery, listQuery,
@@ -158,5 +182,6 @@ export function useFileRepositories () {
handlerQuery, handlerQuery,
del, del,
batchdel, batchdel,
toExport,
} }
} }
+138
Voir le fichier
@@ -0,0 +1,138 @@
<template>
<div>
<el-card class="list-query" shadow="hover">
<el-form inline label-width="80px">
<!-- <el-form-item label="名称">
<el-input v-model="listQuery.name"></el-input>
</el-form-item>-->
<el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
<el-button type="danger" @click="toAdd">{{ T('Add') }}</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<el-table :data="listRes.list" v-loading="listRes.loading" border>
<el-table-column prop="id" label="ID" align="center"></el-table-column>
<el-table-column prop="name" :label="T('Name')" align="center"/>
<el-table-column prop="created_at" :label="T('CreatedAt')" align="center"/>
<el-table-column prop="updated_at" :label="T('UpdatedAt')" align="center"/>
<el-table-column :label="T('Actions')" align="center">
<template #default="{row}">
<el-button @click="toEdit(row)">{{ T('Edit') }}</el-button>
<el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="list-page" shadow="hover">
<el-pagination background
layout="prev, pager, next, sizes, jumper"
:page-sizes="[10,20,50,100]"
v-model:page-size="listQuery.page_size"
v-model:current-page="listQuery.page"
:total="listRes.total">
</el-pagination>
</el-card>
<el-dialog v-model="formVisible" :title="!formData.id?T('Create'):T('Update')" width="800">
<el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
<el-form-item :label="T('Name')" prop="name" required>
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="formVisible = false">{{ T('Cancel') }}</el-button>
<el-button @click="submit" type="primary">{{ T('Submit') }}</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, reactive, watch, ref, onActivated } from 'vue'
import { list, create, update, detail, remove } from '@/api/device_group'
import { ElMessage, ElMessageBox } from 'element-plus'
import { T } from '@/utils/i18n'
const listRes = reactive({
list: [], total: 0, loading: false,
})
const listQuery = reactive({
page: 1,
page_size: 10,
})
const getList = async () => {
listRes.loading = true
const res = await list(listQuery).catch(_ => false)
listRes.loading = false
if (res) {
listRes.list = res.data.list
listRes.total = res.data.total
}
}
const handlerQuery = () => {
if (listQuery.page === 1) {
getList()
} else {
listQuery.page = 1
}
}
const del = async (row) => {
const cf = await ElMessageBox.confirm(T('Confirm?', { param: T('Delete') }), {
confirmButtonText: T('Confirm'),
cancelButtonText: T('Cancel'),
type: 'warning',
}).catch(_ => false)
if (!cf) {
return false
}
const res = await remove({ id: row.id }).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
getList()
}
}
onMounted(getList)
onActivated(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
const formVisible = ref(false)
const formData = reactive({
id: 0,
name: '',
type: 1,
})
const toEdit = (row) => {
formVisible.value = true
formData.id = row.id
formData.name = row.name
formData.type = row.type
}
const toAdd = () => {
formVisible.value = true
formData.id = 0
formData.name = ''
formData.type = 1
}
const submit = async () => {
const api = formData.id ? update : create
const res = await api(formData).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
formVisible.value = false
getList()
}
}
</script>
<style scoped lang="scss">
</style>
+45 -18
Voir le fichier
@@ -1,13 +1,18 @@
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
import { batchDelete, list, remove } from '@/api/login_log' import { list as admin_fetchPeers } from '@/api/peer'
import { list as fetchPeers } from '@/api/peer' import { list as my_fetchPeers } from '@/api/my/peer'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute } from 'vue-router'
import { T } from '@/utils/i18n' import { T } from '@/utils/i18n'
import { batchDelete as admin_batchDelete, list as admin_list, remove as admin_remove } from '@/api/login_log'
import { batchDelete as my_batchDelete, list as my_list, remove as my_remove } from '@/api/my/login_log'
import { downBlob, jsonToCsv } from '@/utils/file'
export function useRepositories () { const apis = {
const route = useRoute() admin: { batchDelete: admin_batchDelete, list: admin_list, remove: admin_remove, fetchPeers: admin_fetchPeers },
const user_id = route.query?.user_id my: { batchDelete: my_batchDelete, list: my_list, remove: my_remove, fetchPeers: my_fetchPeers },
}
export function useRepositories (api_type = 'my') {
const listRes = reactive({ const listRes = reactive({
list: [], total: 0, loading: false, list: [], total: 0, loading: false,
@@ -16,23 +21,29 @@ export function useRepositories () {
page: 1, page: 1,
page_size: 10, page_size: 10,
is_my: 0, is_my: 0,
user_id: user_id ? parseInt(user_id) : null, user_id: null,
}) })
const getList = async () => { const getList = async () => {
listRes.loading = true listRes.loading = true
const res = await list(listQuery).catch(_ => false) const res = await apis[api_type].list(listQuery).catch(_ => false)
listRes.loading = false listRes.loading = false
if (res) { if (res) {
const uuids = res.data.list.filter(item => item.uuid).map(item => item.uuid) //通过uuid补全peer信息
const peers = await fetchPeers({ uuids }).catch(_ => false) const uuids = res.data.list.filter(item => item.uuid && item.client === 'client' && !item.device_id).map(item => item.uuid)
if (peers?.data?.list) { if (uuids.length > 0) {
res.data.list.forEach(item => { //uuids去重
if (item.uuid) { const uniqueUuids = [...new Set(uuids)]
item.peer = peers.data.list.find(peer => peer.uuid === item.uuid) const peers = await apis[api_type].fetchPeers({ uuids: uniqueUuids }).catch(_ => false)
} if (peers?.data?.list) {
}) res.data.list.forEach(item => {
if (item.uuid) {
item.peer = peers.data.list.find(peer => peer.uuid === item.uuid)
}
})
}
} }
listRes.list = res.data.list listRes.list = res.data.list
listRes.total = res.data.total listRes.total = res.data.total
} }
@@ -55,7 +66,7 @@ export function useRepositories () {
return false return false
} }
const res = await remove({ id: row.id }).catch(_ => false) const res = await apis[api_type].remove({ id: row.id }).catch(_ => false)
if (res) { if (res) {
ElMessage.success(T('OperationSuccess')) ElMessage.success(T('OperationSuccess'))
getList() getList()
@@ -77,13 +88,28 @@ export function useRepositories () {
return false return false
} }
const res = await batchDelete({ ids }).catch(_ => false) const res = await apis[api_type].batchDelete({ ids }).catch(_ => false)
if (res) { if (res) {
ElMessage.success(T('OperationSuccess')) ElMessage.success(T('OperationSuccess'))
getList() getList()
} }
} }
// only Admin
const toExport = async () => {
if (api_type !== 'admin') {
return false
}
const q = { ...listQuery }
q.page_size = 1000000
q.page = 1
const res = await admin_list(q).catch(_ => false)
if (res) {
const csv = jsonToCsv(res.data.list)
downBlob(csv, 'loginLog.csv')
}
}
return { return {
listRes, listRes,
listQuery, listQuery,
@@ -91,5 +117,6 @@ export function useRepositories () {
handlerQuery, handlerQuery,
del, del,
batchdel, batchdel,
toExport,
} }
} }
+14 -5
Voir le fichier
@@ -13,8 +13,9 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter')}}</el-button> <el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
<el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button> <el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button>
<el-button type="success" @click="toExport">{{ T('Export') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@@ -28,15 +29,19 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="client" label="client" align="center" width="120"/> <el-table-column prop="client" label="client" align="center" width="120"/>
<el-table-column prop="peer.id" :label="T('Peer')" align="center"/> <el-table-column prop="peer.id" :label="T('Peer')" align="center">
<template #default="{row}">
{{ row.device_id ? row.device_id : peer?.id }}
</template>
</el-table-column>
<el-table-column prop="uuid" label="uuid" align="center"/> <el-table-column prop="uuid" label="uuid" align="center"/>
<el-table-column prop="ip" label="ip" align="center" width="150"/> <el-table-column prop="ip" label="ip" align="center" width="150"/>
<el-table-column prop="type" label="type" align="center" width="100"/> <el-table-column prop="type" label="type" align="center" width="100"/>
<el-table-column prop="platform" label="platform" align="center" width="120" show-overflow-tooltip/> <el-table-column prop="platform" label="Platform/UA" align="center" width="120" show-overflow-tooltip/>
<el-table-column prop="created_at" :label="T('CreatedAt')" align="center"/> <el-table-column prop="created_at" :label="T('CreatedAt')" align="center"/>
<el-table-column :label="T('Actions')" align="center" width="400"> <el-table-column :label="T('Actions')" align="center" width="400">
<template #default="{row}"> <template #default="{row}">
<el-button type="danger" @click="del(row)">{{T('Delete')}}</el-button> <el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -58,6 +63,8 @@
import { loadAllUsers } from '@/global' import { loadAllUsers } from '@/global'
import { useRepositories } from '@/views/login/log.js' import { useRepositories } from '@/views/login/log.js'
import { T } from '@/utils/i18n' import { T } from '@/utils/i18n'
import { list } from '@/api/peer'
import { downBlob, jsonToCsv } from '@/utils/file'
const { allUsers, getAllUsers } = loadAllUsers() const { allUsers, getAllUsers } = loadAllUsers()
getAllUsers() getAllUsers()
@@ -69,7 +76,8 @@
handlerQuery, handlerQuery,
del, del,
batchdel, batchdel,
} = useRepositories() toExport,
} = useRepositories('admin')
onMounted(getList) onMounted(getList)
onActivated(getList) onActivated(getList)
@@ -87,6 +95,7 @@
} }
batchdel(multipleSelection.value) batchdel(multipleSelection.value)
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
+13 -4
Voir le fichier
@@ -3,9 +3,9 @@
<div class="login-card"> <div class="login-card">
<img src="@/assets/logo.png" alt="logo" class="login-logo"/> <img src="@/assets/logo.png" alt="logo" class="login-logo"/>
<el-form label-position="top" class="login-form"> <el-form v-if="!disablePwd" label-position="top" class="login-form">
<el-form-item :label="T('Username')"> <el-form-item :label="T('Username')">
<el-input v-model="form.username" class="login-input"></el-input> <el-input v-model="form.username" type="username" class="login-input"></el-input>
</el-form-item> </el-form-item>
<el-form-item :label="T('Password')"> <el-form-item :label="T('Password')">
@@ -25,7 +25,7 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="divider" v-if="options.length > 0"> <div class="divider" v-if="options.length > 0 && !disablePwd">
<span>{{ T('or login in with') }}</span> <span>{{ T('or login in with') }}</span>
</div> </div>
@@ -78,6 +78,7 @@
password: '', password: '',
platform: platform, platform: platform,
captcha: '', captcha: '',
captcha_id: ''
}) })
const captchaCode = ref('') const captchaCode = ref('')
@@ -87,6 +88,7 @@
if (!res.code) { if (!res.code) {
ElMessage.success(T('LoginSuccess')) ElMessage.success(T('LoginSuccess'))
router.push({ path: redirect || '/', replace: true }) router.push({ path: redirect || '/', replace: true })
return
} }
if (res.code === 110) { if (res.code === 110) {
// need captcha // need captcha
@@ -98,6 +100,7 @@
const captchaRes = await captcha().catch(_ => false) const captchaRes = await captcha().catch(_ => false)
console.log(captchaRes) console.log(captchaRes)
captchaCode.value = captchaRes.data.captcha captchaCode.value = captchaRes.data.captcha
form.captcha_id = captchaRes.data.captcha.id
} }
const handleOIDCLogin = (provider) => { const handleOIDCLogin = (provider) => {
@@ -123,11 +126,17 @@
} }
const allowRegister = ref(false) const allowRegister = ref(false)
const disablePwd = ref(false)
const loadLoginOptions = async () => { const loadLoginOptions = async () => {
try { try {
const res = await loginOptions().catch(_ => false) const res = await loginOptions().catch(_ => false)
if (!res || !res.data) return console.error('No valid response received') if (!res || !res.data) return console.error('No valid response received')
res.data.ops.map(option => (options.push({ name: option }))) // 创建新的对象数组 res.data.ops.map(option => (options.push({ name: option }))) // 创建新的对象数组
if (res.data.auto_oidc) {
// 如果有自动OIDC登录选项,直接调用第一个
handleOIDCLogin(res.data.ops[0])
}
disablePwd.value = res.data.disable_pwd
allowRegister.value = res.data.register allowRegister.value = res.data.register
if (res.data.need_captcha) { if (res.data.need_captcha) {
loadCaptcha() loadCaptcha()
@@ -285,4 +294,4 @@ h1 {
} }
} }
} }
</style> </style>
+8 -4
Voir le fichier
@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<el-card :title="T('Userinfo')" shadow="hover"> <el-card :title="T('Userinfo')" shadow="hover">
<el-form class="info-form" ref="form" label-width="120px" label-suffix="" > <el-form class="info-form" ref="form" label-width="120px" label-suffix="">
<el-form-item :label="T('Username')"> <el-form-item :label="T('Username')">
<div>{{ userStore.username }}</div> <div>{{ userStore.username }}</div>
</el-form-item> </el-form-item>
@@ -31,7 +31,7 @@
</el-form> </el-form>
</el-card> </el-card>
<el-card shadow="hover" style="margin-top: 20px"> <el-card shadow="hover" style="margin-top: 20px">
<div v-html="appStore.setting.hello"></div> <div v-html="html"></div>
</el-card> </el-card>
<changePwdDialog v-model:visible="changePwdVisible"></changePwdDialog> <changePwdDialog v-model:visible="changePwdVisible"></changePwdDialog>
</div> </div>
@@ -39,13 +39,14 @@
<script setup> <script setup>
import changePwdDialog from '@/components/changePwdDialog.vue' import changePwdDialog from '@/components/changePwdDialog.vue'
import { ref } from 'vue' import { computed, ref } from 'vue'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import { bind, unbind } from '@/api/oauth' import { bind, unbind } from '@/api/oauth'
import { myOauth } from '@/api/user' import { myOauth } from '@/api/user'
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import { T } from '@/utils/i18n' import { T } from '@/utils/i18n'
import { marked } from 'marked'
const appStore = useAppStore() const appStore = useAppStore()
const userStore = useUserStore() const userStore = useUserStore()
@@ -63,7 +64,7 @@
} }
getMyOauth() getMyOauth()
const toBind = async (row) => { const toBind = async (row) => {
const res = await bind({ op: row.op}).catch(_ => false) const res = await bind({ op: row.op }).catch(_ => false)
if (res) { if (res) {
const { code, url } = res.data const { code, url } = res.data
window.open(url) window.open(url)
@@ -84,6 +85,9 @@
} }
} }
const html = computed(_ => marked(appStore.setting.hello||''))
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
+82
Voir le fichier
@@ -0,0 +1,82 @@
<template>
<div>
<el-card class="list-query" shadow="hover">
<el-form inline label-width="80px">
<el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
<el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<el-table :data="listRes.list" v-loading="listRes.loading" border @selection-change="handleSelectionChange">
<el-table-column type="selection" align="center" width="50"/>
<el-table-column prop="client" label="client" align="center" width="120"/>
<el-table-column prop="peer.id" :label="T('Peer')" align="center">
<template #default="{row}">
{{ row.device_id ? row.device_id : peer?.id }}
</template>
</el-table-column>
<el-table-column prop="uuid" label="uuid" align="center"/>
<el-table-column prop="ip" label="ip" align="center" width="150"/>
<el-table-column prop="type" label="type" align="center" width="100"/>
<el-table-column prop="platform" label="Platform/UA" align="center" width="120" show-overflow-tooltip/>
<el-table-column prop="created_at" :label="T('CreatedAt')" align="center"/>
<el-table-column :label="T('Actions')" align="center" width="400">
<template #default="{row}">
<el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="list-page" shadow="hover">
<el-pagination background
layout="prev, pager, next, sizes, jumper"
:page-sizes="[10,20,50,100]"
v-model:page-size="listQuery.page_size"
v-model:current-page="listQuery.page"
:total="listRes.total">
</el-pagination>
</el-card>
</div>
</template>
<script setup>
import { onActivated, onMounted, ref, watch } from 'vue'
import { useRepositories } from '@/views/login/log.js'
import { T } from '@/utils/i18n'
const {
listRes,
listQuery,
getList,
handlerQuery,
del,
batchdel,
} = useRepositories('my')
onMounted(getList)
onActivated(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
const multipleSelection = ref([])
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
const toBatchDelete = () => {
if (multipleSelection.value.length === 0) {
return
}
batchdel(multipleSelection.value)
}
</script>
<style scoped lang="scss">
.list-query .el-select {
--el-select-width: 160px;
}
</style>
+1
Voir le fichier
@@ -51,6 +51,7 @@
<el-table-column prop="username" :label="T('Username')" align="center" width="120"/> <el-table-column prop="username" :label="T('Username')" align="center" width="120"/>
<el-table-column prop="uuid" :label="T('Uuid')" align="center" width="120" show-overflow-tooltip/> <el-table-column prop="uuid" :label="T('Uuid')" align="center" width="120" show-overflow-tooltip/>
<el-table-column prop="version" :label="T('Version')" align="center" width="80"/> <el-table-column prop="version" :label="T('Version')" align="center" width="80"/>
<el-table-column prop="alias" :label="T('Alias')" align="center" width="80"/>
<el-table-column prop="created_at" :label="T('CreatedAt')" align="center" width="150"/> <el-table-column prop="created_at" :label="T('CreatedAt')" align="center" width="150"/>
<el-table-column prop="updated_at" :label="T('UpdatedAt')" align="center" width="150"/> <el-table-column prop="updated_at" :label="T('UpdatedAt')" align="center" width="150"/>
<el-table-column :label="T('Actions')" align="center" width="500" class-name="table-actions" fixed="right"> <el-table-column :label="T('Actions')" align="center" width="500" class-name="table-actions" fixed="right">
+67 -9
Voir le fichier
@@ -14,6 +14,8 @@
<el-table-column prop="op" :label="T('IdP')" align="center"/> <el-table-column prop="op" :label="T('IdP')" align="center"/>
<el-table-column prop="oauth_type" :label="T('Type')" align="center"/> <el-table-column prop="oauth_type" :label="T('Type')" align="center"/>
<el-table-column prop="auto_register" :label="T('AutoRegister')" align="center"/> <el-table-column prop="auto_register" :label="T('AutoRegister')" align="center"/>
<el-table-column prop="pkce_enable" :label="T('PkceEnable')" align="center"/>
<el-table-column prop="pkce_method" :label="T('PkceMethod')" align="center"/>
<el-table-column prop="created_at" :label="T('CreatedAt')" align="center"/> <el-table-column prop="created_at" :label="T('CreatedAt')" align="center"/>
<el-table-column prop="updated_at" :label="T('UpdatedAt')" align="center"/> <el-table-column prop="updated_at" :label="T('UpdatedAt')" align="center"/>
<el-table-column :label="T('Actions')" align="center"> <el-table-column :label="T('Actions')" align="center">
@@ -35,7 +37,7 @@
</el-card> </el-card>
<el-dialog v-model="formVisible" :title="!formData.id?T('Create') :T('Update')" width="800"> <el-dialog v-model="formVisible" :title="!formData.id?T('Create') :T('Update')" width="800">
<el-form class="dialog-form" ref="form" :model="formData" :rules="rules" label-width="120px"> <el-form class="dialog-form" ref="form" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="Type" prop=""> <el-form-item label="Type" prop="oauth_type">
<el-radio-group v-model="formData.oauth_type" :disabled="!!formData.id"> <el-radio-group v-model="formData.oauth_type" :disabled="!!formData.id">
<el-radio v-for="item in types" :key="item.value" :value="item.value" style="display: block"> <el-radio v-for="item in types" :key="item.value" :value="item.value" style="display: block">
{{ item.label }} {{ item.label }}
@@ -49,16 +51,38 @@
<el-input v-model="formData.issuer" :placeholder="`${T('Check your IdP docs, without')} '/.well-known/openid-configuration'`"></el-input> <el-input v-model="formData.issuer" :placeholder="`${T('Check your IdP docs, without')} '/.well-known/openid-configuration'`"></el-input>
</el-form-item> </el-form-item>
<el-form-item v-show="formData.oauth_type === 'oidc'" label="Scopes" prop="scopes"> <el-form-item v-show="formData.oauth_type === 'oidc'" label="Scopes" prop="scopes">
<el-input v-model="formData.scopes" :placeholder="`${T('Optional, default is')} 'openid,profile,email'`" ></el-input> <el-input v-model="formData.scopes" :placeholder="`${T('Optional, default is')} 'openid,profile,email'`"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="ClientId" prop="client_id"> <el-form-item label="ClientId" prop="client_id">
<el-input v-model="formData.client_id"></el-input> <el-input v-model="formData.client_id"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="ClientSecret" prop="client_secret"> <el-form-item label="ClientSecret" prop="client_secret">
<el-input v-model="formData.client_secret"></el-input> <el-input
v-model="formData.client_secret"
:type="formData.id ? 'password' : 'text'"
:show-password="!formData.id"
>
</el-input>
</el-form-item> </el-form-item>
<el-form-item label="RedirectUrl" prop="redirect_url"> <el-form-item label="RedirectUrl" prop="redirect_url">
<el-input v-model="formData.redirect_url"></el-input> <div @click="copyRedirectUrl">{{ defaultRedirect() }}
<el-icon>
<CopyDocument></CopyDocument>
</el-icon>
</div>
</el-form-item>
<el-form-item label="PkceEnable" prop="pkce_enable">
<el-switch v-model="formData.pkce_enable"
:active-value="true"
:inactive-value="false">
</el-switch>
</el-form-item>
<el-form-item v-if="formData.pkce_enable" label="PkceMethod" prop="pkce_method">
<el-select v-model="formData.pkce_method" placeholder="Select PKCE Method">
<el-option label="S256 (Recommended)" value="S256"></el-option>
<el-option label="Plain" value="plain"></el-option>
</el-select>
</el-form-item> </el-form-item>
<el-form-item :label="T('AutoRegister')" prop="auto_register"> <el-form-item :label="T('AutoRegister')" prop="auto_register">
<el-switch v-model="formData.auto_register" <el-switch v-model="formData.auto_register"
@@ -81,6 +105,15 @@
import { list, create, update, detail, remove } from '@/api/oauth' import { list, create, update, detail, remove } from '@/api/oauth'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { T } from '@/utils/i18n' import { T } from '@/utils/i18n'
import { handleClipboard } from '@/utils/clipboard'
import { useAppStore } from '@/store/app'
import { CopyDocument } from '@element-plus/icons'
const app = useAppStore()
const copyRedirectUrl = (e) => {
handleClipboard(defaultRedirect(), e)
}
const listRes = reactive({ const listRes = reactive({
list: [], total: 0, loading: false, list: [], total: 0, loading: false,
@@ -92,7 +125,8 @@
const types = [ const types = [
{ value: 'github', label: 'GitHub' }, { value: 'github', label: 'GitHub' },
{ value: 'google', label: 'Google' }, { value: 'google', label: 'Google' },
{ value: 'oidc', label: 'OIDC' } { value: 'linuxdo', label: 'LinuxDo' },
{ value: 'oidc', label: 'OIDC' },
] ]
const getList = async () => { const getList = async () => {
listRes.loading = true listRes.loading = true
@@ -145,14 +179,35 @@
redirect_url: '', redirect_url: '',
scopes: '', scopes: '',
auto_register: false, auto_register: false,
pkce_enable: false,
pkce_method: 'S256',
}) })
const rules = { const rules = {
client_id: [{ required: true, message: T('ParamRequired', { param: 'client_id' }), trigger: 'blur' }], client_id: [{ required: true, message: T('ParamRequired', { param: 'client_id' }), trigger: 'blur' }],
client_secret: [{ required: true, message: T('ParamRequired', { param: 'client_secret' }), trigger: 'blur' }], client_secret: [{ required: true, message: T('ParamRequired', { param: 'client_secret' }), trigger: 'blur' }],
redirect_url: [{ required: true, message: T('ParamRequired', { param: 'redirect_url' }), trigger: 'blur' }], // redirect_url: [{ required: true, message: T('ParamRequired', { param: 'redirect_url' }), trigger: 'blur' }],
oauth_type: [{ required: true, message: T('ParamRequired', { param: 'oauth_type' }), trigger: 'blur' }], oauth_type: [{ required: true, message: T('ParamRequired', { param: 'oauth_type' }), trigger: 'blur' }],
issuer: [{ required: true, message: T('ParamRequired', { param: 'issuer' }), trigger: 'blur' }], issuer: [{ required: true, message: T('ParamRequired', { param: 'issuer' }), trigger: 'blur' }],
pkce_method: [
{ required: false, message: T('ParamRequired', { param: 'pkce_method' }), trigger: 'blur' },
{
validator: (rule, value, callback) => {
const allowedValues = ['S256', 'plain']
if (!allowedValues.includes(value)) {
callback(new Error(T('InvalidParam', { param: 'pkce_method' })))
} else {
callback() // 校验通过
}
},
trigger: 'change',
},
],
} }
const defaultRedirect = () => {
return `${app.setting.rustdeskConfig.api_server || window.location.origin}/api/oidc/callback`
}
const toEdit = (row) => { const toEdit = (row) => {
formVisible.value = true formVisible.value = true
formData.id = row.id formData.id = row.id
@@ -161,10 +216,11 @@
formData.issuer = row.issuer formData.issuer = row.issuer
formData.client_id = row.client_id formData.client_id = row.client_id
formData.client_secret = row.client_secret formData.client_secret = row.client_secret
formData.redirect_url = row.redirect_url // formData.redirect_url = row.redirect_url || defaultRedirect()
formData.scopes = row.scopes formData.scopes = row.scopes
formData.auto_register = row.auto_register formData.auto_register = row.auto_register
formData.pkce_enable = row.pkce_enable
formData.pkce_method = row.pkce_method
} }
const toAdd = () => { const toAdd = () => {
formVisible.value = true formVisible.value = true
@@ -174,9 +230,11 @@
formData.issuer = '' formData.issuer = ''
formData.client_id = '' formData.client_id = ''
formData.client_secret = '' formData.client_secret = ''
formData.redirect_url = '' // formData.redirect_url = defaultRedirect()
formData.scopes = '' formData.scopes = ''
formData.auto_register = false formData.auto_register = false
formData.pkce_enable = false
formData.pkce_method = 'S256'
} }
const form = ref(null) const form = ref(null)
const submit = async () => { const submit = async () => {
+222 -26
Voir le fichier
@@ -1,14 +1,14 @@
<template> <template>
<div> <div>
<el-card class="list-query" shadow="hover"> <el-card class="list-query" shadow="hover">
<el-form inline label-width="150px"> <el-form inline label-width="60px">
<el-form-item label="ID"> <el-form-item label="ID">
<el-input v-model="listQuery.id" clearable/> <el-input v-model="listQuery.id" clearable/>
</el-form-item> </el-form-item>
<el-form-item :label="T('Hostname')"> <el-form-item :label="T('Hostname')">
<el-input v-model="listQuery.hostname" clearable/> <el-input v-model="listQuery.hostname" clearable/>
</el-form-item> </el-form-item>
<el-form-item :label="T('LastOnlineTime')"> <el-form-item :label="T('LastOnlineTime')" label-width="100px">
<el-select v-model="listQuery.time_ago" clearable> <el-select v-model="listQuery.time_ago" clearable>
<el-option <el-option
v-for="item in timeFilters" v-for="item in timeFilters"
@@ -19,40 +19,87 @@
></el-option> ></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="T('Username')">
<el-input v-model="listQuery.username" clearable/>
</el-form-item>
<el-form-item label="IP">
<el-input v-model="listQuery.ip" clearable/>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button> <el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
<el-button type="danger" @click="toAdd">{{ T('Add') }}</el-button> <el-button type="danger" @click="toAdd">{{ T('Add') }}</el-button>
<el-button type="success" @click="toExport">{{ T('Export') }}</el-button> <el-button type="success" @click="toExport">{{ T('Export') }}</el-button>
<el-popover :visible="showImport" placement="bottom" :width="600">
<el-upload
class="upload-demo"
drag
accept=".csv"
:before-upload="parseCsv"
>
<el-icon class="el-icon--upload">
<upload-filled/>
</el-icon>
<div class="el-upload__text">
{{ T('Drop file here or click to upload') }}
</div>
<template #tip>
<div class="el-upload__tip">
{{ T('Please upload csv file') }} <br>
{{ T('Columns') }}: <span style="font-weight: bold;font-size: 15px">id,cpu,hostname,memory,os,username,uuid,version,group_id</span>
<br>
<span>{{ T('You can reference export file') }}</span>
</div>
</template>
</el-upload>
<el-button @click="showImport=false" type="primary">{{ T('Cancel') }}</el-button>
<template #reference>
<el-button @click="showImport=true" type="danger" :icon="ArrowDown">{{ T('Import') }}</el-button>
</template>
</el-popover>
<el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button> <el-button type="danger" @click="toBatchDelete">{{ T('BatchDelete') }}</el-button>
<el-button type="primary" @click="toBatchAddToAB">{{ T('BatchAddToAB') }}</el-button> <el-button type="primary" @click="toBatchAddToAB">{{ T('BatchAddToAB') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
<el-card class="list-body" shadow="hover"> <el-card class="list-body" shadow="hover">
<div style="text-align: right; margin-bottom: 10px">
<el-button :icon="Setting" @click="showColumnSetting"></el-button>
</div>
<el-table :data="listRes.list" v-loading="listRes.loading" border size="small" @selection-change="handleSelectionChange"> <el-table :data="listRes.list" v-loading="listRes.loading" border size="small" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center"/> <el-table-column type="selection" width="55" align="center"/>
<el-table-column prop="id" label="ID" align="center" width="150"> <template v-for="c in visibleColumns.filter(cc => cc.visible)" :key="c">
<template #default="{row}"> <el-table-column v-if="c.name==='id'" prop="id" label="ID" align="center" width="150">
<span>{{ row.id }} <el-icon @click="handleClipboard(row.id, $event)"><CopyDocument/></el-icon></span> <template #default="{row}">
</template> <span>{{ row.id }} <el-icon @click="handleClipboard(row.id, $event)"><CopyDocument/></el-icon></span>
</el-table-column> </template>
<el-table-column prop="cpu" label="CPU" align="center" width="100" show-overflow-tooltip/> </el-table-column>
<el-table-column prop="hostname" :label="T('Hostname')" align="center" width="120"/> <el-table-column v-if="c.name==='cpu'" prop="cpu" label="CPU" align="center" width="100" show-overflow-tooltip/>
<el-table-column prop="memory" :label="T('Memory')" align="center" width="120"/> <el-table-column v-if="c.name==='hostname'" prop="hostname" :label="T('Hostname')" align="center" width="120"/>
<el-table-column prop="os" :label="T('Os')" align="center" width="120" show-overflow-tooltip/> <el-table-column v-if="c.name==='memory'" prop="memory" :label="T('Memory')" align="center" width="120"/>
<el-table-column prop="last_online_time" :label="T('LastOnlineTime')" align="center" min-width="120"> <el-table-column v-if="c.name==='os'" prop="os" :label="T('Os')" align="center" width="120" show-overflow-tooltip/>
<template #default="{row}"> <el-table-column v-if="c.name==='last_online_time'" prop="last_online_time" :label="T('LastOnlineTime')" align="center" min-width="120">
<div class="last_oline_time"> <template #default="{row}">
<span> {{ row.last_online_time ? timeAgo(row.last_online_time * 1000) : '-' }}</span> <span class="dot" :class="{red: timeDis(row.last_online_time) >= 60, green: timeDis(row.last_online_time)< 60}"></span> <div class="last_oline_time">
</div> <span> {{ row.last_online_time ? timeAgo(row.last_online_time * 1000) : '-' }}</span> <span class="dot" :class="{red: timeDis(row.last_online_time) >= 60, green: timeDis(row.last_online_time)< 60}"></span>
</template> </div>
</el-table-column> </template>
<el-table-column prop="last_online_ip" :label="T('LastOnlineIp')" align="center" min-width="120"/> </el-table-column>
<el-table-column prop="username" :label="T('Username')" align="center" width="120"/> <el-table-column v-if="c.name==='last_online_ip'" prop="last_online_ip" :label="T('LastOnlineIp')" align="center" min-width="120"/>
<el-table-column prop="uuid" :label="T('Uuid')" align="center" width="120" show-overflow-tooltip/> <el-table-column v-if="c.name==='username'" prop="username" :label="T('Username')" align="center" width="120"/>
<el-table-column prop="version" :label="T('Version')" align="center" width="80"/> <el-table-column v-if="c.name==='group_id'" prop="group_id" :label="T('Group')" align="center" width="120">
<el-table-column prop="created_at" :label="T('CreatedAt')" align="center" width="150"/> <template #default="{row}">
<el-table-column prop="updated_at" :label="T('UpdatedAt')" align="center" width="150"/> <span v-if="row.group_id"> <el-tag>{{ groupListRes.list?.find(g => g.id === row.group_id)?.name }} </el-tag> </span>
<span v-else> - </span>
</template>
</el-table-column>
<el-table-column v-if="c.name==='uuid'" prop="uuid" :label="T('Uuid')" align="center" width="120" show-overflow-tooltip/>
<el-table-column v-if="c.name==='version'" prop="version" :label="T('Version')" align="center" width="80"/>
<el-table-column v-if="c.name==='alias'" prop="alias" :label="T('Alias')" align="center" width="80"/>
<el-table-column v-if="c.name==='created_at'" prop="created_at" :label="T('CreatedAt')" align="center" width="150"/>
<el-table-column v-if="c.name==='updated_at'" prop="updated_at" :label="T('UpdatedAt')" align="center" width="150"/>
</template>
<el-table-column :label="T('Actions')" align="center" width="500" class-name="table-actions" fixed="right"> <el-table-column :label="T('Actions')" align="center" width="500" class-name="table-actions" fixed="right">
<template #default="{row}"> <template #default="{row}">
<el-button type="success" @click="connectByClient(row.id)">{{ T('Link') }}</el-button> <el-button type="success" @click="connectByClient(row.id)">{{ T('Link') }}</el-button>
@@ -78,6 +125,16 @@
<el-form-item label="ID" prop="id" required> <el-form-item label="ID" prop="id" required>
<el-input v-model="formData.id"></el-input> <el-input v-model="formData.id"></el-input>
</el-form-item> </el-form-item>
<el-form-item :label="T('Group')" prop="group_id">
<el-select v-model="formData.group_id">
<el-option
v-for="item in groupListRes.list"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item :label="T('Username')" prop="username"> <el-form-item :label="T('Username')" prop="username">
<el-input v-model="formData.username"></el-input> <el-input v-model="formData.username"></el-input>
</el-form-item> </el-form-item>
@@ -99,7 +156,9 @@
<el-form-item :label="T('Version')" prop="version"> <el-form-item :label="T('Version')" prop="version">
<el-input v-model="formData.version"></el-input> <el-input v-model="formData.version"></el-input>
</el-form-item> </el-form-item>
<el-form-item :label="T('Alias')" prop="alias">
<el-input v-model="formData.alias"></el-input>
</el-form-item>
<el-form-item> <el-form-item>
<el-button @click="formVisible = false">{{ T('Cancel') }}</el-button> <el-button @click="formVisible = false">{{ T('Cancel') }}</el-button>
<el-button @click="submit" type="primary">{{ T('Submit') }}</el-button> <el-button @click="submit" type="primary">{{ T('Submit') }}</el-button>
@@ -145,12 +204,35 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-dialog> </el-dialog>
<el-dialog v-model="columnSettingVisible" title="Column Setting">
<div v-for="(row, key) in visibleColumns" :key="key" style="margin-bottom: 10px;display: flex;align-items: center">
<div style="width: 200px">
<el-checkbox v-model="row.visible" :label="true">{{ T(row.label) }}</el-checkbox>
</div>
<div @click="upColumn(key)" style="width: 100px;cursor: pointer">
<el-icon :size="20">
<ArrowUp/>
</el-icon>
</div>
<div @click="downColumn(key)" style="width: 100px;cursor: pointer">
<el-icon :size="20">
<ArrowDown/>
</el-icon>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="columnSettingVisible = false">{{ T('Cancel') }}</el-button>
<el-button type="primary" @click="saveColumnSetting">{{ T('Save') }}</el-button>
</span>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue' import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'
import { batchRemove, create, list, remove, update } from '@/api/peer' import { batchRemove, create, list, remove, update } from '@/api/peer'
import { list as groupList } from '@/api/device_group'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { toWebClientLink } from '@/utils/webclient' import { toWebClientLink } from '@/utils/webclient'
import { T } from '@/utils/i18n' import { T } from '@/utils/i18n'
@@ -159,13 +241,35 @@
import { loadAllUsers } from '@/global' import { loadAllUsers } from '@/global'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import { connectByClient } from '@/utils/peer' import { connectByClient } from '@/utils/peer'
import { CopyDocument } from '@element-plus/icons' import { ArrowDown, ArrowUp, CopyDocument, Setting } from '@element-plus/icons'
import { handleClipboard } from '@/utils/clipboard' import { handleClipboard } from '@/utils/clipboard'
import { batchCreateFromPeers } from '@/api/address_book' import { batchCreateFromPeers } from '@/api/address_book'
import { useRepositories as useCollectionRepositories } from '@/views/address_book/collection' import { useRepositories as useCollectionRepositories } from '@/views/address_book/collection'
import createABForm from '@/views/peer/createABForm.vue' import createABForm from '@/views/peer/createABForm.vue'
import { UploadFilled } from '@element-plus/icons-vue'
const appStore = useAppStore() const appStore = useAppStore()
//group
const groupListRes = reactive({
list: [], total: 0, loading: false,
})
const groupListQuery = reactive({
page: 1,
page_size: 999,
})
const getGroupList = async () => {
groupListRes.loading = true
const res = await groupList(groupListQuery).catch(_ => false)
groupListRes.loading = false
if (res) {
groupListRes.list = res.data.list
groupListRes.total = res.data.total
}
}
onMounted(getGroupList)
//
const listRes = reactive({ const listRes = reactive({
list: [], total: 0, loading: false, list: [], total: 0, loading: false,
}) })
@@ -175,6 +279,8 @@
time_ago: null, time_ago: null,
id: '', id: '',
hostname: '', hostname: '',
username: '',
ip: '',
}) })
const getList = async () => { const getList = async () => {
@@ -220,6 +326,7 @@
const formVisible = ref(false) const formVisible = ref(false)
const formData = reactive({ const formData = reactive({
row_id: 0, row_id: 0,
group_id: null,
cpu: '', cpu: '',
hostname: '', hostname: '',
id: '', id: '',
@@ -295,6 +402,54 @@
} }
} }
const showImport = ref(false)
const canKeys = ['id', 'cpu', 'hostname', 'memory', 'os', 'username', 'uuid', 'version', 'group_id']
const parseCsv = (file) => {
const reader = new FileReader()
reader.onload = async (e) => {
const data = e.target.result
console.log(data)
//组装数据
const rows = data.split('\n')
const keys = rows[0].split(',')
console.log(keys, rows.slice(1).map(row => row.split(',')))
const values = rows.slice(1).map(row => {
const obj = {}
row.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).forEach((v, i) => {
//去掉两边的"
obj[keys[i]] = v.trim().replace(/^"|"$/g, '')
})
return obj
}).filter(item => item.id)
// console.log(values)
//移除不需要的key
values.forEach(item => {
item.group_id = parseInt(item.group_id)
Object.keys(item).forEach(key => {
if (!canKeys.includes(key)) {
delete item[key]
}
})
})
console.log(values)
const pa = []
values.map(item => {
pa.push(create(item))
})
const res = await Promise.all(pa).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
getList()
}
}
reader.readAsText(file)
return false
}
const toImport = () => {
ElMessage.warning('暂未实现')
}
const ABFormVisible = ref(false) const ABFormVisible = ref(false)
const clickRow = ref({}) const clickRow = ref({})
const toAddressBook = (row) => { const toAddressBook = (row) => {
@@ -370,6 +525,47 @@
} }
// 批量添加到地址簿 end // 批量添加到地址簿 end
const columnSettingVisible = ref(false)
const allColumns = ref([
{ name: 'id', visible: true, label: 'Id' },
{ name: 'cpu', visible: true, label: 'Cpu' },
{ name: 'hostname', visible: true, label: 'Hostname' },
{ name: 'memory', visible: true, label: 'Memory' },
{ name: 'os', visible: true, label: 'Os' },
{ name: 'last_online_time', visible: true, label: 'LastOnlineTime' },
{ name: 'last_online_ip', visible: true, label: 'LastOnlineIp' },
{ name: 'username', visible: true, label: 'Username' },
{ name: 'group_id', visible: true, label: 'Group' },
{ name: 'uuid', visible: true, label: 'Uuid' },
{ name: 'version', visible: true, label: 'Version' },
{ name: 'alias', visible: true, label: 'Alias' },
{ name: 'created_at', visible: true, label: 'CreatedAt' },
{ name: 'updated_at', visible: true, label: 'UpdatedAt' },
])
const visibleColumns = ref(JSON.parse(localStorage.getItem('peer_visible_columns')) || allColumns.value)
const showColumnSetting = () => {
columnSettingVisible.value = true
}
const saveColumnSetting = () => {
localStorage.setItem('peer_visible_columns', JSON.stringify(visibleColumns.value))
ElMessage.success(T('OperationSuccess'))
columnSettingVisible.value = false
}
const upColumn = (index) => {
if (index === 0) return
const col = visibleColumns.value[index]
visibleColumns.value.splice(index, 1)
visibleColumns.value.splice(index - 1, 0, col)
}
const downColumn = (index) => {
if (index === visibleColumns.value.length - 1) return
const col = visibleColumns.value[index]
visibleColumns.value.splice(index, 1)
visibleColumns.value.splice(index + 1, 0, col)
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
+2
Voir le fichier
@@ -35,6 +35,7 @@
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { register } from '@/api/user' import { register } from '@/api/user'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
import { useAppStore } from '@/store/app'
const router = useRouter() const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
@@ -78,6 +79,7 @@
return return
} }
userStore.saveUserData(res.data) userStore.saveUserData(res.data)
useAppStore().loadConfig()
ElMessage.success('Submit') ElMessage.success('Submit')
router.push('/') router.push('/')
} }
+69
Voir le fichier
@@ -0,0 +1,69 @@
<template>
<el-card class="simple-card" shadow="hover" v-loading="form.loading">
<template #header>
<div class="card-header">
<span>ALWAYS_USE_RELAY</span>
</div>
</template>
<el-form :disabled="!canSend">
<el-form-item>
<el-switch v-model="form.option" active-value="Y" inactive-value="N"></el-switch>
</el-form-item>
<el-form-item>
<el-button @click="get">{{ T('Refresh') }}</el-button>
<el-button @click="save" type="primary">{{ T('Save') }}</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup>
import { T } from '@/utils/i18n'
import { reactive, watch } from 'vue'
import { sendCmd } from '@/api/rustdesk'
import { ElMessage } from 'element-plus'
import { ID_TARGET } from '@/views/rustdesk/options'
const emits = defineEmits('success')
const props = defineProps({
canSend: Boolean,
})
const form = reactive({
cmd: 'aur',
option: '',
target: ID_TARGET,
value: 0,
loading: false,
})
const get = async () => {
form.loading = true
const res = await sendCmd({ cmd: 'aur', target: ID_TARGET }).catch(_ => false)
form.loading = false
if (res) {
if (res.data === 'ALWAYS_USE_RELAY: true' || res.data === 'ALWAYS_USE_RELAY: true\n') {
form.option = 'Y'
} else {
form.option = 'N'
}
}
}
const save = async () => {
const res = await sendCmd(form).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
emits('success')
}
}
watch(() => props.canSend, (v) => {
if (v) {
get()
}
})
</script>
<style scoped lang="scss">
</style>
+93
Voir le fichier
@@ -0,0 +1,93 @@
<template>
<el-card class="simple-card" shadow="hover" v-loading="form.loading">
<template #header>
<div class="card-header">
<span>BLACK_LIST</span>
</div>
</template>
<el-form :disabled="!canSend">
<el-form-item>
<el-input type="textarea" :model-value="form.list.join('|')" :rows="5"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getList">{{ T('Refresh') }}</el-button>
<el-button @click="showForm('add')" type="primary">{{ T('Add') }}</el-button>
<el-button @click="showForm('delete')" type="danger">{{ T('Delete') }}</el-button>
</el-form-item>
</el-form>
<el-dialog v-model="form.form_visible" :title="form.form_type">
<el-form label-width="100px">
<el-form-item label="IP">
<el-input v-model="form.form_input"></el-input>
<div>多个IP以 | 分割</div>
<div v-if="form.form_type==='delete'">, 全部填 <strong>all</strong></div>
</el-form-item>
<el-form-item>
<el-button @click="form.form_visible=false">{{ T('Cancel') }}</el-button>
<el-button @click="form.form_type === 'add' ? add() : remove()" type="primary">{{ T('Submit') }}</el-button>
</el-form-item>
</el-form>
</el-dialog>
</el-card>
</template>
<script setup>
import { T } from '@/utils/i18n'
import { reactive, watch } from 'vue'
import { sendCmd } from '@/api/rustdesk'
import { ElMessage } from 'element-plus'
import { RELAY_TARGET } from '@/views/rustdesk/options'
const props = defineProps({
canSend: Boolean,
})
const form = reactive({
get_cmd: 'blacklist',
add_cmd: 'blacklist-add',
remove_cmd: 'blacklist-remove',
list: [],
target: RELAY_TARGET,
loading: false,
form_visible: false,
form_input: '',
form_type: '',
})
const getList = async () => {
form.loading = true
const res = await sendCmd({ cmd: form.get_cmd, target: RELAY_TARGET }).catch(_ => false)
form.loading = false
if (res) {
form.list = res.data.split('\n').filter(i => i)
}
}
const showForm = (type) => {
form.form_visible = true
form.form_input = ''
form.form_type = type
}
const add = async () => {
const res = await sendCmd({ cmd: form.add_cmd, option: form.form_input, target: RELAY_TARGET }).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
getList()
}
}
const remove = async () => {
const res = await sendCmd({ cmd: form.remove_cmd, option: form.form_input, target: RELAY_TARGET }).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
getList()
}
}
watch(() => props.canSend, (v) => {
if (v) {
getList()
}
})
</script>
<style scoped lang="scss">
</style>
+93
Voir le fichier
@@ -0,0 +1,93 @@
<template>
<el-card class="simple-card" shadow="hover" v-loading="form.loading">
<template #header>
<div class="card-header">
<span>BLOCK_LIST</span>
</div>
</template>
<el-form :disabled="!canSend">
<el-form-item>
<el-input type="textarea" :model-value="form.list.join('|')" :rows="5"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getList">{{ T('Refresh') }}</el-button>
<el-button @click="showForm('add')" type="primary">{{ T('Add') }}</el-button>
<el-button @click="showForm('delete')" type="danger">{{ T('Delete') }}</el-button>
</el-form-item>
</el-form>
<el-dialog v-model="form.form_visible" :title="form.form_type">
<el-form label-width="100px">
<el-form-item label="IP">
<el-input v-model="form.form_input"></el-input>
<div>多个IP以 | 分割</div>
<div v-if="form.form_type==='delete'">, 全部填 <strong>all</strong></div>
</el-form-item>
<el-form-item>
<el-button @click="form.form_visible=false">{{ T('Cancel') }}</el-button>
<el-button @click="form.form_type === 'add' ? add() : remove()" type="primary">{{ T('Submit') }}</el-button>
</el-form-item>
</el-form>
</el-dialog>
</el-card>
</template>
<script setup>
import { T } from '@/utils/i18n'
import { reactive, watch } from 'vue'
import { sendCmd } from '@/api/rustdesk'
import { ElMessage } from 'element-plus'
import { RELAY_TARGET } from '@/views/rustdesk/options'
const props = defineProps({
canSend: Boolean,
})
const form = reactive({
get_cmd: 'blocklist',
add_cmd: 'blocklist-add',
remove_cmd: 'blocklist-remove',
list: [],
target: RELAY_TARGET,
loading: false,
form_visible: false,
form_input: '',
form_type: '',
})
const getList = async () => {
form.loading = true
const res = await sendCmd({ cmd: form.get_cmd, target: RELAY_TARGET }).catch(_ => false)
form.loading = false
if (res) {
form.list = res.data.split('\n').filter(i => i)
}
}
const showForm = (type) => {
form.form_visible = true
form.form_input = ''
form.form_type = type
}
const add = async () => {
const res = await sendCmd({ cmd: form.add_cmd, option: form.form_input, target: RELAY_TARGET }).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
getList()
}
}
const remove = async () => {
const res = await sendCmd({ cmd: form.remove_cmd, option: form.form_input, target: RELAY_TARGET }).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
getList()
}
}
watch(() => props.canSend, (v) => {
if (v) {
getList()
}
})
</script>
<style scoped lang="scss">
</style>
+289
Voir le fichier
@@ -0,0 +1,289 @@
<template>
<div>
<h4 v-html="T('ServerCmdTips', {wiki: '<a target=\'_blank\' href=\'https://github.com/lejianwen/rustdesk-api/wiki/Rustdesk-Command\'>WIKI</a>'})"></h4>
<h5>
<span>ID {{ T('Status') }}: </span>
<el-tag v-if="canSendIdServerCmd" type="success">{{ T('Available') }}</el-tag>
<el-tag v-else type="danger">{{ T('NotAvailable') }}</el-tag>
<el-button size="small" type="text" @click="refreshCanSendIdServerCmd">{{ T('Refresh') }}</el-button>
</h5>
<h5>
<span>RELAY {{ T('Status') }}: </span>
<el-tag v-if="canSendRelayServerCmd" type="success">{{ T('Available') }}</el-tag>
<el-tag v-else type="danger">{{ T('NotAvailable') }}</el-tag>
<el-button size="small" type="text" @click="refreshCanSendRelayServerCmd">{{ T('Refresh') }}</el-button>
</h5>
<el-tabs
v-model="activeName"
type="card"
>
<el-tab-pane :label="T('Simple')" name="Simple">
<el-space wrap>
<RelayServers ref="rs" :can-send="canSendIdServerCmd"/>
<alwaysUseRelay :can-send="canSendIdServerCmd" @success="handleAlwaysUseRelaySuccess"/>
<mustLogin :can-send="canControlMustLogin&&canSendIdServerCmd"/>
<usage :can-send="canSendRelayServerCmd"/>
<blocklist :can-send="canSendRelayServerCmd"/>
<blacklist :can-send="canSendRelayServerCmd"/>
</el-space>
</el-tab-pane>
<el-tab-pane :label="T('Advanced')" name="Advanced">
<el-card class="list-query" shadow="hover">
<el-form inline label-width="80px">
<el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
<el-button type="danger" @click="toAdd">{{ T('Add') }}</el-button>
<el-button type="success" :disabled="!canSendIdServerCmd" @click="showCmd({cmd:'',option:'',target:ID_TARGET})">{{ T('Send') }} To Id</el-button>
<el-button type="success" :disabled="!canSendRelayServerCmd" @click="showCmd({cmd:'',option:'',target:RELAY_TARGET})">{{ T('Send') }} To Relay</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<el-table :data="listRes.list" v-loading="listRes.loading" border>
<el-table-column prop="cmd" label="cmd" align="center"></el-table-column>
<el-table-column prop="alias" label="alias" align="center"></el-table-column>
<el-table-column prop="option" label="option" align="center"></el-table-column>
<el-table-column prop="explain" label="explain" align="center"></el-table-column>
<el-table-column label="actions" align="center">
<template #default="{row}">
<el-button type="success" :disabled="!canSendCmd(row.target)" @click="showCmd(row)">{{ T('Send') }}</el-button>
<el-button v-if="row.id" type="primary" @click="toUpdate(row)">{{ T('Edit') }}</el-button>
<el-button v-if="row.id" type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="formVisible">
<el-form label-width="150">
<el-form-item label="cmd">
<el-input v-model="formData.cmd"></el-input>
</el-form-item>
<el-form-item label="alias">
<el-input v-model="formData.alias"></el-input>
</el-form-item>
<el-form-item label="option">
<el-input v-model="formData.option"></el-input>
</el-form-item>
<el-form-item label="target">
<el-radio-group v-model="formData.target">
<el-radio label="id_server" value="21115"></el-radio>
<el-radio label="relay_server" value="21117"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="explain">
<el-input v-model="formData.explain"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">{{ T('Submit') }}</el-button>
<el-button @click="cancel">{{ T('Cancel') }}</el-button>
</el-form-item>
</el-form>
</el-dialog>
<el-dialog :title="T('SendCmd')" v-model="showCmdForm">
<el-form label-width="150" :disabled="!canSendCmd(customCmd.target)">
<el-form-item label="cmd">
<el-input v-model="customCmd.cmd"></el-input>
</el-form-item>
<el-form-item label="option">
<el-input v-model="customCmd.option"></el-input>
<el-text v-if="customCmd.example.trim()" style="margin-top: 5px">Example:
<el-text type="primary">{{ customCmd.example }}</el-text>
</el-text>
</el-form-item>
<!-- <el-form-item label="target">
<el-radio-group v-model="customCmd.target">
<el-radio label="id_server" value="21115"></el-radio>
<el-radio label="relay_server" value="21117"></el-radio>
</el-radio-group>
</el-form-item>-->
<el-form-item>
<el-button type="primary" @click="submitCmd">{{ T('Send') }}</el-button>
</el-form-item>
<el-form-item :label="T('Result')">
<el-input type="textarea" readonly disabled v-model="customCmd.res" rows="15"></el-input>
</el-form-item>
</el-form>
</el-dialog>
</el-card>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { create, list, remove, sendCmd, update } from '@/api/rustdesk'
import { onMounted, reactive, ref } from 'vue'
import { T } from '@/utils/i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ID_TARGET, RELAY_TARGET } from '@/views/rustdesk/options'
import blocklist from '@/views/rustdesk/blocklist.vue'
import blacklist from '@/views/rustdesk/blacklist.vue'
import alwaysUseRelay from '@/views/rustdesk/always_use_relay.vue'
import RelayServers from '@/views/rustdesk/relay_servers.vue'
import mustLogin from '@/views/rustdesk/must_login.vue'
import usage from '@/views/rustdesk/usage.vue'
const activeName = ref('Simple')
const canSendIdServerCmd = ref(false)
const checkCanSendIdServerCmd = async () => {
const res = await sendCmd({ cmd: 'h', target: ID_TARGET }).catch(_ => false)
canSendIdServerCmd.value = !!res.data
if (canSendIdServerCmd.value) {
const commands = res.data.split('\n').filter(i => i)
console.log(commands)
canControlMustLogin.value = commands.some(i => i.includes('must-login'))
}
}
const canControlMustLogin = ref(false)
const refreshCanSendIdServerCmd = () => {
checkCanSendIdServerCmd()
}
onMounted(refreshCanSendIdServerCmd)
const canSendRelayServerCmd = ref(false)
const checkCanSendRelayServerCmd = async () => {
const res = await sendCmd({ cmd: 'h', target: RELAY_TARGET }).catch(_ => false)
canSendRelayServerCmd.value = !!res.data
}
const refreshCanSendRelayServerCmd = () => {
checkCanSendRelayServerCmd()
}
onMounted(refreshCanSendRelayServerCmd)
const rs = ref(null)
const handleAlwaysUseRelaySuccess = () => {
rs.value.save()
}
const canSendCmd = (target) => {
if (target === ID_TARGET) {
return canSendIdServerCmd.value
}
if (target === RELAY_TARGET) {
return canSendRelayServerCmd.value
}
return false
}
const listRes = reactive({
list: [], total: 0, loading: false,
})
const listQuery = reactive({
page: 1,
page_size: 10,
})
const getList = async () => {
listRes.loading = true
const res = await list(listQuery).catch(_ => false)
listRes.loading = false
if (res) {
listRes.list = res.data.list
listRes.total = res.data.total
}
}
const handlerQuery = () => {
if (listQuery.page === 1) {
getList()
} else {
listQuery.page = 1
}
}
onMounted(getList)
const del = async (row) => {
const cf = await ElMessageBox.confirm(T('Confirm?', { param: T('Delete') }), {
confirmButtonText: T('Confirm'),
cancelButtonText: T('Cancel'),
type: 'warning',
}).catch(_ => false)
if (!cf) {
return false
}
const res = await remove({ id: row.id }).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
getList()
}
}
const formData = reactive({
cmd: '',
alias: '',
option: '',
target: '',
explain: '',
})
const formVisible = ref(false)
const toAdd = () => {
formVisible.value = true
formData.cmd = ''
formData.alias = ''
formData.option = ''
formData.explain = ''
}
const toUpdate = (row) => {
formVisible.value = true
formData.id = row.id
formData.cmd = row.cmd
formData.alias = row.alias
formData.option = row.option
formData.target = row.target
formData.explain = row.explain
}
const submit = async () => {
if (!formData.cmd) {
ElMessage.error(T('ParamRequired', { param: 'cmd' }))
return
}
const api = formData.id ? update : create
const res = await api(formData).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
formVisible.value = false
getList()
}
}
const cancel = () => {
formVisible.value = false
}
const showCmdForm = ref(false)
const customCmd = reactive({
cmd: '',
option: '',
target: '',
res: '',
example: '',
})
const showCmd = (row) => {
showCmdForm.value = true
customCmd.cmd = row.cmd
customCmd.option = ''
customCmd.res = ''
customCmd.target = row.target
customCmd.example = `${row.cmd} ${row.option}`
}
const submitCmd = async () => {
sendCmd(customCmd).then(res => {
console.log(res)
customCmd.res = res.data
ElMessage.success(T('OperationSuccess'))
})
}
</script>
<style scoped lang="scss">
.simple-card {
min-width: 300px;
margin: 10px;
min-height: 300px;
}
</style>
+67
Voir le fichier
@@ -0,0 +1,67 @@
<template>
<el-card class="simple-card" shadow="hover" v-loading="form.loading">
<template #header>
<div class="card-header">
<span>MUST_LOGIN</span>
</div>
</template>
<el-form :disabled="!canSend">
<el-form-item>
<el-switch v-model="form.option" active-value="Y" inactive-value="N"></el-switch>
</el-form-item>
<el-form-item>
<el-button @click="get">{{ T('Refresh') }}</el-button>
<el-button @click="save" type="primary">{{ T('Save') }}</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup>
import { T } from '@/utils/i18n'
import { reactive, watch } from 'vue'
import { sendCmd } from '@/api/rustdesk'
import { ElMessage } from 'element-plus'
import { ID_TARGET } from '@/views/rustdesk/options'
const props = defineProps({
canSend: Boolean,
})
const form = reactive({
cmd: 'ml',
option: '',
target: ID_TARGET,
value: 0,
loading: false,
})
const get = async () => {
form.loading = true
const res = await sendCmd({ cmd: 'ml', target: ID_TARGET }).catch(_ => false)
form.loading = false
if (res) {
if (res.data === 'MUST_LOGIN: true' || res.data === 'MUST_LOGIN: true\n') {
form.option = 'Y'
} else {
form.option = 'N'
}
}
}
const save = async () => {
const res = await sendCmd(form).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
}
}
watch(() => props.canSend, (v) => {
if (v) {
get()
}
})
</script>
<style scoped lang="scss">
</style>
+4
Voir le fichier
@@ -0,0 +1,4 @@
export const ID_TARGET = '21115'
export const RELAY_TARGET = '21117'
+65
Voir le fichier
@@ -0,0 +1,65 @@
<template>
<el-card class="simple-card" shadow="hover" v-loading="form.loading">
<template #header>
<div class="card-header">
<span>RELAY_SERVERS</span>
</div>
</template>
<el-form :disabled="!canSend">
<el-form-item>
<el-input v-model="form.option"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="get">{{ T('Refresh') }}</el-button>
<el-button @click="save" type="primary">{{ T('Save') }}</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup>
import { T } from '@/utils/i18n'
import { reactive, watch } from 'vue'
import { sendCmd } from '@/api/rustdesk'
import { ElMessage } from 'element-plus'
import { ID_TARGET } from '@/views/rustdesk/options'
const props = defineProps({
canSend: Boolean,
})
const form = reactive({
cmd: 'rs',
option: '',
target: ID_TARGET,
loading: false,
})
const get = async () => {
form.loading = true
const res = await sendCmd({ cmd: 'rs', target: ID_TARGET }).catch(_ => false)
form.loading = false
if (res) {
const data = res.data.split('\n').filter(i => i)
form.option = data.join(',')
}
}
const save = async () => {
const res = await sendCmd(form).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
}
}
watch(() => props.canSend, (v) => {
if (v) {
get()
}
})
//为了在设置always_use_relay之后自动重新保存,防止被重置
defineExpose({
save,
});
</script>
<style scoped lang="scss">
</style>
+62
Voir le fichier
@@ -0,0 +1,62 @@
<template>
<el-card class="simple-card" shadow="hover" v-loading="form.loading">
<template #header>
<div class="card-header">
<span>USAGE</span>
</div>
</template>
<el-form :disabled="!canSend">
<el-form-item>
<el-table :data="form.list" size="small">
<el-table-column prop="0" label="IP"></el-table-column>
<el-table-column prop="1" label="TIME"></el-table-column>
<el-table-column prop="2" label="TOTAL"></el-table-column>
<el-table-column prop="3" label="HIGHEST"></el-table-column>
<el-table-column prop="4" label="AVG"></el-table-column>
<el-table-column prop="5" label="SPEED"></el-table-column>
</el-table>
</el-form-item>
<el-form-item>
<el-button @click="getList">{{ T('Refresh') }}</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup>
import { T } from '@/utils/i18n'
import { reactive, watch } from 'vue'
import { sendCmd } from '@/api/rustdesk'
import { RELAY_TARGET } from '@/views/rustdesk/options'
const props = defineProps({
canSend: Boolean,
})
const form = reactive({
get_cmd: 'u',
list: [],
target: RELAY_TARGET,
loading: false,
})
const getList = async () => {
form.loading = true
const res = await sendCmd({ cmd: form.get_cmd, target: RELAY_TARGET }).catch(_ => false)
form.loading = false
if (res) {
form.list = res.data.split('\n').filter(i => i).map(i => i.split(" "))
}
}
watch(() => props.canSend, (v) => {
if (v) {
getList()
}
})
</script>
<style scoped lang="scss">
.simple-card{
width: 500px;
}
</style>
+1 -1
Voir le fichier
@@ -15,7 +15,7 @@ export function useGetDetail (id) {
form.value = { ...res.data } form.value = { ...res.data }
} }
if (id > 0) { if (id > 0) {
onMounted(getDetail(id)) onMounted(_ => {getDetail(id)})
} }
const getGroups = async () => { const getGroups = async () => {
+13 -6
Voir le fichier
@@ -1,9 +1,10 @@
import { onMounted, reactive, watch } from 'vue' import { reactive } from 'vue'
import { list, remove, changePwd } from '@/api/user' import { list, remove, changePwd } from '@/api/user'
import { list as groups } from '@/api/group' import { list as groups } from '@/api/group'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus' import { ElMessageBox, ElMessage } from 'element-plus'
import { T } from '@/utils/i18n' import { T } from '@/utils/i18n'
import { downBlob, jsonToCsv } from '@/utils/file'
export function useRepositories () { export function useRepositories () {
@@ -42,18 +43,24 @@ export function useRepositories () {
listRes.groups = res.data.list listRes.groups = res.data.list
} }
} }
onMounted(getGroups)
onMounted(getList) const toExport = async () => {
const q = { ...listQuery }
watch(() => listQuery.page, getList) q.page_size = 1000000
watch(() => listQuery.page_size, handlerQuery) q.page = 1
const res = await list(q).catch(_ => false)
if (res) {
const csv = jsonToCsv(res.data.list)
downBlob(csv, 'users.csv')
}
}
return { return {
listRes, listRes,
listQuery, listQuery,
handlerQuery, handlerQuery,
getList, getList,
getGroups, getGroups,
toExport,
} }
} }
+3
Voir le fichier
@@ -32,6 +32,9 @@
:inactive-value="DISABLE_STATUS" :inactive-value="DISABLE_STATUS"
></el-switch> ></el-switch>
</el-form-item> </el-form-item>
<el-form-item :label="T('Remark')" prop="remark">
<el-input v-model="form.remark"></el-input>
</el-form-item>
<el-form-item> <el-form-item>
<el-button @click="cancel">{{ T('Cancel') }}</el-button> <el-button @click="cancel">{{ T('Cancel') }}</el-button>
<el-button @click="submit" type="primary">{{ T('Submit') }}</el-button> <el-button @click="submit" type="primary">{{ T('Submit') }}</el-button>
+38
Voir le fichier
@@ -8,6 +8,7 @@
<el-form-item> <el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button> <el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
<el-button type="danger" @click="toAdd">{{ T('Add') }}</el-button> <el-button type="danger" @click="toAdd">{{ T('Add') }}</el-button>
<el-button type="success" @click="toExport">{{ T('Export') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@@ -23,6 +24,16 @@
<span v-else> - </span> <span v-else> - </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="T('Status')" align="center">
<template #default="{row}">
<el-switch v-model="row.status"
:active-value="ENABLE_STATUS"
:inactive-value="DISABLE_STATUS"
@change="changeStatus(row)"
></el-switch>
</template>
</el-table-column>
<el-table-column prop="remark" :label="T('Remark')" align="center"/>
<el-table-column prop="created_at" :label="T('CreatedAt')" align="center"/> <el-table-column prop="created_at" :label="T('CreatedAt')" align="center"/>
<el-table-column prop="updated_at" :label="T('UpdatedAt')" align="center"/> <el-table-column prop="updated_at" :label="T('UpdatedAt')" align="center"/>
<el-table-column :label="T('Actions')" align="center" width="650"> <el-table-column :label="T('Actions')" align="center" width="650">
@@ -51,14 +62,27 @@
<script setup> <script setup>
import { useRepositories, useDel, useToEditOrAdd, useChangePwd } from '@/views/user/composables' import { useRepositories, useDel, useToEditOrAdd, useChangePwd } from '@/views/user/composables'
import { T } from '@/utils/i18n' import { T } from '@/utils/i18n'
import { DISABLE_STATUS, ENABLE_STATUS } from '@/utils/common_options'
import { update } from '@/api/user'
import { ElMessageBox, ElMessage } from 'element-plus'
import { onMounted, watch } from 'vue'
//列表 //列表
const { const {
listRes, listRes,
listQuery, listQuery,
handlerQuery, handlerQuery,
getList, getList,
getGroups,
toExport,
} = useRepositories() } = useRepositories()
onMounted(getGroups)
onMounted(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
const { toEdit, toAdd, toAddressBook, toTag } = useToEditOrAdd() const { toEdit, toAdd, toAddressBook, toTag } = useToEditOrAdd()
const { changePass } = useChangePwd() const { changePass } = useChangePwd()
@@ -72,6 +96,20 @@
} }
} }
const changeStatus = async (row) => {
/*const confirm = await ElMessageBox.confirm(T('Confirm?', { param: T('Update') }), {
confirmButtonText: T('Confirm'),
cancelButtonText: T('Cancel'),
}).catch(_ => false)
if (!confirm) {
return false
}*/
const res = await update(row).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
getList(listQuery)
}
}
</script> </script>