Compare commits

...

33 Commits

Author SHA1 Message Date
  chenyifan01 3c125805db 修改非启智项目的标签逻辑 2 years ago
  chenshihai 56510bc84a 科技项目 2 years ago
  chenshihai adf8500563 科技项目 2 years ago
  chenshihai 1b648b7768 科技项目开发对接 2 years ago
  ychao_1983 632a7e0835 提交代码 2 years ago
  ychao_1983 59d2cc6ec7 提交代码 2 years ago
  ychao_1983 5ebfd6e22f 提交代码 2 years ago
  ychao_1983 eb48f57e6c 提交代码 2 years ago
  ychao_1983 531066b1d4 提交代码 2 years ago
  chenshihai e201f957e0 科技项目开发对接 2 years ago
  chenyifan01 6a447daef3 添加用户角色管理 2 years ago
  chenyifan01 c397f131f5 添加用户角色管理 2 years ago
  chenyifan01 4ac1523b7a 添加用户角色管理 2 years ago
  chenyifan01 ab0968bef9 添加用户角色管理 2 years ago
  chenyifan01 0b7bcd71fc 添加用户角色管理 2 years ago
  ychao_1983 2f1912c1d2 提交代码 2 years ago
  ychao_1983 a9db0f6725 提交代码 2 years ago
  chenyifan01 ea59e675ea 新建非启智项目申请页面项目路径可选的用户和组织 2 years ago
  ychao_1983 abc908326f 提交代码 2 years ago
  chenshihai a071f308a7 科技项目开发 2 years ago
  chenyifan01 dd6d1dbf0f 新建非启智项目申请页面提交02 2 years ago
  ychao_1983 0b641ea6d6 提交代码 2 years ago
  ychao_1983 5bc84c6bd7 提交代码 2 years ago
  ychao_1983 6ca271f30c 增加路由 2 years ago
  chenyifan01 006f99a8f2 新建非启智项目申请页面提交01 2 years ago
  chenyifan01 73c4d22fdd 新建启智项目申请页面提交 2 years ago
  chenyifan01 8dda4d13e6 新建申请页面科技项目查询接口 2 years ago
  ychao_1983 599e5d56be 提交代码 2 years ago
  ychao_1983 ceb83ab06d 提交代码 2 years ago
  ychao_1983 cc48ca8ec1 提交代码 2 years ago
  ychao_1983 9db1328f27 自动建表 2 years ago
  ychao_1983 652a0065d9 表结构字段名更新 2 years ago
  ychao_1983 ba61157bf2 提交代码 2 years ago
51 changed files with 5054 additions and 142 deletions
Split View
  1. +3
    -0
      models/models.go
  2. +30
    -0
      models/repo_list.go
  3. +71
    -0
      models/role.go
  4. +661
    -0
      models/tech_converge_info.go
  5. +18
    -0
      modules/structs/tech.go
  6. +11
    -0
      options/locale/locale_en-US.ini
  7. +13
    -0
      options/locale/locale_zh-CN.ini
  8. +36
    -0
      routers/api/v1/admin/role.go
  9. +32
    -0
      routers/api/v1/api.go
  10. +27
    -0
      routers/api/v1/org/org.go
  11. +4
    -138
      routers/api/v1/repo/migrate.go
  12. +155
    -0
      routers/api/v1/tech/repo.go
  13. +336
    -0
      routers/api/v1/tech/tech.go
  14. +3
    -0
      routers/response/api_response.go
  15. +4
    -0
      routers/response/error.go
  16. +11
    -0
      routers/routes/routes.go
  17. +32
    -0
      routers/tech/tech.go
  18. +146
    -0
      services/repository/migrate.go
  19. +2
    -0
      services/repository/square.go
  20. +49
    -0
      services/repository/url.go
  21. +15
    -0
      services/role/role.go
  22. +45
    -0
      services/role/user_role.go
  23. +234
    -0
      services/tech/tech.go
  24. +3
    -1
      templates/base/head_navbar.tmpl
  25. +3
    -1
      templates/base/head_navbar_fluid.tmpl
  26. +3
    -1
      templates/base/head_navbar_home.tmpl
  27. +3
    -1
      templates/base/head_navbar_pro.tmpl
  28. +5
    -0
      templates/tech/admin_view.tmpl
  29. +5
    -0
      templates/tech/create.tmpl
  30. +5
    -0
      templates/tech/my_view.tmpl
  31. +5
    -0
      templates/tech/repo_view.tmpl
  32. +5
    -0
      templates/tech/tech_view.tmpl
  33. +173
    -0
      web_src/vuepages/apis/modules/tech.js
  34. +365
    -0
      web_src/vuepages/pages/tech/adminview/index.vue
  35. +17
    -0
      web_src/vuepages/pages/tech/adminview/vp-tech-adminview.js
  36. +289
    -0
      web_src/vuepages/pages/tech/components/Filters.vue
  37. +158
    -0
      web_src/vuepages/pages/tech/components/PrjResultsItem.vue
  38. +154
    -0
      web_src/vuepages/pages/tech/components/PrjResultsList.vue
  39. +248
    -0
      web_src/vuepages/pages/tech/components/SciAndTechPrjItem.vue
  40. +150
    -0
      web_src/vuepages/pages/tech/components/SciAndTechPrjList.vue
  41. +191
    -0
      web_src/vuepages/pages/tech/components/SearchBar.vue
  42. +141
    -0
      web_src/vuepages/pages/tech/components/TopHeader.vue
  43. +120
    -0
      web_src/vuepages/pages/tech/components/TopMenu.vue
  44. +641
    -0
      web_src/vuepages/pages/tech/create/index.vue
  45. +17
    -0
      web_src/vuepages/pages/tech/create/vp-tech-create.js
  46. +164
    -0
      web_src/vuepages/pages/tech/myview/index.vue
  47. +17
    -0
      web_src/vuepages/pages/tech/myview/vp-tech-myview.js
  48. +100
    -0
      web_src/vuepages/pages/tech/repoview/index.vue
  49. +17
    -0
      web_src/vuepages/pages/tech/repoview/vp-tech-repoview.js
  50. +100
    -0
      web_src/vuepages/pages/tech/techview/index.vue
  51. +17
    -0
      web_src/vuepages/pages/tech/techview/vp-tech-techview.js

+ 3
- 0
models/models.go View File

@@ -167,6 +167,9 @@ func init() {
new(Badge),
new(BadgeUser),
new(BadgeUserLog),
new(TechConvergeBaseInfo),
new(RepoConvergeInfo),
new(UserRole),
)

