| @@ -167,6 +167,9 @@ func init() { | |||
| new(Badge), | |||
| new(BadgeUser), | |||
| new(BadgeUserLog), | |||
| new(TechConvergeBaseInfo), | |||
| new(RepoConvergeInfo), | |||
| new(UserRole), | |||
| ) | |||
| tablesStatistic = append(tablesStatistic, | |||
| @@ -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 { | |||
| @@ -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"` | |||
| } | |||
| @@ -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, | |||
| }) | |||
| } | |||
| @@ -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"` | |||
| } | |||
| @@ -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 | |||
| @@ -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 = 该项目已迁移到启智,请使用启智社区方式提交申请 | |||
| @@ -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()) | |||
| } | |||
| @@ -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() { | |||
| @@ -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)) | |||
| } | |||
| @@ -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()) | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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)) | |||
| } | |||
| @@ -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()} | |||
| } | |||
| @@ -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} | |||
| } | |||
| @@ -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) | |||
| @@ -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) | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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) | |||
| @@ -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 | |||
| } | |||
| @@ -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] | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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}} | |||
| @@ -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}} | |||
| @@ -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}} | |||
| @@ -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}} | |||
| @@ -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" .}} | |||
| @@ -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" .}} | |||
| @@ -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" .}} | |||
| @@ -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" .}} | |||
| @@ -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" .}} | |||
| @@ -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, | |||
| }); | |||
| } | |||
| @@ -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> | |||
| @@ -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'); | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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'); | |||
| @@ -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> | |||
| @@ -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'); | |||
| @@ -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> | |||
| @@ -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'); | |||
| @@ -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> | |||
| @@ -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'); | |||