From 117e2b34403e29539b5d492da0b06d4a7be4a2d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com>
Date: Thu, 13 Nov 2025 11:28:00 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=AA=E8=A1=A8=E7=9B=98?=
=?UTF-8?q?=E7=AD=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pc/src/api/tasks.js | 42 ++
pc/src/components/CommonHeader.vue | 112 +++---
.../views/apps/oa/tasks/components/detail.vue | 61 +++
.../views/apps/oa/tasks/components/edit.vue | 113 ++++++
pc/src/views/apps/oa/tasks/index.vue | 12 +-
pc/src/views/apps/oa/workbench/index.vue | 93 +----
pc/src/views/dashboard/index.vue | 364 ++++++++++--------
.../views/system/tenant/components/detail.vue | 96 +++--
.../views/system/tenant/components/edit.vue | 111 +++++-
pc/src/views/system/tenant/index.vue | 168 ++++----
server/controllers/task.go | 44 +++
server/database/sys_feedback.sql | 20 +
server/main.go | 3 +
server/models/file.go | 4 +
server/models/task.go | 15 +
server/models/tenant.go | 39 +-
server/routers/router.go | 3 +
server/services/task.go | 5 +
18 files changed, 863 insertions(+), 442 deletions(-)
create mode 100644 server/database/sys_feedback.sql
diff --git a/pc/src/api/tasks.js b/pc/src/api/tasks.js
index 07bc95a..dc4e27d 100644
--- a/pc/src/api/tasks.js
+++ b/pc/src/api/tasks.js
@@ -1,5 +1,10 @@
import request from '@/utils/request'
+/**
+ * 获取任务列表
+ * @param {Object} params 查询参数
+ * @returns {Promise}
+ */
export function listTasks(params) {
return request({
url: '/api/oa/tasks',
@@ -8,6 +13,11 @@ export function listTasks(params) {
})
}
+/**
+ * 获取任务详情
+ * @param {number|string} id 任务ID
+ * @returns {Promise}
+ */
export function getTask(id) {
return request({
url: `/api/oa/tasks/${id}`,
@@ -15,6 +25,11 @@ export function getTask(id) {
})
}
+/**
+ * 创建任务
+ * @param {Object} data 任务数据
+ * @returns {Promise}
+ */
export function createTask(data) {
return request({
url: '/api/oa/tasks',
@@ -23,6 +38,12 @@ export function createTask(data) {
})
}
+/**
+ * 更新任务
+ * @param {number|string} id 任务ID
+ * @param {Object} data 更新的数据
+ * @returns {Promise}
+ */
export function updateTask(id, data) {
return request({
url: `/api/oa/tasks/${id}`,
@@ -31,9 +52,30 @@ export function updateTask(id, data) {
})
}
+/**
+ * 删除任务
+ * @param {number|string} id 任务ID
+ * @returns {Promise}
+ */
export function deleteTask(id) {
return request({
url: `/api/oa/tasks/${id}`,
method: 'delete'
})
}
+
+/**
+ * 获取待办任务列表
+ * @param {Object} params
+ * @param {number} params.tenantId 租户ID
+ * @param {number} params.id 用户ID
+ * @param {number} [params.limit] 限制条数
+ * @returns {Promise}
+ */
+export function getTodoTasks(params = {}) {
+ return request({
+ url: '/api/oa/tasks/todo',
+ method: 'get',
+ params,
+ });
+}
\ No newline at end of file
diff --git a/pc/src/components/CommonHeader.vue b/pc/src/components/CommonHeader.vue
index 7be6a5e..9320972 100644
--- a/pc/src/components/CommonHeader.vue
+++ b/pc/src/components/CommonHeader.vue
@@ -5,36 +5,22 @@
- 首页
-
+ 首页
+
{{ item.label }}
-
-
+
+
-
-
+
+
@@ -43,11 +29,15 @@
-
+
+
+
个人中心
-
+
+
+
退出登录
@@ -56,13 +46,14 @@
+
diff --git a/pc/src/views/apps/oa/tasks/components/edit.vue b/pc/src/views/apps/oa/tasks/components/edit.vue
index 2e7b013..24b9830 100644
--- a/pc/src/views/apps/oa/tasks/components/edit.vue
+++ b/pc/src/views/apps/oa/tasks/components/edit.vue
@@ -46,6 +46,25 @@
+
+
+
+
+
+
+
+
({
// 关联人(仅提交 id 数组在 team_employee_ids)
const teamEmployeeIds = ref>([])
const teamEmployeeList = ref>([])
+// 附件
+const attachmentIds = ref>([])
+const uploadFileList = ref([])
+const previewVisible = ref(false)
+const previewImageUrl = ref('')
+const resolveFileUrl = (path: string): string => {
+ if (!path) return ''
+ if (/^https?:\/\//i.test(path)) return path
+ const base = (request as any)?.defaults?.baseURL || ''
+ try {
+ const origin = new URL(base, window.location.origin).origin
+ return origin.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '')
+ } catch {
+ return path
+ }
+}
const authStore = useAuthStore()
const loadTeamEmployeeNames = async (ids: Array) => {
if (!ids || ids.length === 0) {
@@ -183,6 +221,16 @@ watch(() => props.task, (t) => {
} else {
loadTeamEmployeeNames(teamEmployeeIds.value)
}
+ // 初始化附件
+ if (Array.isArray(t?.attachment_ids)) {
+ attachmentIds.value = [...t.attachment_ids]
+ } else if (typeof t?.attachment_ids === 'string' && t.attachment_ids) {
+ attachmentIds.value = t.attachment_ids.split(',').map((x: string) => x.trim()).filter(Boolean)
+ } else {
+ attachmentIds.value = []
+ }
+ // 无法从任务上还原文件URL时,保持为空列表,由用户重新上传
+ uploadFileList.value = uploadFileList.value
}, { immediate: true })
const rules = reactive>({
@@ -239,6 +287,14 @@ const onSubmit = async () => {
const n = Number(x)
return isNaN(n) ? x : n
})
+ // 追加附件 ID,逗号分隔(后端字段为 attachment_ids),如果返回了 id
+ if (attachmentIds.value.length) {
+ const ids = (attachmentIds.value || []).map((x: any) => {
+ const n = Number(x)
+ return isNaN(n) ? String(x) : String(n)
+ })
+ payload.attachment_ids = ids.join(',')
+ }
if (props.isEdit && form.id) {
res = await updateTask(form.id, payload)
@@ -264,6 +320,63 @@ const onSubmit = async () => {
const onClosed = () => {
// reset if needed
}
+
+// 上传前校验(参考租户编辑)
+const beforeUpload = (file: File) => {
+ const isImage = file.type.startsWith('image/')
+ const isLt5M = file.size / 1024 / 1024 < 5
+ if (!isImage) {
+ ElMessage.error('仅支持图片类型')
+ return false
+ }
+ if (!isLt5M) {
+ ElMessage.error('图片大小不能超过 5MB')
+ return false
+ }
+ return true
+}
+
+// 自定义上传(参考租户编辑)
+const handleCustomUpload = async (options: any) => {
+ const { file, onError, onSuccess } = options
+ try {
+ const formData = new FormData()
+ formData.append('file', file as File)
+ const res = await uploadFile(formData, { category: '图片' })
+ onSuccess && onSuccess(res)
+ } catch (err) {
+ ElMessage.error('上传失败')
+ onError && onError(err)
+ }
+}
+
+// 上传回调(兼容 id 与 file_url 返回)
+const handleUploadSuccess = (response: any, file: any, fileList: any[]) => {
+ const id = response?.data?.id ?? response?.id
+ const rawUrl = response?.data?.file_url || response?.data?.url || response?.url || ''
+ if (id != null) {
+ attachmentIds.value.push(id)
+ }
+ const abs = rawUrl ? resolveFileUrl(rawUrl) : (file?.url || '')
+ // 用服务器返回的绝对地址覆盖当前项的 url,避免后续预览使用失效的 blob: URL
+ uploadFileList.value = fileList.map((f: any) => {
+ if (f.uid === file.uid) {
+ return { ...f, url: abs }
+ }
+ return { ...f, url: f.url }
+ })
+ if (abs) ElMessage.success('上传成功')
+}
+const handleUploadRemove = (file: any, fileList: any[]) => {
+ const id = file?.response?.data?.id ?? file?.name
+ attachmentIds.value = attachmentIds.value.filter(x => String(x) !== String(id))
+ uploadFileList.value = fileList
+}
+const handleUploadPreview = (file: any) => {
+ const url = file?.url || ''
+ previewImageUrl.value = url.startsWith('blob:') ? url : resolveFileUrl(url)
+ previewVisible.value = !!previewImageUrl.value
+}
diff --git a/pc/src/views/system/tenant/components/detail.vue b/pc/src/views/system/tenant/components/detail.vue
index aacb308..e08eb95 100644
--- a/pc/src/views/system/tenant/components/detail.vue
+++ b/pc/src/views/system/tenant/components/detail.vue
@@ -1,11 +1,5 @@
-
+
@@ -14,9 +8,6 @@
{{ tenantData.name }}
-
- {{ tenantData.code }}
-
{{ tenantData.owner }}
@@ -27,12 +18,12 @@
{{ tenantData.email || '未设置' }}
-
+
{{ getAuditStatusText(tenantData.audit_status) }}
-
+
{{ getTenantStatusText(tenantData.status) }}
@@ -40,19 +31,28 @@
{{ formatCapacity(tenantData.capacity) }}
- {{ formatCapacity(tenantData.capacity_used || 0) }}
-
+
+ {{ formatCapacity(tenantData.capacity_used || 0) }}
+
+
+
+
+
+
+
+
+ 无
+
- {{ tenantData.created_at }}
+ {{ formatTime(tenantData.created_at) }}
- {{ tenantData.updated_at || '未更新' }}
+ {{ formatTime(tenantData.updated_at) || '未更新' }}
{{ tenantData.remark || '无' }}
@@ -69,7 +69,9 @@
import { ref, watch, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { getTenantDetail } from '@/api/tenant';
+import request from '@/utils/request';
import { useDictStore } from '@/stores/dict';
+import dayjs from 'dayjs';
interface Props {
modelValue: boolean;
@@ -125,7 +127,12 @@ function getAuditStatusType(status: string): 'warning' | 'success' | 'danger' |
if (dictItem) {
return dictItem.dict_tag_type || 'info';
}
-
+
+ //格式化时间
+ function formatTime(time: string | number | Date): string {
+ return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
+ }
+
// 兜底方案,保持原有逻辑
const statusMap: Record = {
pending: 'warning',
@@ -143,7 +150,7 @@ function getAuditStatusText(status: string) {
if (dictItem) {
return dictItem.dict_label || String(status);
}
-
+
// 兜底方案,保持原有逻辑
const statusMap: Record = {
pending: '待审核',
@@ -162,7 +169,7 @@ function getTenantStatusType(status: string | number): 'success' | 'info' | 'war
if (dictItem) {
return dictItem.dict_tag_type || 'info';
}
-
+
// 兜底方案,保持原有逻辑
return String(status) === '1' ? 'success' : 'info';
}
@@ -175,7 +182,7 @@ function getTenantStatusText(status: string | number): string {
if (dictItem) {
return dictItem.dict_label || String(status);
}
-
+
// 兜底方案,保持原有逻辑
return String(status) === '1' ? '启用' : '禁用';
}
@@ -213,10 +220,22 @@ function getCapacityColor(capacity: number | undefined, used: number | undefined
return '#67c23a'; // 绿色
}
+function resolveFileUrl(path: string): string {
+ if (!path) return '';
+ if (/^https?:\/\//i.test(path)) return path;
+ const base = (request as any)?.defaults?.baseURL || '';
+ if (!base) return path;
+ return base.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
+}
+
+function formatTime(time: string | number | Date): string {
+ return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
+}
+
// 获取租户详情
async function fetchDetail() {
if (!props.tenantId) return;
-
+
loading.value = true;
try {
const res = await getTenantDetail(props.tenantId);
@@ -277,6 +296,11 @@ watch(
margin-top: 0;
}
+ :deep(.el-descriptions__cell) {
+ width: 180px;
+ text-align: right;
+ }
+
:deep(.el-descriptions-item__label) {
font-weight: 600;
color: var(--el-text-color-regular);
@@ -286,5 +310,23 @@ watch(
color: var(--el-text-color-primary);
}
}
-
+.pending-color {
+ color: #e6a23c;
+ background-color: #fdf6ec;
+ border-color: #f5dab1;
+}
+
+.approved-color,
+.color-1 {
+ color: #67c23a;
+ background-color: rgb(240, 249, 235);
+ border-color: rgb(225, 243, 216);
+}
+
+.rejected-color {
+ color: #f56c6c;
+ background-color: #fef0f0;
+ border-color: #fbc4c4;
+}
+
diff --git a/pc/src/views/system/tenant/components/edit.vue b/pc/src/views/system/tenant/components/edit.vue
index 36fda09..f6a9c79 100644
--- a/pc/src/views/system/tenant/components/edit.vue
+++ b/pc/src/views/system/tenant/components/edit.vue
@@ -15,9 +15,6 @@
-
-
-
@@ -56,6 +53,24 @@
placeholder="请输入备注"
/>
+
+
+
+
+
+
+
+
取消
@@ -73,8 +88,14 @@ import {
ElMessageBox,
type FormInstance,
type FormRules,
+ type UploadFile,
+ type UploadUserFile,
+ type UploadRequestOptions,
} from 'element-plus';
+import { Plus } from '@element-plus/icons-vue';
import { createTenant, updateTenant } from '@/api/tenant';
+import { uploadFile } from '@/api/file';
+import request from '@/utils/request';
import { useDictStore } from '@/stores/dict';
interface Props {
@@ -99,6 +120,9 @@ const visible = computed({
const submitting = ref(false);
const tenantFormRef = ref();
+const uploadFileList = ref([]);
+const previewVisible = ref(false);
+const previewUrl = ref('');
// 字典数据
const dictStore = useDictStore();
@@ -122,18 +146,17 @@ const isEditing = computed(() => {
const tenantForm = reactive({
id: null as number | null,
name: '',
- code: '',
owner: '',
phone: '',
email: '',
- status: '1', // 改为字符串类型,与字典数据中的dict_value保持一致
+ status: '1', // 改为字符串类型,与字典数据中的dict_value保持一致
capacity: 0,
remark: '',
+ attachment_url: '' as string,
});
const formRules: FormRules = {
name: [{ required: true, message: '请输入租户名称', trigger: 'blur' }],
- code: [{ required: true, message: '请输入租户编码', trigger: 'blur' }],
owner: [{ required: true, message: '请输入负责人', trigger: 'blur' }],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' },
@@ -146,17 +169,30 @@ const formRules: FormRules = {
],
};
+function resolveFileUrl(path: string): string {
+ if (!path) return '';
+ if (/^https?:\/\//i.test(path)) return path;
+ const base = (request as any)?.defaults?.baseURL || '';
+ try {
+ const origin = new URL(base, window.location.origin).origin;
+ return origin.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
+ } catch {
+ return path;
+ }
+}
+
// 重置表单
function resetForm() {
tenantForm.id = null;
tenantForm.name = '';
- tenantForm.code = '';
tenantForm.owner = '';
tenantForm.phone = '';
tenantForm.email = '';
- tenantForm.status = '1'; // 改为字符串类型,与字典数据中的dict_value保持一致
+ tenantForm.status = '1'; // 改为字符串类型,与字典数据中的dict_value保持一致
tenantForm.capacity = 0;
tenantForm.remark = '';
+ tenantForm.attachment_url = '';
+ uploadFileList.value = [];
tenantFormRef.value?.resetFields();
}
@@ -165,7 +201,6 @@ function initFormData() {
if (props.tenant && props.tenant.id) {
tenantForm.id = props.tenant.id;
tenantForm.name = props.tenant.name || '';
- tenantForm.code = props.tenant.code || '';
tenantForm.owner = props.tenant.owner || '';
tenantForm.phone = props.tenant.phone || '';
tenantForm.email = props.tenant.email || '';
@@ -174,6 +209,10 @@ function initFormData() {
// capacity 后端返回的是 MB,直接使用
tenantForm.capacity = props.tenant.capacity != null ? Number(props.tenant.capacity) : 0;
tenantForm.remark = props.tenant.remark || '';
+ tenantForm.attachment_url = props.tenant.attachment_url || '';
+ uploadFileList.value = tenantForm.attachment_url
+ ? [{ name: '附件', url: resolveFileUrl(tenantForm.attachment_url) }]
+ : [];
} else {
resetForm();
}
@@ -188,14 +227,14 @@ async function handleSubmit() {
try {
const tenantData = {
name: tenantForm.name,
- code: tenantForm.code,
owner: tenantForm.owner,
phone: tenantForm.phone || '',
email: tenantForm.email || '',
- status: Number(tenantForm.status), // 提交时转换为数字类型
+ status: Number(tenantForm.status), // 提交时转换为数字类型
// capacity 前后端都使用 MB
capacity: tenantForm.capacity || 0,
remark: tenantForm.remark || '',
+ attachment_url: tenantForm.attachment_url || '',
};
let res;
@@ -253,6 +292,55 @@ watch(
},
{ deep: true }
);
+
+const beforeUpload = (file: File) => {
+ const isImage = file.type.startsWith('image/');
+ const isLt5M = file.size / 1024 / 1024 < 5;
+ if (!isImage) {
+ ElMessage.error('仅支持图片类型');
+ return false;
+ }
+ if (!isLt5M) {
+ ElMessage.error('图片大小不能超过 5MB');
+ return false;
+ }
+ return true;
+};
+
+const handleUploadSuccess = (response: any, file: UploadFile) => {
+ const raw = response?.data?.file_url || response?.data?.url || response?.url || '';
+ if (raw) {
+ tenantForm.attachment_url = raw;
+ const abs = resolveFileUrl(raw);
+ uploadFileList.value = [{ name: file.name, url: abs }];
+ ElMessage.success('上传成功');
+ } else {
+ ElMessage.error('上传成功但未返回URL');
+ }
+};
+
+const handleRemove = () => {
+ tenantForm.attachment_url = '';
+ uploadFileList.value = [];
+};
+
+const handlePreview = (file: UploadFile) => {
+ previewUrl.value = (file.url as string) || tenantForm.attachment_url || '';
+ if (previewUrl.value) previewVisible.value = true;
+};
+
+const handleCustomUpload = async (options: UploadRequestOptions) => {
+ const { file, onError, onSuccess } = options as any;
+ try {
+ const formData = new FormData();
+ formData.append('file', file as File);
+ const res = await uploadFile(formData, { category: '图片' });
+ onSuccess && onSuccess(res);
+ } catch (err) {
+ ElMessage.error('上传失败');
+ onError && onError(err);
+ }
+};
-
diff --git a/pc/src/views/system/tenant/index.vue b/pc/src/views/system/tenant/index.vue
index a823079..912c4b4 100644
--- a/pc/src/views/system/tenant/index.vue
+++ b/pc/src/views/system/tenant/index.vue
@@ -4,11 +4,15 @@
租户管理
@@ -32,17 +36,28 @@
-
+
-
-
+
+
+
+
+ {{ getAuditStatusText(row.audit_status) }}
+
+ -
+
+
+
+
+
+ {{ getTenantStatusText(row.status) }}
+
+ -
+
+
@@ -51,60 +66,41 @@
/
{{ formatCapacity(row.capacity ?? 0) }}
-
+
-
-
+
-
- {{ getAuditStatusText(row.audit_status) }}
-
-
-
-
-
-
- {{ getTenantStatusText(row.status) }}
-
+ {{ formatTime(row.created_at) }}
-
+
+
+
查看
-
-
+
+
+
+
审核
-
-
+
+
+
+
编辑
-
-
+
+
+
+
删除
@@ -113,36 +109,19 @@
-
+
-
+
-
+
@@ -155,6 +134,7 @@ import { useDictStore } from '@/stores/dict';
import TenantDetail from './components/detail.vue';
import TenantAudit from './components/audit.vue';
import TenantEdit from './components/edit.vue';
+import dayjs from 'dayjs';
// 字典数据
const reviewStatusDict = ref([]);
@@ -229,6 +209,12 @@ const handlePageChange = (val: number) => {
fetchTenants();
};
+//格式化时间
+function formatTime(time: string | null | undefined) {
+ if (!time) return '';
+ return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
+}
+
// 刷新界面
async function refresh() {
try {
@@ -250,14 +236,14 @@ function getAuditStatusType(
if (dictItem) {
return dictItem.dict_tag_type || 'info';
}
-
+
// 兜底方案,保持原有逻辑
const statusMap: Record =
- {
- pending: 'warning',
- approved: 'success',
- rejected: 'danger',
- };
+ {
+ pending: 'warning',
+ approved: 'success',
+ rejected: 'danger',
+ };
return statusMap[status] || 'info';
}
@@ -269,7 +255,7 @@ function getAuditStatusText(status: string) {
if (dictItem) {
return dictItem.dict_label || String(status);
}
-
+
// 兜底方案,保持原有逻辑
const statusMap: Record = {
pending: '待审核',
@@ -288,7 +274,7 @@ function getTenantStatusType(status: string | number): 'success' | 'info' | 'war
if (dictItem) {
return dictItem.dict_tag_type || 'info';
}
-
+
// 兜底方案,保持原有逻辑
return String(status) === '1' ? 'success' : 'info';
}
@@ -301,7 +287,7 @@ function getTenantStatusText(status: string | number): string {
if (dictItem) {
return dictItem.dict_label || String(status);
}
-
+
// 兜底方案,保持原有逻辑
return String(status) === '1' ? '启用' : '禁用';
}
@@ -471,5 +457,23 @@ onMounted(() => {
}
}
}
-
+.pending-color {
+ color: #e6a23c;
+ background-color: #fdf6ec;
+ border-color: #f5dab1;
+}
+
+.approved-color,
+.color-1 {
+ color: #67c23a;
+ background-color: rgb(240, 249, 235);
+ border-color: rgb(225, 243, 216);
+}
+
+.rejected-color {
+ color: #f56c6c;
+ background-color: #fef0f0;
+ border-color: #fbc4c4;
+}
+
diff --git a/server/controllers/task.go b/server/controllers/task.go
index 5b580e3..060d498 100644
--- a/server/controllers/task.go
+++ b/server/controllers/task.go
@@ -304,3 +304,47 @@ func (c *TaskController) DeleteTask() {
}
c.ServeJSON()
}
+
+// 仪表盘获取待办任务
+// GetTodoTasks 获取待办任务列表
+// @router /api/oa/tasks/todo [get]
+func (c *TaskController) GetTodoTasks() {
+ // 优先取查询参数,其次取中间件写入的上下文
+ tenantId, _ := c.GetInt("tenantId", 0)
+ if tenantId == 0 {
+ if v := c.Ctx.Input.GetData("tenantId"); v != nil {
+ if tid, ok := v.(int); ok {
+ tenantId = tid
+ }
+ }
+ }
+
+ id, _ := c.GetInt("id", 0)
+ if id == 0 {
+ if v := c.Ctx.Input.GetData("id"); v != nil {
+ if uid, ok := v.(int); ok {
+ id = uid
+ }
+ }
+ }
+
+ limit, _ := c.GetInt("limit", 10)
+
+ tasks, err := services.GetTodoTasks(tenantId, id, limit)
+ if err != nil {
+ c.Data["json"] = map[string]interface{}{
+ "code": 1,
+ "message": "获取待办任务失败: " + err.Error(),
+ "data": nil,
+ }
+ c.ServeJSON()
+ return
+ }
+
+ c.Data["json"] = map[string]interface{}{
+ "code": 0,
+ "message": "获取成功",
+ "data": tasks,
+ }
+ c.ServeJSON()
+}
diff --git a/server/database/sys_feedback.sql b/server/database/sys_feedback.sql
new file mode 100644
index 0000000..a6d0ed1
--- /dev/null
+++ b/server/database/sys_feedback.sql
@@ -0,0 +1,20 @@
+-- 反馈表:sys_feedback(修复TEXT字段默认值错误)
+CREATE TABLE `sys_feedback` (
+ `id` varchar(36) NOT NULL COMMENT 'ID',
+ `tenant_id` varchar(64) NOT NULL COMMENT '租户ID',
+ `feedback_name` varchar(50) DEFAULT '' COMMENT '反馈人姓名',
+ `module` varchar(30) NOT NULL COMMENT '反馈对应模块',
+ `feedback_type` varchar(20) NOT NULL COMMENT '反馈类型',
+ `content` text NOT NULL COMMENT '反馈详细内容',
+ `attachment_url` varchar(255) DEFAULT '' COMMENT '附件URL',
+ `handle_status` varchar(20) NOT NULL DEFAULT '0' COMMENT '处理状态(0-待处理/1-处理中/2-已解决/3-已驳回/4-无需处理)',
+ `handle_remark` text COMMENT '处理备注(移除默认值,TEXT类型不支持)',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `delete_time` datetime DEFAULT NULL COMMENT '删除时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_tenant_id` (`tenant_id`) COMMENT '租户ID索引,优化租户级查询',
+ KEY `idx_module` (`module`) COMMENT '模块索引,优化模块级反馈统计',
+ KEY `idx_handle_status` (`handle_status`) COMMENT '处理状态索引,优化待处理反馈查询',
+ KEY `idx_create_time` (`create_time`) COMMENT '创建时间索引,优化时间范围查询'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通用反馈表(支持租户隔离、软删除)';
\ No newline at end of file
diff --git a/server/main.go b/server/main.go
index 36c23ab..884df43 100644
--- a/server/main.go
+++ b/server/main.go
@@ -22,5 +22,8 @@ func main() {
}
})
+ // 静态资源:映射 /uploads 到本地 uploads 目录,供前端访问上传文件
+ beego.SetStaticPath("/uploads", "uploads")
+
beego.Run()
}
diff --git a/server/models/file.go b/server/models/file.go
index 02d7967..1f18c93 100644
--- a/server/models/file.go
+++ b/server/models/file.go
@@ -48,6 +48,10 @@ func (f *FileInfo) TableName() string {
return "yz_files"
}
+func init() {
+ // Removed duplicate ORM registration
+}
+
// CanPreview 判断文件是否可以在线预览
func (f *FileInfo) CanPreview() bool {
previewableExts := map[string]bool{
diff --git a/server/models/task.go b/server/models/task.go
index 317096e..b0fedbe 100644
--- a/server/models/task.go
+++ b/server/models/task.go
@@ -121,3 +121,18 @@ func UpdateTask(t *Task, cols ...string) error {
_, err := o.Update(t, cols...)
return err
}
+
+// GetTodoTasks 获取待办任务列表
+func GetTodoTasks(tenantId int, id int, limit int) (tasks []*Task, err error) {
+ o := orm.NewOrm()
+ qs := o.QueryTable(new(Task)).Filter("tenant_id", tenantId).Filter("deleted_time__isnull", true)
+ // 如果传了用户,则筛选负责人为该用户的任务
+ if id > 0 {
+ qs = qs.Filter("principal_id", id)
+ }
+ if limit <= 0 {
+ limit = 10
+ }
+ _, err = qs.OrderBy("-id").Limit(limit).All(&tasks)
+ return
+}
diff --git a/server/models/tenant.go b/server/models/tenant.go
index e6b89ad..6f80022 100644
--- a/server/models/tenant.go
+++ b/server/models/tenant.go
@@ -8,25 +8,26 @@ import (
)
type Tenant struct {
- Id int `orm:"pk;auto" json:"id"`
- Name string `orm:"size(100)" json:"name"`
- Code string `orm:"size(50);unique" json:"code"`
- Owner string `orm:"size(50)" json:"owner"`
- Phone string `orm:"size(20);null" json:"phone"`
- Email string `orm:"size(100);null" json:"email"`
- Status int `orm:"default(1)" json:"status"`
- AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"`
- AuditComment string `orm:"type(text);null" json:"audit_comment"`
- AuditBy string `orm:"size(50);null" json:"audit_by"`
- AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"`
- Capacity int `orm:"default(0)" json:"capacity"`
- CapacityUsed int `orm:"default(0)" json:"capacity_used"`
- Remark string `orm:"type(text);null" json:"remark"`
- CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"`
- UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"update_time"`
- DeleteTime *time.Time `orm:"null;type(datetime)" json:"delete_time"`
- CreateBy string `orm:"size(50);null" json:"create_by"`
- UpdateBy string `orm:"size(50);null" json:"update_by"`
+ Id int `orm:"pk;auto" json:"id"`
+ Name string `orm:"size(100)" json:"name"`
+ Code string `orm:"size(50);unique" json:"code"`
+ Owner string `orm:"size(50)" json:"owner"`
+ Phone string `orm:"size(20);null" json:"phone"`
+ Email string `orm:"size(100);null" json:"email"`
+ Status int `orm:"default(1)" json:"status"`
+ AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"`
+ AuditComment string `orm:"type(text);null" json:"audit_comment"`
+ AuditBy string `orm:"size(50);null" json:"audit_by"`
+ AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"`
+ Capacity int `orm:"default(0)" json:"capacity"`
+ CapacityUsed int `orm:"default(0)" json:"capacity_used"`
+ AttachmentUrl string `orm:"size(255);null" json:"attachment_url"`
+ Remark string `orm:"type(text);null" json:"remark"`
+ CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"`
+ UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"update_time"`
+ DeleteTime *time.Time `orm:"null;type(datetime)" json:"delete_time"`
+ CreateBy string `orm:"size(50);null" json:"create_by"`
+ UpdateBy string `orm:"size(50);null" json:"update_by"`
}
// TableName 设置表名
diff --git a/server/routers/router.go b/server/routers/router.go
index cf2b17a..862283d 100644
--- a/server/routers/router.go
+++ b/server/routers/router.go
@@ -270,6 +270,8 @@ func init() {
// 文件管理路由 - 手动配置以匹配前端的 /api/files 路径
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
+ // 兼容前端上传地址 /api/files/upload -> 复用 Post 处理上传
+ beego.Router("/api/files/upload", &controllers.FileController{}, "post:Post")
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
beego.Router("/api/files/download/:id", &controllers.FileController{}, "get:DownloadFile")
beego.Router("/api/files/preview/:id", &controllers.FileController{}, "get:PreviewFile")
@@ -315,6 +317,7 @@ func init() {
// OA任务管理路由
beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask")
beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask")
+ beego.Router("/api/oa/tasks/todo", &controllers.TaskController{}, "get:GetTodoTasks")
// 权限管理路由
beego.Router("/api/permissions/menus", &controllers.PermissionController{}, "get:GetAllMenuPermissions")
diff --git a/server/services/task.go b/server/services/task.go
index 0fb2d26..36c6e61 100644
--- a/server/services/task.go
+++ b/server/services/task.go
@@ -69,3 +69,8 @@ func DeleteOATask(id int, operatorName string, operatorId int) error {
func genTaskNo(tenantId int) string {
return fmt.Sprintf("TASK%d%s", tenantId, time.Now().Format("20060102150405"))
}
+
+// 仪表盘获取待办任务
+func GetTodoTasks(tenantId int, id int, limit int) (tasks []*models.Task, err error) {
+ return models.GetTodoTasks(tenantId, id, limit)
+}