tablesStatistic = append(tablesStatistic,


+ 30
- 0
models/repo_list.go View File

@@ -192,6 +192,8 @@ type SearchRepoOptions struct {
// False -> include just no courses
Course util.OptionalBool
OnlySearchPrivate bool

RepoIds []int64
}

//SearchOrderBy is used to sort the result
@@ -206,6 +208,7 @@ type FindReposResponse struct {
Page int
PageSize int
Total int64
RepoIds []int64
}

// Strings for sorting result
@@ -281,6 +284,33 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
if opts.StarredByID > 0 {
cond = cond.And(builder.In("id", builder.Select("repo_id").From("star").Where(builder.Eq{"uid": opts.StarredByID})))
}
if len(opts.RepoIds) > 0 {
const MaxINItems = 1000
if len(opts.RepoIds) <= MaxINItems {
cond = cond.And(builder.In("id", opts.RepoIds))
} else {
repoIdsMap := make(map[int][]int64, 0)
i := 0

for j := 0; j < len(opts.RepoIds); j++ {
if _, ok := repoIdsMap[i]; !ok {
repoIdsMap[i] = make([]int64, 0)
}
repoIdsMap[i] = append(repoIdsMap[i], opts.RepoIds[j])
if (j+1)%MaxINItems == 0 {
i += 1
}

}
subCond := builder.NewCond()
for _, repoSplit := range repoIdsMap {
subCond = subCond.Or(builder.In("id", repoSplit))
}
cond = cond.And(subCond)

}

}

// Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate
if opts.OwnerID > 0 {


+ 71
- 0
models/role.go View File

@@ -0,0 +1,71 @@
package models

import (
"code.gitea.io/gitea/modules/timeutil"
"fmt"
)

type RoleType string

const (
TechProgramAdmin RoleType = "TechProgramAdmin"
)

type Role struct {
Type RoleType
Name string
Description string
}

type UserRole struct {
ID int64 `xorm:"pk autoincr"`
UserId int64 `xorm:"INDEX UNIQUE(uq_user_role)"`
RoleType RoleType `xorm:"UNIQUE(uq_user_role)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}

func NewUserRole(r UserRole) (int64, error) {
return x.Insert(&r)
}
func DeleteUserRole(roleType RoleType, userId int64) (int64, error) {
return x.Where("role_type = ? and user_id = ?", roleType, userId).Delete(&UserRole{})
}

func GetUserRoleByUserAndRole(userId int64, roleType RoleType) (*UserRole, error) {
r := &UserRole{}
has, err := x.Where("role_type = ? and user_id = ?", roleType, userId).Get(r)
if err != nil {
return nil, err
} else if !has {
return nil, ErrRecordNotExist{}
}
return r, nil
}

func GetRoleByCode(code string) (*Role, error) {
r := &Role{}
has, err := x.Where("code = ?", code).Get(r)
if err != nil {
return nil, err
} else if !has {
return nil, ErrRecordNotExist{}
}
return r, nil
}

type ErrRoleNotExists struct {
}

func IsErrRoleNotExists(err error) bool {
_, ok := err.(ErrRoleNotExists)
return ok
}

func (err ErrRoleNotExists) Error() string {
return fmt.Sprintf("role is not exists")
}

type OperateRoleReq struct {
UserName string `json:"user_name" binding:"Required"`
RoleType RoleType `json:"role_type" binding:"Required"`
}

+ 661
- 0
models/tech_converge_info.go View File

@@ -0,0 +1,661 @@
package models

import (
"strconv"
"strings"
"time"

"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
)

const (
TechShow = 1
TechHide = 2
TechMigrating = 3
TechMigrateFailed = 4
TechNotExist = 5
)

const DefaultTechApprovedStatus = TechShow

type TechConvergeBaseInfo struct {
ID int64 `xorm:"pk autoincr"`
ProjectNumber string `xorm:"UNIQUE NOT NULL"` //项目立项编号
ProjectName string //科技项目名称
Institution string //项目承担单位
ApplyYear int //申报年度
Province string //所属省(省市)
Category string //单位性质
Recommend string //推荐单位
Owner string //项目负责人
Phone string //负责人电话
Email string //负责人邮箱
Contact string //项目联系人
ContactPhone string //联系人电话
ContactEmail string //联系人邮箱
ExecuteMonth int //执行周期(月)
ExecuteStartYear int //执行开始年份
ExecuteEndYear int //执行结束年份
ExecutePeriod string //执行期限
Type string //项目类型
StartUp string //启动会时间
NumberTopic int
Topic1 string
Topic2 string
Topic3 string
Topic4 string
Topic5 string
Topic6 string
Topic7 string
AllInstitution string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}

func (t *TechConvergeBaseInfo) Brief() *TechConvergeBrief {
return &TechConvergeBrief{
ProjectNumber: t.ProjectNumber,
ProjectName: t.ProjectName,
Institution: t.Institution,
AllInstitution: t.AllInstitution,
}
}
func (t *TechConvergeBaseInfo) IsValidInstitution(institution string) bool {
if t.AllInstitution == "" && t.Institution == "" {
return false
}

allInstitution := make([]string, 0)
if t.AllInstitution != "" {
allInstitution = strings.Split(t.AllInstitution, ",")
}
if t.Institution != "" {
allInstitution = append(allInstitution, t.Institution)
}

newInstitution := strings.Split(institution, ",")
total := len(newInstitution)
matched := 0
for _, n := range newInstitution {
for _, s := range allInstitution {
if s == n {
matched++
break
}
}
}
if matched == total {
return true
}
return false
}

type RepoConvergeInfo struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64
Url string
BaseInfoID int64
Institution string
UID int64
Status int
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
User *User `xorm:"-"`
Repo *Repository `xorm:"-"`
BaseInfo *TechConvergeBaseInfo `xorm:"-"`
}

func (r *RepoConvergeInfo) InsertOrUpdate() error {
if r.ID != 0 {
_, err := x.ID(r.ID).Update(r)
return err
} else {
_, err := x.InsertOne(r)
return err
}
}

func GetTechConvergeBaseInfoByProjectNumber(projectNumber string) (*TechConvergeBaseInfo, error) {
tb := &TechConvergeBaseInfo{ProjectNumber: projectNumber}
return getTechConvergeBaseInfo(tb)
}

func GetTechConvergeBaseInfoById(id int64) (*TechConvergeBaseInfo, error) {
tb := &TechConvergeBaseInfo{ID: id}
return getTechConvergeBaseInfo(tb)
}

func (baseInfo *TechConvergeBaseInfo) InsertOrUpdate() error {
if baseInfo.ID != 0 {
_, err := x.ID(baseInfo.ID).Update(baseInfo)
return err
} else {
_, err := x.InsertOne(baseInfo)
return err
}
}

type ErrTechConvergeBaseInfoNotExist struct {
ID string
}

func (err ErrTechConvergeBaseInfoNotExist) Error() string {
return "tech.tech_not_exist"
}

func IsErrTechConvergeBaseInfoNotExist(err error) bool {
_, ok := err.(ErrTechConvergeBaseInfoNotExist)
return ok
}

func getTechConvergeBaseInfo(tb *TechConvergeBaseInfo) (*TechConvergeBaseInfo, error) {
has, err := x.Get(tb)
if err != nil {
return nil, err
} else if !has {
if tb.ProjectNumber != "" {
return nil, ErrTechConvergeBaseInfoNotExist{tb.ProjectNumber}
} else {
return nil, ErrTechConvergeBaseInfoNotExist{strconv.FormatInt(tb.ID, 10)}
}
}
return tb, nil
}

func GetProjectNames() []string {
var names []string
x.Table("tech_converge_base_info").Distinct("project_name").Find(&names)
return names
}
func GetIdByProjectName(name string) []string {
var ids []int64
x.Table("tech_converge_base_info").Cols("id").Where("project_name=?", name).Find(&ids)

idStrs := make([]string, 0, len(ids))
for _, id := range ids {
idStrs = append(idStrs, strconv.FormatInt(id, 10))
}
return idStrs
}
func GetSummitRepoIds() []int64 {
var ids []int64
x.Table("repo_converge_info").Cols("repo_id").Find(&ids)
return ids
}

func GetTechRepoTopics(limit int) []string {

repoIds := GetSummitRepoIds()
if len(repoIds) == 0 {
return []string{}
}

//select name, repo_count from topic a, repo_topic b
//where a.id=b.topic_id and repo_id in (1,3) order by repo_count desc
inCondition := "repo_id in ("

const MaxINItems = 1000
for i := 0; i < len(repoIds); i++ {
if i == len(repoIds)-1 {
inCondition += strconv.FormatInt(repoIds[i], 10)
} else if (i+1)%MaxINItems == 0 {
inCondition += strconv.FormatInt(repoIds[i], 10) + ") or repo_id in ("
} else {
inCondition += strconv.FormatInt(repoIds[i], 10) + ","
}

}
inCondition += ")"

sql := "select name, repo_count from topic a, repo_topic b where a.id=b.topic_id and (" + inCondition + ") order by repo_count desc"
if limit > 0 {
sql += "limit " + strconv.Itoa(limit)
}

result, err := x.QueryString(sql)
if err != nil {
return []string{}
}
var topics []string
for _, record := range result {
topics = append(topics, record["name"])
}
return topics

}

func GetProjectTypes() []string {

sql := "SELECT COUNT(id) AS theCount, type from tech_converge_base_info GROUP BY type ORDER BY theCount DESC"
result, err := x.QueryString(sql)
if err != nil {
return []string{}
}
var projectTypes []string
for _, record := range result {
projectTypes = append(projectTypes, record["type"])
}
return projectTypes

}
func GetApplyExecuteYears() ([]int, []int) {
apply, executeStart, executeEnd := GetYearInfos()
applyEnd := time.Now().Year()

var applyArray []int
var executeArray []int

for i := apply; i <= applyEnd; i++ {
applyArray = append(applyArray, i)

}
for i := executeStart; i <= executeEnd; i++ {
executeArray = append(executeArray, i)

}
return applyArray, executeArray
}
func GetYearInfos() (int, int, int) {

sql := "select min(apply_year) as apply_year,min(CASE WHEN execute_start_year != 0 THEN execute_start_year END) as execute_start_year,max(execute_end_year) as execute_end_year from tech_converge_base_info"
result, err := x.QueryString(sql)
if err != nil {
return 2018, 2019, 2024
}

for _, record := range result {
apply, _ := strconv.Atoi(record["apply_year"])
executeStart, _ := strconv.Atoi(record["execute_start_year"])
executeEnd, _ := strconv.Atoi(record["execute_end_year"])
return apply, executeStart, executeEnd
}
return 2018, 2019, 2024
}

func GetAllInstitutions() []string {
var names []string
x.Table("tech_converge_base_info").Cols("all_institution").Find(&names)
var allNames []string
for _, name := range names {
singleNames := strings.Split(name, ",")
for _, singleName := range singleNames {
if singleName != "" {
if !contains(allNames, singleName) {
allNames = append(allNames, singleName)
}
}
}

}
return allNames
}

func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

type SearchTechOpt struct {
Q string //科技项目名称
ProjectType string
Institution string
ApplyYear int
ExecuteYear int
OrderBy string
ListOptions
}

type SearchRepoOpt struct {
Q string //项目名称,简介
ProjectName string
Topic string
Institution string
OrderBy string
ListOptions
}

type SearchUserRepoOpt struct {
User *User
ListOptions
}

type TechRepoInfoUser struct {
ID int64 `json:"id"`
ProjectNumber string `json:"no"`
ProjectName string `json:"name"`
Institution string `json:"institution"`
AllInstitution string `json:"all_institution"`
Url string `json:"url"`
RepoName string `json:"repo_name"`
RepoOwnerName string `json:"repo_owner_name"`
ContributionInstitution string `json:"contribution_institution"`
UserName string `json:"user_name"`
Status int `json:"status"`
CreatedUnix timeutil.TimeStamp `json:"created_unix"`
UpdatedUnix timeutil.TimeStamp `json:"updated_unix"`
}
type TechRepoInfoAdmin struct {
ID int64 `json:"id"`
Url string `json:"url"`
ContributionInstitution string `json:"contribution_institution"`
Status int `json:"status"`
ProjectNumber string `json:"no"`
ProjectName string `json:"name"`
ApplyYear int `json:"apply_year"`
Institution string `json:"institution"`
Province string `json:"province"`
Category string `json:"category"`
Recommend string `json:"recommend"`
Owner string `json:"owner"`
Phone string `json:"phone"`
Email string `json:"email"`
Contact string `json:"contact"`
ContactPhone string `json:"contact_phone"`
ContactEmail string `json:"contact_email"`
ExecuteMonth int `json:"execute_month"`
ExecutePeriod string `json:"execute_period"`
Type string `json:"type"`
StartUp string `json:"start_up"`
NumberTopic int `json:"number_topic"`
Topic1 string `json:"topic1"`
Topic2 string `json:"topic2"`
Topic3 string `json:"topic3"`
Topic4 string `json:"topic4"`
Topic5 string `json:"topic5"`
AllInstitution string `json:"all_institution"`
RepoName string `json:"repo_name"`
RepoOwnerName string `json:"repo_owner_name"`
UserName string `json:"user_name"`
CreatedUnix timeutil.TimeStamp `json:"created_unix"`
UpdatedUnix timeutil.TimeStamp `json:"updated_unix"`
}

type RepoWithInstitution struct {
ID int64 `json:"id"`
OwnerID int64 `json:"owner_id"`
OwnerName string `json:"owner_name"`
Name string `json:"name"`
Alias string `json:"alias"`
Topics []string `json:"topics"`
Description string `json:"description"`
Institution string `json:"institution"`
RelAvatarLink string `json:"rel_avatar_link"`
UpdatedUnix timeutil.TimeStamp `json:"updated_unix"`
}

type TechRepoInfo struct {
ID int64 `json:"id"`
ProjectName string `json:"project_name"`
Institution string `json:"institution"`
Type string `json:"type"`
ApplyYear int `json:"apply_year"`
ExecutePeriod string `json:"execute_period"`
AllInstitution string `json:"all_institution"`
RepoCount int `json:"repo_numer"`
Repos []*RepoWithInstitution
}

func ShowTechRepo(ids []int64, show bool) error {

status := TechShow
if !show {
status = TechHide
}

idStrs := make([]string, 0, len(ids))
for _, id := range ids {
idStrs = append(idStrs, strconv.FormatInt(id, 10))
}

sql := "update repo_converge_info set status=? where id in (" + strings.Join(idStrs, ",") + ") and (status=" + strconv.Itoa(TechShow) + " or status=" + strconv.Itoa(TechHide) + ")"
_, err := x.Exec(sql, status)
return err
}

func GetTechRepoInfoForUser(opts *SearchUserRepoOpt) ([]*RepoConvergeInfo, int64, error) {
cond := buildTechRepoForUserCondition(opts)
total, err := x.Where(cond).Count(new(RepoConvergeInfo))
if err != nil {
return nil, 0, err
}
repoConvergeInfos := make([]*RepoConvergeInfo, 0)
err = x.Where(cond).Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).Desc("status").Find(&repoConvergeInfos)
if err != nil {
return nil, 0, err
}

loadAttributes(repoConvergeInfos)

return repoConvergeInfos, total, nil

}

func loadAttributes(infos []*RepoConvergeInfo) {
for _, info := range infos {
info.User, _ = GetUserByID(info.UID)
info.Repo, _ = GetRepositoryByID(info.RepoID)
info.BaseInfo, _ = GetTechConvergeBaseInfoById(info.BaseInfoID)
}
}

func buildTechRepoForUserCondition(opts *SearchUserRepoOpt) builder.Cond {
var cond = builder.NewCond()
if opts.User != nil {
cond = cond.And(builder.Eq{"uid": opts.User.ID})
}
return cond
}

func GetAvailableRepoConvergeInfo(opt *SearchRepoOpt) ([]*RepoConvergeInfo, error) {

repos := make([]*RepoConvergeInfo, 0)
err := x.Table("repo_converge_info").Where(buildRepoFilterCond(opt)).Find(&repos)
return repos, err

}

func buildRepoFilterCond(opt *SearchRepoOpt) string {

sql := "status=" + strconv.Itoa(TechShow)

if opt.Institution != "" {
sql += " and (institution like '%" + opt.Institution + ",%'" + " or institution like '%," + opt.Institution + "%'" + " or institution = '" + opt.Institution + "')"
}

if opt.ProjectName != "" {
baseInfoIds := GetIdByProjectName(opt.ProjectName)
if len(baseInfoIds) > 0 {

sql += " and base_info_id in (" + strings.Join(baseInfoIds, ",") + ")"
}

}
return sql
}

func SearchTechRepoInfo(opt *SearchTechOpt) ([]*TechRepoInfo, int64, error) {

sql := `select a.*,COALESCE(b.count,0) as repo_count, COALESCE(b.max,0) as max from tech_converge_base_info a left join
(select base_info_id,count(id),max(updated_unix) from repo_converge_info where status=` + strconv.Itoa(TechShow) + ` GROUP BY base_info_id ) b
on a.id=b.base_info_id`
totalSql := "select count(*) from (" + sql + ") c" + buildTechFilterCond(opt)
total, err := x.SQL(totalSql).Count(new(TechConvergeBaseInfo))
resultList := make([]*TechRepoInfo, 0)
if err != nil {
return resultList, total, err

}
resultSql := "select id,project_name, institution,type,apply_year,execute_period,all_institution,repo_count from (" +
sql + ") c " + buildTechFilterCond(opt) + opt.OrderBy + " offset " + strconv.Itoa((opt.Page-1)*opt.PageSize) + " limit " + strconv.Itoa(opt.PageSize)

resultMap, err := x.QueryInterface(resultSql)
if err == nil {
for _, record := range resultMap {
resultList = append(resultList, &TechRepoInfo{
ID: record["id"].(int64),
ProjectName: record["project_name"].(string),
Institution: record["institution"].(string),
Type: record["type"].(string),
ApplyYear: int(record["apply_year"].(int64)),
ExecutePeriod: record["execute_period"].(string),
AllInstitution: record["all_institution"].(string),
RepoCount: int(record["repo_count"].(int64)),
})
}
}

loadRepoInfoForTech(resultList)

return resultList, total, err

}

func buildTechFilterCond(opt *SearchTechOpt) string {

sql := ""

if opt.Q != "" {
sql += getWherePrefix(sql) + " project_name like '%" + opt.Q + "%'"

}
if opt.ProjectType != "" {
sql += getWherePrefix(sql) + " type ='" + opt.ProjectType + "'"
}
if opt.ApplyYear != 0 {
sql += getWherePrefix(sql) + " apply_year =" + strconv.Itoa(opt.ApplyYear)
}
if opt.Institution != "" {
sql += getWherePrefix(sql) + " (all_institution like '%" + opt.Institution + ",%'" + " or all_institution like '%," + opt.Institution + "%'" + " or all_institution = '" + opt.Institution + "')"
}

if opt.ExecuteYear != 0 {
sql += getWherePrefix(sql) + " execute_start_year <=" + strconv.Itoa(opt.ExecuteYear) + " and execute_end_year >=" + strconv.Itoa(opt.ExecuteYear)
}
return sql
}

func getWherePrefix(sql string) string {
if sql == "" {
return " where "
}
return " and "
}

func loadRepoInfoForTech(list []*TechRepoInfo) {

for _, techRepo := range list {
techRepo.Repos = []*RepoWithInstitution{}
if techRepo.RepoCount > 0 {
var repoIds []int64
x.Table("repo_converge_info").Cols("repo_id").Where("base_info_id=? and status=?", techRepo.ID, TechShow).Limit(2).Desc("updated_unix").Find(&repoIds)

resultMap, err := GetRepositoriesMapByIDs(repoIds)

if err == nil {

for _, repoId := range repoIds {
repo, ok := resultMap[repoId]
if ok {
techRepo.Repos = append(techRepo.Repos, &RepoWithInstitution{
ID: repo.ID,
Institution: techRepo.Institution,
OwnerID: repo.OwnerID,
OwnerName: repo.OwnerName,
Name: repo.Name,
Alias: repo.Alias,
Topics: repo.Topics,
Description: repo.Description,
RelAvatarLink: repo.RelAvatarLink(),
UpdatedUnix: repo.UpdatedUnix,
})
}

}

}
}
}

}

type TechConvergeBrief struct {
ProjectNumber string `json:"no"` //项目立项编号
ProjectName string `json:"name"` //科技项目名称
Institution string `json:"institution"` //项目承担单位
AllInstitution string `json:"all_institution"`
}

type FindTechOpt struct {
TechNo string
ProjectName string
Institution string
}

func FindTech(opt FindTechOpt) ([]*TechConvergeBaseInfo, error) {
var cond = builder.NewCond()
if opt.TechNo != "" {
cond = cond.And(builder.Like{"project_number", opt.TechNo})
}
if opt.ProjectName != "" {
cond = cond.And(builder.Like{"project_name", opt.ProjectName})
}
if opt.Institution != "" {
cond = cond.And(builder.Like{"institution", opt.Institution}.Or(builder.Like{"all_institution", opt.Institution}))
}

r := make([]*TechConvergeBaseInfo, 0)
err := x.Where(cond).OrderBy("updated_unix desc").Find(&r)
if err != nil {
return nil, err
}
return r, nil
}

func GetTechByTechNo(techNo string) (*TechConvergeBaseInfo, error) {
var tech = &TechConvergeBaseInfo{}
has, err := x.Where("project_number = ?", techNo).Get(tech)
if err != nil {
return nil, err
} else if !has {
return nil, ErrTechConvergeBaseInfoNotExist{}
}
return tech, nil

}

type GetRepoConvergeOpts struct {
RepoId int64
BaseInfoId int64
Status []int
}

func GetRepoConverge(opts GetRepoConvergeOpts) ([]*RepoConvergeInfo, error) {
r := make([]*RepoConvergeInfo, 0)
cond := builder.NewCond()
if opts.RepoId > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoId})
}
if opts.BaseInfoId > 0 {
cond = cond.And(builder.Eq{"base_info_id": opts.BaseInfoId})
}
if len(opts.Status) > 0 {
cond = cond.And(builder.In("status", opts.Status))
}
err := x.Where(cond).Find(&r)
if err != nil {
return nil, err
}
return r, nil

}

func UpdateRepoConvergeStatus(id int64, status int) (int64, error) {
return x.ID(id).Update(&RepoConvergeInfo{
Status: status,
})
}

+ 18
- 0
modules/structs/tech.go View File

@@ -0,0 +1,18 @@
package structs

type NotOpenITechRepo struct {
Url string `json:"url" binding:"Required"`
TechNo string `json:"no"`
Institution string `json:"institution"`
UID int64 `json:"uid"` //启智项目uid
RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
Alias string `json:"alias" binding:"Required;AlphaDashDotChinese;MaxSize(100)"`
Topics []string `json:"topics"` //关键词
Description string `json:"description" binding:"MaxSize(255)"`
}

type OpenITechRepo struct {
Url string `json:"url" binding:"Required"`
TechNo string `json:"no"`
Institution string `json:"institution"`
}

+ 11
- 0
options/locale/locale_en-US.ini View File

@@ -3358,3 +3358,14 @@ get_file_fail= Can not get the image content, please try again later.
content_type_unsupported=Allowed image type is jpg, jpeg or png.
process_image_fail=Fail to process image, please try again later.

[tech]
get_file_fail= Can not get the file content, please try again later.
content_type_unsupported=The format of the file or file content is wrong.
sql_err=Fail to process data, please try again later.
show_or_hide_fail=Failed to %s the repo(s).
incorrect_openi_format = The OpenI address format is incorrect
openi_repo_not_exist = OpenI repository is not exists
tech_not_exist = The project approval number does not exist
institution_not_valid = The submitted contributing unit is not among the participating units of the technology project
repo_converge_exists = The technology project [%s] already has [%s], please do not submit it again
to_migrate_repo_exists = The project has been migrated to OpenI, please use the OpenI way to submit

+ 13
- 0
options/locale/locale_zh-CN.ini View File

@@ -3378,4 +3378,17 @@ new_model_security_evaluation_tips = 模型安全评测只适用于图像分类
get_file_fail= 获取上传文件失败,请稍后再试。
content_type_unsupported=请上传jpg、jpeg或png图片。
process_image_fail=图片处理失败,请稍后再试。
[tech]
get_file_fail= 获取上传文件失败。
content_type_unsupported=上传文件的格式有误。
sql_err=数据处理错误,请稍后再试。
show_or_hide_fail=%s失败。
incorrect_openi_format = 启智项目地址格式错误
openi_repo_not_exist = 启智项目不存在
tech_not_exist = 项目立项编号不存在
institution_not_valid = 当前提交的贡献单位不在该科技项目的参与单位中
repo_converge_exists = 科技项目[%s]中已存在[%s],请勿重复提交
to_migrate_repo_exists = 该项目已迁移到启智,请使用启智社区方式提交申请




+ 36
- 0
routers/api/v1/admin/role.go View File

@@ -0,0 +1,36 @@
package admin

import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/response"
"code.gitea.io/gitea/services/role"
"net/http"
)

func AddUserRole(ctx *context.APIContext, form models.OperateRoleReq) {
user, err := models.GetUserByName(form.UserName)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError("User not exists"))
return
}
err = role.AddUserRole(user.ID, form.RoleType)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterResponseError(err))
return
}
ctx.JSON(http.StatusOK, response.OuterSuccess())
}
func DeleteUserRole(ctx *context.APIContext, form models.OperateRoleReq) {
user, err := models.GetUserByName(form.UserName)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError("User not exists"))
return
}
err = role.DeleteUserRole(user.ID, form.RoleType)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterResponseError(err))
return
}
ctx.JSON(http.StatusOK, response.OuterSuccess())
}

+ 32
- 0
routers/api/v1/api.go View File

@@ -59,9 +59,12 @@
package v1

import (
"code.gitea.io/gitea/services/role"
"net/http"
"strings"

"code.gitea.io/gitea/routers/api/v1/tech"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"

@@ -212,6 +215,16 @@ func reqSiteAdmin() macaron.Handler {
}
}

// reqTechAdmin user should be the tech program admin
func hasRole(roleType models.RoleType) macaron.Handler {
return func(ctx *context.Context) {
if !ctx.IsSigned || !role.UserHasRole(ctx.User.ID, roleType) {
ctx.Error(http.StatusForbidden)
return
}
}
}

// reqOwner user should be the owner of the repo or site admin.
func reqOwner() macaron.Handler {
return func(ctx *context.Context) {
@@ -535,6 +548,20 @@ func RegisterRoutes(m *macaron.Macaron) {

}, reqToken())

m.Group("/tech", func() {
m.Get("", tech.FindTech)
m.Post("/basic", hasRole(models.TechProgramAdmin), tech.ImportBasicInfo)
m.Get("/filter", tech.GetFilterInfo)
m.Get("/search", tech.SearchTechProjectInfo)
m.Get("/repo_search", tech.SearchRepoInfo)
m.Get("/admin", hasRole(models.TechProgramAdmin), tech.GetAdminRepoInfo)
m.Get("/my", tech.GetMyRepoInfo)
m.Post("/admin/:action", hasRole(models.TechProgramAdmin), bind(tech.ActionIDs{}), tech.Action)
m.Post("/openi", bind(api.OpenITechRepo{}), tech.CommitOpenIRepo)
m.Post("/no_openi", bind(api.NotOpenITechRepo{}), tech.CommitNotOpenIRepo)
m.Get("/is_admin", tech.IsAdmin)
}, reqToken())

m.Group("/attachments", func() {

m.Get("/:uuid", repo.GetAttachment)
@@ -1084,6 +1111,7 @@ func RegisterRoutes(m *macaron.Macaron) {

// Organizations
m.Get("/user/orgs", reqToken(), org.ListMyOrgs)
m.Get("/user/owners", reqToken(), org.GetMyOwners)
m.Get("/users/:username/orgs", org.ListUserOrgs)
m.Post("/orgs", reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll)
@@ -1164,6 +1192,10 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
})
})
m.Group("/role", func() {
m.Combo("").Post(bind(models.OperateRoleReq{}), admin.AddUserRole).
Delete(bind(models.OperateRoleReq{}), admin.DeleteUserRole)
})
}, reqToken(), reqSiteAdmin())

m.Group("/topics", func() {


+ 27
- 0
routers/api/v1/org/org.go View File

@@ -6,6 +6,7 @@
package org

import (
"code.gitea.io/gitea/routers/response"
"net/http"

"code.gitea.io/gitea/models"
@@ -278,3 +279,29 @@ func Delete(ctx *context.APIContext) {
}
ctx.Status(http.StatusNoContent)
}

func GetMyOwners(ctx *context.APIContext) {
result := make([]*models.User4Front, 0)
result = append(result, ctx.User.ToFrontFormat())

orgs, err := models.GetOrgsCanCreateRepoByUserID(ctx.User.ID)
if err != nil {
ctx.JSON(http.StatusOK, response.ResponseError(err))
return
}

if !ctx.User.IsAdmin {
orgsAvailable := []*models.User{}
for i := 0; i < len(orgs); i++ {
if orgs[i].CanCreateRepo() {
orgsAvailable = append(orgsAvailable, orgs[i])
}
}
orgs = orgsAvailable
}

for _, o := range orgs {
result = append(result, o.ToFrontFormat())
}
ctx.JSON(http.StatusOK, response.SuccessWithData(result))
}

+ 4
- 138
routers/api/v1/repo/migrate.go View File

@@ -6,8 +6,8 @@ package repo

import (
"bytes"
"code.gitea.io/gitea/modules/task"
"code.gitea.io/gitea/routers/response"
"code.gitea.io/gitea/services/repository"
"errors"
"fmt"
"net/http"
@@ -221,143 +221,9 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteA

func MigrateSubmit(ctx *context.APIContext, form auth.MigrateRepoForm) {
log.Info("receive MigrateSubmit request")
ctxUser, bizErr := checkContextUser(ctx, form.UID)
if bizErr != nil {
ctx.JSON(http.StatusOK, response.ResponseError(bizErr))
return
}

remoteAddr, err := form.ParseRemoteAddr(ctx.User)
err := repository.MigrateSubmit(ctx.User, form)
if err != nil {
if models.IsErrInvalidCloneAddr(err) {
addrErr := err.(models.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
ctx.JSON(http.StatusOK, response.PARAM_ERROR)
case addrErr.IsPermissionDenied:
ctx.JSON(http.StatusOK, response.INSUFFICIENT_PERMISSION)
case addrErr.IsInvalidPath:
ctx.JSON(http.StatusOK, response.PARAM_ERROR)
default:
ctx.JSON(http.StatusOK, response.SYSTEM_ERROR)
}
} else {
ctx.JSON(http.StatusOK, response.SYSTEM_ERROR)
}
return
}

var gitServiceType = api.PlainGitService
u, err := url.Parse(form.CloneAddr)
if err == nil && strings.EqualFold(u.Host, "github.com") {
gitServiceType = api.GithubService
}

var opts = migrations.MigrateOptions{
OriginalURL: form.CloneAddr,
GitServiceType: gitServiceType,
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Alias: form.Alias,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Mirror: form.Mirror,
AuthUsername: form.AuthUsername,
AuthPassword: form.AuthPassword,
Wiki: form.Wiki,
Issues: form.Issues,
Milestones: form.Milestones,
Labels: form.Labels,
Comments: true,
PullRequests: form.PullRequests,
Releases: form.Releases,
ctx.JSON(http.StatusOK, err)
}
if opts.Mirror {
opts.Issues = false
opts.Milestones = false
opts.Labels = false
opts.Comments = false
opts.PullRequests = false
opts.Releases = false
}

err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, opts.Alias)
if err != nil {
handleMigrateError4Api(ctx, ctxUser, remoteAddr, err)
return
}

err = task.MigrateRepository(ctx.User, ctxUser, opts)
if err == nil {
r := make(map[string]string)
r["OpenIUrl"] = strings.TrimSuffix(setting.AppURL, "/") + "/" + ctxUser.Name + "/" + opts.RepoName
r["OriginUrl"] = form.CloneAddr
ctx.JSON(http.StatusOK, response.SuccessWithData(r))
return
}

handleMigrateError4Api(ctx, ctxUser, remoteAddr, err)
}

func checkContextUser(ctx *context.APIContext, uid int64) (*models.User, *response.BizError) {
if uid == ctx.User.ID || uid == 0 {
return ctx.User, nil
}

org, err := models.GetUserByID(uid)
if models.IsErrUserNotExist(err) {
return ctx.User, nil
}

if err != nil {
return nil, response.SYSTEM_ERROR
}

// Check ownership of organization.
if !org.IsOrganization() {
return nil, nil
}
if !ctx.User.IsAdmin {
canCreate, err := org.CanCreateOrgRepo(ctx.User.ID)
if err != nil {
return nil, response.NewBizError(err)
} else if !canCreate {
return nil, response.INSUFFICIENT_PERMISSION
}
}
return org, nil
}

func handleMigrateError4Api(ctx *context.APIContext, repoOwner *models.User, remoteAddr string, err error) {
switch {
case models.IsErrRepoAlreadyExist(err):
ctx.JSON(http.StatusOK, response.Error(3, "The repository with the same name already exists."))
case migrations.IsRateLimitError(err):
ctx.JSON(http.StatusOK, response.ServerError("Remote visit addressed rate limitation."))
case migrations.IsTwoFactorAuthError(err):
ctx.JSON(http.StatusOK, response.ServerError("Remote visit required two factors authentication."))
case models.IsErrReachLimitOfRepo(err):
ctx.JSON(http.StatusOK, response.ServerError(fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit())))
case models.IsErrNameReserved(err):
ctx.JSON(http.StatusOK, response.ServerError(fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name)))
case models.IsErrNameCharsNotAllowed(err):
ctx.JSON(http.StatusOK, response.ServerError(fmt.Sprintf("The username '%s' contains invalid characters.", err.(models.ErrNameCharsNotAllowed).Name)))
case models.IsErrNamePatternNotAllowed(err):
ctx.JSON(http.StatusOK, response.ServerError(fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern)))
default:
err = util.URLSanitizedError(err, remoteAddr)
if strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "Bad credentials") ||
strings.Contains(err.Error(), "could not read Username") {
ctx.JSON(http.StatusOK, response.ServerError(fmt.Sprintf("Authentication failed: %v.", err)))
} else if strings.Contains(err.Error(), "fatal:") {
ctx.JSON(http.StatusOK, response.ServerError(fmt.Sprintf("Migration failed: %v.", err)))
} else {
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
}
}
}

func QueryRepoSatus(ctx *context.APIContext, form auth.MigrateRepoForm) {

ctx.JSON(http.StatusOK, response.Success())
}

+ 155
- 0
routers/api/v1/tech/repo.go View File

@@ -0,0 +1,155 @@
package tech

import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/response"
"code.gitea.io/gitea/services/repository"
techService "code.gitea.io/gitea/services/tech"
"errors"
"fmt"
"net/http"
)

//CommitOpenIRepo 新建启智项目申请页面提交
func CommitOpenIRepo(ctx *context.APIContext, form api.OpenITechRepo) {
//解析项目路径,查找项目
repo, err := repository.FindRepoByUrl(form.Url)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr(err.Error())))
return
}
//查找项目编号
tech, err := models.GetTechByTechNo(form.TechNo)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr(err.Error())))
return
}

//判断承接单位是否在科技项目的参与单位中
if !tech.IsValidInstitution(form.Institution) {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr("tech.institution_not_valid")))
return
}

//判断是否已经存在了该项目
exist, err := techService.IsValidRepoConvergeExists(repo.ID, tech.ID)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr(err.Error())))
return
}
if exist {
ctx.JSON(http.StatusOK, response.OuterServerError(fmt.Sprintf(ctx.Tr("tech.repo_converge_exists"), tech.ProjectName, repo.Alias)))
return
}

//写入数据库
rci := &models.RepoConvergeInfo{
RepoID: repo.ID,
Url: form.Url,
BaseInfoID: tech.ID,
Institution: form.Institution,
UID: ctx.User.ID,
Status: models.DefaultTechApprovedStatus,
}
err = rci.InsertOrUpdate()
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr(err.Error())))
return
}
ctx.JSON(http.StatusOK, response.OuterSuccess())
}

//CommitNotOpenIRepo 新建非启智项目申请页面提交
func CommitNotOpenIRepo(ctx *context.APIContext, form api.NotOpenITechRepo) {
//触发更新迁移状态
go techService.UpdateTechMigrateStatus()

//查找项目编号
tech, err := models.GetTechByTechNo(form.TechNo)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr(err.Error())))
return
}

//判断承接单位是否在科技项目的参与单位中
if !tech.IsValidInstitution(form.Institution) {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr("tech.institution_not_valid")))
return
}

//调用迁移接口
bizErr := repository.MigrateSubmit(ctx.User, auth.MigrateRepoForm{
CloneAddr: form.Url,
UID: form.UID,
RepoName: form.RepoName,
Alias: form.Alias,
Description: form.Description,
Labels: false,
Mirror: true,
})
if bizErr != nil {
if bizErr.Code == 3 {
ctx.JSON(http.StatusOK, response.OuterError(bizErr.Code, ctx.Tr("tech.to_migrate_repo_exists")))
return
}
ctx.JSON(http.StatusOK, response.OuterError(bizErr.Code, ctx.Tr(bizErr.Err)))
return
}

repo, err := models.GetRepositoryByName(form.UID, form.RepoName)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr(err.Error())))
return
}

//判断是否已经存在了该项目
exist, err := techService.IsValidRepoConvergeExists(repo.ID, tech.ID)
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr(err.Error())))
return
}
if exist {
ctx.JSON(http.StatusOK, response.OuterServerError(fmt.Sprintf(ctx.Tr("tech.repo_converge_exists"), tech.ProjectName, repo.Alias)))
return
}

//给仓库加标签
updateTopics(repo.ID, form.Topics)

//写入数据库
rci := &models.RepoConvergeInfo{
RepoID: repo.ID,
Url: form.Url,
BaseInfoID: tech.ID,
Institution: form.Institution,
UID: ctx.User.ID,
Status: models.DefaultTechApprovedStatus,
}
err = rci.InsertOrUpdate()
if err != nil {
ctx.JSON(http.StatusOK, response.OuterServerError(ctx.Tr(err.Error())))
return
}
ctx.JSON(http.StatusOK, response.OuterSuccess())
}

func updateTopics(repoId int64, topicNames []string) error {
validTopics, invalidTopics := models.SanitizeAndValidateTopics(topicNames)

if len(validTopics) > 25 {
return errors.New("Exceeding maximum number of topics per repo")
}

if len(invalidTopics) > 0 {
return errors.New("Topic names are invalid")
}

err := models.SaveTopics(repoId, validTopics...)
if err != nil {
return err
}
return nil
}

+ 336
- 0
routers/api/v1/tech/tech.go View File

@@ -0,0 +1,336 @@
package tech

import (
"code.gitea.io/gitea/routers/response"
"code.gitea.io/gitea/services/role"
techService "code.gitea.io/gitea/services/tech"
"net/http"
"strconv"
"strings"

"code.gitea.io/gitea/modules/log"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"github.com/360EntSecGroup-Skylar/excelize/v2"
)

func GetFilterInfo(ctx *context.APIContext) {
filterType := ctx.QueryInt("type")
limit := ctx.QueryInt("limit")
if filterType == 0 {
projectTypes := models.GetProjectTypes()
allInstitutions := models.GetAllInstitutions()
applyYears, executeYears := models.GetApplyExecuteYears()
ctx.JSON(http.StatusOK, map[string]interface{}{
"type_name": projectTypes,
"institution_name": allInstitutions,
"execute_year": executeYears,
"apply_year": applyYears,
})
return
}
if filterType == 1 {
projectNames := models.GetProjectNames()
topics := models.GetTechRepoTopics(limit)
allInstitutions := models.GetAllInstitutions()
ctx.JSON(http.StatusOK, map[string]interface{}{
"topic": topics,
"institution_name": allInstitutions,
"project_name": projectNames,
})
return

}
ctx.Error(http.StatusBadRequest, "parameter error", "")

}

func GetAdminRepoInfo(ctx *context.APIContext) {
go techService.UpdateTechStatus()
opts := &models.SearchUserRepoOpt{}
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
opts.Page = page
pageSize := ctx.QueryInt("pageSize")
if pageSize <= 0 {
pageSize = 15
}
opts.PageSize = pageSize
infos, total, err := techService.GetAdminRepoInfo(opts)
if err != nil {
log.Error("search has error", err)
ctx.JSON(http.StatusOK, map[string]interface {
}{"total": 0, "data": []*models.TechRepoInfoAdmin{}})
} else {
ctx.JSON(http.StatusOK, map[string]interface {
}{"total": total, "data": infos})
}

}

func Action(ctx *context.APIContext, ids ActionIDs) {
if len(ids.ID) == 0 {
ctx.JSON(http.StatusOK, models.BaseOKMessageApi)
}
var err error
switch ctx.Params(":action") {

case "show":
err = models.ShowTechRepo(ids.ID, true)
case "hide":
err = models.ShowTechRepo(ids.ID, false)
}
if err != nil {
ctx.JSON(http.StatusOK, models.BaseErrorMessage(ctx.Tr("tech.show_or_hide_fail", ctx.Params(":action"))))
} else {
ctx.JSON(http.StatusOK, models.BaseOKMessage)
}

}

type ActionIDs struct {
ID []int64 `json:"id"`
}

func GetMyRepoInfo(ctx *context.APIContext) {
go techService.UpdateTechStatus()
opts := &models.SearchUserRepoOpt{}
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
opts.Page = page
pageSize := ctx.QueryInt("pageSize")
if pageSize <= 0 {
pageSize = 15
}
opts.PageSize = pageSize
opts.User = ctx.User
infos, total, err := techService.GetMyRepoInfo(opts)
if err != nil {
log.Error("search has error", err)
ctx.JSON(http.StatusOK, map[string]interface {
}{"total": 0, "data": []*models.TechRepoInfoUser{}})
} else {
ctx.JSON(http.StatusOK, map[string]interface {
}{"total": total, "data": infos})
}

}

func SearchRepoInfo(ctx *context.APIContext) {
go techService.UpdateTechStatus()
opts := &models.SearchRepoOpt{}
opts.Q = ctx.Query("name")
opts.ProjectName = ctx.Query("tech_name")
opts.Topic = ctx.Query("topic")
opts.Institution = ctx.Query("institution_name")
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
opts.Page = page
pageSize := ctx.QueryInt("pageSize")
if pageSize <= 0 {
pageSize = 30
}
opts.PageSize = pageSize
orderBy := ctx.Query("sort")
opts.OrderBy = orderBy
infos, total, err := techService.SearchRepoInfoWithInstitution(opts)
if err != nil {
log.Error("search has error", err)
ctx.JSON(http.StatusOK, map[string]interface {
}{"total": 0, "data": []*models.RepoWithInstitution{}})
} else {
ctx.JSON(http.StatusOK, map[string]interface {
}{"total": total, "data": infos})
}

}

func SearchTechProjectInfo(ctx *context.APIContext) {
go techService.UpdateTechStatus()
opts := &models.SearchTechOpt{}
opts.Q = ctx.Query("name")
opts.ApplyYear = ctx.QueryInt("apply_year")
opts.ExecuteYear = ctx.QueryInt("execute_year")
opts.Institution = ctx.Query("institution_name")
opts.ProjectType = ctx.Query("type_name")
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
opts.Page = page
pageSize := ctx.QueryInt("pageSize")
if pageSize <= 0 {
pageSize = 15
}
opts.PageSize = pageSize
orderBy := ctx.Query("sort")
if orderBy == "" || orderBy == "count" {
opts.OrderBy = "order by repo_count desc, max desc "
} else {
opts.OrderBy = "order by max desc, repo_count desc "
}

infos, total, err := models.SearchTechRepoInfo(opts)
if err != nil {

log.Error("search has error", err)
ctx.JSON(http.StatusOK, map[string]interface {
}{"total": 0, "data": []*models.TechRepoInfo{}})
} else {
ctx.JSON(http.StatusOK, map[string]interface {
}{"total": total, "data": infos})
}

}

func ImportBasicInfo(ctx *context.APIContext) {
file, _, err := ctx.GetFile("file")
if err != nil {
log.Error("get file err", err)
ctx.JSON(http.StatusBadRequest, models.BaseErrorMessageApi(ctx.Tr("tech.get_file_fail")))
return
}
defer file.Close()

f, err := excelize.OpenReader(file)
if err != nil {
log.Error("open file err", err)
ctx.JSON(http.StatusBadRequest, models.BaseErrorMessageApi(ctx.Tr("tech.content_type_unsupported")))
return
}
rows, err := f.GetRows(f.GetSheetName(1))
if err != nil {
ctx.JSON(http.StatusBadRequest, models.BaseErrorMessageApi(ctx.Tr("tech.content_type_unsupported")))
return
}
for i, row := range rows {
if i != 0 {
baseInfo := &models.TechConvergeBaseInfo{}
baseInfo.ProjectNumber = strings.TrimSpace(row[2])
tbInDB, err := models.GetTechConvergeBaseInfoByProjectNumber(baseInfo.ProjectNumber)
if err != nil && !models.IsErrTechConvergeBaseInfoNotExist(err) {
log.Error("query err ", i, err)
ctx.JSON(http.StatusBadRequest, models.BaseErrorMessageApi(ctx.Tr("tech.sql_err")))
return
}
if tbInDB != nil {
baseInfo = tbInDB
}

baseInfo.ApplyYear, err = strconv.Atoi(strings.TrimSpace(row[1]))
if err != nil {
log.Warn("base info apply year parse err ", i)
}
baseInfo.ProjectName = strings.TrimSpace(row[3])
baseInfo.Type = strings.TrimSpace(row[16])
baseInfo.Owner = strings.TrimSpace(row[8])
baseInfo.Email = strings.TrimSpace(row[10])
baseInfo.Phone = strings.TrimSpace(row[9])
baseInfo.Recommend = strings.TrimSpace(row[7])
baseInfo.Institution = strings.TrimSpace(row[4])
baseInfo.Category = strings.TrimSpace(row[6])
baseInfo.Province = strings.TrimSpace(row[5])
baseInfo.Contact = strings.TrimSpace(row[11])
baseInfo.ContactPhone = strings.TrimSpace(row[12])
baseInfo.ContactEmail = strings.TrimSpace(row[13])
baseInfo.ExecuteMonth, _ = strconv.Atoi(strings.TrimSpace(row[14]))
baseInfo.ExecutePeriod = strings.TrimSpace(row[15])
baseInfo.ExecuteStartYear, baseInfo.ExecuteEndYear = getBeginEndYear(baseInfo.ExecutePeriod)
baseInfo.StartUp = strings.TrimSpace(row[17])
baseInfo.NumberTopic, _ = strconv.Atoi(strings.TrimSpace(row[30]))
baseInfo.Topic1 = strings.TrimSpace(row[31])
baseInfo.Topic2 = strings.TrimSpace(row[32])
baseInfo.Topic3 = strings.TrimSpace(row[33])
baseInfo.Topic4 = strings.TrimSpace(row[34])
baseInfo.Topic5 = strings.TrimSpace(row[35])

allIns := replaceNewLineWithComma(strings.TrimSpace(row[36]))
if allIns != "" {
allInsArray := strings.Split(allIns, ",")
var allInsValid []string
for _, ins := range allInsArray {
if ins != "" {
allInsValid = append(allInsValid, ins)
}
}
baseInfo.AllInstitution = strings.Join(allInsValid, ",")
}
if baseInfo.AllInstitution == "" {
baseInfo.AllInstitution = baseInfo.Institution
}

err = baseInfo.InsertOrUpdate()
if err != nil {
log.Error("update err", i, err)
ctx.JSON(http.StatusBadRequest, models.BaseErrorMessageApi(ctx.Tr("tech.sql_err")))
return
}

}
}
ctx.JSON(http.StatusOK, models.BaseOKMessageApi)
}

func replaceNewLineWithComma(s string) string {
const replacement = ","

var replacer = strings.NewReplacer(
"\r\n", replacement,
"\r", replacement,
"\n", replacement,
"\v", replacement,
"\f", replacement,
"\u0085", replacement,
"\u2028", replacement,
"\u2029", replacement,
)
return replacer.Replace(s)
}

/**
input:2019.12-2023.12 output: 2019,2023
*/
func getBeginEndYear(executePeriod string) (int, int) {
period := strings.Split(executePeriod, "-")
if len(period) == 2 {
start := strings.Split(period[0], ".")
end := strings.Split(period[1], ".")

if len(start) == 2 && len(end) == 2 {
startYear, err := strconv.Atoi(start[0])
endYear, err1 := strconv.Atoi(end[0])
if err == nil && err1 == nil {
return startYear, endYear
}

}

}
return 0, 0
}

func FindTech(ctx *context.APIContext) {
techNo := ctx.Query("no")
name := ctx.Query("name")
institution := ctx.Query("institution")
r, err := techService.FindTech(models.FindTechOpt{TechNo: techNo, ProjectName: name, Institution: institution})
if err != nil {
ctx.JSON(http.StatusOK, response.OuterResponseError(err))
return
}
ctx.JSON(http.StatusOK, response.OuterSuccessWithData(r))
}

func IsAdmin(ctx *context.APIContext) {
isAdmin := role.UserHasRole(ctx.User.ID, models.TechProgramAdmin)
r := map[string]interface{}{}
r["is_admin"] = isAdmin
ctx.JSON(http.StatusOK, response.OuterSuccessWithData(r))
}

+ 3
- 0
routers/response/api_response.go View File

@@ -28,3 +28,6 @@ func OuterSuccessWithData(data interface{}) *AiforgeOuterResponse {
func OuterErrorWithData(code int, msg string, data interface{}) *AiforgeOuterResponse {
return &AiforgeOuterResponse{Code: code, Msg: msg, Data: data}
}
func OuterResponseError(err error) *AiforgeOuterResponse {
return &AiforgeOuterResponse{Code: RESPONSE_CODE_ERROR_DEFAULT, Msg: err.Error()}
}

+ 4
- 0
routers/response/error.go View File

@@ -12,3 +12,7 @@ func (b BizError) Error() string {
func NewBizError(err error) *BizError {
return &BizError{Code: RESPONSE_CODE_ERROR_DEFAULT, Err: err.Error()}
}

func BuildBizError(code int, msg string) *BizError {
return &BizError{Code: code, Err: msg}
}

+ 11
- 0
routers/routes/routes.go View File

@@ -12,6 +12,8 @@ import (
"text/template"
"time"

"code.gitea.io/gitea/routers/tech"

"code.gitea.io/gitea/routers/badge"
"code.gitea.io/gitea/routers/reward/point"
"code.gitea.io/gitea/routers/task"
@@ -896,6 +898,15 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/check_name", repo.CheckName)
}, reqSignIn)

m.Group("/tech", func() {
m.Get("/new", tech.Create)
m.Get("/tech_view", tech.TechView)
m.Get("/repo_view", tech.RepoView)
m.Get("/admin_view", tech.AdminView)
m.Get("/my_view", tech.MyView)

}, reqSignIn)

// ***** Release Attachment Download without Signin
m.Get("/:username/:reponame/releases/download/:vTag/:fileName", ignSignIn, context.RepoAssignment(), repo.MustBeNotEmpty, repo.RedirectDownload)



+ 32
- 0
routers/tech/tech.go View File

@@ -0,0 +1,32 @@
package tech

import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
)

const (
tplCreate base.TplName = "tech/create"
tplTech base.TplName = "tech/tech_view"
tplRepo base.TplName = "tech/repo_view"
tplAdmin base.TplName = "tech/admin_view"
tplMine base.TplName = "tech/my_view"
)

func Create(ctx *context.Context) {
ctx.HTML(200, tplCreate)
}

func TechView(ctx *context.Context) {
ctx.HTML(200, tplTech)
}
func RepoView(ctx *context.Context) {
ctx.HTML(200, tplRepo)
}
func AdminView(ctx *context.Context) {
ctx.HTML(200, tplAdmin)
}

func MyView(ctx *context.Context) {
ctx.HTML(200, tplMine)
}

+ 146
- 0
services/repository/migrate.go View File

@@ -0,0 +1,146 @@
package repository

import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/migrations"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/task"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/response"
"errors"
"fmt"
"net/url"
"strings"
)

func MigrateSubmit(currentUser *models.User, form auth.MigrateRepoForm) *response.BizError {
log.Info("receive MigrateSubmit request")
ctxUser, bizErr := checkMigrateUser(currentUser, form.UID)
if bizErr != nil {
return bizErr
}

remoteAddr, err := form.ParseRemoteAddr(currentUser)
if err != nil {
if models.IsErrInvalidCloneAddr(err) {
addrErr := err.(models.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
return response.PARAM_ERROR
case addrErr.IsPermissionDenied:
return response.INSUFFICIENT_PERMISSION
case addrErr.IsInvalidPath:
return response.PARAM_ERROR
default:
}
}
return response.NewBizError(err)
}

var gitServiceType = api.PlainGitService
u, err := url.Parse(form.CloneAddr)
if err == nil && strings.EqualFold(u.Host, "github.com") {
gitServiceType = api.GithubService
}

var opts = migrations.MigrateOptions{
OriginalURL: form.CloneAddr,
GitServiceType: gitServiceType,
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Alias: form.Alias,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Mirror: form.Mirror,
AuthUsername: form.AuthUsername,
AuthPassword: form.AuthPassword,
Wiki: form.Wiki,
Issues: form.Issues,
Milestones: form.Milestones,
Labels: form.Labels,
Comments: true,
PullRequests: form.PullRequests,
Releases: form.Releases,
}
if opts.Mirror {
opts.Issues = false
opts.Milestones = false
opts.Labels = false
opts.Comments = false
opts.PullRequests = false
opts.Releases = false
}

err = models.CheckCreateRepository(currentUser, ctxUser, opts.RepoName, opts.Alias)
if err != nil {
return handleMigrateError4Api(ctxUser, remoteAddr, err)
}

err = task.MigrateRepository(currentUser, ctxUser, opts)
if err != nil {
return handleMigrateError4Api(ctxUser, remoteAddr, err)
}
return nil
}

func checkMigrateUser(currentUser *models.User, uid int64) (*models.User, *response.BizError) {
if uid == currentUser.ID || uid == 0 {
return currentUser, nil
}

org, err := models.GetUserByID(uid)
if models.IsErrUserNotExist(err) {
return currentUser, nil
}

if err != nil {
return nil, response.SYSTEM_ERROR
}

// Check ownership of organization.
if !org.IsOrganization() {
return nil, nil
}
if !currentUser.IsAdmin {
canCreate, err := org.CanCreateOrgRepo(currentUser.ID)
if err != nil {
return nil, response.NewBizError(err)
} else if !canCreate {
return nil, response.INSUFFICIENT_PERMISSION
}
}
return org, nil
}

func handleMigrateError4Api(repoOwner *models.User, remoteAddr string, err error) *response.BizError {
switch {
case models.IsErrRepoAlreadyExist(err):
return response.BuildBizError(3, "The repository with the same name already exists.")
case migrations.IsRateLimitError(err):
return response.NewBizError(errors.New("Remote visit addressed rate limitation."))
case migrations.IsTwoFactorAuthError(err):
return response.NewBizError(errors.New("Remote visit required two factors authentication."))
case models.IsErrReachLimitOfRepo(err):
return response.NewBizError(errors.New(fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit())))
case models.IsErrNameReserved(err):
return response.NewBizError(errors.New(fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name)))
case models.IsErrNameCharsNotAllowed(err):
return response.NewBizError(errors.New(fmt.Sprintf("The username '%s' contains invalid characters.", err.(models.ErrNameCharsNotAllowed).Name)))
case models.IsErrNamePatternNotAllowed(err):
return response.NewBizError(errors.New(fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern)))

default:
err = util.URLSanitizedError(err, remoteAddr)
if strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "Bad credentials") ||
strings.Contains(err.Error(), "could not read Username") {
return response.NewBizError(errors.New((fmt.Sprintf("Authentication failed: %v.", err))))
} else if strings.Contains(err.Error(), "fatal:") {
return response.NewBizError(errors.New((fmt.Sprintf("Migration failed: %v.", err))))
}
}
return response.NewBizError(err)
}

+ 2
- 0
services/repository/square.go View File

@@ -169,6 +169,7 @@ type FindReposOptions struct {
Topic string
Private bool
OwnerID int64
RepoIds []int64
}

func FindRepos(opts FindReposOptions) (*models.FindReposResponse, error) {
@@ -224,6 +225,7 @@ func FindRepos(opts FindReposOptions) (*models.FindReposResponse, error) {
AllLimited: true,
TopicName: opts.Topic,
IncludeDescription: setting.UI.SearchRepoDescription,
RepoIds: opts.RepoIds,
})
if err != nil {
log.Error("FindRepos error when SearchRepository.%v", err)


+ 49
- 0
services/repository/url.go View File

@@ -0,0 +1,49 @@
package repository

import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/setting"
"errors"
"net/url"
"strings"
)

func FindRepoByUrl(url string) (*models.Repository, error) {
ownerName, repoName := parseOpenIUrl(url)
if ownerName == "" || repoName == "" {
return nil, errors.New("tech.incorrect_openi_format")
}
r, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
if err != nil {
if models.IsErrRepoNotExist(err) {
return nil, errors.New("tech.openi_repo_not_exist")
}
return nil, err
}
return r, nil
}

//parseOpenIUrl parse openI repo url,return ownerName and repoName
func parseOpenIUrl(u string) (string, string) {
url, err := url.Parse(u)
if err != nil {
return "", ""
}

appUrl, err := url.Parse(setting.AppURL)
if err != nil {
return "", ""
}
if appUrl.Host != url.Host {
return "", ""
}

array := strings.Split(url.Path, "/")
if len(array) < 3 {
return "", ""
}

ownerName := array[1]
repoName := array[2]
return ownerName, repoName
}

+ 15
- 0
services/role/role.go View File

@@ -0,0 +1,15 @@
package role

import "code.gitea.io/gitea/models"

var roleMap = map[models.RoleType]*models.Role{
models.TechProgramAdmin: {
Type: models.TechProgramAdmin,
Name: "科技项目管理员",
Description: "拥有科技项目管理相关功能的管理员权限",
},
}

func GetRole(roleType models.RoleType) *models.Role {
return roleMap[roleType]
}

+ 45
- 0
services/role/user_role.go View File

@@ -0,0 +1,45 @@
package role

import (
"code.gitea.io/gitea/models"
pg "github.com/lib/pq"
)

func AddUserRole(userId int64, roleType models.RoleType) error {
role := GetRole(roleType)
if role == nil {
return models.ErrRoleNotExists{}
}
_, err := models.NewUserRole(models.UserRole{UserId: userId, RoleType: roleType})
if err != nil {
e := err.(*pg.Error)
//23505 is postgrey error code for unique_violation
//see https://www.postgresql.org/docs/current/errcodes-appendix.html
//if unique_violation,user role record exists,just return
if e.Code == "23505" {
return nil
}
}
return err
}

func DeleteUserRole(userId int64, roleType models.RoleType) error {
role := GetRole(roleType)
if role == nil {
return models.ErrRoleNotExists{}
}
_, err := models.DeleteUserRole(roleType, userId)
return err
}

func UserHasRole(userId int64, roleType models.RoleType) bool {
role := GetRole(roleType)
if role == nil {
return false
}
_, err := models.GetUserRoleByUserAndRole(userId, roleType)
if err != nil {
return false
}
return true
}

+ 234
- 0
services/tech/tech.go View File

@@ -0,0 +1,234 @@
package tech

import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/repository"
)

func FindTech(opt models.FindTechOpt) ([]*models.TechConvergeBrief, error) {
techList, err := models.FindTech(opt)
if err != nil {
return nil, err
}
r := make([]*models.TechConvergeBrief, len(techList))
for i := 0; i < len(techList); i++ {
r[i] = techList[i].Brief()
}
return r, nil
}
func GetMyRepoInfo(opts *models.SearchUserRepoOpt) ([]*models.TechRepoInfoUser, int64, error) {
infos, total, err := models.GetTechRepoInfoForUser(opts)
if err != nil {
return nil, total, err
}
result := make([]*models.TechRepoInfoUser, 0, total)
for _, info := range infos {

techRepoInfoUser := &models.TechRepoInfoUser{
ID: info.ID,
Url: info.Url,
Status: info.Status,
ContributionInstitution: info.Institution,
CreatedUnix: info.CreatedUnix,
UpdatedUnix: info.UpdatedUnix,
}
if info.User != nil {
techRepoInfoUser.UserName = info.User.Name
}
if info.BaseInfo != nil {
baseInfo := info.BaseInfo
techRepoInfoUser.ProjectNumber = baseInfo.ProjectNumber
techRepoInfoUser.ProjectName = baseInfo.ProjectName
techRepoInfoUser.Institution = baseInfo.Institution

techRepoInfoUser.AllInstitution = baseInfo.AllInstitution
}
if info.Repo != nil {
techRepoInfoUser.RepoName = info.Repo.Name
techRepoInfoUser.RepoOwnerName = info.Repo.OwnerName
}

result = append(result, techRepoInfoUser)
}
return result, total, nil
}
func GetAdminRepoInfo(opts *models.SearchUserRepoOpt) ([]*models.TechRepoInfoAdmin, int64, error) {
infos, total, err := models.GetTechRepoInfoForUser(opts)
if err != nil {
return nil, total, err
}
result := make([]*models.TechRepoInfoAdmin, 0, total)
for _, info := range infos {

techRepoInfoAdmin := &models.TechRepoInfoAdmin{
ID: info.ID,
Url: info.Url,
Status: info.Status,
ContributionInstitution: info.Institution,
CreatedUnix: info.CreatedUnix,
UpdatedUnix: info.UpdatedUnix,
}
if info.User != nil {
techRepoInfoAdmin.UserName = info.User.Name
}
if info.BaseInfo != nil {
baseInfo := info.BaseInfo
techRepoInfoAdmin.ProjectNumber = baseInfo.ProjectNumber
techRepoInfoAdmin.ProjectName = baseInfo.ProjectName
techRepoInfoAdmin.Institution = baseInfo.Institution
techRepoInfoAdmin.ApplyYear = baseInfo.ApplyYear
techRepoInfoAdmin.Province = baseInfo.Province
techRepoInfoAdmin.Category = baseInfo.Category
techRepoInfoAdmin.Owner = baseInfo.Owner
techRepoInfoAdmin.Recommend = baseInfo.Recommend
techRepoInfoAdmin.Phone = baseInfo.Phone
techRepoInfoAdmin.Email = baseInfo.Email

techRepoInfoAdmin.Contact = baseInfo.Contact
techRepoInfoAdmin.ContactPhone = baseInfo.ContactPhone
techRepoInfoAdmin.ContactEmail = baseInfo.ContactEmail
techRepoInfoAdmin.ExecuteMonth = baseInfo.ExecuteMonth
techRepoInfoAdmin.ExecutePeriod = baseInfo.ExecutePeriod
techRepoInfoAdmin.Type = baseInfo.Type
techRepoInfoAdmin.StartUp = baseInfo.StartUp
techRepoInfoAdmin.NumberTopic = baseInfo.NumberTopic
techRepoInfoAdmin.Topic1 = baseInfo.Topic1
techRepoInfoAdmin.Topic2 = baseInfo.Topic2
techRepoInfoAdmin.Topic3 = baseInfo.Topic3
techRepoInfoAdmin.Topic4 = baseInfo.Topic4
techRepoInfoAdmin.Topic5 = baseInfo.Topic5
techRepoInfoAdmin.AllInstitution = baseInfo.AllInstitution
}
if info.Repo != nil {
techRepoInfoAdmin.RepoName = info.Repo.Name
techRepoInfoAdmin.RepoOwnerName = info.Repo.OwnerName
}

result = append(result, techRepoInfoAdmin)
}
return result, total, nil

}

func SearchRepoInfoWithInstitution(opt *models.SearchRepoOpt) ([]*models.RepoWithInstitution, int64, error) {
infos, err := models.GetAvailableRepoConvergeInfo(opt)
if err != nil {
return nil, 0, err
}
if len(infos) == 0 {
return []*models.RepoWithInstitution{}, 0, nil
}
repoIds, institutionMap := getRepoIdAndInstitutionMap(infos)

result, err := repository.FindRepos(repository.FindReposOptions{
ListOptions: models.ListOptions{Page: opt.Page, PageSize: opt.PageSize},
Sort: opt.OrderBy,
Keyword: opt.Q,
Topic: opt.Topic,
RepoIds: repoIds,
})

if err != nil {
return nil, 0, err
}
repoResult := make([]*models.RepoWithInstitution, 0, len(result.Repos))

for _, repo := range result.Repos {
repoResult = append(repoResult, &models.RepoWithInstitution{
ID: repo.ID,
OwnerID: repo.OwnerID,
OwnerName: repo.OwnerName,
Name: repo.Name,
Alias: repo.Alias,
Topics: repo.Topics,
Description: repo.Description,
Institution: institutionMap[repo.ID],
RelAvatarLink: repo.RelAvatarLink,
UpdatedUnix: repo.UpdatedUnix,
})
}

return repoResult, result.Total, nil

}

func getRepoIdAndInstitutionMap(infos []*models.RepoConvergeInfo) ([]int64, map[int64]string) {
repoIds := make([]int64, 0, len(infos))
institutionMap := make(map[int64]string, 0)
// We only need the keys
for _, info := range infos {
repoIds = append(repoIds, info.RepoID)
institutionMap[info.RepoID] = info.Institution
}
return repoIds, institutionMap
}


func IsValidRepoConvergeExists(repoId, baseInfoId int64) (bool, error) {
list, err := models.GetRepoConverge(models.GetRepoConvergeOpts{
Status: []int{models.TechHide, models.TechShow, models.TechMigrating},
RepoId: repoId,
BaseInfoId: baseInfoId,
})
if err != nil {
return false, err
}
return len(list) > 0, nil
}

func UpdateTechStatus() {
err := UpdateTechRepoDeleteStatus()
if err != nil {
log.Error("update delete status fail", err)
}

err = UpdateTechMigrateStatus()
if err != nil {
log.Error("update migrate status fail", err)
}
}

func UpdateTechRepoDeleteStatus() error {
needDeleteCheckRepos, err := models.GetRepoConverge(models.GetRepoConvergeOpts{
Status: []int{models.TechShow, models.TechHide},
})
if err != nil {
return err
}

for _, r := range needDeleteCheckRepos {
_, err := models.GetRepositoryByID(r.RepoID)
if err != nil && models.IsErrRepoNotExist(err) {
models.UpdateRepoConvergeStatus(r.ID, models.TechNotExist)
continue
}

}
return nil

}

func UpdateTechMigrateStatus() error {
migratingRepos, err := models.GetRepoConverge(models.GetRepoConvergeOpts{
Status: []int{models.TechMigrating},
})
if err != nil {
return err
}
for _, r := range migratingRepos {
repo, err := models.GetRepositoryByID(r.RepoID)
if err != nil {
if models.IsErrRepoNotExist(err) {
models.UpdateRepoConvergeStatus(r.ID, models.TechMigrateFailed)
continue
}
continue
}
if repo.Status == models.RepositoryReady {
models.UpdateRepoConvergeStatus(r.ID, models.DefaultTechApprovedStatus)
continue
}
}
return nil
}

+ 3
- 1
templates/base/head_navbar.tmpl View File

@@ -55,7 +55,8 @@
{{/* <a class="item" href="{{AppSubUrl}}/explore/data_analysis">{{.i18n.Tr "explore.data_analysis"}}</a> */}}
<a class="item" href="{{AppSubUrl}}/kanban/index.html" target="_blank" rel="opener">{{.i18n.Tr "explore.data_analysis"}}</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/tech/tech_view">2030科技项目</a>
</div>
</div>
{{else if .IsLandingPageHome}}
@@ -97,6 +98,7 @@
<a class="item" href="{{AppSubUrl}}/kanban/index.html" target="_blank" rel="opener">{{.i18n.Tr "explore.data_analysis"}}</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/tech/tech_view">2030科技项目</a>
</div>
</div>
{{else if .IsLandingPageExplore}}


+ 3
- 1
templates/base/head_navbar_fluid.tmpl View File

@@ -52,7 +52,8 @@
{{/* <a class="item" href="{{AppSubUrl}}/explore/data_analysis">{{.i18n.Tr "explore.data_analysis"}}</a> */}}
<a class="item" href="{{AppSubUrl}}/kanban/index.html" target="_blank" rel="opener">{{.i18n.Tr "explore.data_analysis"}}</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/tech/tech_view">2030科技项目</a>
</div>
</div>
{{else if .IsLandingPageHome}}
@@ -92,6 +93,7 @@
<a class="item" href="{{AppSubUrl}}/kanban/index.html" target="_blank" rel="opener">{{.i18n.Tr "explore.data_analysis"}}</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/tech/tech_view">2030科技项目</a>
</div>
</div>
{{else if .IsLandingPageExplore}}


+ 3
- 1
templates/base/head_navbar_home.tmpl View File

@@ -44,7 +44,8 @@
{{/* <a class="item" href="{{AppSubUrl}}/explore/data_analysis">{{.i18n.Tr "explore.data_analysis"}}</a> */}}
<a class="item" href="{{AppSubUrl}}/kanban/index.html" target="_blank" rel="opener">{{.i18n.Tr "explore.data_analysis"}}</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/tech/tech_view">2030科技项目</a>
</div>
</div>
{{else if .IsLandingPageHome}}
@@ -85,6 +86,7 @@
<a class="item" href="{{AppSubUrl}}/kanban/index.html" target="_blank" rel="opener">{{.i18n.Tr "explore.data_analysis"}}</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/tech/tech_view">2030科技项目</a>
</div>
</div>
{{else if .IsLandingPageExplore}}


+ 3
- 1
templates/base/head_navbar_pro.tmpl View File

@@ -54,7 +54,8 @@
{{/* <a class="item" href="{{AppSubUrl}}/explore/data_analysis">{{.i18n.Tr "explore.data_analysis"}}</a> */}}
<a class="item" href="{{AppSubUrl}}/kanban/index.html" target="_blank" rel="opener">{{.i18n.Tr "explore.data_analysis"}}</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/tech/tech_view">2030科技项目</a>
</div>
</div>
{{else if .IsLandingPageHome}}
@@ -95,6 +96,7 @@
<a class="item" href="{{AppSubUrl}}/kanban/index.html" target="_blank" rel="opener">{{.i18n.Tr "explore.data_analysis"}}</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/OpenI">{{.i18n.Tr "custom.head.openi.repo"}}</a>
<a class="item" href="{{AppSubUrl}}/tech/tech_view">2030科技项目</a>
</div>
</div>
{{else if .IsLandingPageExplore}}


+ 5
- 0
templates/tech/admin_view.tmpl View File

@@ -0,0 +1,5 @@
{{template "base/head_home" .}}
<link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-tech-adminview.css?v={{MD5 AppVer}}" />
<div id="__vue-root"></div>
<script src="{{StaticUrlPrefix}}/js/vp-tech-adminview.js?v={{MD5 AppVer}}"></script>
{{template "base/footer" .}}

+ 5
- 0
templates/tech/create.tmpl View File

@@ -0,0 +1,5 @@
{{template "base/head_home" .}}
<link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-tech-create.css?v={{MD5 AppVer}}" />
<div id="__vue-root"></div>
<script src="{{StaticUrlPrefix}}/js/vp-tech-create.js?v={{MD5 AppVer}}"></script>
{{template "base/footer" .}}

+ 5
- 0
templates/tech/my_view.tmpl View File

@@ -0,0 +1,5 @@
{{template "base/head_home" .}}
<link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-tech-myview.css?v={{MD5 AppVer}}" />
<div id="__vue-root"></div>
<script src="{{StaticUrlPrefix}}/js/vp-tech-myview.js?v={{MD5 AppVer}}"></script>
{{template "base/footer" .}}

+ 5
- 0
templates/tech/repo_view.tmpl View File

@@ -0,0 +1,5 @@
{{template "base/head_home" .}}
<link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-tech-repoview.css?v={{MD5 AppVer}}" />
<div id="__vue-root"></div>
<script src="{{StaticUrlPrefix}}/js/vp-tech-repoview.js?v={{MD5 AppVer}}"></script>
{{template "base/footer" .}}

+ 5
- 0
templates/tech/tech_view.tmpl View File

@@ -0,0 +1,5 @@
{{template "base/head_home" .}}
<link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-tech-techview.css?v={{MD5 AppVer}}" />
<div id="__vue-root"></div>
<script src="{{StaticUrlPrefix}}/js/vp-tech-techview.js?v={{MD5 AppVer}}"></script>
{{template "base/footer" .}}

+ 173
- 0
web_src/vuepages/apis/modules/tech.js View File

@@ -0,0 +1,173 @@
/* 科技项目汇聚功能相关 */
import service from '../service';

// 新建申请页面科技项目查询接口 no-项目编号,name-科技项目名称,institution-参与单位
export const getTechs = (params) => {
return service({
url: '/api/v1/tech',
method: 'get',
params: params,
});
}

// 新建启智项目申请页面提交 url-启智项目地址,no-项目立项编号,institution-贡献单位,多个单位用逗号分隔
export const setOpenIApply = (data) => {
return service({
url: '/api/v1/tech/openi',
method: 'post',
data: data,
params: {},
});
}

// 新建启智项目申请页面提交
// url-启智项目地址,uid-启智项目uid,repo_name-启智项目名称,topics-关键词,description-简介,
// no-项目立项编号,institution-贡献单位,多个单位用逗号分隔
export const setNoOpenIApply = (data) => {
return service({
url: '/api/v1/tech/no_openi',
method: 'post',
data: data,
params: {},
});
}

// 新建非启智项目申请页面项目路径可选的用户和组织
// 返回 [{id:用户ID,name:用户名字,rel_avatar_link:用户图像地址,short_name:用户短名称},...]
export const getCreateRepoUser = () => {
return service({
url: '/api/v1/user/owners',
method: 'get',
params: {},
});
}

// 项目过滤信息列表-筛选信息 type=0查询科技项目页, type=1查询启智项目页
// 返回 {type_name:项目类型的名称, 字符串数组,institution_name:参与单位的名称,字符串数组,execute_year:执行年份,字符串数组,
// apply_year:申请年份,字符串数组,topic-关键词,字符串数组,project_name:科技项目名称,字符串数组}
export const getTechFilterInfo = (params) => {
return service({
url: '/api/v1/tech/filter',
method: 'get',
params: {
type: params.type,
},
});
}

// 按科技项目页查询功能
// 输入 name-科技项目名称,模糊匹配,type_name-项目类型的名称,精确匹配,institution_name-参与单位的名称,精确匹配,execute_year-执行年份,精确匹配,apply_year-申请年份,精确匹配,page,pageSize,sort
// 返回 {"total":10,"data":[{}]}
// data: {id:科技项目id,project_name:科技项目名称,institution:承担单位,all_institution:参与单位,apply_year:申报年份,execute_period:执行周期,repo_numer:项目成果数,repo:[]项目信息数组}
export const getTechSearch = (params) => {
return service({
url: '/api/v1/tech/search',
method: 'get',
params: params,
});
}

// 按启智项目页查询功能
// 输入 name-项目名称,模糊匹配,tech_name-科技项目名称,精确匹配,institution_name-参与单位的名称,精确匹配,topic-关键词,精确匹配,page,pageSize,sort
// 返回 {"total":10,"data":[{}]}
// data: {id:项目id,owner_id:项目拥有者id,name:项目名称,alias:项目别名,topics:关键词 字符串数组,description:项目简介,updated_unix:最后更新时间,institution:贡献单位}
export const getTechOpenISearch = (params) => {
return service({
url: '/api/v1/tech/repo_search',
method: 'get',
params: params,
});
}

// 我申请的项目列表
// 输入 page,pageSize
// 返回 {"total":10,"data":[{}]}
// data: {id:记录的id,name:启智项目名称,owner_name:启智项目拥有者名称,tech_name:科技项目名称,tech_number:科技项目编号,institution:项目承担单位,
// execute_period:执行周期,contact:联系人,contact_phone:联系电话,contact_email:联系邮件,apply_user:申请账号,status:状态}
export const getTechMyList = (params) => {
return service({
url: '/api/v1/tech/my',
method: 'get',
params: params,
});
}

// 后台管理项目列表
// 输入 page,pageSize
// 返回 {"total":10,"data":[{}]}
// data: {id:记录的id,name:启智项目名称,owner_name:启智项目拥有者名称,tech_name:科技项目名称,tech_number:科技项目编号,institution:项目承担单位,
// execute_period:执行周期,contact:联系人,contact_phone:联系电话,contact_email:联系邮件,apply_user:申请账号,status:状态}
export const getTechAdminList = (params) => {
return service({
url: '/api/v1/tech/admin',
method: 'get',
params: params,
});
}

// 后台管理项目列表交互(显示或隐藏)
// 输入 type-show/hide,id:[1,2,3]
// 返回 { code:0, message:"" }
export const setTechAdminOperation = (data) => {
return service({
url: `/api/v1/tech/admin/${data.type}`,
method: 'post',
data: {
id: data.id
},
params: {},
});
}

// 后台管理科技项目编号基本信息导入功能(科技项目管理员有权限)
// 输入 form file
export const setTechImportExcel = () => {
return service({
url: `/api/v1/tech/basic`,
method: 'post',
data: {},
params: {},
});
}

// 增加科技项目管理员(管理员才有权限)
// 输入 name:['111', '222']
// 返回 { code:0, message:"" }
export const setTechAdminAdd = (data) => {
return service({
url: '/api/v1/tech/admin_add',
method: 'post',
data: data,
params: {},
});
}

// 判断是否是科技项目管理员
// 输入 name:['111', '222']
export const getIsTechAdmin = () => {
return service({
url: '/api/v1/tech/is_admin',
method: 'get',
params: {},
});
}

// =========================================================
// 搜索Topics q-topic
// 返回 {topics: [{id,repo_count,topic_name,created,updated}]}
export const getTopics = (params) => {
return service({
url: '/api/v1/topics/search',
method: 'get',
params: params,
});
}

// 检测 repo name q:名称, owner:所属者
export const getCheckRepoName = (params) => {
return service({
url: '/repo/check_name',
method: 'get',
params: params,
});
}

+ 365
- 0
web_src/vuepages/pages/tech/adminview/index.vue View File

@@ -0,0 +1,365 @@
<template>
<div>
<TopHeader :menu="'admin_view'"></TopHeader>
<div class="ui container">
<div class="top-container">
<el-checkbox class="check-toggle" v-model="allChecked" @change="allSelectChange">全选/全不选</el-checkbox>
<el-button class="btn-agree" @click="batchOperate(true)">批量同意展示</el-button>
<el-button class="btn-cancel" @click="batchOperate(false)">批量取消展示</el-button>
</div>
<div class="table-container">
<div class="table-title">2030科技项目管理</div>
<div class="table-wrap">
<el-table ref="tableRef" border :data="tableData" style="width:100%;" v-loading="loading" stripe row-key="id">
<el-table-column label="" align="center" header-align="center" width="40" fixed>
<template slot-scope="scope">
<el-checkbox v-model="scope.row.checked" @change="rowSelectChange(scope.row)"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="启智项目名称" align="center" header-align="center" fixed min-width="150">
<template slot-scope="scope">
<span v-if="scope.row.status == 5">
{{ `${scope.row.url.split('/')[3]}/${scope.row.url.split('/')[4]}` }}
</span>
<a v-else target="_blank" :href="`/${scope.row.repo_owner_name}/${scope.row.repo_name}`">
{{ `${scope.row.repo_owner_name}/${scope.row.repo_name}` }}
</a>
</template>
</el-table-column>
<el-table-column prop="name" label="科技项目名称" align="center" header-align="center" fixed
min-width="200"></el-table-column>
<el-table-column prop="status" label="展示状态" align="center" header-align="center" fixed>
<template slot-scope="scope">
<span style="color:rgb(255, 37, 37)"
:style="scope.row.status == 1 ? { color: 'rgb(56, 158, 13)' } : ''">{{
statusMap[scope.row.status] }}</span>
</template>
</el-table-column>
<el-table-column prop="no" label="项目立项编号" align="center" header-align="center"
min-width="120"></el-table-column>
<el-table-column prop="institution" label="项目承担单位" align="center" header-align="center"
min-width="160"></el-table-column>
<el-table-column prop="province" label="所属省(省市)" align="center" header-align="center"
min-width="100"></el-table-column>
<el-table-column prop="category" label="单位性质" align="center" header-align="center"
min-width="120"></el-table-column>
<el-table-column prop="recommend" label="推荐单位" align="center" header-align="center"
min-width="120"></el-table-column>
<el-table-column prop="owner" label="项目负责人" align="center" header-align="center"
min-width="100"></el-table-column>
<el-table-column prop="phone" label="负责人电话" align="center" header-align="center"
min-width="120"></el-table-column>
<el-table-column prop="email" label="负责人邮箱" align="center" header-align="center"
min-width="150"></el-table-column>
<el-table-column prop="contact" label="项目联系人" align="center" header-align="center"
min-width="100"></el-table-column>
<el-table-column prop="contact_phone" label="联系人电话" align="center" header-align="center"
min-width="120"></el-table-column>
<el-table-column prop="contact_email" label="联系人邮箱" align="center" header-align="center"
min-width="150"></el-table-column>
<el-table-column prop="apply_year" label="申报年度" align="center" header-align="center"
min-width="80"></el-table-column>
<el-table-column prop="execute_month" label="执行周期(月)" align="center" header-align="center"
min-width="120"></el-table-column>
<el-table-column prop="execute_period" label="执行期限" align="center" header-align="center"
min-width="120"></el-table-column>
<el-table-column prop="type" label="项目类型" align="center" header-align="center"
min-width="100"></el-table-column>
<el-table-column prop="start_up" label="启动会时间" align="center" header-align="center"
min-width="100"></el-table-column>
<el-table-column prop="number_topic" label="课题数量" align="center" header-align="center"
min-width="80"></el-table-column>
<el-table-column prop="topic1" label="课题一级名称及承担单位" align="center" header-align="center"
min-width="200"></el-table-column>
<el-table-column prop="topic2" label="课题二级名称及承担单位" align="center" header-align="center"
min-width="200"></el-table-column>
<el-table-column prop="topic3" label="课题三级名称及承担单位" align="center" header-align="center"
min-width="200"></el-table-column>
<el-table-column prop="topic4" label="课题四级名称及承担单位" align="center" header-align="center"
min-width="200"></el-table-column>
<el-table-column prop="topic5" label="课题五级名称及承担单位" align="center" header-align="center"
min-width="200"></el-table-column>
<el-table-column prop="all_institution" label="所有参与单位" align="center" header-align="center"
min-width="300"></el-table-column>
<el-table-column prop="contribution_institution" label="成果贡献单位" align="center" header-align="center"
min-width="300"></el-table-column>
<el-table-column prop="user_name" label="申请账号" align="center" header-align="center" width="120">
<template slot-scope="scope">
<a target="_blank" :href="`/${scope.row.user_name}`">{{
`${scope.row.user_name}` }}</a>
</template>
</el-table-column>
<el-table-column prop="" label="操作" align="center" header-align="center" fixed="right" width="100">
<template slot-scope="scope">
<span v-if="scope.row.status == 2" class="op-btn agree" @click="toggleStatus(scope.row)">
<i class="el-icon-check"></i>
<span>同意展示</span>
</span>
<span v-if="scope.row.status == 1" class="op-btn cancel" @click="toggleStatus(scope.row)">
<i class="el-icon-close"></i>
<span>取消展示</span>
</span>
</template>
</el-table-column>
</el-table>
</div>
<div class="op-tips">
<i class="el-icon-info"></i>
提示:批量操作只会对【展示状态】为 “未展示” 或 “已展示” 的数据生效。
</div>
<div class="center">
<el-pagination ref="paginationRef" background @current-change="currentChange" @size-change="sizeChange"
:current-page.sync="page" :page-sizes="pageSizes" :page-size.sync="pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="total">
</el-pagination>
</div>
</div>
</div>
</div>
</template>

<script>
import TopHeader from '../components/TopHeader.vue';
import { getTechAdminList, setTechAdminOperation } from '~/apis/modules/tech';

export default {
data() {
return {
allChecked: false,
loading: false,
tableData: [],

page: 1,
pageSizes: [15, 30, 50, 100],
pageSize: 50,
total: 0,

statusMap: {
'1': '已展示',
'2': '未展示',
'3': '项目迁移中',
'4': '项目迁移失败',
'5': '项目不存在',
},
};
},
components: { TopHeader },
methods: {
getData() {
this.loading = true;
getTechAdminList({
page: this.page,
pageSize: this.pageSize,
}).then(res => {
this.loading = false;
const { total, data } = res.data;
this.tableData = data.map((item, index) => {
return {
checked: false,
...item,
}
});
this.allChecked = false;
this.total = total;
}).catch(err => {
this.loading = false;
console.log(err);
});
},
allSelectChange() {
for (let i = 0, iLen = this.tableData.length; i < iLen; i++) {
this.tableData[i].checked = this.allChecked;
}
this.tableData.splice(1, 0);
},
rowSelectChange(row) {
if (row.checked == false) {
this.allChecked = false;
} else {
if (this.tableData.filter((item) => item.checked).length == this.tableData.length) {
this.allChecked = true;
}
}
this.tableData.splice(1, 0);
},
currentChange(page) {
this.page = page;
this.getData();
},
sizeChange(pageSize) {
this.pageSize = pageSize;
this.getData();
},
toggleStatus(row) {
if (row.status != 1 && row.status != 2) return;
setTechAdminOperation({
type: row.status == 1 ? 'hide' : 'show',
id: [row.id],
}).then(res => {
res = res.data;
if (res.Code == 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully'),
});
row.status = row.status == 1 ? 2 : 1;
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed'),
});
}
}).catch(err => {
console.log(err);
this.$message({
type: 'error',
message: this.$t('submittedFailed'),
});
});
},
batchOperate(showOr) {
const selectedData = this.tableData.filter((item) => {
return item.checked && item.status == (showOr ? 2 : 1);
});
if (!selectedData.length) {
this.$message({
type: 'info',
message: '请选择符合条件的数据进行操作!',
});
return;
}
setTechAdminOperation({
type: showOr ? 'show' : 'hide',
id: selectedData.map(item => item.id),
}).then(res => {
res = res.data;
if (res.Code == 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully'),
});
this.getData();
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed'),
});
}
}).catch(err => {
console.log(err);
this.$message({
type: 'error',
message: this.$t('submittedFailed'),
});
});
},
},
beforeMount() {
this.getData();
},
mounted() { },
beforeDestroy() { },
};
</script>

<style scoped lang="less">
.top-container {
display: flex;
align-items: center;
margin: 30px 0 15px 0;

.check-toggle {
margin-right: 20px;
}

.btn-agree {
margin-right: 10px;
background: rgb(50, 145, 248);
color: rgb(255, 255, 255);

&:hover {
opacity: 0.9;
}

&:active {
opacity: 0.8;
}
}

.btn-cancel {
margin-right: 10px;
background: rgb(250, 140, 22);
color: rgb(255, 255, 255);

&:hover {
opacity: 0.9;
}

&:active {
opacity: 0.8;
}
}
}

.table-container {
.table-title {
height: 43px;
font-size: 16px;
font-weight: 700;
padding: 15px;
color: rgb(16, 16, 16);
border-color: rgb(212, 212, 213);
border-width: 1px;
border-style: solid;
border-radius: 5px 5px 0px 0px;
background: rgb(240, 240, 240);
display: flex;
align-items: center;
}

.table-wrap {
overflow-x: auto;

/deep/ .el-table__header {
th {
background: rgb(249, 249, 249);
font-size: 12px;
color: rgb(136, 136, 136);
font-weight: normal;
}
}

/deep/ .el-table__body {
td {
font-size: 12px;
}
}

/deep/ .el-radio__label {
display: none;
}
}
}

.op-tips {
margin: 10px 0 10px 0;
color: #606266;
font-size: 12px;

i {
margin-right: 4px;
}
}

.op-btn {
cursor: pointer;

&.agree {
color: rgb(56, 158, 13);
}

&.cancel {
color: rgb(50, 145, 248);
}
}
</style>

+ 17
- 0
web_src/vuepages/pages/tech/adminview/vp-tech-adminview.js View File

@@ -0,0 +1,17 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import localeEn from 'element-ui/lib/locale/lang/en';
import localeZh from 'element-ui/lib/locale/lang/zh-CN';
import { i18n, lang } from '~/langs';
import App from './index.vue';

Vue.use(ElementUI, {
locale: lang === 'zh-CN' ? localeZh : localeEn,
size: 'small',
});

new Vue({
i18n,
render: (h) => h(App),
}).$mount('#__vue-root');

+ 289
- 0
web_src/vuepages/pages/tech/components/Filters.vue View File

@@ -0,0 +1,289 @@
<template>
<div>
<div class="filter-main" v-show="!showSecond">
<div class="filter-c" v-for="(item, index) in mainData">
<div class="filter-title">{{ item.title }}</div>
<div class="filter-item-c">
<div class="filter-item" v-for="(_item, _index) in item.showData" @click="changeFilter(item, _item)"
:style="conds[item.key] == _item ? { backgroundColor: item.focusBgColor, color: item.focusColor } : { backgroundColor: item.bgColor, color: item.color }">
{{ _item }}
</div>
</div>
<div class="filter-view-more-c" v-if="item.data.length > item.showMaxLen">
<span class="filter-view-more" @click="goMore(item)">
<i class="el-icon-arrow-down"></i><span>展开更多</span>
</span>
</div>
</div>
</div>
<div class="filter-second" v-show="showSecond">
<div class="filter-second-hd">
<span class="filter-second-go-back" @click="goBack()">
<i class="el-icon-back"></i><span>返回上一级</span>
</span>
</div>
<div class="filter-title">{{ secondData.title }}</div>
<div class="filter-search">
<el-input placeholder="搜索" prefix-icon="el-icon-search" v-model="searchKeyword" clearable
@input="searchFilterItem">
</el-input>
</div>
<div class="filter-item-c">
<div class="filter-item" v-for="(_item, _index) in secondData.showData" @click="changeFilter(secondData, _item)"
:style="conds[secondData.key] == _item ? { backgroundColor: secondData.focusBgColor, color: secondData.focusColor } : { backgroundColor: secondData.bgColor, color: secondData.color }">
{{ _item }}
</div>
</div>
</div>
</div>
</template>

<script>
import { getTechFilterInfo } from '~/apis/modules/tech';

export default {
name: "Filters",
props: {
type: { type: Number, default: -1 }, // 0-tech_view, 1-repo_view
condition: { type: Object, default: () => ({}) },
},
components: {},
data() {
return {
list_tech: [{
title: '项目类型',
key: 'type_name',
bgColor: 'rgb(237, 234, 251)',
color: 'rgb(100, 59, 159)',
focusBgColor: 'rgb(100, 59, 159)',
focusColor: 'rgb(255, 255, 255)',
data: [],
showData: [],
showMaxLen: 6,
}, {
title: '项目参与单位',
key: 'institution_name',
bgColor: 'rgb(234, 241, 251)',
color: 'rgb(18, 76, 157)',
focusBgColor: 'rgb(18, 76, 157)',
focusColor: 'rgb(255, 255, 255)',
data: [],
showData: [],
showMaxLen: 10,
}, {
title: '执行周期包含年份',
key: 'execute_year',
bgColor: 'rgb(225, 242, 234)',
color: 'rgb(8, 96, 96)',
focusBgColor: 'rgb(8, 96, 96)',
focusColor: 'rgb(255, 255, 255)',
data: [],
showData: [],
showMaxLen: 20,
}, {
title: '申报年份',
key: 'apply_year',
bgColor: 'rgb(231, 249, 222)',
color: 'rgb(55, 94, 2)',
focusBgColor: 'rgb(55, 94, 2)',
focusColor: 'rgb(255, 255, 255)',
data: [],
showData: [],
showMaxLen: 20,
}],
list_repo: [{
title: '关键词',
key: 'topic',
bgColor: 'rgb(234, 250, 251)',
color: 'rgb(0, 167, 132)',
focusBgColor: 'rgb(0, 167, 132)',
focusColor: 'rgb(255, 255, 255)',
data: [],
showData: [],
showMaxLen: 10,
}, {
title: '所属科技项目',
key: 'project_name',
bgColor: 'rgb(234, 245, 251)',
color: 'rgb(8, 0, 148)',
focusBgColor: 'rgb(8, 0, 148)',
focusColor: 'rgb(255, 255, 255)',
data: [],
showData: [],
showMaxLen: 8,
}, {
title: '成果贡献单位',
key: 'institution_name',
bgColor: 'rgb(234, 241, 251)',
color: 'rgb(18, 76, 157)',
focusBgColor: 'rgb(18, 76, 157)',
focusColor: 'rgb(255, 255, 255)',
data: [],
showData: [],
showMaxLen: 10,
},
],
conds: {
type_name: '',
institution_name: '',
execute_year: '',
apply_year: '',

topic: '',
project_name: '',
},
mainData: [],
showSecond: false,
secondData: {},
searchKeyword: '',
};
},
methods: {
goMore(item) {
this.secondData = item;
this.searchKeyword = '';
this.secondData.showData = item.data;
this.showSecond = true;
},
goBack() {
this.showSecond = false;
},
changeFilter(item, _item) {
const value = this.conds[item.key] == _item ? '' : _item;
this.$emit('changeCondition', {
[item.key]: value
});
},
searchFilterItem() {
const keyword = this.searchKeyword.trim().toLocaleLowerCase();
this.secondData.showData = this.secondData.data.filter(item => {
return item.toString().toLocaleLowerCase().indexOf(keyword) >= 0;
});
},
},
watch: {
condition: {
handler(newVal) {
this.conds.type_name = newVal.type_name || '';
this.conds.institution_name = newVal.institution_name || '';
this.conds.execute_year = newVal.execute_year || '';
this.conds.apply_year = newVal.apply_year || '';

this.conds.topic = newVal.topic || '';
this.conds.project_name = newVal.project_name || '';
},
immediate: true,
deep: true,
},
},
beforeMount() {
if (this.type == 0) {
this.mainData = this.list_tech;
} else if (this.type == 1) {
this.mainData = this.list_repo;
}
getTechFilterInfo({
type: this.type,
}).then(res => {
const data = res.data;
if (data) {
for (let i = 0, iLen = this.mainData.length; i < iLen; i++) {
const filterItem = this.mainData[i];
const key = filterItem.key;
const max = filterItem.showMaxLen;
if (data[key]) {
filterItem.data = data[key].map(item => item.toString());
filterItem.showData = filterItem.data.slice(0, max);
if (this.conds[key] && filterItem.showData.indexOf(this.conds[key]) < 0) {
filterItem.showData.push(this.conds[key]);
}
}
}
}
}).catch(err => {
console.log(err);
});
},
mounted() { },
};
</script>

<style scoped lang="less">
.filter-c {
margin-bottom: 32px;

.filter-title {
font-size: 18px;
color: rgb(16, 16, 16);
margin: 14px 0;
}

.filter-item-c {
display: flex;
flex-wrap: wrap;

.filter-item {
border-radius: 3px;
padding: 3px 10px;
margin-right: 10px;
margin-bottom: 10px;
cursor: pointer;
}
}

.filter-view-more-c {
margin-top: 6px;

.filter-view-more {
color: rgb(50, 145, 248);
font-size: 14px;
cursor: pointer;

i {
margin-right: 4px;
}
}
}
}

.filter-second {
.filter-second-hd {
margin: 14px 0 24px 0;

.filter-second-go-back {
color: rgb(50, 145, 248);
font-size: 14px;
cursor: pointer;

i {
margin-right: 4px;
}
}
}

.filter-title {
font-size: 18px;
color: rgb(16, 16, 16);
margin: 14px 0;
}

.filter-search {
margin: 5px 10px 10px 0;
}

.filter-item-c {
display: flex;
flex-wrap: wrap;
max-height: 605px;
overflow-y: auto;

.filter-item {
border-radius: 3px;
padding: 3px 10px;
margin-right: 10px;
margin-bottom: 10px;
cursor: pointer;
}
}
}
</style>

+ 158
- 0
web_src/vuepages/pages/tech/components/PrjResultsItem.vue View File

@@ -0,0 +1,158 @@
<template>
<div class="item">
<div class="title-c">
<div class="avatar-c">
<img v-if="data.rel_avatar_link" class="avatar" :src="data.rel_avatar_link" />
<img v-else class="avatar" :avatar="data.owner_name" />
</div>
<div class="title">
<a target="_blank" :href="`/${data.owner_name}/${data.name}`">
<span :title="data.alias">{{ data.alias }}</span>
</a>
</div>
</div>
<div class="descr" :title="data.description">
{{ data.description }}
</div>
<div class="topics">
<a v-for="(item, index) in data.topics" :key="index" class="topic" target="_blank"
:href="`/explore/repos?q=&topic=${item}&sort=hot`">{{ item }}</a>
</div>
<div class="footer">
<div class="contractor" :title="data.institution.split(',').join('、')">{{ data.institution.split(',').join('、') }}
</div>
<div class="update-time">
<span>{{ $t('repos.updated') }}</span>
<el-tooltip effect="dark" :content="dateFormat(data.updated_unix)" placement="top-start">
<span>{{ calcFromNow(data.updated_unix) }}</span>
</el-tooltip>
</div>
</div>
</div>
</template>

<script>
import dayjs from 'dayjs';
import { lang } from '~/langs';
import { timeSinceUnix } from '~/utils';

export default {
name: "PrjResultsItem",
props: {
data: { type: Object, default: () => ({}) },
},
components: {},
data() {
return {};
},
methods: {
calcFromNow(unix) {
return timeSinceUnix(unix, Date.now() / 1000);
},
dateFormat(unix) {
return lang == 'zh-CN' ? dayjs(unix * 1000).format('YYYY年MM月DD日 HH时mm分ss秒') :
dayjs(unix * 1000).format('ddd, D MMM YYYY HH:mm:ss [CST]');
}
},
mounted() { },
};
</script>

<style scoped lang="less">
.item {
border-color: rgb(232, 224, 236);
border-width: 1px;
border-style: solid;
box-shadow: rgba(168, 157, 226, 0.2) 0px 5px 10px 0px;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(-0.01900000000000005%2C%200.997%2C%20-0.12862587255310284%2C%20-0.01900000000000005%2C%200.995%2C%200.014)%22%3E%3Cstop%20stop-color%3D%22%23f2edf5%22%20stop-opacity%3D%221%22%20offset%3D%220.01%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23ffffff%22%20stop-opacity%3D%221%22%20offset%3D%220.31%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
overflow: hidden;
padding: 15px;
}

.title-c {
display: flex;
align-items: center;

.avatar-c {
height: 28px;
width: 28px;
margin-right: 6px;

img {
border-radius: 100%;
height: 100%;
width: 100%;
}
}

.title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

span {
color: rgb(16, 16, 16);
font-size: 16px;
font-weight: 400;
}
}
}

.descr {
height: 40px;
margin-top: 8px;
font-size: 12px;
font-weight: 300;
color: rgb(136, 136, 136);
text-overflow: ellipsis;
word-break: break-all;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
max-height: 41px;
overflow: hidden;
}

.topics {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 5px;
height: 20px;

.topic {
color: rgba(16, 16, 16, 0.8);
border-radius: 4px;
font-size: 12px;
background: rgba(232, 232, 232, 0.6);
padding: 2px 6px;
margin-right: 8px;
}
}

.footer {
margin-top: 12px;
display: flex;
align-items: center;
justify-content: space-between;
height: 20px;

.contractor {
font-size: 14px;
font-weight: 400;
color: rgba(0, 40, 192, 0.73);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.update-time {
text-align: right;
min-width: 140px;
font-size: 12px;
font-weight: 300;
color: rgb(136, 136, 136);
}
}
</style>

+ 154
- 0
web_src/vuepages/pages/tech/components/PrjResultsList.vue View File

@@ -0,0 +1,154 @@
<template>
<div class="list-container">
<div style="min-height:540px;">
<div class="list-item-container" v-loading="loading">
<div class="item-container" v-for="(item, index) in list" :key="item.ID">
<PrjResultsItem :data="item"></PrjResultsItem>
</div>
</div>
<div v-show="(!list.length && !loading)" class="no-data">
<div class="item-empty">
<div class="item-empty-icon bgtask-header-pic"></div>
<div class="item-empty-tips">没有找到相关的项目</div>
</div>
</div>
</div>
<div class="center" v-show="list.length">
<el-pagination ref="paginationRef" background @current-change="currentChange" @size-change="sizeChange"
:current-page.sync="iPage" :page-sizes="iPageSizes" :page-size.sync="iPageSize"
layout="total, sizes, prev, pager, next, jumper" :total="total">
</el-pagination>
</div>
</div>
</template>

<script>
import PrjResultsItem from './PrjResultsItem.vue';
import LetterAvatar from '~/utils/letteravatar';
import { getTechOpenISearch } from '~/apis/modules/tech';

export default {
name: "PrjResultsList",
props: {
condition: { type: Object, default: () => ({}) },
},
components: { PrjResultsItem },
data() {
return {
loading: false,
list: [],
iPageSizes: [15, 30, 50],
iPageSize: 15,
iPage: 1,
total: 0,
};
},
methods: {
getListData() {
this.loading = true;
getTechOpenISearch({
name: this.condition.q,
tech_name: this.condition.project_name,
institution_name: this.condition.institution_name,
topic: this.condition.topic,
page: this.condition.page,
pageSize: this.condition.pageSize,
sort: this.condition.sort,
}).then(res => {
res = res.data;
this.loading = false;
this.total = res.total || 0;
this.list = res.data || [];
this.$nextTick(() => {
LetterAvatar.transform();
});
}).catch(err => {
console.log(err);
this.loading = false;
this.list = [];
this.total = 0;
});
},
search() {
this.getListData();
},
currentChange(page) {
this.iPage = page;
this.$emit('changeCondition', {
page: this.iPage,
pageSize: this.iPageSize,
changePage: true,
});
},
sizeChange(pageSize) {
this.iPageSize = pageSize;
this.$emit('changeCondition', {
page: this.iPage,
pageSize: this.iPageSize,
});
},
},
watch: {
condition: {
handler(newVal) {
this.iPage = newVal.page;
this.iPageSize = newVal.pageSize;
},
immediate: true,
deep: true,
},
},
mounted() { },
};
</script>

<style scoped lang="less">
.list-container {
.list-item-container {
display: flex;
flex-wrap: wrap;

.item-container {
width: 50%;
padding: 12px;
}
}
}

.center {
text-align: center;
}

.no-data {
display: flex;
justify-content: center;
align-items: center;
padding: 0 12px;

.item-empty {
height: 180px;
width: 100%;
padding: 12px;
border-color: rgb(232, 224, 236);
border-width: 1px;
border-style: solid;
box-shadow: rgba(168, 157, 226, 0.2) 0px 5px 10px 0px;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(-0.01900000000000005%2C%200.997%2C%20-0.06169646324801269%2C%20-0.01900000000000005%2C%200.995%2C%200.014)%22%3E%3Cstop%20stop-color%3D%22%23f2edf5%22%20stop-opacity%3D%221%22%20offset%3D%220.01%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23ffffff%22%20stop-opacity%3D%221%22%20offset%3D%220.31%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
display: flex;
flex-direction: column;
justify-content: center;

.item-empty-icon {
height: 80px;
width: 100%;
}

.item-empty-tips {
font-size: 16px;
color: rgb(16, 16, 16);
text-align: center;
margin-top: 2px;
}
}
}
</style>

+ 248
- 0
web_src/vuepages/pages/tech/components/SciAndTechPrjItem.vue View File

@@ -0,0 +1,248 @@
<template>
<div class="item">
<div class="header">
<div class="header-l">
<div class="title" :title="data.project_name">{{ data.project_name }}</div>
<div class="prj-type">{{ data.type }}</div>
</div>
<div class="header-r">
<a :href="`/tech/repo_view?project_name=${data.project_name}`">
<span>更多成果</span>
<i class="el-icon-arrow-right"></i>
</a>
</div>
</div>
<div class="content">
<div class="item-l-c">
<div class="row">
<span class="tit">负责单位:</span>
<span class="val" :title="data.institution">{{ data.institution }}</span>
</div>
<div class="row">
<span class="tit">参与单位:</span>
<span class="val" :title="data.all_institution.split(',').join('、')">{{
data.all_institution.split(',').join('、')
}}</span>
</div>
<div class="row">
<span class="tit">申报年份:</span>
<span class="val">{{ data.apply_year }}</span>
</div>
<div class="row">
<span class="tit">执行周期:</span>
<span class="val">{{ data.execute_period }}</span>
</div>
<div class="row">
<span class="tit">项目成果数:</span>
<span class="val">{{ data.repo_numer }}</span>
</div>
</div>
<div class="item-r-c">
<a class="repo-item-c" target="_blank" :href="`/${item.owner_name}/${item.name}`"
v-for="(item, index) in data.Repos">
<div class="repo-item">
<div class="repo-hd">
<div class="repo-avatar">
<img v-if="data.rel_avatar_link" class="avatar" :src="data.rel_avatar_link" />
<img v-else class="avatar" :avatar="item.owner_name" />
</div>
<div class="repo-tit">{{ item.alias }}</div>
</div>
<div class="repo-content">
<div class="repo-descr">
{{ item.description }}
</div>
</div>
</div>
</a>
<div class="repo-item-c" v-if="!data.Repos.length">
<div class="bgtask-header-pic" style="margin-top:-20px;"></div>
<div class="repo-item" style="text-align:center;">项目成果待展示</div>
</div>
</div>
</div>
</div>
</template>

<script>
import dayjs from 'dayjs';
import { lang } from '~/langs';
import { timeSinceUnix } from '~/utils';

export default {
name: "SciAndTechPrjItem",
props: {
data: { type: Object, default: () => ({}) },
},
components: {},
data() {
return {};
},
methods: {
calcFromNow(unix) {
return timeSinceUnix(unix, Date.now() / 1000);
},
dateFormat(unix) {
return lang == 'zh-CN' ? dayjs(unix * 1000).format('YYYY年MM月DD日 HH时mm分ss秒') :
dayjs(unix * 1000).format('ddd, D MMM YYYY HH:mm:ss [CST]');
}
},
mounted() { },
};
</script>

<style scoped lang="less">
.item {
border-color: rgb(232, 224, 236);
border-width: 1px;
border-style: solid;
box-shadow: rgba(168, 157, 226, 0.2) 0px 5px 10px 0px;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(-0.01900000000000005%2C%200.997%2C%20-0.12862587255310284%2C%20-0.01900000000000005%2C%200.995%2C%200.014)%22%3E%3Cstop%20stop-color%3D%22%23f2edf5%22%20stop-opacity%3D%221%22%20offset%3D%220.01%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23ffffff%22%20stop-opacity%3D%221%22%20offset%3D%220.31%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
overflow: hidden;
padding: 15px;

.header {
display: flex;
align-items: center;
justify-content: space-between;

.header-l {
display: flex;
align-items: center;

.title {
color: rgb(16, 16, 16);
font-size: 18px;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 800px;
}

.prj-type {
margin-left: 14px;
font-size: 12px;
font-weight: 400;
color: rgb(100, 59, 159);
background: rgb(237, 234, 251);
padding: 2px 5px;
border-radius: 4px;
}
}

.header-r {
display: flex;
align-items: baseline;
text-align: right;
color: rgb(50, 145, 248);
font-size: 12px;
width: 80px;
justify-content: flex-end;

i {
margin-left: 1px;
}
}
}

.content {
display: flex;
margin-top: 10px;

.item-l-c {
flex: 1;

.row {
display: flex;
font-size: 14px;
color: rgb(16, 16, 16);
margin: 8px 0;
font-weight: 400;

.tit {
color: rgba(136, 136, 136, 1);
}

.val {
flex: 1;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

&:last-child {
margin-bottom: 0;
}
}
}

.item-r-c {
flex: 2;
margin-left: 10px;
display: flex;

.repo-item-c {
display: block;
flex: 1;
margin: 10px;
padding: 20px;
border-color: rgba(157, 197, 226, 0.4);
border-width: 1px;
border-style: solid;
border-radius: 5px;
box-shadow: rgba(157, 197, 226, 0.2) 0px 5px 10px 0px;
background: rgb(255, 255, 255);
width: 0;

.repo-item {
.repo-hd {
display: flex;
align-items: center;

.repo-avatar {
width: 28px;
height: 28px;

img {
width: 100%;
height: 100%;
border-radius: 100%;
}
}

.repo-tit {
flex: 1;
margin-left: 8px;
width: 0;
font-size: 14px;
color: rgb(50, 145, 248);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

.repo-content {
margin-top: 10px;

.repo-descr {
width: 100%;
font-size: 12px;
color: rgb(136, 136, 136);
text-overflow: ellipsis;
word-break: break-all;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
max-height: 41px;
overflow: hidden;
}
}
}
}
}
}
}
</style>

+ 150
- 0
web_src/vuepages/pages/tech/components/SciAndTechPrjList.vue View File

@@ -0,0 +1,150 @@
<template>
<div class="list-container">
<div class="list-item-container" style="min-height:540px;" v-loading="loading">
<div class="item-container" v-for="(item, index) in list" :key="item.ID">
<SciAndTechPrjItem :data="item"></SciAndTechPrjItem>
</div>
<div v-show="(!list.length && !loading)" class="no-data">
<div class="item-empty">
<div class="item-empty-icon bgtask-header-pic"></div>
<div class="item-empty-tips">没有找到相关的科技项目</div>
</div>
</div>
</div>
<div class="center" v-show="list.length">
<el-pagination ref="paginationRef" background @current-change="currentChange" @size-change="sizeChange"
:current-page.sync="iPage" :page-sizes="iPageSizes" :page-size.sync="iPageSize"
layout="total, sizes, prev, pager, next, jumper" :total="total">
</el-pagination>
</div>
</div>
</template>

<script>
import SciAndTechPrjItem from './SciAndTechPrjItem.vue';
import LetterAvatar from '~/utils/letteravatar';
import { getTechSearch } from '~/apis/modules/tech';

export default {
name: "SciAndTechPrjList",
props: {
condition: { type: Object, default: () => ({}) },
},
components: { SciAndTechPrjItem },
data() {
return {
loading: false,
list: [],
iPageSizes: [15, 30, 50],
iPageSize: 15,
iPage: 1,
total: 0,
};
},
methods: {
getListData() {
this.loading = true;
getTechSearch({
name: this.condition.q,
type_name: this.condition.type_name,
institution_name: this.condition.institution_name,
execute_year: this.condition.execute_year,
apply_year: this.condition.apply_year,
sort: this.condition.sort,
page: this.condition.page,
pageSize: this.condition.pageSize,
}).then(res => {
res = res.data;
this.loading = false;
this.total = res.total || 0;
this.list = res.data;
this.$nextTick(() => {
LetterAvatar.transform();
});
}).catch(err => {
console.log(err);
this.loading = false;
this.list = [];
this.total = 0;
});
},
search() {
this.getListData();
},
currentChange(page) {
this.iPage = page;
this.$emit('changeCondition', {
page: this.iPage,
pageSize: this.iPageSize,
changePage: true,
});
},
sizeChange(pageSize) {
this.iPageSize = pageSize;
this.$emit('changeCondition', {
page: this.iPage,
pageSize: this.iPageSize,
});
},
},
watch: {
condition: {
handler(newVal) {
this.iPage = newVal.page;
this.iPageSize = newVal.pageSize;
},
immediate: true,
deep: true,
},
},
mounted() { },
};
</script>

<style scoped lang="less">
.list-container {
.list-item-container {
.item-container {
width: 100%;
padding: 12px;
}
}
}

.center {
text-align: center;
}

.no-data {
display: flex;
justify-content: center;
align-items: center;
padding: 0 12px;

.item-empty {
height: 205px;
width: 100%;
padding: 12px;
border-color: rgb(232, 224, 236);
border-width: 1px;
border-style: solid;
box-shadow: rgba(168, 157, 226, 0.2) 0px 5px 10px 0px;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(-0.01900000000000005%2C%200.997%2C%20-0.06169646324801269%2C%20-0.01900000000000005%2C%200.995%2C%200.014)%22%3E%3Cstop%20stop-color%3D%22%23f2edf5%22%20stop-opacity%3D%221%22%20offset%3D%220.01%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23ffffff%22%20stop-opacity%3D%221%22%20offset%3D%220.31%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
display: flex;
flex-direction: column;
justify-content: center;

.item-empty-icon {
height: 80px;
width: 100%;
}

.item-empty-tips {
font-size: 16px;
color: rgb(16, 16, 16);
text-align: center;
margin-top: 10px;
}
}
}
</style>

+ 191
- 0
web_src/vuepages/pages/tech/components/SearchBar.vue View File

@@ -0,0 +1,191 @@
<template>
<div class="search-bar">
<div class="search-bar-l">
<div class="search-c">
<div class="search-input-c">
<input type="text" :placeholder="serchPlaceHolder" v-model="keyword" @keyup.enter="search">
</div>
<div class="search-btn" @click="search">{{ $t('repos.search') }}</div>
</div>
<el-button class="apply-btn" type="primary" icon="el-icon-plus" size="medium" @click="apply">申请展示成果</el-button>
<div class="openi-link-c">
<a class="openi-link" target="_blank" href="javascript:;">OpenI启智社区开源指南</a>
</div>
</div>
<div class="sort-c">
<el-select class="select" size="medium" v-model="sortType" @change="changeSort" placeholder="排序" clearable>
<el-option v-for="item in sortList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
</template>

<script>

export default {
name: "SearchBar",
props: {
type: { type: Number, default: -1 }, // 0-tech_view, 1-repo_view
condition: { type: Object, default: () => ({}) },
},
components: {},
data() {
return {
keyword: '',
serchPlaceHolder: '',
sortType: '',
techSortList: [{
k: 'time',
v: this.$t('repos.recentlyUpdated'),
}, {
k: 'count',
v: '项目成果数',
}],
repoSortList: [
{
k: 'mostpopular',
v: this.$t('repos.mostPopular'),
}, {
k: 'recentupdate',
v: this.$t('repos.recentlyUpdated'),
}, {
k: 'newest',
v: this.$t('repos.newest'),
}
],
sortList: [],
isTechAdmin: false,
};
},
methods: {
search() {
this.$emit('changeCondition', {
q: this.keyword.trim()
});
},
apply() {
window.location.href = '/tech/new';
},
manage() {
window.location.href = '/tech/admin_view';
},
changeSort() {
this.$emit('changeCondition', {
sort: this.sortType
});
}
},
watch: {
condition: {
handler(newVal) {
this.keyword = newVal.q;
this.sortType = newVal.sort;
},
immediate: true,
deep: true,
},
},
beforeMount() {
if (this.type == 0) {
this.sortList = this.techSortList;
this.serchPlaceHolder = '搜索科技项目名称';
} else if (this.type == 1) {
this.serchPlaceHolder = '搜索项目';
this.sortList = this.repoSortList;
}
},
mounted() { },
};
</script>

<style scoped lang="less">
.search-bar {
margin: 30px 0;
display: flex;
justify-content: space-between;

.search-bar-l {
display: flex;

.search-c {
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;

.search-input-c {
width: 268px;
height: 40px;
border-color: rgba(0, 61, 192, 0.73);
border-width: 2px;
border-style: solid;
font-size: 14px;
line-height: 20px;
padding: 8px;
display: flex;
color: rgb(136, 136, 136);
align-items: center;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(6.123233995736766e-17%2C%201%2C%20-0.014005111865831027%2C%206.123233995736766e-17%2C%200.5%2C%200)%22%3E%3Cstop%20stop-color%3D%22%23e2d1ea%22%20stop-opacity%3D%221%22%20offset%3D%220%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23ffffff%22%20stop-opacity%3D%221%22%20offset%3D%220.3%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");

input {
width: 100%;
border: none;
outline: none;
}
}

.search-btn {
height: 40px;
font-size: 14px;
line-height: 20px;
padding: 0px;
display: flex;
color: rgb(255, 255, 255);
align-items: center;
text-align: center;
justify-content: center;
padding: 0 20px;
cursor: pointer;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(-1.1800000000000002%2C%200.9499999999999998%2C%20-0.23749999999999996%2C%20-1.1800000000000002%2C%201.024%2C%200.047)%22%3E%3Cstop%20stop-color%3D%22%23bbd2f2%22%20stop-opacity%3D%221%22%20offset%3D%220.02%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23003dc0%22%20stop-opacity%3D%220.73%22%20offset%3D%221%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
}
}

.apply-btn {
margin-left: 20px;
border-color: rgb(31, 1, 115);
border-width: 1px;
border-style: solid;
border-radius: 6px;
font-size: 14px;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(6.123233995736766e-17%2C%201%2C%20-0.08281144868278038%2C%206.123233995736766e-17%2C%200.5%2C%200)%22%3E%3Cstop%20stop-color%3D%22%233291f8%22%20stop-opacity%3D%221%22%20offset%3D%220%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23060075%22%20stop-opacity%3D%220.73%22%20offset%3D%221%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");

&:hover {
opacity: 0.9;
}

&:active {
opacity: 0.8;
}
}

.openi-link-c {
margin-left: 20px;
display: flex;
align-items: center;
justify-content: center;

.openi-link {
text-decoration: underline;
color: rgb(50, 145, 248);
}
}
}

.sort-c {
display: flex;
align-items: center;
justify-content: center;
justify-self: flex-end;
}
}
</style>

+ 141
- 0
web_src/vuepages/pages/tech/components/TopHeader.vue View File

@@ -0,0 +1,141 @@
<template>
<div>
<div class="bg-container">
<div class="bg">
<div class="bg-01"></div>
<div class="bg-02"></div>
<div class="bg-03"></div>
<div class="bg-04"></div>
</div>
<div class="title-c">
<div class="title-main">科技创新2030</div>
<div class="title-second">新一代人工智能重大项目成果展示</div>
</div>
<div class="menu">
<TopMenu :menu="menu"></TopMenu>
</div>
</div>
</div>
</template>

<script>
import TopMenu from './TopMenu.vue';

export default {
name: "TopHeader",
props: {
menu: { type: String, default: '' },
},
components: {
TopMenu
},
data() {
return {
list: [],
};
},
methods: {},
mounted() { },
};
</script>

<style scoped lang="less">
.bg-container {
height: 200px;
width: 100%;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(0.9059999999999998%2C%20-0.8390000000000001%2C%200.016184413580246918%2C%200.9059999999999998%2C%20-0.183%2C%200.683)%22%3E%3Cstop%20stop-color%3D%22%233bb6fe%22%20stop-opacity%3D%221%22%20offset%3D%220%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%232250cf%22%20stop-opacity%3D%221%22%20offset%3D%220.61%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%237c23b4%22%20stop-opacity%3D%221%22%20offset%3D%220.99%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
overflow: hidden;
position: relative;

.bg {
position: absolute;
width: 100%;
height: 100%;
left: 67%;
top: 23%;

.bg-01 {
position: absolute;
top: -296.85px;
left: -100.21px;
width: 11.52px;
height: 430.36px;
transform: rotate(46deg);
border-radius: 45px;
display: flex;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(2.6570000000000005%2C%20-1.366%2C%201906.3802884596842%2C%202.6570000000000005%2C%20-0.714%2C%201.313)%22%3E%3Cstop%20stop-color%3D%22%2315cf16%22%20stop-opacity%3D%221%22%20offset%3D%220%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23ed6bc9%22%20stop-opacity%3D%220%22%20offset%3D%221%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
}

.bg-02 {
position: absolute;
top: -130.68px;
left: -213.01px;
width: 3.64px;
height: 430.36px;
transform: rotate(46deg);
border-radius: 45px;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(2.6570000000000005%2C%20-1.366%2C%2019094.65120710059%2C%202.6570000000000005%2C%20-0.714%2C%201.313)%22%3E%3Cstop%20stop-color%3D%22%23ffffff%22%20stop-opacity%3D%221%22%20offset%3D%220%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23ed6bc9%22%20stop-opacity%3D%220%22%20offset%3D%221%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
}

.bg-03 {
position: absolute;
top: -178.07px;
left: -50.045px;
width: 111.15px;
height: 306.28px;
transform: rotate(46deg);
border-radius: 63px;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(2.6570000000000005%2C%20-1.366%2C%2010.372156049382713%2C%202.6570000000000005%2C%20-0.714%2C%201.313)%22%3E%3Cstop%20stop-color%3D%22%23f8da77%22%20stop-opacity%3D%221%22%20offset%3D%220%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23ed6bc9%22%20stop-opacity%3D%220%22%20offset%3D%221%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
}

.bg-04 {
position: absolute;
top: -218.72px;
left: 239.81px;
width: 20.38px;
height: 306.28px;
transform: rotate(46deg);
border-radius: 45px;
background: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%221%22%20x1%3D%220%22%20x2%3D%221%22%20y1%3D%220%22%20y2%3D%220%22%20gradientTransform%3D%22matrix(2.6570000000000005%2C%20-1.366%2C%20308.5173674049777%2C%202.6570000000000005%2C%20-0.714%2C%201.313)%22%3E%3Cstop%20stop-color%3D%22%2377f8f8%22%20stop-opacity%3D%220.6%22%20offset%3D%220%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23ed6bc9%22%20stop-opacity%3D%220%22%20offset%3D%221%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%231)%22%3E%3C%2Frect%3E%3C%2Fsvg%3E");
}
}

.title-c {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;

.title-main {
text-align: center;
color: rgba(255, 230, 21, 1);
font-weight: 700;
text-shadow: rgb(34 7 94) 0px 2px 6px;
line-height: 39px;
font-size: 28px;
margin: 26px 0 4px;
}

.title-second {
text-align: center;
font-weight: 700;
text-shadow: rgb(34 7 94) 0px 2px 6px;
color: rgb(255, 255, 255);
font-size: 36px;
font-weight: 500;
line-height: 50px;
}
}

.menu {
position: absolute;
bottom: 0;
height: 40px;
width: 100%;
z-index: 2;
padding: 0 140px;
}
}
</style>

+ 120
- 0
web_src/vuepages/pages/tech/components/TopMenu.vue View File

@@ -0,0 +1,120 @@
<template>
<div class="container ui" style="padding:0 75px;">
<div class="menu">
<div class="menu-l">
<div class="menu-item" v-for="(item, index) in listL" :key="index"
:class="focusMenu == item.key ? 'focused' : ''" @click="changeMenu(item)">
{{ item.title }}
</div>
</div>
<div class="menu-r">
<div class="menu-item" v-for="(item, index) in listR" :key="index"
:class="focusMenu == item.key ? 'focused' : ''" @click="changeMenu(item)">
{{ item.title }}
</div>
</div>
</div>
</div>
</template>

<script>
import { getIsTechAdmin } from '~/apis/modules/tech';

export default {
name: "TopMenu",
props: {
menu: { type: String, default: '' },
},
components: {},
data() {
return {
focusMenu: '',
listL: [{
key: 'tech_view',
title: '按科技项目查看',
url: '/tech/tech_view',

}, {
key: 'repo_view',
title: '按项目成果查看',
url: '/tech/repo_view',
}],
listR: [{
key: 'my_view',
title: '我申请的项目',
url: '/tech/my_view',
},],
isTechAdmin: false,
};
},
methods: {
changeMenu(item, index) {
this.focusMenu = item.key;
window.location.href = item.url;
}
},
beforeMount() {
this.focusMenu = this.menu;
getIsTechAdmin().then(res => {
res = res.data;
if (res.data && res.data.is_admin) {
this.isTechAdmin = true;
this.listR.push({
key: 'admin_view',
title: '管理展示项目',
url: '/tech/admin_view',
});
}
}).catch(err => {
console.log(err);
});
},
mounted() { },
};
</script>

<style scoped lang="less">
.menu {
display: flex;
align-items: center;
justify-content: space-between;

.menu-l {
display: flex;
align-items: center;
justify-content: flex-start;
}

.menu-r {
display: flex;
align-items: center;
justify-content: flex-end;
}

.menu-item {
display: flex;
border-color: rgb(187, 187, 187);
border-width: 1px 1px 0px 0px;
border-style: solid;
font-size: 14px;
line-height: 20px;
color: rgba(255, 255, 255, 0.7);
align-items: center;
text-align: center;
justify-content: center;
background: rgba(104, 50, 165, 0.2);
height: 40px;
width: 178px;
cursor: pointer;

&:first-child {
border-left-width: 1px;
}

&.focused {
color: rgb(255, 255, 255);
background: rgba(249, 249, 249, 0.2);
}
}
}
</style>

+ 641
- 0
web_src/vuepages/pages/tech/create/index.vue View File

@@ -0,0 +1,641 @@
<template>
<div>
<TopHeader :menu="''"></TopHeader>
<div class="ui container">
<div class="title">您申请的项目将在2030科技项目页面展示</div>
<div class="form-c" v-show="!selectTechPrj">
<div class="form-wrap">
<div class="form-header">申请展示项目</div>
<div class="form-content">
<div class="form-row">
<div class="row-label required">申请项目存放于</div>
<div class="row-content">
<el-radio v-model="form.type" label="openi" @input="changeType()">启智社区</el-radio>
<el-radio v-model="form.type" label="no-openi" @input="changeType()">非启智社区</el-radio>
</div>
</div>
<div class="form-row" :class="form.url_err ? 'form-row-err' : ''">
<div class="row-label required">现项目地址</div>
<div class="row-content">
<el-input size="medium" v-model="form.url" @input="changeUrl" class="can-err"
placeholder='请输入现有项目的 HTTP(s) 或 Git " clone" URL,如:https://openi.pcl.ac.cn/OpenI/aiforge'></el-input>
</div>
</div>
<div v-show="form.type == 'no-openi'">
<div class="form-row" :class="form.alias_err ? 'form-row-err' : ''">
<div class="row-label required baseline">启智项目名称</div>
<div class="row-content">
<div>
<el-input size="medium" v-model="form.repo_alias" @input="changeAlias" class="can-err"></el-input>
</div>
<div class="tips">
请输入中文、字母、数字和-_ .,最多100个字符。
</div>
</div>
</div>
<div class="form-row" :class="form.name_err ? 'form-row-err' : ''">
<div class="row-label required baseline">启智项目路径</div>
<div class="row-content">
<div class="reop-url-c">
<el-select size="medium" class="owner-sel" v-model="form.uid" @change="changeOwner">
<div slot="prefix" class="owner-item" style="height:100%;padding-left:10px">
<img class="owner-img" :src="ownerSelect.RelAvatarLink">
</div>
<el-option v-for="item in ownerList" :key="item.value" :value="item.value" :label="item.label">
<div class="owner-item">
<img class="owner-img" :src="item.RelAvatarLink">
<span class="owner-name">{{(item.FullName || item.Name)}}</span>
</div>
</el-option>
</el-select>
<span style="margin: 0 8px;font-size:22px;"> / </span>
<el-input size="medium" v-model="form.repo_name" @input="changeRepoName" class="can-err"></el-input>
</div>
<div class="tips">
启智项目地址:<span class="openi-repo-url">{{ form.repo_url }}</span>
</div>
</div>
</div>
<div class="form-row" :class="form.topic_err ? 'form-row-err' : ''">
<div class="row-label required">关键词</div>
<div class="row-content">
<el-select style="width:100%" size="medium" v-model="form.topics" multiple filterable remote
allow-create default-first-option placeholder="请选择标签" :remote-method="searchTopics"
:loading="form.topicLoading" class="can-err">
<el-option v-for="item in topicsList" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</div>
</div>
<div class="form-row" :class="form.descr_err ? 'form-row-err' : ''">
<div class="row-label required baseline">项目简介</div>
<div class="row-content">
<el-input size="medium" type="textarea" :rows="4" placeholder="请输入项目简介(长度不超过255)" :maxlength="255"
show-word-limit v-model="form.description" class="can-err"></el-input>
</div>
</div>
</div>
<div class="form-row" :class="form.tech_err ? 'form-row-err' : ''">
<div class="row-label required">科技项目</div>
<div class="row-content">
<el-input placeholder="请选择科技项目" size="medium" v-model="form.tech_show" :readonly="true" class="can-err">
<el-button size="medium" slot="append" class="btn-select" @click="goSelectTechPrj">选择科技项目</el-button>
<el-button size="medium" slot="append" class="btn-clear" @click="clearTechPrj">清除</el-button>
</el-input>
</div>
</div>
<div class="form-row" :class="form.institution_err ? 'form-row-err' : ''">
<div class="row-label required">成果贡献单位</div>
<div class="row-content">
<el-select style="width:100%" size="medium" v-model="form.institution" multiple class="can-err">
<el-option v-for="item in institutionList" :key="item.key" :label="item.value"
:value="item.value"></el-option>
</el-select>
</div>
</div>
<div class="form-btn-group">
<el-button size="medium" type="primary" :loading="submitLoading" class="btn confirm-btn"
@click="submit">提交申请</el-button>
<el-button size="medium" class="btn" @click="cancel">{{ $t('cancel') }}</el-button>
</div>
</div>
</div>
</div>
<div class="form-select-tech-prj" v-show="selectTechPrj">
<div class="form-wrap">
<div class="form-header">
<span>选择科技项目</span>
<span>
<el-button class="form-btn-go-back" @click="goBack">返回</el-button>
</span>
</div>
<div class="form-content">
<div class="form-row">
<div class="row-label">请输入科技项目</div>
<div class="row-content">
<el-input placeholder="请输入搜索内容" size="medium" v-model="form.tech_search_keyword"
@keyup.enter.native="searchTechList" class="input-with-select">
<el-select v-model="form.tech_search_sel" style="width:142px" size="medium" slot="prepend">
<el-option label="项目立项编号" value="0"></el-option>
<el-option label="参与单位" value="1"></el-option>
<el-option label="项目名称" value="2"></el-option>
</el-select>
<el-button size="medium" slot="append" icon="el-icon-search" @click="searchTechList"></el-button>
</el-input>
</div>
</div>
<div class="form-table">
<div style="margin: 0 20px 30px 20px">
<el-table ref="tableRef" border :data="tableData" style="width:100%" v-loading="loading" stripe>
<el-table-column prop="no" label="项目立项编号" align="center" header-align="center"
width="120"></el-table-column>
<el-table-column prop="name" label="科技项目名称" align="center" header-align="center"
width="200"></el-table-column>
<el-table-column prop="institution" label="项目承担单位" align="center" header-align="center"
width="200"></el-table-column>
<el-table-column prop="all_institution" label="所有参与单位" align="center"
header-align="center"></el-table-column>
<el-table-column width="100" :label="$t('operation')" align="center" header-align="center">
<template slot-scope="scope">
<el-button type="primary" @click="selectedTechPrj(scope.row)">选择</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
import TopHeader from '../components/TopHeader.vue';
import { getTechs, setOpenIApply, setNoOpenIApply, getCreateRepoUser, getCheckRepoName, getTopics } from '~/apis/modules/tech';

export default {
data() {
return {
form: {
type: 'openi',
url: '',
url_err: false,
repo_alias: '',
alias_err: false,
uid: '',
repo_name: '',
name_err: false,
repo_url: '',
topics: [],
topic_err: false,
topicLoading: false,

description: '',
descr_err: false,

tech_search_sel: '0',
tech_search_keyword: '',
tech_obj: null,
tech_show: '',
tech_err: false,

institution: [],
institution_err: false,
},
selectTechPrj: false,
loading: false,
tableData: [],
topicsList: [],
institutionList: [],

ownerList: [],
ownerSelect: {},

submitLoading: false,
};
},
components: {
TopHeader,
},
methods: {
resetData() {
this.form.url = '';
this.form.url_err = false;
this.form.repo_alias = '';
this.form.alias_err = false;
this.form.uid = this.ownerList.length ? this.ownerList[0].value : '';
this.form.repo_name = '';
this.form.repo_url = '';
this.form.name_err = false;
this.form.topics = [];
this.form.topic_err = false;
this.form.topicLoading = false;
this.form.description = '';
this.form.descr_err = false;
this.form.tech_search_sel = '0';
this.form.tech_search_keyword = '';
this.form.tech_obj = null;
this.form.tech_show = '';
this.form.tech_err = false;
this.form.institution = [];
this.form.institution_err = false;
this.tableData = [];
this.topicsList = [];
this.institutionList = [];
this.submitLoading = false;
},
changeType() {
this.resetData();
},
checkUrl() {
this.form.url_err = !this.form.url;
return !this.form.url_err;
},
checkRepoAlias() {
const reg = /^[\u4E00-\u9FA5A-Za-z0-9_.-]{1,100}$/;
const res = reg.test(this.form.repo_alias);
this.form.alias_err = !res;
return res;
},
checkRepoName() {
const reg = /^[A-Za-z0-9_.-]{1,100}$/;
const res = reg.test(this.form.repo_name);
this.form.name_err = !res;
return res;
},
checkTopic() {
this.form.topic_err = this.form.topics.length == 0;
return !this.form.topic_err;
},
checkDescr() {
this.form.descr_err = !this.form.description;
return !this.form.descr_err;
},
checkTech() {
this.form.tech_err = !this.form.tech_obj;
return !this.form.tech_err;
},
checkinstitution() {
this.form.institution_err = this.form.institution.length == 0;
return !this.form.institution_err;
},
changeUrl() {
if (this.form.type == 'openi') return;
const owner = this.ownerSelect.Name;
const urlAdd = location.href.split("/")[0] + "//" + location.href.split("/")[2];
const repoValue = this.form.url.match(/^(.*\/)?((.+?)(\.git)?)$/)[3];
this.form.repo_alias = repoValue;
this.checkRepoAlias();
getCheckRepoName({ owner: owner, q: repoValue }).then(res => {
const repo_name = res.data.name;
this.form.repo_name = repo_name;
this.form.repo_url = `${urlAdd}/${owner}/${repo_name}.git`;
this.checkRepoName();
}).catch(err => {
console.log(err);
this.form.repo_name = '';
this.form.repo_url = '';
});
},
changeAlias() {
const owner = this.ownerSelect.Name;
const aliasValue = this.form.repo_alias;
const urlAdd = location.href.split("/")[0] + "//" + location.href.split("/")[2];
if (aliasValue && this.checkRepoAlias()) {
getCheckRepoName({ owner: owner, q: aliasValue }).then(res => {
const repo_name = res.data.name;
this.form.repo_name = repo_name;
this.form.repo_url = `${urlAdd}/${owner}/${repo_name}.git`;
}).catch(err => {
console.log(err);
this.form.repo_name = '';
this.form.repo_url = '';
});
} else {
this.form.repo_name = '';
this.form.repo_url = '';
}
},
changeOwner(value) {
this.ownerSelect = this.ownerList.filter(item => item.value == value)[0];
this.form.repo_name && this.changeRepoName();
},
changeRepoName() {
const owner = this.ownerSelect.Name;
const repo_name = this.form.repo_name;
const urlAdd = location.href.split("/")[0] + "//" + location.href.split("/")[2];
if (this.checkRepoName()) {
this.form.repo_url = `${urlAdd}/${owner}/${repo_name}.git`;
} else {
this.form.repo_url = '';
}
},
searchTopics(query) {
if (query !== '') {
this.form.topicLoading = true;
getTopics({ q: query }).then(res => {
this.form.topicLoading = false;
const topics = res.data.topics || [];
this.topicsList = topics.map(item => {
return {
value: item.topic_name,
label: item.topic_name,
}
});
}).catch(err => {
this.topicsList = [];
});
} else {
this.topicsList = [];
}
},
goSelectTechPrj() {
this.form.tech_search_sel = '0';
this.form.tech_search_keyword = '';
this.tableData = [];
this.selectTechPrj = true;
this.searchTechList();
},
clearTechPrj() {
this.form.institution = [];
this.institutionList = [];
this.form.tech_show = '';
this.form.tech_obj = null;
this.form.tech_err = false;
},
goBack() {
this.selectTechPrj = false;
},
searchTechList() {
this.loading = true;
getTechs({
no: this.form.tech_search_sel == '0' ? this.form.tech_search_keyword : '',
institution: this.form.tech_search_sel == '1' ? this.form.tech_search_keyword : '',
name: this.form.tech_search_sel == '2' ? this.form.tech_search_keyword : '',
}).then(res => {
this.loading = false;
res = res.data;
this.tableData = res.data || [];
}).catch(err => {
console.log(err);
this.loading = false;
this.tableData = [];
});
},
selectedTechPrj(item) {
this.form.institution = [];
this.institutionList = item.all_institution.split(',').map((item, index) => {
return {
key: item,
value: item,
}
});
this.form.tech_show = `【${item.no}】 ${item.name}`;
this.form.tech_obj = item;
this.checkTech();
this.goBack();
},
submit() {
const subData = {};
let setApi = null;
if (this.form.type == 'openi') {
setApi = setOpenIApply;
const r1 = this.checkUrl();
const r2 = this.checkTech();
const r3 = this.checkinstitution();
if (r1 && r2 && r3) {
subData.url = this.form.url;
subData.no = this.form.tech_obj.no;
subData.institution = this.form.institution.join(',');
} else {
return;
}
} else {
setApi = setNoOpenIApply;
const r1 = this.checkUrl();
const r2 = this.checkTech();
const r3 = this.checkinstitution();
const r4 = this.checkRepoAlias();
const r5 = this.checkRepoName();
const r6 = this.checkTopic();
const r7 = this.checkDescr();
if (r1 && r2 && r3 && r4 && r5 && r6 && r7) {
subData.url = this.form.url;
subData.uid = this.form.uid;
subData.alias = this.form.repo_alias;
subData.repo_name = this.form.repo_name;
subData.topics = [...this.form.topics];
subData.description = this.form.description.trim();
subData.no = this.form.tech_obj.no;
subData.institution = this.form.institution.join(',');
} else {
return;
}
}
// console.log(subData);
// return;
this.submitLoading = true;
setApi(subData).then(res => {
if (res.data && res.data.code == 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully'),
});
setTimeout(() => {
window.location.href = '/tech/tech_view';
}, 1000);
} else {
this.submitLoading = false;
this.$message({
type: 'info',
message: res.data.msg
});
}
}).catch(err => {
this.submitLoading = false;
this.$message({
type: 'error',
message: this.$t('submittedFailed'),
});
});
},
cancel() {
window.history.back();
}
},
beforeMount() {
getCreateRepoUser().then(res => {
const data = res.data.Data || [];
this.ownerList = data.map(item => {
return {
value: item.ID,
label: item.FullName || item.Name,
...item,
}
});
this.ownerSelect = this.ownerList.length ? this.ownerList[0] : {};
this.form.uid = this.ownerList.length ? this.ownerList[0].value : '';
}).catch(err => {
console.log(err);
});
},
mounted() { },
beforeDestroy() { },
};
</script>

<style scoped lang="less">
.title {
text-align: center;
margin: 30px 0;
font-size: 14px;
color: rgb(16, 16, 16);
}

.form-c,
.form-select-tech-prj {
display: flex;
justify-content: center;

.form-wrap {
width: 1000px;
background: rgb(255, 255, 255);
border-color: rgb(212, 212, 213);
border-width: 1px;
border-style: solid;
border-radius: 5px;
box-sizing: border-box;

.form-header {
height: 45px;
background: rgb(240, 240, 240);
border-bottom: 1px solid rgb(212, 212, 213);
color: rgb(16, 16, 16);
padding-left: 16px;
padding-right: 16px;
font-size: 16px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: space-between;
}

.form-content {
margin-top: 40px;

.form-row {
display: flex;
align-items: center;
width: 80%;
margin: 20px auto;
padding-right: 50px;

.row-label {
flex: 2;
text-align: right;
margin-right: 24px;
position: relative;

&.required {
&::after {
position: absolute;
top: -2px;
right: -10px;
content: '*';
color: #db2828;
}
}

&.baseline {
align-self: baseline;
margin-top: 11px;
}
}

.row-content {
flex: 9;

.tips {
margin-top: 4px;
font-size: 14px;
color: rgb(136, 136, 136);


}
}

.reop-url-c {
display: flex;
align-items: center;
}

.openi-repo-url {
color: rgba(16, 16, 16, 1);
}

&.form-row-err {
.row-label {
color: #9f3a38;
}

/deep/ .can-err .el-input__inner,
/deep/ .can-err .el-textarea__inner {
color: #9f3a38;
background: #fff6f6;
border-color: #e0b4b4;
}
}
}

.form-table {
margin-top: 30px;
margin-bottom: 30px;

/deep/ .el-table__header {
th {
background: rgb(249, 249, 249);
font-size: 12px;
color: rgb(136, 136, 136);
font-weight: normal;
}
}

/deep/ .el-table__body {
td {
font-size: 12px;
}
}

/deep/ .el-radio__label {
display: none;
}
}

.form-btn-group {
display: flex;
justify-content: center;
align-items: center;
margin: 30px 0 40px;
}
}
}
}

.btn {
color: rgb(2, 0, 4);
background-color: rgb(194, 199, 204);
border-color: rgb(194, 199, 204);

&.confirm-btn {
color: #fff;
background-color: rgb(56, 158, 13);
border-color: rgb(56, 158, 13);
margin-right: 20px;
}
}

.btn-select {
background-color: rgb(50, 145, 248) !important;
color: white !important;
border-radius: 0 !important;
height: 35px;
}

.owner-sel {
/deep/ .el-input__inner {
padding-left: 48px;
}
}

.owner-item {
display: flex;
align-items: center;

.owner-img {
width: 24px;
height: 24px;
margin-right: 10px;
}
}
</style>

+ 17
- 0
web_src/vuepages/pages/tech/create/vp-tech-create.js View File

@@ -0,0 +1,17 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import localeEn from 'element-ui/lib/locale/lang/en';
import localeZh from 'element-ui/lib/locale/lang/zh-CN';
import { i18n, lang } from '~/langs';
import App from './index.vue';

Vue.use(ElementUI, {
locale: lang === 'zh-CN' ? localeZh : localeEn,
size: 'small',
});

new Vue({
i18n,
render: (h) => h(App),
}).$mount('#__vue-root');

+ 164
- 0
web_src/vuepages/pages/tech/myview/index.vue View File

@@ -0,0 +1,164 @@
<template>
<div>
<TopHeader :menu="'my_view'"></TopHeader>
<div class="ui container">
<div class="table-container">
<div class="table-title">2030科技项目</div>
<div class="table-wrap">
<el-table ref="tableRef" border :data="tableData" style="width:100%;" v-loading="loading" stripe row-key="id">
<el-table-column label="序号" type="index" align="center" header-align="center" width="50"
fixed></el-table-column>
<el-table-column label="启智项目名称" align="center" header-align="center" fixed min-width="140">
<template slot-scope="scope">
<span v-if="scope.row.status == 5">
{{ `${scope.row.url.split('/')[3]}/${scope.row.url.split('/')[4]}` }}
</span>
<a v-else target="_blank" :href="`/${scope.row.repo_owner_name}/${scope.row.repo_name}`">
{{ `${scope.row.repo_owner_name}/${scope.row.repo_name}` }}
</a>
</template>
</el-table-column>
<el-table-column prop="name" label="科技项目名称" align="center" header-align="center" fixed
min-width="200"></el-table-column>
<el-table-column prop="status" label="展示状态" align="center" header-align="center" fixed>
<template slot-scope="scope">
<span style="color:rgb(255, 37, 37)"
:style="scope.row.status == 1 ? { color: 'rgb(56, 158, 13)' } : ''">{{
statusMap[scope.row.status] }}</span>
</template>
</el-table-column>
<el-table-column prop="no" label="项目立项编号" align="center" header-align="center"
min-width="120"></el-table-column>
<el-table-column prop="institution" label="项目承担单位" align="center" header-align="center"
min-width="120"></el-table-column>
<el-table-column prop="all_institution" label="所有参与单位" align="center" header-align="center"
min-width="280"></el-table-column>
<el-table-column prop="contribution_institution" label="成果贡献单位" align="center" header-align="center"
min-width="280"></el-table-column>
<el-table-column prop="contribution_institution" label="申请时间" align="center" header-align="center"
min-width="120">
<template slot-scope="scope">
<span>{{ dateFormat(scope.row.created_unix) }}</span>
</template>
</el-table-column>
</el-table>
</div>
<div class="center">
<el-pagination ref="paginationRef" background @current-change="currentChange" @size-change="sizeChange"
:current-page.sync="page" :page-sizes="pageSizes" :page-size.sync="pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="total">
</el-pagination>
</div>
</div>
</div>
</div>
</template>

<script>
import dayjs from 'dayjs';
import TopHeader from '../components/TopHeader.vue';
import { getTechMyList } from '~/apis/modules/tech';

export default {
data() {
return {
loading: false,
tableData: [],

page: 1,
pageSizes: [15, 30, 50],
pageSize: 15,
total: 0,

statusMap: {
'1': '已展示',
'2': '未展示',
'3': '项目迁移中',
'4': '项目迁移失败',
'5': '项目不存在',
},
};
},
components: { TopHeader },
methods: {
getData() {
this.loading = true;
getTechMyList({
page: this.page,
pageSize: this.pageSize,
}).then(res => {
this.loading = false;
const { total, data } = res.data;
this.tableData = data.map((item, index) => {
return { ...item, }
});
this.total = total;
}).catch(err => {
this.loading = false;
console.log(err);
});
},
currentChange(page) {
this.page = page;
this.getData();
},
sizeChange(pageSize) {
this.pageSize = pageSize;
this.getData();
},
dateFormat(unix) {
return dayjs(unix * 1000).format('YYYY-MM-DD HH:mm');
}
},
beforeMount() {
this.getData();
},
mounted() { },
beforeDestroy() { },
};
</script>

<style scoped lang="less">
.table-container {
margin-top: 30px;

.table-title {
height: 43px;
font-size: 16px;
font-weight: 700;
padding: 15px;
color: rgb(16, 16, 16);
border-color: rgb(212, 212, 213);
border-width: 1px;
border-style: solid;
border-radius: 5px 5px 0px 0px;
background: rgb(240, 240, 240);
display: flex;
align-items: center;
}

.table-wrap {
overflow-x: auto;
margin-bottom: 20px;

/deep/ .el-table__header {
th {
background: rgb(249, 249, 249);
font-size: 12px;
color: rgb(136, 136, 136);
font-weight: normal;
}
}

/deep/ .el-table__body {
td {
font-size: 12px;
}
}

/deep/ .el-radio__label {
display: none;
}
}
}
</style>

+ 17
- 0
web_src/vuepages/pages/tech/myview/vp-tech-myview.js View File

@@ -0,0 +1,17 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import localeEn from 'element-ui/lib/locale/lang/en';
import localeZh from 'element-ui/lib/locale/lang/zh-CN';
import { i18n, lang } from '~/langs';
import App from './index.vue';

Vue.use(ElementUI, {
locale: lang === 'zh-CN' ? localeZh : localeEn,
size: 'small',
});

new Vue({
i18n,
render: (h) => h(App),
}).$mount('#__vue-root');

+ 100
- 0
web_src/vuepages/pages/tech/repoview/index.vue View File

@@ -0,0 +1,100 @@
<template>
<div>
<TopHeader :menu="'repo_view'"></TopHeader>
<div class="ui container">
<SearchBar :type="1" :condition="condition" @changeCondition="changeCondition"></SearchBar>
<div class="conent-c">
<div class="filter-c">
<Filters :type="1" :condition="condition" @changeCondition="changeCondition"></Filters>
</div>
<div class="result-c">
<PrjResultsList ref="resultListRef" :condition="condition" @changeCondition="changeCondition">
</PrjResultsList>
</div>
</div>
</div>
</div>
</template>

<script>
import TopHeader from '../components/TopHeader.vue';
import SearchBar from '../components/SearchBar.vue';
import Filters from '../components/Filters.vue';
import PrjResultsList from '../components/PrjResultsList.vue';
import { getUrlSearchParams } from '~/utils';

export default {
data() {
return {
condition: {
q: '', // 搜索框
topic: '', // 关键词
project_name: '', // 项目类型
institution_name: '', // 成果贡献单位

page: '',
pageSize: '',
sort: '',
},
pageSizes: [15, 30, 50],
};
},
components: {
TopHeader,
SearchBar,
Filters,
PrjResultsList
},
methods: {
changeCondition(params) {
this.condition = {
...this.condition,
...params,
};
if (!params.changePage) {
this.condition.page = 1;
}
window.location.href = `/tech/repo_view?` + `q=${encodeURIComponent(this.condition.q.trim())}` +
`&topic=${encodeURIComponent(this.condition.topic)}` +
`&project_name=${encodeURIComponent(this.condition.project_name)}` +
`&institution_name=${encodeURIComponent(this.condition.institution_name)}` +
`&page=${encodeURIComponent(this.condition.page)}` +
`&pageSize=${encodeURIComponent(this.condition.pageSize)}` +
`&sort=${encodeURIComponent(this.condition.sort)}`;
},
},
beforeMount() {
const urlParams = getUrlSearchParams();
this.condition.q = urlParams.q || '';
this.condition.topic = urlParams.topic || '';
this.condition.project_name = urlParams.project_name || '';
this.condition.institution_name = urlParams.institution_name || '';
this.condition.sort = urlParams.sort || '';
this.condition.page = Number(urlParams.page) || 1;
this.condition.pageSize = this.pageSizes.indexOf(Number(urlParams.pageSize)) >= 0 ? Number(urlParams.pageSize) : 15;
this.$nextTick(() => {
this.$refs.resultListRef.search();
});
},
mounted() { },
beforeDestroy() { },
};
</script>

<style scoped lang="less">
.conent-c {
display: flex;
}

.filter-c {
flex: 1;
padding-left: 12px;
}

.result-c {
margin-left: 10px;
flex: 3;
width: 0;
}
</style>


+ 17
- 0
web_src/vuepages/pages/tech/repoview/vp-tech-repoview.js View File

@@ -0,0 +1,17 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import localeEn from 'element-ui/lib/locale/lang/en';
import localeZh from 'element-ui/lib/locale/lang/zh-CN';
import { i18n, lang } from '~/langs';
import App from './index.vue';

Vue.use(ElementUI, {
locale: lang === 'zh-CN' ? localeZh : localeEn,
size: 'small',
});

new Vue({
i18n,
render: (h) => h(App),
}).$mount('#__vue-root');

+ 100
- 0
web_src/vuepages/pages/tech/techview/index.vue View File

@@ -0,0 +1,100 @@
<template>
<div>
<TopHeader :menu="'tech_view'"></TopHeader>
<div class="ui container">
<SearchBar :type="0" :condition="condition" @changeCondition="changeCondition"></SearchBar>
<div class="conent-c">
<div class="filter-c">
<Filters :type="0" :condition="condition" @changeCondition="changeCondition"></Filters>
</div>
<div class="result-c">
<SciAndTechPrjList ref="resultListRef" :condition="condition" @changeCondition="changeCondition">
</SciAndTechPrjList>
</div>
</div>
</div>
</div>
</template>

<script>
import TopHeader from '../components/TopHeader.vue';
import SearchBar from '../components/SearchBar.vue';
import Filters from '../components/Filters.vue';
import SciAndTechPrjList from '../components/SciAndTechPrjList.vue';
import { getUrlSearchParams } from '~/utils';

export default {
data() {
return {
condition: {
q: '', // 搜索框
type_name: '', // 项目类型
institution_name: '', // 项目参与单位
execute_year: '', // 执行周期包含年份
apply_year: '',
page: '',
pageSize: '',
sort: '',
},
pageSizes: [15, 30, 50],
};
},
components: {
TopHeader,
SearchBar,
Filters,
SciAndTechPrjList
},
methods: {
changeCondition(params) {
this.condition = {
...this.condition,
...params,
};
if (!params.changePage) {
this.condition.page = 1;
}
window.location.href = `/tech/tech_view?` + `q=${encodeURIComponent(this.condition.q.trim())}` +
`&type_name=${encodeURIComponent(this.condition.type_name)}` +
`&institution_name=${encodeURIComponent(this.condition.institution_name)}` +
`&execute_year=${encodeURIComponent(this.condition.execute_year)}` +
`&apply_year=${encodeURIComponent(this.condition.apply_year)}` +
`&page=${encodeURIComponent(this.condition.page)}` +
`&pageSize=${encodeURIComponent(this.condition.pageSize)}` +
`&sort=${encodeURIComponent(this.condition.sort)}`;
},
},
beforeMount() {
const urlParams = getUrlSearchParams();
this.condition.q = urlParams.q || '';
this.condition.type_name = urlParams.type_name || '';
this.condition.institution_name = urlParams.institution_name || '';
this.condition.execute_year = urlParams.execute_year || '';
this.condition.apply_year = urlParams.apply_year || '';
this.condition.sort = urlParams.sort || '';
this.condition.page = Number(urlParams.page) || 1;
this.condition.pageSize = this.pageSizes.indexOf(Number(urlParams.pageSize)) >= 0 ? Number(urlParams.pageSize) : 15;
this.$nextTick(() => {
this.$refs.resultListRef.search();
});
},
mounted() { },
beforeDestroy() { },
};
</script>

<style scoped lang="less">
.conent-c {
display: flex;
}

.filter-c {
flex: 1;
padding-left: 12px;
}

.result-c {
margin-left: 10px;
flex: 3;
}
</style>

+ 17
- 0
web_src/vuepages/pages/tech/techview/vp-tech-techview.js View File

@@ -0,0 +1,17 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import localeEn from 'element-ui/lib/locale/lang/en';
import localeZh from 'element-ui/lib/locale/lang/zh-CN';
import { i18n, lang } from '~/langs';
import App from './index.vue';

Vue.use(ElementUI, {
locale: lang === 'zh-CN' ? localeZh : localeEn,
size: 'small',
});

new Vue({
i18n,
render: (h) => h(App),
}).$mount('#__vue-root');

Loading…
Cancel
Save