Files
ruoyi-vue/src/views/jarvis/docSync/components/KdocsCloudConfig.vue
2026-03-23 23:56:45 +08:00

446 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="kdocs-config">
<div class="config-container">
<div class="config-left">
<el-card class="config-section">
<div slot="header" class="section-header">
<i class="el-icon-key"></i>
<span>授权状态</span>
</div>
<div class="auth-status">
<el-tag v-if="isAuthorized" type="success" size="medium">
<i class="el-icon-circle-check"></i> 已授权
</el-tag>
<el-tag v-else type="danger" size="medium">
<i class="el-icon-circle-close"></i> 未授权
</el-tag>
<el-button
v-if="!isAuthorized"
type="primary"
size="small"
icon="el-icon-unlock"
@click="handleAuthorize"
style="margin-left: 10px;"
>立即授权</el-button>
<el-button
v-else
type="info"
size="small"
icon="el-icon-refresh"
@click="handleRefreshAuth"
style="margin-left: 10px;"
>刷新状态</el-button>
</div>
<div v-if="isAuthorized && tokenInfo" class="token-info" style="margin-top: 15px;">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="用户ID">{{ tokenInfo.userId || '-' }}</el-descriptions-item>
<el-descriptions-item label="Token状态">
<el-tag v-if="tokenInfo.isValid" type="success" size="small">有效</el-tag>
<el-tag v-else type="warning" size="small">已过期</el-tag>
</el-descriptions-item>
<el-descriptions-item v-if="tokenInfo.expiresIn" label="有效期">
{{ Math.floor(tokenInfo.expiresIn / 60) }} 分钟
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<el-card class="config-section">
<div slot="header" class="section-header">
<i class="el-icon-document"></i>
<span>H-TF订单自动写入配置</span>
<el-tag v-if="config.isConfigured" type="success" size="mini" style="margin-left: 10px;">已配置</el-tag>
<el-tag v-else type="warning" size="mini" style="margin-left: 10px;">未配置</el-tag>
</div>
<el-form ref="form" :model="form" :rules="rules" label-width="120px" size="small">
<el-form-item label="file_token" prop="fileId">
<el-input
v-model="form.fileId"
placeholder="个人云文档列表接口返回的 file_token文档 open_id"
clearable
>
<el-button
slot="append"
icon="el-icon-search"
:disabled="!form.fileId || !isAuthorized"
@click="handleTestRead"
>测试读取</el-button>
</el-input>
</el-form-item>
<el-form-item label="工作表 sheet_idx">
<el-input-number v-model="form.sheetIdx" :min="0" controls-position="right" style="width: 100%;" />
</el-form-item>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="表头行号" prop="headerRow">
<el-input-number v-model="form.headerRow" :min="1" controls-position="right" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="起始行号" prop="startRow">
<el-input-number v-model="form.startRow" :min="1" controls-position="right" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" :loading="saveLoading" icon="el-icon-check" @click="handleSave">保存配置</el-button>
<el-button :loading="testLoading" icon="el-icon-setting" @click="handleTest">测试配置</el-button>
<el-button type="danger" plain :loading="clearLoading" icon="el-icon-delete" @click="handleClear">清除配置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<div class="config-right">
<el-card class="status-card-wrapper">
<div class="status-card" :class="config.isConfigured ? 'success' : 'warning'">
<div class="status-icon" :class="config.isConfigured ? 'success' : 'warning'">
<i :class="config.isConfigured ? 'el-icon-success' : 'el-icon-warning'"></i>
</div>
<div class="status-text">
<div class="status-title">{{ config.isConfigured ? '配置完成' : '配置未完成' }}</div>
<div class="status-desc">
{{ config.hint || (config.isConfigured ? 'H-TF订单将写入金山文档在线表格工作表' : '请完成配置') }}
</div>
</div>
</div>
</el-card>
<el-card class="help-card-wrapper">
<div slot="header" class="card-header">
<i class="el-icon-question"></i>
<span>说明</span>
</div>
<div class="help-content">
<div class="help-item"><i class="el-icon-check"></i><span>file_token 来自在线表格管理文件列表或开放平台文档列表</span></div>
<div class="help-item"><i class="el-icon-check"></i><span>工作表读写使用 KSheet 单元格接口sheet_idx 与后台 sheet 一致</span></div>
<div class="help-item"><i class="el-icon-check"></i><span>数据表db需改用数据表记录类 API本页为工作表et场景</span></div>
</div>
</el-card>
</div>
</div>
</div>
</template>
<script>
import {
getKdocsAuthUrl,
getKdocsTokenStatus,
readKdocsCells,
updateKdocsCells
} from '@/api/jarvis/kdocs'
const LS_KEY = 'kdocs_auto_write_config'
export default {
name: 'KdocsCloudConfig',
data() {
return {
isAuthorized: false,
userId: 'default_user',
tokenInfo: null,
config: { isConfigured: false, hint: '' },
form: {
fileId: '',
sheetIdx: 0,
headerRow: 2,
startRow: 3
},
rules: {
fileId: [{ required: true, message: '请输入 file_token', trigger: 'blur' }],
headerRow: [
{ required: true, message: '请输入表头行号', trigger: 'blur' },
{ type: 'number', min: 1, message: '表头行号必须大于0', trigger: 'blur' }
],
startRow: [
{ required: true, message: '请输入数据起始行', trigger: 'blur' },
{ type: 'number', min: 1, message: '数据起始行必须大于0', trigger: 'blur' }
]
},
saveLoading: false,
testLoading: false,
clearLoading: false
}
},
created() {
this.checkAuthStatus()
this.loadConfig()
},
methods: {
refresh() {
this.checkAuthStatus()
this.loadConfig()
},
async checkAuthStatus() {
try {
const response = await getKdocsTokenStatus(this.userId)
if (response.code === 200) {
// 有 token 即视为已授权isValid 单独展示(避免 expires_in 缺失时误判未保存)
this.isAuthorized = !!response.data.hasToken
this.tokenInfo = response.data
if (response.data.userId) {
this.userId = response.data.userId
}
}
} catch (e) {
console.error(e)
}
},
loadConfig() {
try {
const raw = localStorage.getItem(LS_KEY)
if (raw) {
const c = JSON.parse(raw)
this.form.fileId = c.fileId || ''
this.form.sheetIdx = c.sheetIdx != null ? c.sheetIdx : 0
this.form.headerRow = c.headerRow || 2
this.form.startRow = c.startRow || 3
this.config.isConfigured = !!c.fileId
}
} catch (e) {
console.error(e)
}
},
extractValues(payload) {
if (!payload) return []
if (Array.isArray(payload.values)) return payload.values
if (payload.data && Array.isArray(payload.data.values)) return payload.data.values
return []
},
async handleAuthorize() {
try {
const response = await getKdocsAuthUrl()
if (response.code === 200) {
const w = 600
const h = 700
window.open(
response.data,
'KdocsAuth',
`width=${w},height=${h},left=${(screen.width - w) / 2},top=${(screen.height - h) / 2},resizable=yes,scrollbars=yes`
)
this.$message.success('请在弹出窗口完成授权')
const handler = (event) => {
if (event.data && event.data.type === 'kdocs_oauth_callback') {
window.removeEventListener('message', handler)
if (event.data.userId) {
this.userId = event.data.userId
}
setTimeout(() => {
this.checkAuthStatus()
this.$message.success('授权完成')
}, 500)
}
}
window.addEventListener('message', handler)
setTimeout(() => this.checkAuthStatus(), 3000)
}
} catch (e) {
this.$message.error(e.msg || e.message || '获取授权地址失败')
}
},
async handleRefreshAuth() {
await this.checkAuthStatus()
this.$message.success('已刷新')
},
async handleTestRead() {
if (!this.isAuthorized) {
this.$message.warning('请先授权')
return
}
if (!this.form.fileId) {
this.$message.warning('请输入 file_token')
return
}
try {
const response = await readKdocsCells({
userId: this.userId,
fileToken: this.form.fileId,
sheetIdx: this.form.sheetIdx,
range: 'A1:B5'
})
if (response.code === 200) {
this.$message.success('读取成功')
console.log('read', response.data)
} else {
this.$message.warning(response.msg || '读取失败')
}
} catch (e) {
this.$message.error(e.msg || e.message || '读取失败')
}
},
handleSave() {
this.$refs.form.validate(async (valid) => {
if (!valid) return
if (!this.isAuthorized) {
this.$message.warning('请先授权')
return
}
this.saveLoading = true
try {
localStorage.setItem(
LS_KEY,
JSON.stringify({
fileId: this.form.fileId,
sheetIdx: this.form.sheetIdx,
headerRow: this.form.headerRow,
startRow: this.form.startRow
})
)
this.config.isConfigured = true
this.config.hint = '将使用金山文档 KSheet 写入工作表'
this.$message.success('已保存')
} finally {
this.saveLoading = false
}
})
},
handleTest() {
if (!this.isAuthorized) {
this.$message.warning('请先授权')
return
}
this.$refs.form.validate(async (valid) => {
if (!valid) return
this.testLoading = true
try {
const readRes = await readKdocsCells({
userId: this.userId,
fileToken: this.form.fileId,
sheetIdx: this.form.sheetIdx,
range: 'A1:B5'
})
if (readRes.code !== 200) {
this.$message.error(readRes.msg || '读失败')
return
}
const testRange = `A${this.form.startRow}:B${this.form.startRow}`
const writeRes = await updateKdocsCells({
userId: this.userId,
fileToken: this.form.fileId,
sheetIdx: this.form.sheetIdx,
range: testRange,
values: [['测试1', '测试2']]
})
if (writeRes.code === 200) {
this.$message.success('读写测试成功')
} else {
this.$message.warning(writeRes.msg || '写失败')
}
} catch (e) {
this.$message.error(e.msg || e.message || '测试失败')
} finally {
this.testLoading = false
}
})
},
handleClear() {
this.$confirm('确定清除本地配置?', '提示', { type: 'warning' })
.then(() => {
localStorage.removeItem(LS_KEY)
this.form.fileId = ''
this.form.sheetIdx = 0
this.form.headerRow = 2
this.form.startRow = 3
this.config.isConfigured = false
this.config.hint = ''
this.$message.success('已清除')
})
.catch(() => {})
}
}
}
</script>
<style scoped>
.kdocs-config {
padding: 0;
}
.config-container {
display: flex;
gap: 20px;
}
.config-left {
flex: 1;
min-width: 0;
}
.config-right {
width: 300px;
flex-shrink: 0;
}
.config-section {
margin-bottom: 20px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
}
.auth-status {
display: flex;
align-items: center;
}
.status-card-wrapper {
margin-bottom: 20px;
}
.status-card {
display: flex;
align-items: center;
padding: 15px;
border-radius: 4px;
}
.status-card.success {
background-color: #f0f9ff;
border: 1px solid #b3d8ff;
}
.status-card.warning {
background-color: #fef0f0;
border: 1px solid #fbc4c4;
}
.status-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-right: 15px;
}
.status-icon.success {
background-color: #67c23a;
color: white;
}
.status-icon.warning {
background-color: #e6a23c;
color: white;
}
.status-text {
flex: 1;
}
.status-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 5px;
}
.status-desc {
font-size: 12px;
color: #909399;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
}
.help-item {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 10px;
font-size: 13px;
color: #606266;
}
.help-item i {
color: #67c23a;
margin-top: 3px;
}
</style>