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>
<html lang="zh">
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rustdesk-Api-Admin</title>
<title>Rustdesk API Admin</title>
</head>
<body>
<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"
},
"dependencies": {
"axios": "1.7.4",
"axios": "1.8.2",
"clipboard": "2.0.4",
"element-plus": "^2.8.2",
"fast-sha256": "^1.3.0",
"js-cookie": "^3.0.1",
"marked": "^15.0.4",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"pinia": "2.0.3",
"vue": "3.2.37",
"vue-router": "^4.0.12",
"fast-sha256": "^1.3.0",
"clipboard": "2.0.4"
"pinia": "2.2.8",
"vue": "3.5.13",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@element-plus/icons": "0.0.11",
"@vitejs/plugin-vue": "^1.9.3",
"@vitejs/plugin-vue": "5.2.1",
"dotenv": "^10.0.0",
"qs": "^6.10.2",
"sass-loader": "^12.3.0",
"sass": "^1.43.4",
"vite": "^2.9.18",
"ts-proto": "^1.141.1"
"sass-loader": "^12.3.0",
"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: '',
confirmPwd: '',
})
const chagePwdRules = reactive({
old_password: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
const chagePwdRules = computed(_ => ({
old_password: [{ required: true, message: T('ParamRequired', { param: T('OldPassword') }), trigger: 'blur' }],
new_password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ required: true, message: T('ParamRequired', { param: T('NewPassword') }), trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === changePwdForm.old_password) {
callback(new Error('新密码不能与旧密码相同'))
callback(new Error(T('NewPasswordEqualOldPassword'))) //'新密码不能与旧密码相同'
} else {
callback()
}
@@ -67,11 +67,11 @@
trigger: 'blur',
}],
confirmPwd: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{ required: true, message: T('ParamRequired', { param: T('ConfirmPassword') }), trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== changePwdForm.new_password) {
callback(new Error('两次输入密码不一致'))
callback(new Error(T('PasswordNotMatchConfirmPassword')))
} else {
callback()
}
@@ -79,7 +79,7 @@
trigger: 'blur',
},
],
})
}))
const cpwd = ref(null)
const cancelChangePwd = () => {
emit('update:visible', false)
@@ -94,9 +94,9 @@
return
}
console.log('changePassword')
const confirm = await ElMessageBox.confirm('确定修改密码么?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
const confirm = await ElMessageBox.confirm(T('Confirm?', { param: T('ChangePassword') }), {
confirmButtonText: T('Confirm'),
cancelButtonText: T('Cancel'),
}).catch(_ => false)
if (!confirm) {
return
@@ -105,7 +105,7 @@
if (!res) {
return
}
ElMessageBox.alert('修改成功', '修改密码', {
ElMessageBox.alert(T('OperationSuccess'), T('ChangePassword'), {
autofocus: true,
confirmButtonText: 'OK',
callback: (action) => {
+4 -4
Voir le fichier
@@ -13,8 +13,8 @@
})
</script>
<style scoped>
.scroll-sidebar{
position: fixed;
}
<style scoped lang="scss">
.scroll-sidebar {
background-color: #2d3a4b;
}
</style>
+1 -2
Voir le fichier
@@ -15,7 +15,7 @@
<el-main class="app-main">
<router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="[...cachedTags]">
<keep-alive :include="cachedTags">
<component :is="Component"/>
</keep-alive>
</transition>
@@ -60,7 +60,6 @@
}
.app-left {
height: 100%;
transition: width 0.5s;
}
+4 -1
Voir le fichier
@@ -13,6 +13,7 @@ NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/register']
const routeStore = useRouteStore(pinia)
const appStore = useAppStore(pinia)
appStore.getAdminConfig()
router.beforeEach(async (to, from, next) => {
document.title = T(to.meta?.title) + ' - ' + appStore.setting.title
@@ -40,7 +41,9 @@ router.beforeEach(async (to, from, next) => {
} else {
next({ ...to, replace: true })
}
} else {
}/* else if (to.path === '/404') {
next({path: to.redirectedFrom?.fullPath, replace: true})
}*/ else {
next()
}
}
+19 -1
Voir le fichier
@@ -51,7 +51,7 @@ export const asyncRoutes = [
{
path: '/my',
name: 'My',
redirect: '/my/info',
redirect: '/',
meta: { title: 'My', icon: 'UserFilled' },
component: () => import('@/layout/index.vue'),
children: [
@@ -91,6 +91,12 @@ export const asyncRoutes = [
meta: { title: 'ShareRecord', icon: 'Share' /*keepAlive: true*/ },
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*/ },
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',
name: 'UserList',
@@ -184,6 +196,12 @@ export const asyncRoutes = [
meta: { title: 'ShareRecord', icon: 'Share' /*keepAlive: true*/ },
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 fr from 'element-plus/es/locale/lang/fr'
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 = {
'zh-CN': { name: '中文', value: zhCn, sideBarWidth: '210px' },
'en': { name: 'English', value: en, sideBarWidth: '230px' },
'fr': { name: 'Français', value: fr, sideBarWidth: '280px' },
'ko': { name: '한국어', value: ko, sideBarWidth: '230px' },
'ru': { name: 'русский', value: ru, sideBarWidth: '250px' },
'es': { name: 'español', value: es, sideBarWidth: '280px' },
'ru': { name: 'Русский', value: ru, sideBarWidth: '250px' },
'es': { name: 'Español', value: es, sideBarWidth: '280px' },
'zh-TW': { name: '中文繁体', value: zhTw, sideBarWidth: '210px' },
}
const defaultLang = localStorage.getItem('lang') || navigator.language || 'zh-CN'
export const useAppStore = defineStore({
id: 'App',
state: () => ({
setting: {
title: 'Rustdesk-Api-Admin',
title: 'Rustdesk API Admin',
hello: '',
sideIsCollapse: false,
logo,
@@ -31,6 +33,12 @@ export const useAppStore = defineStore({
appConfig: {
web_client: 1,
},
rustdeskConfig: {
'id_server': '',
'key': '',
'relay_server': '',
'api_server': '',
},
},
}),
@@ -50,20 +58,37 @@ export const useAppStore = defineStore({
loadConfig () {
this.getAppConfig()
this.getAdminConfig()
this.loadRustdeskConfig()
},
getAppConfig () {
console.log('getAppConfig')
app().then(res => {
return app().then(res => {
this.setting.appConfig = res.data
})
},
getAdminConfig () {
console.log('getAdminConfig')
admin().then(res => {
this.setting.title = res.data.title
return admin().then(res => {
this.replaceAdminTitle(res.data.title)
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;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
.list-body {
margin: 10px 0;
}
+1 -1
Voir le fichier
@@ -38,7 +38,7 @@ export function jsonToCsv (data) {
let keys = Object.keys(data[0])
csv += keys.join(',') + '\n'
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' })
}
+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 ru from '@/utils/i18n/ru.json'
import es from '@/utils/i18n/es.json'
import zhTW from '@/utils/i18n/zh_TW.json'
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) {
const appStore = useAppStore()
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]
if (!tran) {
return key
+63
Voir le fichier
@@ -439,6 +439,15 @@
"LastOnlineIp": {
"One": "Last Online Ip"
},
"OldPassword": {
"One": "Old Password"
},
"NewPasswordEqualOldPassword": {
"One": "New Password cannot be the same as Old Password"
},
"NewPassword": {
"One": "New Password"
},
"ConfirmPassword": {
"One": "Confirm Password"
},
@@ -468,5 +477,59 @@
},
"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": {
"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": {
"One": "Confirmar contraseña"
},
@@ -471,5 +480,56 @@
},
"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": {
"One": "Connexion"
"One": "Connexion"
},
"Logout": {
"One": "Déconnexion"
"One": "Déconnexion"
},
"Register": {
"One": "S'inscrire"
"One": "S'inscrire"
},
"Confirm": {
"One": "Confirmer"
"One": "Confirmer"
},
"Username": {
"One": "Nom d'utilisateur"
"One": "Nom d'utilisateur"
},
"Password": {
"One": "Mot de passe"
"One": "Mot de passe"
},
"LoginSuccess": {
"One": "Connexion réussie"
"One": "Connexion réussie"
},
"ForgotPassword": {
"One": "Mot de passe oublié"
"One": "Mot de passe oublié"
},
"ResetPassword": {
"One": "Réinitialiser le mot de passe"
"One": "Réinitialiser le mot de passe"
},
"ChangePassword": {
"One": "Modifier le mot de passe"
"One": "Modifier le mot de passe"
},
"Userinfo": {
"One": "Informations utilisateur"
"One": "Informations utilisateur"
},
"ParamRequired": {
"One": "{param} est obligatoire"
"One": "{param} est obligatoire"
},
"HasBind": {
"One": "A une liaison"
"One": "A une liaison"
},
"NoBind": {
"One": "Aucune liaison"
"One": "Aucune liaison"
},
"UnBind": {
"One": "Délier"
"One": "Délier"
},
"ToBind": {
"One": "Lier"
"One": "Lier"
},
"Confirm?": {
"One": "{param} Confirmer?"
"One": "{param} Confirmer?"
},
"Cancel": {
"One": "Annuler"
"One": "Annuler"
},
"Platform": {
"One": "Plateforme"
"One": "Plateforme"
},
"Status": {
"One": "État"
"One": "État"
},
"Actions": {
"One": "Actions"
"One": "Actions"
},
"Filter": {
"One": "Filtre"
"One": "Filtre"
},
"Add": {
"One": "Ajouter"
"One": "Ajouter"
},
"Hostname": {
"One": "Nom d'hôte"
"One": "Nom d'hôte"
},
"Alias": {
"One": "Alias"
"One": "Alias"
},
"Hash": {
"One": "Hachage"
"One": "Hachage"
},
"Tags": {
"One": "Etiquettes"
"One": "Etiquettes"
},
"Edit": {
"One": "Modifier"
"One": "Modifier"
},
"Delete": {
"One": "Supprimer"
"One": "Supprimer"
},
"Create": {
"One": "Créer"
"One": "Créer"
},
"Update" : {
"One" : "Mise à jour"
"Update": {
"One": "Mise à jour"
},
"LoginName" : {
"One" : "Nom de connexion"
"LoginName": {
"One": "Nom de connexion"
},
"Submit" : {
"One" : "Envoyer"
"Submit": {
"One": "Envoyer"
},
"OperationSuccess" : {
"One" : "Opération réussie"
"OperationSuccess": {
"One": "Opération réussie"
},
"Owner" : {
"One" : "Propriétaire"
"Owner": {
"One": "Propriétaire"
},
"Name" : {
"One" : "Nom"
"Name": {
"One": "Nom"
},
"Color" : {
"One" : "Couleur"
"Color": {
"One": "Couleur"
},
"CreatedAt" : {
"One" : "Créé le"
"CreatedAt": {
"One": "Créé le"
},
"UpdatedAt" : {
"One" : "Mis à jour le"
"UpdatedAt": {
"One": "Mis à jour le"
},
"Memory" : {
"One" : "Mémoire"
"Memory": {
"One": "Mémoire"
},
"Os" : {
"One" : "Os"
"Os": {
"One": "Os"
},
"Uuid" : {
"One" : "Uuid"
"Uuid": {
"One": "Uuid"
},
"Version" : {
"One": "Version"
"Version": {
"One": "Version"
},
"Type": {
"One": "Type"
"One": "Type"
},
"Group": {
"One": "Groupe"
"One": "Groupe"
},
"CommonGroup": {
"One": "Groupe commun"
"One": "Groupe commun"
},
"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": {
"One": "Groupe partagé"
"One": "Groupe partagé"
},
"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": {
"One": "Pseudonyme"
"One": "Pseudonyme"
},
"UserTags": {
"One": "Etiquettes utilisateur"
"One": "Etiquettes utilisateur"
},
"UserAddressBook": {
"One": "Carnet d'adresses utilisateur"
"One": "Carnet d'adresses utilisateur"
},
"IsAdmin": {
"One": "Est administrateur"
"One": "Est administrateur"
},
"PleaseInputNewPassword": {
"One": "Veuillez saisir nouveau mot de passe"
"One": "Veuillez saisir nouveau mot de passe"
},
"AutoRegister": {
"One": "Enregistrement automatique"
"One": "Enregistrement automatique"
},
"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": {
"One": "Troisième nom"
"One": "Troisième nom"
},
"Close": {
"One": "Fermer"
"One": "Fermer"
},
"OauthBinding": {
"One": "Vous autorisez la liaison"
"One": "Vous autorisez la liaison"
},
"OauthLogining": {
"One": "Vous autorisez la connexion"
"One": "Vous autorisez la connexion"
},
"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": {
"One": "Opération réussie, fermer après 3 secondes"
"One": "Opération réussie, fermer après 3 secondes"
},
"ConfirmOauth": {
"One": "Confirmer Oauth"
"One": "Confirmer Oauth"
},
"View": {
"One": "Informations"
@@ -195,261 +195,270 @@
"One": "Appareil"
},
"ChangeLang": {
"One": "Changer la langue"
"One": "Changer la langue"
},
"My": {
"One": "Moi"
"One": "Moi"
},
"Info": {
"One": "Info"
"One": "Info"
},
"AddressBooks": {
"One": "Carnets d'adresses"
"One": "Carnets d'adresses"
},
"System": {
"One": "Système"
"One": "Système"
},
"PeerManage": {
"One": "Appareils"
"One": "Appareils"
},
"AddressBookManage": {
"One": "Carnets d'adresses"
"One": "Carnets d'adresses"
},
"GroupManage": {
"One": "Groupes"
"One": "Groupes"
},
"UserManage": {
"One": "Utilisateurs"
"One": "Utilisateurs"
},
"UserAdd": {
"One": "Ajout d'utilisateur"
"One": "Ajout d'utilisateur"
},
"UserEdit": {
"One": "Utilisateur Modifier"
"One": "Utilisateur Modifier"
},
"TagsManage": {
"One": "Etiquettes"
"One": "Etiquettes"
},
"OauthManage": {
"One": "Oauth"
"One": "Oauth"
},
"LoginLog": {
"One": "Journal de connexion"
"One": "Journal de connexion"
},
"LastOnlineTime": {
"One": "Dernière heure de connexion"
"One": "Dernière heure de connexion"
},
"JustNow": {
"One": "Juste maintenant"
"One": "Juste maintenant"
},
"MinutesAgo": {
"One": "Il y a {param} minute",
"Other": "Il y a {param} minutes"
"One": "Il y a {param} minute",
"Other": "Il y a {param} minutes"
},
"HoursAgo": {
"One": "Il y a {param} heure",
"Other": "Il y a {param} heures"
"One": "Il y a {param} heure",
"Other": "Il y a {param} heures"
},
"DaysAgo": {
"One": "Il y a {param} jour",
"Other": "Il y a {param} jours"
"One": "Il y a {param} jour",
"Other": "Il y a {param} jours"
},
"MonthsAgo": {
"One": " Il y a {param} mois",
"Autre": "Il y a {param} mois"
"One": " Il y a {param} mois",
"Autre": "Il y a {param} mois"
},
"YearsAgo": {
"One": "Il y a {param} an",
"Autre": "Il y a {param} ans"
"One": "Il y a {param} an",
"Autre": "Il y a {param} ans"
},
"MinutesLess": {
"One": "Moins de {param} minute",
"Autre": "Moins de {param} minutes"
"One": "Moins de {param} minute",
"Autre": "Moins de {param} minutes"
},
"HoursLess": {
"One": "Moins de {param} heure",
"Autre": "Moins de {param} heures"
"One": "Moins de {param} heure",
"Autre": "Moins de {param} heures"
},
"DaysLess": {
"One": "Moins de {param} jour",
"Autre": "Moins de {param} jours"
"One": "Moins de {param} jour",
"Autre": "Moins de {param} jours"
},
"Export": {
"One": "Exporter"
"One": "Exporter"
},
"AddToAddressBook": {
"One": "Ajouter au carnet d'adresses"
"One": "Ajouter au carnet d'adresses"
},
"BatchDelete": {
"One": "Suppression par lot"
"One": "Suppression par lot"
},
"PleaseSelectData": {
"One": "Veuillez sélectionner des données"
"One": "Veuillez sélectionner des données"
},
"PasswordType": {
"One": "Type de mot de passe"
"One": "Type de mot de passe"
},
"OncePassword": {
"One": "Mot de passe à usage unique"
"One": "Mot de passe à usage unique"
},
"FixedPassword": {
"One": "Mot de passe fixe"
"One": "Mot de passe fixe"
},
"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": {
"One": "Heure d'expiration"
"One": "Heure d'expiration"
},
"Minutes": {
"One": "{param} Minute",
"Other": "{param} Minutes"
"One": "{param} Minute",
"Other": "{param} Minutes"
},
"Hours": {
"One": "{param} Heure",
"Other": "{param} Heures"
"One": "{param} Heure",
"Other": "{param} Heures"
},
"Days": {
"One": "{param} Jour",
"Other": "{param} Jours"
"One": "{param} Jour",
"Other": "{param} Jours"
},
"Weeks": {
"One": "{param} Semaine",
"Other": "{param} Semaines"
"One": "{param} Semaine",
"Other": "{param} Semaines"
},
"Months": {
"One": "{param} Mois",
"Other": "{param} Mois"
"One": "{param} Mois",
"Other": "{param} Mois"
},
"Forever": {
"One": "Forever"
"One": "Forever"
},
"Error": {
"One": "Erreur"
"One": "Erreur"
},
"IDNotExist": {
"One": "L'ID n'existe pas"
"One": "L'ID n'existe pas"
},
"RemoteDesktopOffline": {
"One": "Le bureau à distance est hors ligne"
"One": "Le bureau à distance est hors ligne"
},
"KeyMismatch": {
"One": "Non-correspondance de clé"
"One": "Non-correspondance de clé"
},
"KeyOveruse": {
"One": "Surutilisation de clé"
"One": "Surutilisation de clé"
},
"Link": {
"One": "Lien"
"One": "Lien"
},
"CopySuccess": {
"One": "Copie réussie"
"One": "Copie réussie"
},
"CopyFailed": {
"One": "Copie échouée"
"One": "Copie échouée"
},
"Timeout": {
"One": "Expiration du délai"
"One": "Expiration du délai"
},
"AuditConnLog": {
"One": "Journal de connexion"
"One": "Journal de connexion"
},
"Peer": {
"One": "Appareil",
"Other": "Appareils distants"
"One": "Appareil",
"Other": "Appareils distants"
},
"FromPeer": {
"One": "Par Appareil distant"
"One": "Par Appareil distant"
},
"FromName": {
"One": "Par nom"
"One": "Par nom"
},
"CloseTime": {
"One": "Heure de clôture"
"One": "Heure de clôture"
},
"AuditFileLog": {
"One": "Fichier Journal"
"One": "Fichier Journal"
},
"Common": {
"One": "Commun"
"One": "Commun"
},
"File": {
"One": "Fichier"
"One": "Fichier"
},
"Num": {
"One": "Num"
"One": "Num"
},
"Ip": {
"One": "Ip"
"One": "Ip"
},
"FileName": {
"One": "Nom du fichier"
"One": "Nom du fichier"
},
"FileInfo": {
"One": "Informations sur le fichier"
"One": "Informations sur le fichier"
},
"Path": {
"One": "Chemin"
"One": "Chemin"
},
"IndexNum": {
"One": "Num. index"
"One": "Num. index"
},
"ToRemote": {
"One": "Vers distant"
"One": "Vers distant"
},
"ToLocal": {
"One": "Vers local"
"One": "Vers local"
},
"AddressBookName": {
"One": "Nom du carnet d'adresses"
"One": "Nom du carnet d'adresses"
},
"AddRule": {
"One": "Ajouter une règle"
"One": "Ajouter une règle"
},
"ShareRules": {
"One": "Partager des règles"
"One": "Partager des règles"
},
"Rule": {
"One": "Règle"
"One": "Règle"
},
"Read": {
"One": "Lecture"
"One": "Lecture"
},
"ReadWrite": {
"One": "Lecture et écriture"
"One": "Lecture et écriture"
},
"FullControl": {
"One": "Contrôle total"
"One": "Contrôle total"
},
"ShareTo": {
"One": "Partager avec"
"One": "Partager avec"
},
"MyAddressBook": {
"One": "Mon carnet d'adresses"
"One": "Mon carnet d'adresses"
},
"AddressBook": {
"One": "Carnet d'adresses"
"One": "Carnet d'adresses"
},
"AddressBookNameManage": {
"One": "Noms du carnet d'adresses"
"One": "Noms du carnet d'adresses"
},
"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": {
"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": {
"One": "Confirmer le mot de passe"
"One": "Confirmer le mot de passe"
},
"PasswordNotMatchConfirmPassword": {
"One": "Les mots de passe ne correspondent pas"
"One": "Les mots de passe ne correspondent pas"
},
"ToLogin": {
"One": "Pour se connecter"
"One": "Pour se connecter"
},
"UserToken": {
"One": "Jeton Utilisateur"
"One": "Jeton Utilisateur"
},
"Token": {
"One": "Jeton"
@@ -458,21 +467,72 @@
"One": "Mon appareil"
},
"BatchAddToAB": {
"One": "Ajout par lots au carnet d'adresses"
"One": "Ajout par lots au carnet d'adresses"
},
"BatchEditTags": {
"One": "Modification par lots des étiquettes"
"One": "Modification par lots des étiquettes"
},
"User": {
"One": "Utilisateur"
"One": "Utilisateur"
},
"ShareRecord": {
"One": "Enregistrement des partages"
"One": "Enregistrement des partages"
},
"Second": {
"One": "Seconde"
"One": "Seconde"
},
"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": {
"One": "마지막 온라인 IP"
},
"OldPassword": {
"One": "이전 비밀번호"
},
"NewPasswordEqualOldPassword": {
"One": "새 비밀번호는 이전 비밀번호와 같을 수 없습니다"
},
"NewPassword": {
"One": "새 비밀번호"
},
"ConfirmPassword": {
"One": "비밀번호 확인"
},
@@ -457,5 +466,56 @@
},
"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": "Данные пользователя"
},
"ParamRequired": {
"One": "{param} обязательный"
"One": "Поле {param} обязательно"
},
"HasBind": {
"One": "Связано"
@@ -42,7 +42,7 @@
"One": "Не связано"
},
"UnBind": {
"One": "Разъединить"
"One": "Отвязать"
},
"ToBind": {
"One": "Привязать"
@@ -168,7 +168,7 @@
"One": "Если включено, аккаунт будет автоматически зарегистрирован, когда пользователь входит через OAuth без привязки к существующему аккаунту"
},
"ThirdName": {
"One": "Третье имя"
"One": "Название стороннего сервиса"
},
"Close": {
"One": "Закрыть"
@@ -192,7 +192,7 @@
"One": "Устройство"
},
"ChangeLang": {
"One": "Сменить на китайский"
"One": "Переключить на китайский"
},
"My": {
"One": "Мой"
@@ -431,7 +431,7 @@
"One": "Адресная книга"
},
"AddressBookNameManage": {
"One": "именами адресных книг"
"One": "Имена адресных книг"
},
"MyAddressBookTips": {
"One": "«Моя адресная книга» является стандартным для системы, не может быть изменена или удалена"
@@ -439,6 +439,15 @@
"LastOnlineIp": {
"One": "Последний IP онлайн"
},
"OldPassword": {
"One": "Старый пароль"
},
"NewPasswordEqualOldPassword": {
"One": "Новый пароль совпадает со старым паролем"
},
"NewPassword": {
"One": "Новый пароль"
},
"ConfirmPassword": {
"One": "Подтвердите пароль"
},
@@ -471,6 +480,60 @@
},
"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": {
"One": "新密码"
},
"NewPasswordEqualOldPassword": {
"One": "新密码不能与旧密码相同"
},
"ConfirmPassword": {
"One": "确认密码"
},
@@ -487,5 +490,62 @@
},
"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": "备注"
}
}
+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 * as rendezvous from '@/utils/webclient/rendezvous'
import * as message from '@/utils/webclient/message'
import { ElMessageBox } from 'element-plus'
import { T } from '@/utils/i18n'
import { useAppStore } from '@/store/app'
const prefix = 'wc-'
const app = useAppStore()
export const toWebClientLink = (row) => {
//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) {
const [addr, port] = rustdeskConfig.value.id_server.split(':')
const [addr, port] = app.setting.rustdeskConfig.id_server.split(':')
if (!addr) {
return
}
@@ -45,7 +27,7 @@ export async function getPeerSlat (id) {
const nat_type = rendezvous.NatType.SYMMETRIC
const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({
id,
licence_key: rustdeskConfig.value.key || undefined,
licence_key: app.setting.rustdeskConfig.value.key || undefined,
conn_type,
nat_type,
token: undefined,
@@ -87,7 +69,7 @@ export async function getPeerSlat (id) {
await _ws.open()
console.log(new Date() + ': Connected to relay server')
const request_relay = rendezvous.RequestRelay.fromPartial({
licence_key: rustdeskConfig.value.key || undefined,
licence_key: app.setting.rustdeskConfig.key || undefined,
uuid,
})
_ws.sendRendezvous({ request_relay })
@@ -112,5 +94,5 @@ export async function getPeerSlat (id) {
}
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('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 formData = reactive({
id: 0,
collection_id: null,
type: 1,
type: TYPE_U,
rule: 1,
g_id: null,
u_id: null,
to_id: null,
user_id: null,
})
@@ -76,28 +83,60 @@ export function useRepositories (api_type = 'my') {
Object.keys(formData).forEach(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 = () => {
//初始化formData
formData.id = 0
formData.type = TYPE_U
formData.rule = 1
formData.g_id = null
formData.u_id = null
formVisible.value = true
}
const submit = async () => {
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) {
ElMessage.success(T('OperationSuccess'))
formVisible.value = false
getList()
}
}
const groupUsersList = ref([])
const groups = ref([])
const users = ref([])
const getGroupUsers = async () => {
const res = await groupUsers({ user_id: formData.user_id }).catch(_ => false)
const res = await groupUsers().catch(_ => false)
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 {
listRes,
listQuery,
@@ -110,7 +149,12 @@ export function useRepositories (api_type = 'my') {
toAdd,
submit,
rules,
groupUsersList,
types,
groups,
users,
getGroupUsers,
TYPE_G,
TYPE_U,
changeGId,
}
}
+47 -14
Voir le fichier
@@ -17,10 +17,20 @@
</div>
</template>
</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">
<template #default="{row}">
<div v-if="row.type===1">
{{ groupUsersList.find(u => u.id === row.to_id)?.username }}
<div v-if="row.type===TYPE_U">
{{ 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>
</template>
</el-table-column>
@@ -50,22 +60,40 @@
</el-form-item>
<el-form-item :label="T('Rule')" prop="rule" required>
<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 }}
</el-radio>
</el-radio-group>
</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-select v-model="formData.to_id">
<el-option
v-for="item in groupUsersList"
:key="item.id"
:label="item.username"
:value="item.id"
:disabled="!item.status"
></el-option>
</el-select>
<div style="width: 30%">
<el-select v-model="formData.g_id" @change="changeGId">
<el-option
v-for="item in groups"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</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-button @click="formVisible = false">{{ T('Cancel') }}</el-button>
@@ -104,8 +132,13 @@
toAdd,
submit,
rules,
groupUsersList,
types,
groups,
users,
getGroupUsers,
TYPE_G,
TYPE_U,
changeGId,
} = useRepositories(props.is_my ? 'my' : 'admin')
formData.collection_id = props.collection.id
+2
Voir le fichier
@@ -11,6 +11,7 @@
<el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</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>
</el-card>
@@ -62,6 +63,7 @@
handlerQuery,
del,
batchdel,
toExport,
} = useRepositories()
onMounted(getList)
+2
Voir le fichier
@@ -11,6 +11,7 @@
<el-form-item>
<el-button type="primary" @click="handlerQuery">{{ T('Filter') }}</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>
</el-card>
@@ -109,6 +110,7 @@
handlerQuery,
del,
batchdel,
toExport,
} = useFileRepositories()
onMounted(getList)
+26 -1
Voir le fichier
@@ -1,9 +1,9 @@
import { reactive } from 'vue'
import { list, remove, fileList, fileRemove, batchDelete, fileBatchDelete } from '@/api/audit'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute } from 'vue-router'
import { formatTime } from '@/utils/time'
import { T } from '@/utils/i18n'
import { downBlob, jsonToCsv } from '@/utils/file'
export function useRepositories () {
const listRes = reactive({
@@ -73,6 +73,17 @@ export function useRepositories () {
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 {
listRes,
listQuery,
@@ -80,6 +91,7 @@ export function useRepositories () {
handlerQuery,
del,
batchdel,
toExport,
}
}
@@ -151,6 +163,18 @@ export function useFileRepositories () {
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 {
listRes,
listQuery,
@@ -158,5 +182,6 @@ export function useFileRepositories () {
handlerQuery,
del,
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 { batchDelete, list, remove } from '@/api/login_log'
import { list as fetchPeers } from '@/api/peer'
import { list as admin_fetchPeers } from '@/api/peer'
import { list as my_fetchPeers } from '@/api/my/peer'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute } from 'vue-router'
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 route = useRoute()
const user_id = route.query?.user_id
const apis = {
admin: { batchDelete: admin_batchDelete, list: admin_list, remove: admin_remove, fetchPeers: admin_fetchPeers },
my: { batchDelete: my_batchDelete, list: my_list, remove: my_remove, fetchPeers: my_fetchPeers },
}
export function useRepositories (api_type = 'my') {
const listRes = reactive({
list: [], total: 0, loading: false,
@@ -16,23 +21,29 @@ export function useRepositories () {
page: 1,
page_size: 10,
is_my: 0,
user_id: user_id ? parseInt(user_id) : null,
user_id: null,
})
const getList = async () => {
listRes.loading = true
const res = await list(listQuery).catch(_ => false)
const res = await apis[api_type].list(listQuery).catch(_ => false)
listRes.loading = false
if (res) {
const uuids = res.data.list.filter(item => item.uuid).map(item => item.uuid)
const peers = await fetchPeers({ uuids }).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)
}
})
//通过uuid补全peer信息
const uuids = res.data.list.filter(item => item.uuid && item.client === 'client' && !item.device_id).map(item => item.uuid)
if (uuids.length > 0) {
//uuids去重
const uniqueUuids = [...new Set(uuids)]
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.total = res.data.total
}
@@ -55,7 +66,7 @@ export function useRepositories () {
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) {
ElMessage.success(T('OperationSuccess'))
getList()
@@ -77,13 +88,28 @@ export function useRepositories () {
return false
}
const res = await batchDelete({ ids }).catch(_ => false)
const res = await apis[api_type].batchDelete({ ids }).catch(_ => false)
if (res) {
ElMessage.success(T('OperationSuccess'))
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 {
listRes,
listQuery,
@@ -91,5 +117,6 @@ export function useRepositories () {
handlerQuery,
del,
batchdel,
toExport,
}
}
+14 -5
Voir le fichier
@@ -13,8 +13,9 @@
</el-select>
</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="success" @click="toExport">{{ T('Export') }}</el-button>
</el-form-item>
</el-form>
</el-card>
@@ -28,15 +29,19 @@
</template>
</el-table-column>
<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="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" 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 :label="T('Actions')" align="center" width="400">
<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>
</el-table-column>
</el-table>
@@ -58,6 +63,8 @@
import { loadAllUsers } from '@/global'
import { useRepositories } from '@/views/login/log.js'
import { T } from '@/utils/i18n'
import { list } from '@/api/peer'
import { downBlob, jsonToCsv } from '@/utils/file'
const { allUsers, getAllUsers } = loadAllUsers()
getAllUsers()
@@ -69,7 +76,8 @@
handlerQuery,
del,
batchdel,
} = useRepositories()
toExport,
} = useRepositories('admin')
onMounted(getList)
onActivated(getList)
@@ -87,6 +95,7 @@
}
batchdel(multipleSelection.value)
}
</script>
<style scoped lang="scss">
+13 -4
Voir le fichier
@@ -3,9 +3,9 @@
<div class="login-card">
<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-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 :label="T('Password')">
@@ -25,7 +25,7 @@
</el-form-item>
</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>
</div>
@@ -78,6 +78,7 @@
password: '',
platform: platform,
captcha: '',
captcha_id: ''
})
const captchaCode = ref('')
@@ -87,6 +88,7 @@
if (!res.code) {
ElMessage.success(T('LoginSuccess'))
router.push({ path: redirect || '/', replace: true })
return
}
if (res.code === 110) {
// need captcha
@@ -98,6 +100,7 @@
const captchaRes = await captcha().catch(_ => false)
console.log(captchaRes)
captchaCode.value = captchaRes.data.captcha
form.captcha_id = captchaRes.data.captcha.id
}
const handleOIDCLogin = (provider) => {
@@ -123,11 +126,17 @@
}
const allowRegister = ref(false)
const disablePwd = ref(false)
const loadLoginOptions = async () => {
try {
const res = await loginOptions().catch(_ => false)
if (!res || !res.data) return console.error('No valid response received')
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
if (res.data.need_captcha) {
loadCaptcha()
@@ -285,4 +294,4 @@ h1 {
}
}
}
</style>
</style>
+8 -4
Voir le fichier
@@ -1,7 +1,7 @@
<template>
<div>
<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')">
<div>{{ userStore.username }}</div>
</el-form-item>
@@ -31,7 +31,7 @@
</el-form>
</el-card>
<el-card shadow="hover" style="margin-top: 20px">
<div v-html="appStore.setting.hello"></div>
<div v-html="html"></div>
</el-card>
<changePwdDialog v-model:visible="changePwdVisible"></changePwdDialog>
</div>
@@ -39,13 +39,14 @@
<script setup>
import changePwdDialog from '@/components/changePwdDialog.vue'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { useUserStore } from '@/store/user'
import { useAppStore } from '@/store/app'
import { bind, unbind } from '@/api/oauth'
import { myOauth } from '@/api/user'
import { ElMessageBox } from 'element-plus'
import { T } from '@/utils/i18n'
import { marked } from 'marked'
const appStore = useAppStore()
const userStore = useUserStore()
@@ -63,7 +64,7 @@
}
getMyOauth()
const toBind = async (row) => {
const res = await bind({ op: row.op}).catch(_ => false)
const res = await bind({ op: row.op }).catch(_ => false)
if (res) {
const { code, url } = res.data
window.open(url)
@@ -84,6 +85,9 @@
}
}
const html = computed(_ => marked(appStore.setting.hello||''))
</script>
<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="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="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="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">
+67 -9
Voir le fichier
@@ -14,6 +14,8 @@
<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="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="updated_at" :label="T('UpdatedAt')" align="center"/>
<el-table-column :label="T('Actions')" align="center">
@@ -35,7 +37,7 @@
</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" :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 v-for="item in types" :key="item.value" :value="item.value" style="display: block">
{{ 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-form-item>
<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 label="ClientId" prop="client_id">
<el-input v-model="formData.client_id"></el-input>
</el-form-item>
<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 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 :label="T('AutoRegister')" prop="auto_register">
<el-switch v-model="formData.auto_register"
@@ -81,6 +105,15 @@
import { list, create, update, detail, remove } from '@/api/oauth'
import { ElMessage, ElMessageBox } from 'element-plus'
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({
list: [], total: 0, loading: false,
@@ -92,7 +125,8 @@
const types = [
{ value: 'github', label: 'GitHub' },
{ value: 'google', label: 'Google' },
{ value: 'oidc', label: 'OIDC' }
{ value: 'linuxdo', label: 'LinuxDo' },
{ value: 'oidc', label: 'OIDC' },
]
const getList = async () => {
listRes.loading = true
@@ -145,14 +179,35 @@
redirect_url: '',
scopes: '',
auto_register: false,
pkce_enable: false,
pkce_method: 'S256',
})
const rules = {
client_id: [{ required: true, message: T('ParamRequired', { param: 'client_id' }), 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' }],
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) => {
formVisible.value = true
formData.id = row.id
@@ -161,10 +216,11 @@
formData.issuer = row.issuer
formData.client_id = row.client_id
formData.client_secret = row.client_secret
formData.redirect_url = row.redirect_url
// formData.redirect_url = row.redirect_url || defaultRedirect()
formData.scopes = row.scopes
formData.auto_register = row.auto_register
formData.pkce_enable = row.pkce_enable
formData.pkce_method = row.pkce_method
}
const toAdd = () => {
formVisible.value = true
@@ -174,9 +230,11 @@
formData.issuer = ''
formData.client_id = ''
formData.client_secret = ''
formData.redirect_url = ''
// formData.redirect_url = defaultRedirect()
formData.scopes = ''
formData.auto_register = false
formData.pkce_enable = false
formData.pkce_method = 'S256'
}
const form = ref(null)
const submit = async () => {
+222 -26
Voir le fichier
@@ -1,14 +1,14 @@
<template>
<div>
<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-input v-model="listQuery.id" clearable/>
</el-form-item>
<el-form-item :label="T('Hostname')">
<el-input v-model="listQuery.hostname" clearable/>
</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-option
v-for="item in timeFilters"
@@ -19,40 +19,87 @@
></el-option>
</el-select>
</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-button type="primary" @click="handlerQuery">{{ T('Filter') }}</el-button>
<el-button type="danger" @click="toAdd">{{ T('Add') }}</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="primary" @click="toBatchAddToAB">{{ T('BatchAddToAB') }}</el-button>
</el-form-item>
</el-form>
</el-card>
<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-column type="selection" width="55" align="center"/>
<el-table-column prop="id" label="ID" align="center" width="150">
<template #default="{row}">
<span>{{ row.id }} <el-icon @click="handleClipboard(row.id, $event)"><CopyDocument/></el-icon></span>
</template>
</el-table-column>
<el-table-column prop="cpu" label="CPU" align="center" width="100" show-overflow-tooltip/>
<el-table-column prop="hostname" :label="T('Hostname')" align="center" width="120"/>
<el-table-column prop="memory" :label="T('Memory')" align="center" width="120"/>
<el-table-column prop="os" :label="T('Os')" align="center" width="120" show-overflow-tooltip/>
<el-table-column prop="last_online_time" :label="T('LastOnlineTime')" align="center" min-width="120">
<template #default="{row}">
<div class="last_oline_time">
<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>
</template>
</el-table-column>
<el-table-column prop="last_online_ip" :label="T('LastOnlineIp')" align="center" min-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="version" :label="T('Version')" align="center" width="80"/>
<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"/>
<template v-for="c in visibleColumns.filter(cc => cc.visible)" :key="c">
<el-table-column v-if="c.name==='id'" prop="id" label="ID" align="center" width="150">
<template #default="{row}">
<span>{{ row.id }} <el-icon @click="handleClipboard(row.id, $event)"><CopyDocument/></el-icon></span>
</template>
</el-table-column>
<el-table-column v-if="c.name==='cpu'" prop="cpu" label="CPU" align="center" width="100" show-overflow-tooltip/>
<el-table-column v-if="c.name==='hostname'" prop="hostname" :label="T('Hostname')" align="center" width="120"/>
<el-table-column v-if="c.name==='memory'" prop="memory" :label="T('Memory')" align="center" width="120"/>
<el-table-column v-if="c.name==='os'" prop="os" :label="T('Os')" align="center" width="120" show-overflow-tooltip/>
<el-table-column v-if="c.name==='last_online_time'" prop="last_online_time" :label="T('LastOnlineTime')" align="center" min-width="120">
<template #default="{row}">
<div class="last_oline_time">
<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>
</template>
</el-table-column>
<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 v-if="c.name==='username'" prop="username" :label="T('Username')" align="center" width="120"/>
<el-table-column v-if="c.name==='group_id'" prop="group_id" :label="T('Group')" align="center" width="120">
<template #default="{row}">
<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">
<template #default="{row}">
<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-input v-model="formData.id"></el-input>
</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-input v-model="formData.username"></el-input>
</el-form-item>
@@ -99,7 +156,9 @@
<el-form-item :label="T('Version')" prop="version">
<el-input v-model="formData.version"></el-input>
</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-button @click="formVisible = false">{{ T('Cancel') }}</el-button>
<el-button @click="submit" type="primary">{{ T('Submit') }}</el-button>
@@ -145,12 +204,35 @@
</el-form-item>
</el-form>
</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>
</template>
<script setup>
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'
import { batchRemove, create, list, remove, update } from '@/api/peer'
import { list as groupList } from '@/api/device_group'
import { ElMessage, ElMessageBox } from 'element-plus'
import { toWebClientLink } from '@/utils/webclient'
import { T } from '@/utils/i18n'
@@ -159,13 +241,35 @@
import { loadAllUsers } from '@/global'
import { useAppStore } from '@/store/app'
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 { batchCreateFromPeers } from '@/api/address_book'
import { useRepositories as useCollectionRepositories } from '@/views/address_book/collection'
import createABForm from '@/views/peer/createABForm.vue'
import { UploadFilled } from '@element-plus/icons-vue'
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({
list: [], total: 0, loading: false,
})
@@ -175,6 +279,8 @@
time_ago: null,
id: '',
hostname: '',
username: '',
ip: '',
})
const getList = async () => {
@@ -220,6 +326,7 @@
const formVisible = ref(false)
const formData = reactive({
row_id: 0,
group_id: null,
cpu: '',
hostname: '',
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 clickRow = ref({})
const toAddressBook = (row) => {
@@ -370,6 +525,47 @@
}
// 批量添加到地址簿 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>
<style scoped lang="scss">
+2
Voir le fichier
@@ -35,6 +35,7 @@
import { useRoute, useRouter } from 'vue-router'
import { register } from '@/api/user'
import { useUserStore } from '@/store/user'
import { useAppStore } from '@/store/app'
const router = useRouter()
const userStore = useUserStore()
@@ -78,6 +79,7 @@
return
}
userStore.saveUserData(res.data)
useAppStore().loadConfig()
ElMessage.success('Submit')
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 }
}
if (id > 0) {
onMounted(getDetail(id))
onMounted(_ => {getDetail(id)})
}
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 as groups } from '@/api/group'
import { useRouter } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus'
import { T } from '@/utils/i18n'
import { downBlob, jsonToCsv } from '@/utils/file'
export function useRepositories () {
@@ -42,18 +43,24 @@ export function useRepositories () {
listRes.groups = res.data.list
}
}
onMounted(getGroups)
onMounted(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
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, 'users.csv')
}
}
return {
listRes,
listQuery,
handlerQuery,
getList,
getGroups,
toExport,
}
}
+3
Voir le fichier
@@ -32,6 +32,9 @@
:inactive-value="DISABLE_STATUS"
></el-switch>
</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-button @click="cancel">{{ T('Cancel') }}</el-button>
<el-button @click="submit" type="primary">{{ T('Submit') }}</el-button>
+38
Voir le fichier
@@ -8,6 +8,7 @@
<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" @click="toExport">{{ T('Export') }}</el-button>
</el-form-item>
</el-form>
</el-card>
@@ -23,6 +24,16 @@
<span v-else> - </span>
</template>
</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="updated_at" :label="T('UpdatedAt')" align="center"/>
<el-table-column :label="T('Actions')" align="center" width="650">
@@ -51,14 +62,27 @@
<script setup>
import { useRepositories, useDel, useToEditOrAdd, useChangePwd } from '@/views/user/composables'
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 {
listRes,
listQuery,
handlerQuery,
getList,
getGroups,
toExport,
} = useRepositories()
onMounted(getGroups)
onMounted(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
const { toEdit, toAdd, toAddressBook, toTag } = useToEditOrAdd()
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>